Forms - Multiselect Box
Multiselect box is a widget used to select more than one option from the list of given choices.
NOTE:
- New to accessibility or uncertain of requirements, it will be helpful to review all sections below.
- Already familiar with requirements, skip to the “Working Example” section for sample HTML, CSS and JavaScript (when needed), along with a working demo.
- A multiselect box CAN be identified visually by a downward/upward pointing arrow icon provided within the field border.
-
The arrow icon SHOULD NOT be defined through CSS
background-image
property. - A checkmark icon SHOULD be provided to visually identify the selected options.
- Color contrast requirements MUST be met for the visual label or instructions if any as mentioned in the “Label placement and structure” component.
- The color contrast requirement of 3:1 ratio MUST be met with the adjacent color for downward/upward pointing arrow icon.
- The contrast requirement of 3:1 ratio SHOULD be met with the adjacent colors for custom borders.
- The contrast requirement of 3:1 ratio MUST be met with the adjacent colors for the checkmark that indicates the selected state of an option.
- The contrast requirement of 3:1 ratio MUST be met with the adjacent colors for the custom focus indicator of the multiselect box.
-
The color contrast ratio requirement of 4.5:1 MUST be met considering standard text
size of option and current value of the combobox against the background color in default and selected
state.
- In case of custom focus indicator wherein the background color of the option text is changed, then contrast requirement of 4.5:1 ratio MUST be met considering standard text size of option against the background color.
- Avoid using color only to convey the selection of an option.
Multiselect box CAN be implemented using following approaches:
The multiselect box CAN be implemented using native <select>
and
<option>
element along with multiple attribute. But there are some drawbacks to it as it
has accessibility issues.
- The selected options are denoted by color alone.
- The screen reader users can select multiple options using Shift+Arrow keys but there is no update regarding how many options have been selected.
- If the user presses arrow keys mistakenly after selecting some options, then the selection is removed.
For these reasons, use the ARIA approach.
-
The multiselect box MUST be defined using
role="listbox"
on the<ul>
container. - A descriptive visual label MUST be provided for the multiselect box. See “Label placement and structure” component for more information on labelling.
- In case of any additional instructions to be provided for single select box see “Label placement and structure” component for more information.
- For more information on additional requirements for form fields, see “Input Structure” component.
-
The
aria-activedescendant
attribute MUST be provided to the<ul>
element containing therole="listbox"
. It refers to that option in the listbox that currently has visual focus. It helps assistive technologies know which element has application focus when the DOM focus is on the input field. For more information, refer to Managing Focus in Composites Using aria-activedescendant . -
The
<ul>
element havingrole="listbox"
MUST be providedtabindex="0"
attribute to bring the listbox in focus. - Appropriate JavaScript event handlers MUST be used to make the combobox accessible by keyboard and mouse.
- Appropriate JavaScript event handlers SHOULD be used to make the listbox accessible by keyboard and mouse.
-
Each of the individual
<li>
element containing the options SHOULD be marked withrole="option"
. -
The
aria-selected="true"
SHOULD be used when an option is visually selected in the combobox. -
The
aria-multiselectable="true"
attribute SHOULD be used for the<ul>
element withrole="listbox"
. - Roving tabindex mechanism SHOULD be used for traversing between different options. For more information, refer to Managing Focus Within Components Using a Roving tabindex .
When the focus is on the multiselect box, focus SHOULD be managed as follows:
- Down arrow key – moves visual focus to the next option. In case of last option, focus remains on the last option.
- Up arrow key – moves visual focus to the previous option. In case of first option, focus remains on the first option.
- Space/ Enter keys – selects the option that has visual focus currently.
- Shift + Up/Shift+ Down arrow keys (optional) – mechanism to select multiple options at one go CAN be provided.
For example,
<!-- Visual label -->
<span class="listbox-label" id="listbox_label">Choose your courses:</span>
<!-- Code for the multiselect box -->
<ul id="list-items" tabindex="0" role="listbox" aria-multiselectable="true" aria-labelledby="listbox_label" aria-activedescendant="one">
<li id="one" role="option" aria-selected="false">General Chemistry> /li>
<li id="two" role="option" aria-selected="false">Physics> /li>
<li id="three" role="option" aria-selected="false">General Biology> /li>
<li id="four" role="option" aria-selected="false">Financial Accounting> /li>
<li id="five" role="option" aria-selected="true">Psychology> /li>
...
</ul>
-
The editable multiselect box MUST be defined using
role="combobox"
on the<input>
element. - A descriptive visual label MUST be defined for the multiselect box. See “Label placement and structure” component for more information on labelling.
- In case of any additional instructions to be provided for single select box see “Label placement and structure” component for more information.
- For more information on additional requirements for form fields, see “Input Structure” component.
-
The
aria-autocomplete="none"
attribute MUST be provided to the<input>
element containing therole="combobox"
. -
The
aria-expanded
attribute MUST be provided for the combobox to define its state. The default value of this attribute SHOULD be set to false as the combobox is in collapsed state. The value SHOULD be updated to true when the combobox is in expanded state. -
The
aria-controls
attribute MUST be provided to the<input>
element containing therole="combobox"
. The value of id attribute SHOULD be referenced with the parent neutral container such as<div>
element containing the options. -
The
aria-activedescendant
attribute MUST be provided to the<input>
element containing therole="combobox"
. It refers to that option in the listbox that currently has visual focus. It helps assistive technologies know which element has application focus when the DOM focus is on the input field. For more information, refer to Managing Focus in Composites Using aria-activedescendant . -
For users who use speech recognition software for performing tasks or screen reader on their
mobile to perform the tasks, a native HTML
<button>
element SHOULD be provided adjacent to the<input>
element that hasrole="combobox"
.- This button helps to expand/collapse the options available within the listbox.
-
The button SHOULD be provided with
tabindex="-1"
such that the button DOES NOT receive keyboard focus and is available ONLY for users who use a screen reader or speech recognition software.
Note: This button also helps mobile users to just perform a Tap on the button to expand/collapse the available options in the listbox. -
The
aria-controls
attribute MUST be provided to the<button>
element. The value of id attribute SHOULD be referenced with the parent neutral container such as<div>
element containing the options. - The focus SHOULD remain on the button itself after it is activated.
-
The
aria-expanded
attribute MUST be provided for the<button>
element to define its state. The default value of this attribute SHOULD be set to false as the button is in collapsed state. The value SHOULD be updated to true when the button is in expanded state.
-
When a user selects any option, the selected option SHOULD appear visually within the
input field.
-
The selected options that appears within the input field SHOULD be defined
using native HTML
<button>
element. -
This button can be used to remove the respective selected options.
Note: Alternate way to remove the selected option will be by de-selecting the option from the listbox that appears for the combobox. - Visually an “X” icon SHOULD be there as a part of the button that indicates it will remove the respective option.
- The “X” icon SHOULD meet the color contrast ratio requirement of 3:1 with the background color in different states.
- The button text SHOULD meet the color contrast ratio requirement of 4.5:1 with the background color in different states.
-
These buttons SHOULD be structured using list markup i.e.,
<ul>
and<li>
elements. -
These buttons SHOULD be placed before the
<input>
element that hasrole="combobox"
in DOM order.
-
The selected options that appears within the input field SHOULD be defined
using native HTML
-
Descriptive label MUST be provided for these buttons using
aria-label
attribute that helps users to understand that it will remove the respective option. - CSS MUST be used for maintaining the visual presentation of the editable multiselect box when multiple selected options are to be shown within the input field.
- Roving tabindex mechanism CAN be used for traversing between selected options in the input field. For more information, refer to Managing Focus Within Components Using a Roving tabindex .
When the focus is on the input field, focus SHOULD be managed as follows:
- Down arrow key – opens the listbox and moves the visual focus moves to the first option.
- Down arrow key after typing a string in the input field that matches with the option – moves the visual focus to the first option.
- Alt+Down arrow key – opens the listbox but the focus remains on the input field.
- Up arrow key – opens the listbox and moves the visual focus to the last option.
- Up arrow key after typing a string in the input field – moves the visual focus to the last suggested option.
- Enter/Space keys – selects an option.
- Escape key – Closes the listbox if it’s displayed and focus is moved back to the combobox.
-
The neutral container such as
<div>
element containing the list of options MUST be provided withrole="listbox"
. - Appropriate JavaScript event handlers SHOULD be used to make the listbox accessible by keyboard and mouse.
-
Each of the individual neutral containers such as
<div>
element containing the options SHOULD be marked withrole="option"
. -
The
aria-selected="true"
SHOULD be used when an option is visually selected in the combobox. -
The
aria-multiselectable="true"
attribute SHOULD be used for the neutral container such as<div>
element withrole="listbox"
.
When the focus is on the listbox, focus SHOULD be managed as follows:
- Enter key – when the focus is on any option in the listbox, selects the option.
- The listbox SHOULD NOT collapse after selecting an option.
- Escape key – closes the listbox and moves visual focus on the combobox.
- Down arrow key – moves visual focus to the next option. In case of last option, focus remains on the last option.
- Up arrow key – moves visual focus to the previous option. In case of first option, focus remains on the first option.
- Right/Left arrow keys – In case of editable multiselect box, moves visual focus to the input field and the cursor in the input field by one character to the right/left.
- Tab key – closes the listbox and moves the focus to the next focusable element on the page.
- Escape key – Closes the listbox if it’s displayed and focus moves back to the combobox.
For example,
<!-- Visual label -->
<!--suppress XmlDuplicatedId, XmlDuplicatedId -->lDuplicatedId -->lDuplicatedId -->
<label for="divinput">Select Multiple Programming Languages</label>
<!-- Code for editable multiselect box before selecting any option -->
<div class="select-btn">
<ul id="selected-options"></ul>
<input type="text" role="combobox" aria-autocomplete="list" aria-controls="languageList" aria-expanded="false" class="divinput" id="divinput">
<!-- Button for users who use speech recognition software for performing tasks or screen reader on their mobile to perform the tasks -->
<button class="arrow-dwn" type="button" id="arr-button" aria-label="Programming Language" aria-expanded="false" aria-controls="lsanguageList" tabindex="-1"></button>
<em aria-hidden="true" class="fa-solid fa-chevron-down arrow-dwn" ></em>
</div>
<!-- Code for editable multiselect box after selecting 2 options -->
<div class="select-btn">
<ul id="selected-options">
<li class="selected-option">
<button class="btn" type="button" id="divinput-remove-0" aria-label="remove HTML & CSS">HTML & CSS </button>
</li>
<li class="selected-option">
<button class="btn" type="button" id="divinput-remove-1" aria-label="remove Bootstrap">Bootstrap </button>
</li>
</ul>
<input type="text" role="combobox" aria-autocomplete="list" aria-controls="languageList" aria-expanded="false" class="divinput" id="divinput" aria-activedescendant="divinput-1">
...
</div>
<!-- Defining the listbox -->
<ul class="list-items" id="languageList" role="listbox" aria-multiselectable="true">
<li role="option" id="divinput-0" class="item option-current" aria-selected="false">
HTML & CSS
</li>
<li role="option" id="divinput-1" class="item" aria-selected="false">
Bootstrap
</li>
<li role="option" id="divinput-2" class="item" aria-selected="false">
Javascript
</li>
<li role="option" id="divinput-3" class="item" aria-selected="false">
Node JS
</li>
<li role="option" id="divinput-4" class="item" aria-selected="false">
React Js
</li>
<li role="option" id="divinput-5" class="item" aria-selected="false">
Mongo DB
</li>
</ul>
A well-defined multiselect box benefits majorly the below users.
- People with cognitive disabilities
- People using speech input
- People with limited dexterity
- People using keyboard only
- People using screen readers
<div class="listbox-area">
<div>
<span class="listbox-label" id="listbox_label">Choose your courses:</span>
<ul id="list-items" tabindex="0" role="listbox" aria-multiselectable="true" aria-labelledby="listbox_label">
<li id="one" role="option" aria-selected="false">General Chemistry</li>
<li id="two" role="option" aria-selected="false">Physics</li>
<li id="three" role="option" aria-selected="false">General Biology</li>
<li id="four" role="option" aria-selected="false">Financial Accounting</li>
<li id="five" role="option" aria-selected="false">Psychology</li>
<li id="six" role="option" aria-selected="false">Microeconomics</li>
<li id="seven" role="option" aria-selected="false">Anatomy & Physiology</li>
<li id="eight" role="option" aria-selected="false">College Algebra</li>
<li id="nine" role="option" aria-selected="false">Organic Chemistry</li>
<li id="ten" role="option" aria-selected="false">Genetics</li>
<li id="eleven" role="option" aria-selected="false">Trigonometry</li>
</ul>
</div>
</div>
.listbox-area {
display: grid;
grid-gap: 2em;
grid-template-columns: repeat(2, 1fr);
padding: 20px;
}
[role="listbox"] {
margin: 1em 0 0;
padding: 0;
min-height: 18em;
border: 1px solid #aaa;
background: white;
}
[role="listbox"]#list-items {
position: relative;
overflow-y: auto;
}
[role="listbox"] + *, .listbox-label + * {
margin-top: 1em;
}
[role="option"] {
position: relative;
display: block;
padding: 0 1em 0 1.5em;
line-height: 1.8em;
font-size: 1.1rem;
}
[role="option"].focused {
background: #bde4ff;
}
[role="option"][aria-selected="true"]::before {
position: absolute;
left: 0.5em;
content: "✓";
}
.option-current{
background-color: #007a9c;
color: #fff;
}
let listitems = document.querySelector('#list-items[role="listbox"]');
let optionList = document.querySelectorAll('#list-items li[role="option"]');
let allOptions = Array.prototype.slice.call(document.querySelectorAll('[role="option"]'));
items = document.querySelectorAll(".item");
let activeIndex = -1;
listitems.addEventListener('keydown', (e) => {
switch (e.key) {
case 'ArrowUp':
e.preventDefault();
if (activeIndex > 0) {
activeIndex--;
setcurrentindex(activeIndex);
}
break;
case 'ArrowDown':
e.preventDefault();
if (activeIndex < optionList.length - 1) {
activeIndex++;
setcurrentindex(activeIndex);
}
break;
case 'Enter':
e.preventDefault();
optionList[activeIndex].setAttribute('aria-selected', (optionList[activeIndex].getAttribute('aria-selected') === 'true') ? 'false' : 'true');
if(optionList[activeIndex].classList.contains('option-selected')) {
optionList[activeIndex].classList.remove('option-selected');
} else if(!optionList[activeIndex].classList.contains('option-selected')) {
optionList[activeIndex].classList.add('option-selected');
}
countselected();
break;
case ' ':
e.preventDefault();
optionList[activeIndex].setAttribute('aria-selected', (optionList[activeIndex].getAttribute('aria-selected') === 'true') ? 'false' : 'true');
if(optionList[activeIndex].classList.contains('option-selected') && selectBtn.classList.contains('open')) {
optionList[activeIndex].classList.remove('option-selected');
} else if(!optionList[activeIndex].classList.contains('option-selected') && selectBtn.classList.contains('open')) {
optionList[activeIndex].classList.add('option-selected');
}
countselected();
break;
case 'Home':
e.preventDefault();
activeIndex = allOptions.indexOf(optionList[0]);
setcurrentindex(activeIndex);
break;
case 'End':
e.preventDefault();
let endOption = optionList.length - 1;
activeIndex = allOptions.indexOf(optionList[endOption]);
setcurrentindex(activeIndex);
}
if (activeIndex !== -1) {
optionList[activeIndex].focus();
}
});
function setcurrentindex(activeIndex){
optionList.forEach(p => p.classList.remove('option-current'));
optionList[activeIndex].classList.add('option-current');
optionList[activeIndex].focus();
listitems.setAttribute('aria-activedescendant', optionList[activeIndex].id);
return optionList[activeIndex];
}
optionList.forEach(item => {
item.addEventListener("click", (e) => {
// inputbtn.focus();
item.classList.toggle("option-selected");
let selected = item.classList.contains("option-selected");
if(selected) {
item.setAttribute('aria-selected',true);
} else {
item.setAttribute('aria-selected',false);
}
let clickedItem = e.target;
let itemsArray = Array.from(allOptions);
let index = itemsArray.indexOf(clickedItem);
setcurrentindex(index);
});
});
- General Chemistry
- Physics
- General Biology
- Financial Accounting
- Psychology
- Microeconomics
- Anatomy & Physiology
- College Algebra
- Organic Chemistry
- Genetics
- Trigonometry
<head>
<title>Combobox HTML Code</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css">
</head>
<body>
<div class="container inline-button">
<h1>Multi Select Box</h1>
<label for="divinput">Select Multiple Programming Languages</label>
<div class="select-btn">
<ul id="selected-options"></ul>
<input type="text" role="combobox" aria-autocomplete="list" aria-controls="languageList" aria-expanded="false" class="divinput" id="divinput">
<button class="arrow-dwn" type="button" id="arr-button" aria-label="Programming Language" aria-expanded="false" aria-controls="languageList" tabindex="-1"></button>
<i class="fa-solid fa-chevron-down arrow-dwn" aria-hidden="true"></i>
</div>
<ul class="list-items" id="languageList" role="listbox" aria-multiselectable="true"></ul>
</div>
</body>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Poppins', sans-serif;
}
body {
background-color: #fff;
}
.container{
position: relative;
max-width: 320px;
width: 100%;
margin: 80px auto 30px;
}
h1 {
margin:2rem
}
label {
margin: 8px;
}
.select-btn {
margin: 3px;
border-radius: 8px;
cursor: pointer;
background-color: #ffffff;
border: 1px solid #02485d;
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
padding: 5px;
position: relative;
}
.select-btn:focus {
border-color: #0067b8 !important;
box-shadow: 0 0 4px 2px #0067b8 !important;
outline: 5px solid transparent !important;
}
.select-btn .btn-text {
font-size: 17px;
font-weight: 400;
color: #333;
}
.select-btn .arrow-dwn {
height: 21px;
width: 21px;
color: #000;
font-size: 14px;
border-radius: 50%;
background: #ffffff00;
transition: 0.3s;
right: 0;
margin-right: 12px;
position: absolute;
top: 20px;
border: none;
}
.select-btn.open .arrow-dwn {
transform: rotate(-180deg);
}
.list-items {
position: relative;
margin-top: 2px;
border-radius: 8px;
padding: 16px;
background-color: #fff;
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
display: none;
}
.select-btn.open ~ .list-items {
display: block;
}
.list-items .item {
display: flex;
align-items: center;
list-style: none;
height: 50px;
transition: 0.3s;
padding: 0 15px;
border-radius: 8px;
}
.list-items .item:hover {
background-color: #007a9c63;
}
.item .item-text {
font-size: 16px;
font-weight: 400;
color: #333;
}
.item .checkbox {
display: flex;
align-items: center;
justify-content: center;
height: 16px;
width: 16px;
border-radius: 4px;
margin-right: 12px;
border: 1.5px solid #c0c0c0;
transition: all 0.3s ease-in-out;
}
.item.checked .checkbox {
background-color: #4070f4;
border-color: #4070f4;
}
.checkbox .check-icon {
color: #fff;
font-size: 11px;
transform: scale(0);
transition: all 0.2s ease-in-out;
}
.item.checked .check-icon {
transform: scale(1);
}
:focus{
border-color: #0067b8;
outline: 2px solid #333;
}
.select-btn:focus-within {
border-color: #0067b8;
box-shadow: 0 0 4px 2px #0067b8;
outline: 5px solid transparent;
}
.btn {
background-color: #6200ee;
border: 1px solid #6200ee;
border-radius: 3px;
color: #fff;
font-size: 0.75em;
font-weight: bold;
margin-bottom: 6px;
margin-right: 6px;
padding: 0.25em 1.75em 0.25em 0.25em;
position: relative;
}
.btn::before, .btn::after {
border-right: 2px solid #fff;
content: "";
height: 1em;
right: 0.75em;
position: absolute;
top: 50%;
width: 0;
}
.btn::after {
transform: translate(0, -50%) rotate(-45deg);
}
.btn::before {
transform: translate(0, -50%) rotate(45deg);
}
.divinput {
height: 46px;
width: 100%;
background-color: #ffffff;
border: none;
border-radius: 8px;
padding: 6px;
font-size: 16px;
position: relative;
}
input.divinput:focus {
outline: 0 !important;
border-color: transparent !important;
box-shadow: none !important
}
.item.option-selected {
padding-right: 30px;
position: relative;
}
.item.option-selected.option-current::after {
border-bottom: 2px solid #fff;
border-right: 2px solid #fff;
}
.item.option-selected::after {
border-bottom: 2px solid #000;
border-right: 2px solid #000;
content: '';
height: 16px;
position: absolute;
right: 15px;
top: 45%;
transform: translate(0, -50%) rotate(45deg);
width: 8px;
}
.option-current {
background-color: #007a9c !important;
color: #fff;
}
.selected-option {
list-style: none;
}
ul#selected-options {
display: flex;
flex-wrap: wrap;
}
.fa-chevron-down:before {
position: absolute;
top: 5px;
right: 4px;
}
#arr-button {
z-index: 10000!important;
}
.btn:focus {
outline: 2px solid #000;
outline-offset: 1px;
}
const options = ['HTML & CSS', 'Bootstrap', 'Javascript', 'Node JS', 'React Js', 'Mongo DB'];
const Keys = {
Backspace: 'Backspace',
Clear: 'Clear',
Down: 'ArrowDown',
End: 'End',
Enter: 'Enter',
Escape: 'Escape',
Home: 'Home',
Left: 'ArrowLeft',
PageDown: 'PageDown',
PageUp: 'PageUp',
Right: 'ArrowRight',
Space: ' ',
Tab: 'Tab',
Up: 'ArrowUp'
}
const MenuActions = {
Close: 0,
CloseSelect: 1,
First: 2,
Last: 3,
Next: 4,
Open: 5,
Previous: 6,
Select: 7,
Space: 8,
Type: 9
}
function filterOptions(options = [], filter, exclude = []) {
return options.filter((option) => {
const matches = option.toLowerCase().indexOf(filter.toLowerCase()) === 0;
return matches && exclude.indexOf(option) < 0;
});
}
function getActionFromKey(key, menuOpen) {
if (!menuOpen && key === Keys.Down) {
return MenuActions.Open;
}
if (!menuOpen && key === Keys.Enter) {
return MenuActions.Open;
}
if (key === Keys.Down) {
return MenuActions.Next;
} else if (key === Keys.Up) {
return MenuActions.Previous;
} else if (key === Keys.Home) {
return MenuActions.First;
} else if (key === Keys.End) {
return MenuActions.Last;
} else if (key === Keys.Escape) {
return MenuActions.Close;
} else if (key === Keys.Enter) {
return MenuActions.CloseSelect;
} else if (key === Keys.Backspace || key === Keys.Clear || key.length === 1) {
return MenuActions.Type;
}
}
function getUpdatedIndex(current, max, action) {
switch(action) {
case MenuActions.First:
return 0;
case MenuActions.Last:
return max;
case MenuActions.Previous:
return Math.max(0, current - 1);
case MenuActions.Next:
return Math.min(max, current + 1);
default:
return current;
}
}
const MultiselectButtons = function(el, options) {
// element refs
this.el = el;
this.combobox = el.querySelector('.select-btn');
this.inputbox = el.querySelector('input');
this.listbox = el.querySelector('[role=listbox]');
this.arrowbtn = document.getElementById('arr-button');
this.idBase = this.inputbox.id;
this.selectedEl = document.getElementById('selected-options');
this.options = options;
this.filteredOptions = options;
this.activeIndex = 0;
this.open = false;
window.addEventListener('mouseup', event => {
const ariaExpand = this.arrowbtn.getAttribute('aria-expanded');
if (event.target != this.combobox && event.target.parentNode != this.listbox && event.target != this.arrowbtn) {
this.updateMenuState(false);
}
});
}
MultiselectButtons.prototype.init = function() {
this.inputbox.addEventListener('input', this.onInput.bind(this));
this.inputbox.addEventListener('click', () => this.updateMenuState(true));
this.inputbox.addEventListener('keydown', this.onInputKeyDown.bind(this));
this.arrowbtn.addEventListener('click', () => {
this.combobox.classList.toggle('open')
open = this.combobox.classList.contains('open');
if(open) {
this.inputbox.setAttribute('aria-expanded', `${open}`);
this.arrowbtn.setAttribute('aria-expanded', `${open}`);
this.inputbox.focus();
} else {
this.inputbox.setAttribute('aria-expanded', `${open}`);
this.arrowbtn.setAttribute('aria-expanded', `${open}`);
}
});
this.options.map((option, index) => {
const optionali = document.createElement('li');
optionali.setAttribute('role', 'option');
optionali.id = `${this.idBase}-${index}`;
optionali.className = index === 0 ? 'item option-current' : 'item';
optionali.setAttribute('aria-selected', 'false');
optionali.innerText = option;
optionali.addEventListener('click', () => { this.onOptionClick(index); });
optionali.addEventListener('mousedown', this.onOptionMouseDown.bind(this));
this.listbox.appendChild(optionali);
});
}
MultiselectButtons.prototype.filterOptions = function(value) {
this.filteredOptions = filterOptions(this.options, value);
const options = this.el.querySelectorAll('[role=option]');
[...options].forEach((optionali) => {
const value = optionali.innerText;
if (this.filteredOptions.indexOf(value) > -1) {
optionali.style.display = 'flex';
} else {
optionali.style.display = 'none';
}
});
}
MultiselectButtons.prototype.onInput = function() {
const curValue = this.inputbox.value;
this.filterOptions(curValue);
// if active option is not in filtered options, set it to first filtered option
if (this.filteredOptions.indexOf(this.options[this.activeIndex]) < 0) {
const firstFilteredIndex = this.options.indexOf(this.filteredOptions[0]);
this.onOptionChange(firstFilteredIndex);
}
const menuState = this.filteredOptions.length > 0;
if (this.open !== menuState) {
this.updateMenuState(menuState, false);
}
}
MultiselectButtons.prototype.onInputKeyDown = function(event) {
const { key } = event;
const max = this.filteredOptions.length - 1;
const activeFilteredIndex = this.filteredOptions.indexOf(this.options[this.activeIndex]);
const action = getActionFromKey(key, this.open);
switch(action) {
case MenuActions.Next:
case MenuActions.Last:
case MenuActions.First:
case MenuActions.Previous:
event.preventDefault();
const nextFilteredIndex = getUpdatedIndex(activeFilteredIndex, max, action);
const nextRealIndex = this.options.indexOf(this.filteredOptions[nextFilteredIndex]);
return this.onOptionChange(nextRealIndex);
case MenuActions.CloseSelect:
event.preventDefault();
return this.updateOption(this.activeIndex);
case MenuActions.Close:
event.preventDefault();
return this.updateMenuState(false);
case MenuActions.Open:
return this.updateMenuState(true);
}
}
MultiselectButtons.prototype.onOptionChange = function(index) {
this.activeIndex = index;
this.inputbox.setAttribute('aria-activedescendant', `${this.idBase}-${index}`);
// update active style
const options = this.el.querySelectorAll('[role=option]');
[...options].forEach((optionali) => {
optionali.classList.remove('option-current');
});
options[index].classList.add('option-current');
event.preventDefault();
}
MultiselectButtons.prototype.onOptionClick = function(index) {
this.onOptionChange(index);
this.updateOption(index);
this.inputbox.focus();
}
MultiselectButtons.prototype.onOptionMouseDown = function() {
this.ignoreBlur = true;
}
MultiselectButtons.prototype.removeOption = function(index) {
const option = this.options[index];
// update aria-selected
const options = this.el.querySelectorAll('[role=option]');
options[index].setAttribute('aria-selected', 'false');
options[index].classList.remove('option-selected');
// remove button
const buttonli = document.getElementById(`${this.idBase}-remove-${index}`);
this.selectedEl.removeChild(buttonli.parentElement);
this.inputbox.focus();
}
MultiselectButtons.prototype.selectOption = function(index) {
const selected = this.options[index];
this.activeIndex = index;
// update aria-selected
const options = this.el.querySelectorAll('[role=option]');
if(this.combobox.classList.contains('open')) {
options[index].setAttribute('aria-selected', 'true');
options[index].classList.add('option-selected');
// add remove option button
const buttonli = document.createElement('button');
const listItem = document.createElement('li');
listItem.className = 'selected-option';
buttonli.className = 'btn';
buttonli.type = 'button';
buttonli.id = `${this.idBase}-remove-${index}`;
buttonli.setAttribute('aria-label', 'remove '+selected);
buttonli.addEventListener('click', () => { this.removeOption(index); });
buttonli.innerHTML = selected + ' ';
listItem.appendChild(buttonli);
this.selectedEl.appendChild(listItem);
}
}
MultiselectButtons.prototype.updateOption = function(index) {
const option = this.options[index];
const optionalis = this.el.querySelectorAll('[role=option]');
const optionali = optionalis[index];
const isSelected = optionali.getAttribute('aria-selected') === 'true';
if (isSelected) {
this.removeOption(index);
} else {
this.selectOption(index);
}
this.inputbox.value = '';
this.filterOptions('');
}
MultiselectButtons.prototype.updateMenuState = function(open, callFocus = true) {
this.open = open;
this.inputbox.setAttribute('aria-expanded', `${open}`);
this.arrowbtn.setAttribute('aria-expanded', `${open}`);
open ? this.combobox.classList.add('open') : this.combobox.classList.remove('open');
callFocus && this.inputbox.focus();
}
const inlinebuttonli = document.querySelector('.inline-button');
const inlineButtonComponent = new MultiselectButtons(inlinebuttonli, options);
inlineButtonComponent.init();