Skip to content

Commit 1d3aef3

Browse files
committed
refactor(MultiSelect): improve accessibility handling
1 parent 596aab6 commit 1d3aef3

File tree

1 file changed

+25
-3
lines changed

1 file changed

+25
-3
lines changed

js/src/multi-select.js

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
defineJQueryPlugin,
1616
getNextActiveElement,
1717
getElement,
18+
getUID,
1819
isVisible,
1920
isRTL
2021
} from './util/index.js'
@@ -86,10 +87,12 @@ const CLASS_NAME_TAG_DELETE = 'form-multi-select-tag-delete'
8687
const Default = {
8788
allowList: DefaultAllowlist,
8889
ariaCleanerLabel: 'Clear all selections',
90+
ariaIndicatorLabel: 'Toggle visibility of options menu',
8991
cleaner: true,
9092
clearSearchOnSelect: false,
9193
container: false,
9294
disabled: false,
95+
id: null,
9396
invalid: false,
9497
multiple: true,
9598
name: null,
@@ -115,10 +118,12 @@ const Default = {
115118
const DefaultType = {
116119
allowList: 'object',
117120
ariaCleanerLabel: 'string',
121+
ariaIndicatorLabel: 'string',
118122
cleaner: 'boolean',
119123
clearSearchOnSelect: 'boolean',
120124
container: '(string|element|boolean)',
121125
disabled: 'boolean',
126+
id: '(string|null)',
122127
invalid: 'boolean',
123128
multiple: 'boolean',
124129
name: '(string|null)',
@@ -151,6 +156,8 @@ class MultiSelect extends BaseComponent {
151156
constructor(element, config) {
152157
super(element, config)
153158

159+
this._uniqueId = this._config.id || this._element.id || getUID(`${this.constructor.NAME}`)
160+
this._uniqueName = this._config.name || this._element.name || this._uniqueId
154161
this._configureNativeSelect()
155162
this._indicatorElement = null
156163
this._selectAllElement = null
@@ -542,7 +549,10 @@ class MultiSelect extends BaseComponent {
542549
multiSelectEl.classList.add(CLASS_NAME_SELECT)
543550
multiSelectEl.classList.toggle('is-invalid', this._config.invalid)
544551
multiSelectEl.classList.toggle('is-valid', this._config.valid)
552+
multiSelectEl.role = 'combobox'
545553
multiSelectEl.setAttribute('aria-expanded', 'false')
554+
multiSelectEl.setAttribute('aria-haspopup', 'listbox')
555+
multiSelectEl.setAttribute('aria-owns', `${this._uniqueId}-listbox`)
546556

547557
if (this._config.disabled) {
548558
this._element.classList.add(CLASS_NAME_DISABLED)
@@ -562,9 +572,8 @@ class MultiSelect extends BaseComponent {
562572
this._updateSearch()
563573
}
564574

565-
if (this._config.name || this._element.id || this._element.name) {
566-
this._element.setAttribute('name', (this._config.name || this._element.name || `multi-select-${this._element.id}`))
567-
}
575+
this._element.setAttribute('id', this._uniqueId)
576+
this._element.setAttribute('name', this._uniqueName)
568577

569578
this._createOptionsContainer()
570579
this._hideNativeSelect()
@@ -612,6 +621,7 @@ class MultiSelect extends BaseComponent {
612621
const indicator = document.createElement('button')
613622
indicator.type = 'button'
614623
indicator.classList.add('form-multi-select-indicator')
624+
indicator.setAttribute('aria-label', this._config.ariaIndicatorLabel)
615625

616626
if (this._config.disabled) {
617627
indicator.tabIndex = -1
@@ -656,6 +666,9 @@ class MultiSelect extends BaseComponent {
656666
input.disabled = true
657667
}
658668

669+
input.setAttribute('id', `search-${this._uniqueId}`)
670+
input.setAttribute('name', `search-${this._uniqueName}`)
671+
659672
this._searchElement = input
660673
this._updateSearchSize()
661674

@@ -665,6 +678,12 @@ class MultiSelect extends BaseComponent {
665678
_createOptionsContainer() {
666679
const dropdownDiv = document.createElement('div')
667680
dropdownDiv.classList.add(CLASS_NAME_SELECT_DROPDOWN)
681+
dropdownDiv.role = 'listbox'
682+
dropdownDiv.setAttribute('id', `${this._uniqueId}-listbox`)
683+
684+
if (this._config.multiple) {
685+
dropdownDiv.setAttribute('aria-multiselectable', 'true')
686+
}
668687

669688
if (this._config.selectAll && this._config.multiple) {
670689
const selectAllButton = document.createElement('button')
@@ -715,6 +734,7 @@ class MultiSelect extends BaseComponent {
715734

716735
optionDiv.dataset.value = String(option.value)
717736
optionDiv.tabIndex = 0
737+
optionDiv.role = 'option'
718738

719739
if (this._config.optionsTemplate && typeof this._config.optionsTemplate === 'function') {
720740
optionDiv.innerHTML = this._config.sanitize ?
@@ -851,6 +871,7 @@ class MultiSelect extends BaseComponent {
851871
const option = SelectorEngine.findOne(`[data-value="${value}"]`, this._optionsElement)
852872
if (option) {
853873
option.classList.add(CLASS_NAME_SELECTED)
874+
option.setAttribute('aria-selected', 'true')
854875
}
855876

856877
EventHandler.trigger(this._element, EVENT_CHANGED, {
@@ -871,6 +892,7 @@ class MultiSelect extends BaseComponent {
871892
const option = SelectorEngine.findOne(`[data-value="${value}"]`, this._optionsElement)
872893
if (option) {
873894
option.classList.remove(CLASS_NAME_SELECTED)
895+
option.setAttribute('aria-selected', 'false')
874896
}
875897

876898
EventHandler.trigger(this._element, EVENT_CHANGED, {

0 commit comments

Comments
 (0)