Components

Phone number input

scss:Readyfigma:Ready
vue
:Ready

Import

Import the settings, the text-input and the phone-number scss files.

@import "settings-tools/all-settings";
@import "components/_c.text-input";
@import "components/_c.phone-number";

Usage

A phone number input is composed of 2 main zones:

  • A dropdown with the class : mc-phone-number__dropdown
  • A number input with the class : mc-phone-number__input
<div class="mc-phone-number">
<div class="mc-phone-number__dropdown">
<button
type="button"
aria-haspopup="listbox"
aria-labelledby="dropdown_country"
id="dropdown_country"
class="mc-phone-number__button"
>
<svg class="mc-phone-number__flag">
<use xlink:href="#flag" />
</svg>
<span class="mc-phone_number__indicator">+33</span>
</button>
<ul
id="phone_number_list"
tabindex="-1"
role="listbox"
aria-labelledby="phone_number_list"
class="mc-phone-number__list mc-phone-number__list--hidden"
>
<li id="FR" role="option" class="mc-phone-number__item">
<svg class="mc-phone-number__flag">
<use xlink:href="#flag" />
</svg>
<span class="mc-phone-number__country">France, </span>
<span>+33</span>
</li>
</ul>
</div>
<input
type="text"
class="mc-phone-number__input mc-text-input mc-text-input--m"
id="smallField"
placeholder="00 00 00 00 00"
name="example-small"
/>
</div>
Viewport: px
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Input phone number</title>
<script src="https://www.w3.org/TR/wai-aria-practices-1.1/examples/js/utils.js"></script>
</head>
<body>
<div class="example">
<div id="mc-phone-number" class="mc-phone-number">
<div class="mc-phone-number__dropdown">
<button
type="button"
aria-haspopup="listbox"
aria-labelledby="dropdown_country"
id="dropdown_country"
class="mc-phone-number__button"
>
<span class="mc-phone-number__flag">
🇫🇷
</span>
<span>+33</span>
</button>
<ul
id="phone_number_list"
tabindex="-1"
role="listbox"
aria-labelledby="phone_number_list"
class="mc-phone-number__list mc-phone-number__list--hidden"
>
<li id="FR" role="option" class="mc-phone-number__item">
<span class="mc-phone-number__flag">
🇫🇷
</span>
<span class="mc-phone-number__country">France, </span>
<span>+33</span>
</li>
<li id="IT" role="option" class="mc-phone-number__item">
<span class="mc-phone-number__flag">
🇮🇹
</span>
<span class="mc-phone-number__country">Italie, </span>
<span>+39</span>
</li>
<li id="BE" role="option" class="mc-phone-number__item">
<span class="mc-phone-number__flag">
🇧🇪
</span>
<span class="mc-phone-number__country">Belgique, </span>
<span>+32</span>
</li>
<li id="GE" role="option" class="mc-phone-number__item">
<span class="mc-phone-number__flag">
🇩🇪
</span>
<span class="mc-phone-number__country">Allemagne, </span>
<span>+49</span>
</li>
<li id="HO" role="option" class="mc-phone-number__item">
<span class="mc-phone-number__flag">
🇳🇱
</span>
<span class="mc-phone-number__country">Holland, </span>
<span>+31</span>
</li>
<li id="RU" role="option" class="mc-phone-number__item">
<span class="mc-phone-number__flag">
🇷🇺
</span>
<span class="mc-phone-number__country">Russie, </span>
<span>+7</span>
</li>
</ul>
</div>
<input
type="text"
class="mc-phone-number__input mc-text-input mc-text-input--m"
id="smallField"
placeholder="00 00 00 00 00"
name="example-small"
/>
</div>
</div>
</body>
<script>
/*
* This content is licensed according to the W3C Software License at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
*/
/**
* @namespace aria
*/
var aria = aria || {}
/**
* @constructor
*
* @desc
* Listbox object representing the state and interactions for a listbox widget
*
* @param listboxNode
* The DOM node pointing to the listbox
*/
aria.Listbox = function (listboxNode) {
this.listboxNode = listboxNode
this.activeDescendant = this.listboxNode.getAttribute(
"aria-activedescendant"
)
this.multiselectable = this.listboxNode.hasAttribute(
"aria-multiselectable"
)
this.moveUpDownEnabled = false
this.siblingList = null
this.upButton = null
this.downButton = null
this.moveButton = null
this.keysSoFar = ""
this.handleFocusChange = function () {}
this.handleItemChange = function (event, items) {}
this.registerEvents()
}
/**
* @desc
* Register events for the listbox interactions
*/
aria.Listbox.prototype.registerEvents = function () {
this.listboxNode.addEventListener("focus", this.setupFocus.bind(this))
this.listboxNode.addEventListener(
"keydown",
this.checkKeyPress.bind(this)
)
this.listboxNode.addEventListener("click", this.checkClickItem.bind(this))
}
/**
* @desc
* If there is no activeDescendant, focus on the first option
*/
aria.Listbox.prototype.setupFocus = function () {
if (this.activeDescendant) {
return
}
this.focusFirstItem()
}
/**
* @desc
* Focus on the first option
*/
aria.Listbox.prototype.focusFirstItem = function () {
var firstItem
firstItem = this.listboxNode.querySelector('[role="option"]')
if (firstItem) {
this.focusItem(firstItem)
}
}
/**
* @desc
* Focus on the last option
*/
aria.Listbox.prototype.focusLastItem = function () {
var itemList = this.listboxNode.querySelectorAll('[role="option"]')
if (itemList.length) {
this.focusItem(itemList[itemList.length - 1])
}
}
/**
* @desc
* Handle various keyboard controls; UP/DOWN will shift focus; SPACE selects
* an item.
*
* @param evt
* The keydown event object
*/
aria.Listbox.prototype.checkKeyPress = function (evt) {
var key = evt.which || evt.keyCode
var nextItem = document.getElementById(this.activeDescendant)
if (!nextItem) {
return
}
switch (key) {
case aria.KeyCode.PAGE_UP:
case aria.KeyCode.PAGE_DOWN:
if (this.moveUpDownEnabled) {
evt.preventDefault()
if (key === aria.KeyCode.PAGE_UP) {
this.moveUpItems()
} else {
this.moveDownItems()
}
}
break
case aria.KeyCode.UP:
case aria.KeyCode.DOWN:
evt.preventDefault()
if (this.moveUpDownEnabled && evt.altKey) {
if (key === aria.KeyCode.UP) {
this.moveUpItems()
} else {
this.moveDownItems()
}
return
}
if (key === aria.KeyCode.UP) {
nextItem = nextItem.previousElementSibling
} else {
nextItem = nextItem.nextElementSibling
}
if (nextItem) {
this.focusItem(nextItem)
}
break
case aria.KeyCode.HOME:
evt.preventDefault()
this.focusFirstItem()
break
case aria.KeyCode.END:
evt.preventDefault()
this.focusLastItem()
break
case aria.KeyCode.SPACE:
evt.preventDefault()
this.toggleSelectItem(nextItem)
break
case aria.KeyCode.BACKSPACE:
case aria.KeyCode.DELETE:
case aria.KeyCode.RETURN:
if (!this.moveButton) {
return
}
var keyshortcuts = this.moveButton.getAttribute("aria-keyshortcuts")
if (
key === aria.KeyCode.RETURN &&
keyshortcuts.indexOf("Enter") === -1
) {
return
}
if (
(key === aria.KeyCode.BACKSPACE || key === aria.KeyCode.DELETE) &&
keyshortcuts.indexOf("Delete") === -1
) {
return
}
evt.preventDefault()
var nextUnselected = nextItem.nextElementSibling
while (nextUnselected) {
if (nextUnselected.getAttribute("aria-selected") != "true") {
break
}
nextUnselected = nextUnselected.nextElementSibling
}
if (!nextUnselected) {
nextUnselected = nextItem.previousElementSibling
while (nextUnselected) {
if (nextUnselected.getAttribute("aria-selected") != "true") {
break
}
nextUnselected = nextUnselected.previousElementSibling
}
}
this.moveItems()
if (!this.activeDescendant && nextUnselected) {
this.focusItem(nextUnselected)
}
break
default:
var itemToFocus = this.findItemToFocus(key)
if (itemToFocus) {
this.focusItem(itemToFocus)
}
break
}
}
aria.Listbox.prototype.findItemToFocus = function (key) {
var itemList = this.listboxNode.querySelectorAll('[role="option"]')
var character = String.fromCharCode(key)
if (!this.keysSoFar) {
for (var i = 0; i < itemList.length; i++) {
if (itemList[i].getAttribute("id") == this.activeDescendant) {
this.searchIndex = i
}
}
}
this.keysSoFar += character
this.clearKeysSoFarAfterDelay()
var nextMatch = this.findMatchInRange(
itemList,
this.searchIndex + 1,
itemList.length
)
if (!nextMatch) {
nextMatch = this.findMatchInRange(itemList, 0, this.searchIndex)
}
return nextMatch
}
aria.Listbox.prototype.clearKeysSoFarAfterDelay = function () {
if (this.keyClear) {
clearTimeout(this.keyClear)
this.keyClear = null
}
this.keyClear = setTimeout(
function () {
this.keysSoFar = ""
this.keyClear = null
}.bind(this),
500
)
}
aria.Listbox.prototype.findMatchInRange = function (
list,
startIndex,
endIndex
) {
// Find the first item starting with the keysSoFar substring, searching in
// the specified range of items
for (var n = startIndex; n < endIndex; n++) {
var label = list[n].innerText
if (label && label.toUpperCase().indexOf(this.keysSoFar) === 0) {
return list[n]
}
}
return null
}
/**
* @desc
* Check if an item is clicked on. If so, focus on it and select it.
*
* @param evt
* The click event object
*/
aria.Listbox.prototype.checkClickItem = function (evt) {
if (evt.target.getAttribute("role") === "option") {
this.focusItem(evt.target)
this.toggleSelectItem(evt.target)
aria.Utils.addClass(this.listboxNode, "mc-phone-number__list--hidden")
}
}
/**
* @desc
* Toggle the aria-selected value
*
* @param element
* The element to select
*/
aria.Listbox.prototype.toggleSelectItem = function (element) {
if (this.multiselectable) {
element.setAttribute(
"aria-selected",
element.getAttribute("aria-selected") === "true" ? "false" : "true"
)
if (this.moveButton) {
if (this.listboxNode.querySelector('[aria-selected="true"]')) {
this.moveButton.setAttribute("aria-disabled", "false")
} else {
this.moveButton.setAttribute("aria-disabled", "true")
}
}
}
}
/**
* @desc
* Defocus the specified item
*
* @param element
* The element to defocus
*/
aria.Listbox.prototype.defocusItem = function (element) {
if (!element) {
return
}
if (!this.multiselectable) {
element.removeAttribute("aria-selected")
}
aria.Utils.removeClass(element, "mc-phone-number__item--focused")
}
/**
* @desc
* Focus on the specified item
*
* @param element
* The element to focus
*/
aria.Listbox.prototype.focusItem = function (element) {
this.defocusItem(document.getElementById(this.activeDescendant))
if (!this.multiselectable) {
element.setAttribute("aria-selected", "true")
}
aria.Utils.addClass(element, "mc-phone-number__item--focused")
this.listboxNode.setAttribute("aria-activedescendant", element.id)
this.activeDescendant = element.id
if (this.listboxNode.scrollHeight > this.listboxNode.clientHeight) {
var scrollBottom =
this.listboxNode.clientHeight + this.listboxNode.scrollTop
var elementBottom = element.offsetTop + element.offsetHeight
if (elementBottom > scrollBottom) {
this.listboxNode.scrollTop =
elementBottom - this.listboxNode.clientHeight
} else if (element.offsetTop < this.listboxNode.scrollTop) {
this.listboxNode.scrollTop = element.offsetTop
}
}
if (!this.multiselectable && this.moveButton) {
this.moveButton.setAttribute("aria-disabled", false)
}
this.checkUpDownButtons()
this.handleFocusChange(element)
}
/**
* @desc
* Enable/disable the up/down arrows based on the activeDescendant.
*/
aria.Listbox.prototype.checkUpDownButtons = function () {
var activeElement = document.getElementById(this.activeDescendant)
if (!this.moveUpDownEnabled) {
return false
}
if (!activeElement) {
this.upButton.setAttribute("aria-disabled", "true")
this.downButton.setAttribute("aria-disabled", "true")
return
}
if (this.upButton) {
if (activeElement.previousElementSibling) {
this.upButton.setAttribute("aria-disabled", false)
} else {
this.upButton.setAttribute("aria-disabled", "true")
}
}
if (this.downButton) {
if (activeElement.nextElementSibling) {
this.downButton.setAttribute("aria-disabled", false)
} else {
this.downButton.setAttribute("aria-disabled", "true")
}
}
}
/**
* @desc
* Add the specified items to the listbox. Assumes items are valid options.
*
* @param items
* An array of items to add to the listbox
*/
aria.Listbox.prototype.addItems = function (items) {
if (!items || !items.length) {
return false
}
items.forEach(
function (item) {
this.defocusItem(item)
this.toggleSelectItem(item)
this.listboxNode.append(item)
}.bind(this)
)
if (!this.activeDescendant) {
this.focusItem(items[0])
}
this.handleItemChange("added", items)
}
/**
* @desc
* Remove all of the selected items from the listbox; Removes the focused items
* in a single select listbox and the items with aria-selected in a multi
* select listbox.
*
* @returns items
* An array of items that were removed from the listbox
*/
aria.Listbox.prototype.deleteItems = function () {
var itemsToDelete
if (this.multiselectable) {
itemsToDelete = this.listboxNode.querySelectorAll(
'[aria-selected="true"]'
)
} else if (this.activeDescendant) {
itemsToDelete = [document.getElementById(this.activeDescendant)]
}
if (!itemsToDelete || !itemsToDelete.length) {
return []
}
itemsToDelete.forEach(
function (item) {
item.remove()
if (item.id === this.activeDescendant) {
this.clearActiveDescendant()
}
}.bind(this)
)
this.handleItemChange("removed", itemsToDelete)
return itemsToDelete
}
aria.Listbox.prototype.clearActiveDescendant = function () {
this.activeDescendant = null
this.listboxNode.setAttribute("aria-activedescendant", null)
if (this.moveButton) {
this.moveButton.setAttribute("aria-disabled", "true")
}
this.checkUpDownButtons()
}
/**
* @desc
* Shifts the currently focused item up on the list. No shifting occurs if the
* item is already at the top of the list.
*/
aria.Listbox.prototype.moveUpItems = function () {
var previousItem
if (!this.activeDescendant) {
return
}
currentItem = document.getElementById(this.activeDescendant)
previousItem = currentItem.previousElementSibling
if (previousItem) {
this.listboxNode.insertBefore(currentItem, previousItem)
this.handleItemChange("moved_up", [currentItem])
}
this.checkUpDownButtons()
}
/**
* @desc
* Shifts the currently focused item down on the list. No shifting occurs if
* the item is already at the end of the list.
*/
aria.Listbox.prototype.moveDownItems = function () {
var nextItem
if (!this.activeDescendant) {
return
}
currentItem = document.getElementById(this.activeDescendant)
nextItem = currentItem.nextElementSibling
if (nextItem) {
this.listboxNode.insertBefore(nextItem, currentItem)
this.handleItemChange("moved_down", [currentItem])
}
this.checkUpDownButtons()
}
/**
* @desc
* Delete the currently selected items and add them to the sibling list.
*/
aria.Listbox.prototype.moveItems = function () {
if (!this.siblingList) {
return
}
var itemsToMove = this.deleteItems()
this.siblingList.addItems(itemsToMove)
}
/**
* @desc
* Enable Up/Down controls to shift items up and down.
*
* @param upButton
* Up button to trigger up shift
*
* @param downButton
* Down button to trigger down shift
*/
aria.Listbox.prototype.enableMoveUpDown = function (upButton, downButton) {
this.moveUpDownEnabled = true
this.upButton = upButton
this.downButton = downButton
upButton.addEventListener("click", this.moveUpItems.bind(this))
downButton.addEventListener("click", this.moveDownItems.bind(this))
}
/**
* @desc
* Enable Move controls. Moving removes selected items from the current
* list and adds them to the sibling list.
*
* @param button
* Move button to trigger delete
*
* @param siblingList
* Listbox to move items to
*/
aria.Listbox.prototype.setupMove = function (button, siblingList) {
this.siblingList = siblingList
this.moveButton = button
button.addEventListener("click", this.moveItems.bind(this))
}
aria.Listbox.prototype.setHandleItemChange = function (handlerFn) {
this.handleItemChange = handlerFn
}
aria.Listbox.prototype.setHandleFocusChange = function (
focusChangeHandler
) {
this.handleFocusChange = focusChangeHandler
}
/**
* Phone number example
* @function onload
* @desc Initialize the phoneNumber example once the page has loaded
*/
window.addEventListener("load", function () {
var phoneNumberEl = document.getElementById("mc-phone-number")
var button = document.getElementById("dropdown_country")
var exListbox = new aria.Listbox(
document.getElementById("phone_number_list")
)
var listboxButton = new aria.ListboxButton(
button,
exListbox,
phoneNumberEl
)
})
var phoneNumber = aria || {}
phoneNumber.ListboxButton = function (button, listbox, phoneNumberEl) {
this.button = button
this.listbox = listbox
this.phoneNumberEl = phoneNumberEl
this.registerEvents()
}
phoneNumber.ListboxButton.prototype.registerEvents = function () {
this.button.addEventListener("click", this.showListbox.bind(this))
this.button.addEventListener("keyup", this.checkShow.bind(this))
this.listbox.listboxNode.addEventListener(
"blur",
this.hideListbox.bind(this)
)
this.listbox.listboxNode.addEventListener(
"keydown",
this.checkHide.bind(this)
)
this.listbox.setHandleFocusChange(this.onFocusChange.bind(this))
}
phoneNumber.ListboxButton.prototype.checkShow = function (evt) {
var key = evt.which || evt.keyCode
switch (key) {
case phoneNumber.KeyCode.UP:
case phoneNumber.KeyCode.DOWN:
evt.preventDefault()
this.showListbox()
this.listbox.checkKeyPress(evt)
break
}
}
phoneNumber.ListboxButton.prototype.checkHide = function (evt) {
var key = evt.which || evt.keyCode
switch (key) {
case phoneNumber.KeyCode.RETURN:
case phoneNumber.KeyCode.ESC:
evt.preventDefault()
this.hideListbox()
this.button.focus()
break
}
}
phoneNumber.ListboxButton.prototype.showListbox = function () {
phoneNumber.Utils.removeClass(
this.listbox.listboxNode,
"mc-phone-number__list--hidden"
)
this.button.setAttribute("aria-expanded", "true")
phoneNumber.Utils.addClass(this.phoneNumberEl, "mc-phone-number--focused")
this.listbox.listboxNode.focus()
}
phoneNumber.ListboxButton.prototype.hideListbox = function () {
phoneNumber.Utils.addClass(
this.listbox.listboxNode,
"mc-phone-number__list--hidden"
)
phoneNumber.Utils.removeClass(
this.phoneNumberEl,
"mc-phone-number--focused"
)
this.button.removeAttribute("aria-expanded")
}
phoneNumber.ListboxButton.prototype.onFocusChange = function (focusedItem) {
this.button.innerHTML = focusedItem.innerHTML
}
</script>
</html>

Detail of areas

The dropdown contains:

  • The dropdown trigger (mc-phone-number__button)
  • The list of the countries available (mc-phone-number__list)

Each element of list contains:

  • The country flag (mc-phone-number__flag)
  • The country name (mc-phone-number__country)
  • The country indicator (mc-phone-number__country)

Variation

With label

When using a phone number input with a label, you must make sure to import the fields.scss file in addition to the others:

@import "settings-tools/all-settings";
@import "components/_c.text-input";
@import "components/_c.fields";
@import "components/_c.phone-number";
Viewport: px
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Input phone number with label</title>
<script src="https://www.w3.org/TR/wai-aria-practices-1.1/examples/js/utils.js"></script>
</head>
<body>
<div class="example">
<div class="mc-field">
<label class="mc-field__label" for="default"> Label </label>
<div id="mc-phone-number" class="mc-phone-number mc-field__element">
<div class="mc-phone-number__dropdown">
<button
type="button"
aria-haspopup="listbox"
aria-labelledby="dropdown_country"
id="dropdown_country"
class="mc-phone-number__button"
>
<span class="mc-phone-number__flag">
🇫🇷
</span>
<span class="mc-phone_number__indicator">+33</span>
</button>
<ul
id="phone_number_list"
tabindex="-1"
role="listbox"
aria-labelledby="phone_number_list"
class="mc-phone-number__list mc-phone-number__list--hidden"
>
<li id="FR" role="option" class="mc-phone-number__item">
<span class="mc-phone-number__flag">
🇫🇷
</span>
<span class="mc-phone-number__country">France, </span>
<span>+33</span>
</li>
<li id="IT" role="option" class="mc-phone-number__item">
<span class="mc-phone-number__flag">
🇮🇹
</span>
<span class="mc-phone-number__country">Italie, </span>
<span>+39</span>
</li>
<li id="BE" role="option" class="mc-phone-number__item">
<span class="mc-phone-number__flag">
🇧🇪
</span>
<span class="mc-phone-number__country">Belgique, </span>
<span>+32</span>
</li>
<li id="GE" role="option" class="mc-phone-number__item">
<span class="mc-phone-number__flag">
🇩🇪
</span>
<span class="mc-phone-number__country">Allemagne, </span>
<span>+49</span>
</li>
<li id="HO" role="option" class="mc-phone-number__item">
<span class="mc-phone-number__flag">
🇳🇱
</span>
<span class="mc-phone-number__country">Holland, </span>
<span>+31</span>
</li>
<li id="RU" role="option" class="mc-phone-number__item">
<span class="mc-phone-number__flag">
🇷🇺
</span>
<span class="mc-phone-number__country">Russie, </span>
<span>+7</span>
</li>
</ul>
</div>
<input
type="text"
class="mc-phone-number__input mc-text-input mc-text-input--m"
id="smallField"
placeholder="00 00 00 00 00"
name="example-small"
/>
</div>
</div>
</div>
</body>
<script>
/*
* This content is licensed according to the W3C Software License at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
*/
/**
* @namespace aria
*/
var aria = aria || {}
/**
* @constructor
*
* @desc
* Listbox object representing the state and interactions for a listbox widget
*
* @param listboxNode
* The DOM node pointing to the listbox
*/
aria.Listbox = function (listboxNode) {
this.listboxNode = listboxNode
this.activeDescendant = this.listboxNode.getAttribute(
"aria-activedescendant"
)
this.multiselectable = this.listboxNode.hasAttribute(
"aria-multiselectable"
)
this.moveUpDownEnabled = false
this.siblingList = null
this.upButton = null
this.downButton = null
this.moveButton = null
this.keysSoFar = ""
this.handleFocusChange = function () {}
this.handleItemChange = function (event, items) {}
this.registerEvents()
}
/**
* @desc
* Register events for the listbox interactions
*/
aria.Listbox.prototype.registerEvents = function () {
this.listboxNode.addEventListener("focus", this.setupFocus.bind(this))
this.listboxNode.addEventListener(
"keydown",
this.checkKeyPress.bind(this)
)
this.listboxNode.addEventListener("click", this.checkClickItem.bind(this))
}
/**
* @desc
* If there is no activeDescendant, focus on the first option
*/
aria.Listbox.prototype.setupFocus = function () {
if (this.activeDescendant) {
return
}
this.focusFirstItem()
}
/**
* @desc
* Focus on the first option
*/
aria.Listbox.prototype.focusFirstItem = function () {
var firstItem
firstItem = this.listboxNode.querySelector('[role="option"]')
if (firstItem) {
this.focusItem(firstItem)
}
}
/**
* @desc
* Focus on the last option
*/
aria.Listbox.prototype.focusLastItem = function () {
var itemList = this.listboxNode.querySelectorAll('[role="option"]')
if (itemList.length) {
this.focusItem(itemList[itemList.length - 1])
}
}
/**
* @desc
* Handle various keyboard controls; UP/DOWN will shift focus; SPACE selects
* an item.
*
* @param evt
* The keydown event object
*/
aria.Listbox.prototype.checkKeyPress = function (evt) {
var key = evt.which || evt.keyCode
var nextItem = document.getElementById(this.activeDescendant)
if (!nextItem) {
return
}
switch (key) {
case aria.KeyCode.PAGE_UP:
case aria.KeyCode.PAGE_DOWN:
if (this.moveUpDownEnabled) {
evt.preventDefault()
if (key === aria.KeyCode.PAGE_UP) {
this.moveUpItems()
} else {
this.moveDownItems()
}
}
break
case aria.KeyCode.UP:
case aria.KeyCode.DOWN:
evt.preventDefault()
if (this.moveUpDownEnabled && evt.altKey) {
if (key === aria.KeyCode.UP) {
this.moveUpItems()
} else {
this.moveDownItems()
}
return
}
if (key === aria.KeyCode.UP) {
nextItem = nextItem.previousElementSibling
} else {
nextItem = nextItem.nextElementSibling
}
if (nextItem) {
this.focusItem(nextItem)
}
break
case aria.KeyCode.HOME:
evt.preventDefault()
this.focusFirstItem()
break
case aria.KeyCode.END:
evt.preventDefault()
this.focusLastItem()
break
case aria.KeyCode.SPACE:
evt.preventDefault()
this.toggleSelectItem(nextItem)
break
case aria.KeyCode.BACKSPACE:
case aria.KeyCode.DELETE:
case aria.KeyCode.RETURN:
if (!this.moveButton) {
return
}
var keyshortcuts = this.moveButton.getAttribute("aria-keyshortcuts")
if (
key === aria.KeyCode.RETURN &&
keyshortcuts.indexOf("Enter") === -1
) {
return
}
if (
(key === aria.KeyCode.BACKSPACE || key === aria.KeyCode.DELETE) &&
keyshortcuts.indexOf("Delete") === -1
) {
return
}
evt.preventDefault()
var nextUnselected = nextItem.nextElementSibling
while (nextUnselected) {
if (nextUnselected.getAttribute("aria-selected") != "true") {
break
}
nextUnselected = nextUnselected.nextElementSibling
}
if (!nextUnselected) {
nextUnselected = nextItem.previousElementSibling
while (nextUnselected) {
if (nextUnselected.getAttribute("aria-selected") != "true") {
break
}
nextUnselected = nextUnselected.previousElementSibling
}
}
this.moveItems()
if (!this.activeDescendant && nextUnselected) {
this.focusItem(nextUnselected)
}
break
default:
var itemToFocus = this.findItemToFocus(key)
if (itemToFocus) {
this.focusItem(itemToFocus)
}
break
}
}
aria.Listbox.prototype.findItemToFocus = function (key) {
var itemList = this.listboxNode.querySelectorAll('[role="option"]')
var character = String.fromCharCode(key)
if (!this.keysSoFar) {
for (var i = 0; i < itemList.length; i++) {
if (itemList[i].getAttribute("id") == this.activeDescendant) {
this.searchIndex = i
}
}
}
this.keysSoFar += character
this.clearKeysSoFarAfterDelay()
var nextMatch = this.findMatchInRange(
itemList,
this.searchIndex + 1,
itemList.length
)
if (!nextMatch) {
nextMatch = this.findMatchInRange(itemList, 0, this.searchIndex)
}
return nextMatch
}
aria.Listbox.prototype.clearKeysSoFarAfterDelay = function () {
if (this.keyClear) {
clearTimeout(this.keyClear)
this.keyClear = null
}
this.keyClear = setTimeout(
function () {
this.keysSoFar = ""
this.keyClear = null
}.bind(this),
500
)
}
aria.Listbox.prototype.findMatchInRange = function (
list,
startIndex,
endIndex
) {
// Find the first item starting with the keysSoFar substring, searching in
// the specified range of items
for (var n = startIndex; n < endIndex; n++) {
var label = list[n].innerText
if (label && label.toUpperCase().indexOf(this.keysSoFar) === 0) {
return list[n]
}
}
return null
}
/**
* @desc
* Check if an item is clicked on. If so, focus on it and select it.
*
* @param evt
* The click event object
*/
aria.Listbox.prototype.checkClickItem = function (evt) {
if (evt.target.getAttribute("role") === "option") {
this.focusItem(evt.target)
this.toggleSelectItem(evt.target)
aria.Utils.addClass(this.listboxNode, "mc-phone-number__list--hidden")
}
}
/**
* @desc
* Toggle the aria-selected value
*
* @param element
* The element to select
*/
aria.Listbox.prototype.toggleSelectItem = function (element) {
if (this.multiselectable) {
element.setAttribute(
"aria-selected",
element.getAttribute("aria-selected") === "true" ? "false" : "true"
)
if (this.moveButton) {
if (this.listboxNode.querySelector('[aria-selected="true"]')) {
this.moveButton.setAttribute("aria-disabled", "false")
} else {
this.moveButton.setAttribute("aria-disabled", "true")
}
}
}
}
/**
* @desc
* Defocus the specified item
*
* @param element
* The element to defocus
*/
aria.Listbox.prototype.defocusItem = function (element) {
if (!element) {
return
}
if (!this.multiselectable) {
element.removeAttribute("aria-selected")
}
aria.Utils.removeClass(element, "mc-phone-number__item--focused")
}
/**
* @desc
* Focus on the specified item
*
* @param element
* The element to focus
*/
aria.Listbox.prototype.focusItem = function (element) {
this.defocusItem(document.getElementById(this.activeDescendant))
if (!this.multiselectable) {
element.setAttribute("aria-selected", "true")
}
aria.Utils.addClass(element, "mc-phone-number__item--focused")
this.listboxNode.setAttribute("aria-activedescendant", element.id)
this.activeDescendant = element.id
if (this.listboxNode.scrollHeight > this.listboxNode.clientHeight) {
var scrollBottom =
this.listboxNode.clientHeight + this.listboxNode.scrollTop
var elementBottom = element.offsetTop + element.offsetHeight
if (elementBottom > scrollBottom) {
this.listboxNode.scrollTop =
elementBottom - this.listboxNode.clientHeight
} else if (element.offsetTop < this.listboxNode.scrollTop) {
this.listboxNode.scrollTop = element.offsetTop
}
}
if (!this.multiselectable && this.moveButton) {
this.moveButton.setAttribute("aria-disabled", false)
}
this.checkUpDownButtons()
this.handleFocusChange(element)
}
/**
* @desc
* Enable/disable the up/down arrows based on the activeDescendant.
*/
aria.Listbox.prototype.checkUpDownButtons = function () {
var activeElement = document.getElementById(this.activeDescendant)
if (!this.moveUpDownEnabled) {
return false
}
if (!activeElement) {
this.upButton.setAttribute("aria-disabled", "true")
this.downButton.setAttribute("aria-disabled", "true")
return
}
if (this.upButton) {
if (activeElement.previousElementSibling) {
this.upButton.setAttribute("aria-disabled", false)
} else {
this.upButton.setAttribute("aria-disabled", "true")
}
}
if (this.downButton) {
if (activeElement.nextElementSibling) {
this.downButton.setAttribute("aria-disabled", false)
} else {
this.downButton.setAttribute("aria-disabled", "true")
}
}
}
/**
* @desc
* Add the specified items to the listbox. Assumes items are valid options.
*
* @param items
* An array of items to add to the listbox
*/
aria.Listbox.prototype.addItems = function (items) {
if (!items || !items.length) {
return false
}
items.forEach(
function (item) {
this.defocusItem(item)
this.toggleSelectItem(item)
this.listboxNode.append(item)
}.bind(this)
)
if (!this.activeDescendant) {
this.focusItem(items[0])
}
this.handleItemChange("added", items)
}
/**
* @desc
* Remove all of the selected items from the listbox; Removes the focused items
* in a single select listbox and the items with aria-selected in a multi
* select listbox.
*
* @returns items
* An array of items that were removed from the listbox
*/
aria.Listbox.prototype.deleteItems = function () {
var itemsToDelete
if (this.multiselectable) {
itemsToDelete = this.listboxNode.querySelectorAll(
'[aria-selected="true"]'
)
} else if (this.activeDescendant) {
itemsToDelete = [document.getElementById(this.activeDescendant)]
}
if (!itemsToDelete || !itemsToDelete.length) {
return []
}
itemsToDelete.forEach(
function (item) {
item.remove()
if (item.id === this.activeDescendant) {
this.clearActiveDescendant()
}
}.bind(this)
)
this.handleItemChange("removed", itemsToDelete)
return itemsToDelete
}
aria.Listbox.prototype.clearActiveDescendant = function () {
this.activeDescendant = null
this.listboxNode.setAttribute("aria-activedescendant", null)
if (this.moveButton) {
this.moveButton.setAttribute("aria-disabled", "true")
}
this.checkUpDownButtons()
}
/**
* @desc
* Shifts the currently focused item up on the list. No shifting occurs if the
* item is already at the top of the list.
*/
aria.Listbox.prototype.moveUpItems = function () {
var previousItem
if (!this.activeDescendant) {
return
}
currentItem = document.getElementById(this.activeDescendant)
previousItem = currentItem.previousElementSibling
if (previousItem) {
this.listboxNode.insertBefore(currentItem, previousItem)
this.handleItemChange("moved_up", [currentItem])
}
this.checkUpDownButtons()
}
/**
* @desc
* Shifts the currently focused item down on the list. No shifting occurs if
* the item is already at the end of the list.
*/
aria.Listbox.prototype.moveDownItems = function () {
var nextItem
if (!this.activeDescendant) {
return
}
currentItem = document.getElementById(this.activeDescendant)
nextItem = currentItem.nextElementSibling
if (nextItem) {
this.listboxNode.insertBefore(nextItem, currentItem)
this.handleItemChange("moved_down", [currentItem])
}
this.checkUpDownButtons()
}
/**
* @desc
* Delete the currently selected items and add them to the sibling list.
*/
aria.Listbox.prototype.moveItems = function () {
if (!this.siblingList) {
return
}
var itemsToMove = this.deleteItems()
this.siblingList.addItems(itemsToMove)
}
/**
* @desc
* Enable Up/Down controls to shift items up and down.
*
* @param upButton
* Up button to trigger up shift
*
* @param downButton
* Down button to trigger down shift
*/
aria.Listbox.prototype.enableMoveUpDown = function (upButton, downButton) {
this.moveUpDownEnabled = true
this.upButton = upButton
this.downButton = downButton
upButton.addEventListener("click", this.moveUpItems.bind(this))
downButton.addEventListener("click", this.moveDownItems.bind(this))
}
/**
* @desc
* Enable Move controls. Moving removes selected items from the current
* list and adds them to the sibling list.
*
* @param button
* Move button to trigger delete
*
* @param siblingList
* Listbox to move items to
*/
aria.Listbox.prototype.setupMove = function (button, siblingList) {
this.siblingList = siblingList
this.moveButton = button
button.addEventListener("click", this.moveItems.bind(this))
}
aria.Listbox.prototype.setHandleItemChange = function (handlerFn) {
this.handleItemChange = handlerFn
}
aria.Listbox.prototype.setHandleFocusChange = function (
focusChangeHandler
) {
this.handleFocusChange = focusChangeHandler
}
/**
* Phone number example
* @function onload
* @desc Initialize the phoneNumber example once the page has loaded
*/
window.addEventListener("load", function () {
var phoneNumberEl = document.getElementById("mc-phone-number")
var button = document.getElementById("dropdown_country")
var exListbox = new aria.Listbox(
document.getElementById("phone_number_list")
)
var listboxButton = new aria.ListboxButton(
button,
exListbox,
phoneNumberEl
)
})
var phoneNumber = aria || {}
phoneNumber.ListboxButton = function (button, listbox, phoneNumberEl) {
this.button = button
this.listbox = listbox
this.phoneNumberEl = phoneNumberEl
this.registerEvents()
}
phoneNumber.ListboxButton.prototype.registerEvents = function () {
this.button.addEventListener("click", this.showListbox.bind(this))
this.button.addEventListener("keyup", this.checkShow.bind(this))
this.listbox.listboxNode.addEventListener(
"blur",
this.hideListbox.bind(this)
)
this.listbox.listboxNode.addEventListener(
"keydown",
this.checkHide.bind(this)
)
this.listbox.setHandleFocusChange(this.onFocusChange.bind(this))
}
phoneNumber.ListboxButton.prototype.checkShow = function (evt) {
var key = evt.which || evt.keyCode
switch (key) {
case phoneNumber.KeyCode.UP:
case phoneNumber.KeyCode.DOWN:
evt.preventDefault()
this.showListbox()
this.listbox.checkKeyPress(evt)
break
}
}
phoneNumber.ListboxButton.prototype.checkHide = function (evt) {
var key = evt.which || evt.keyCode
switch (key) {
case phoneNumber.KeyCode.RETURN:
case phoneNumber.KeyCode.ESC:
evt.preventDefault()
this.hideListbox()
this.button.focus()
break
}
}
phoneNumber.ListboxButton.prototype.showListbox = function () {
phoneNumber.Utils.removeClass(
this.listbox.listboxNode,
"mc-phone-number__list--hidden"
)
this.button.setAttribute("aria-expanded", "true")
phoneNumber.Utils.addClass(this.phoneNumberEl, "mc-phone-number--focused")
this.listbox.listboxNode.focus()
}
phoneNumber.ListboxButton.prototype.hideListbox = function () {
phoneNumber.Utils.addClass(
this.listbox.listboxNode,
"mc-phone-number__list--hidden"
)
phoneNumber.Utils.removeClass(
this.phoneNumberEl,
"mc-phone-number--focused"
)
this.button.removeAttribute("aria-expanded")
}
phoneNumber.ListboxButton.prototype.onFocusChange = function (focusedItem) {
this.button.innerHTML = focusedItem.innerHTML
}
</script>
</html>

Javascript behaviour

Useful tip
The javascript to manage the dropdown is based on the listbox from W3C

To have a fully fonctionnal phone number input, make sure to add the following script to your code.

listbox.js
utils.js

On the dropdown opening add the mc-phone-number--focused class and remove the mc-phone-number__list--hidden class.