Add missing ARIA attributes and keyboard controls to multiselect

I use MDN’s documentation[0] as guid for both.

[0]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/combobox_role
This commit is contained in:
jordi fita mas 2023-03-18 07:17:28 +01:00
parent 2dde25c862
commit 041017adc3
2 changed files with 81 additions and 8 deletions

View File

@ -667,7 +667,7 @@ main > nav {
padding: 1rem 2rem;
}
[is="numerus-multiselect"] .options li:hover {
[is="numerus-multiselect"] .options li:hover, [is="numerus-multiselect"] .options .highlight {
background-color: var(--numerus--color--light-gray);
}

View File

@ -3,6 +3,7 @@ class Multiselect extends HTMLDivElement {
super();
this.initialized = false;
this.toSearch = '';
this.highlighted = 0;
}
connectedCallback() {
@ -40,6 +41,10 @@ class Multiselect extends HTMLDivElement {
this.tags.append(this.search);
this.search.id = this.select.id;
this.select.removeAttribute('id');
this.select.setAttribute('aria-hidden', 'true');
this.search.setAttribute('spellcheck', 'false');
this.search.setAttribute('autocomplete', 'false');
this.search.setAttribute('role', 'combobox');
this.search.addEventListener('input', (e) => {
this.open();
this.filter(e.target.value);
@ -48,7 +53,23 @@ class Multiselect extends HTMLDivElement {
switch (e.code) {
case "Enter":
e.preventDefault();
this.selectFirst();
this.selectHighlighted();
break;
case "ArrowDown":
if (e.altKey) {
this.open();
} else {
e.preventDefault();
this.highlightNext();
}
break;
case "ArrowUp":
if (e.altKey) {
this.close();
} else {
e.preventDefault();
this.highlightPrev();
}
break;
case "Backspace":
if (e.target.value === '') {
@ -66,7 +87,10 @@ class Multiselect extends HTMLDivElement {
this.options = document.createElement('ul');
this.append(this.options);
this.options.id = this.search.id + '-options-' + Array.from(crypto.getRandomValues(new Uint8Array(4))).map(n => n.toString(16)).join('');
this.search.setAttribute('aria-controls', this.options.id);
this.options.classList.add('options');
this.options.setAttribute('role', 'listbox');
this.close();
window.addEventListener('focusin', (e) => {
@ -85,18 +109,64 @@ class Multiselect extends HTMLDivElement {
rebuildOptions() {
this.options.innerHTML = '';
let highlight = true;
for (const option of this.select.options) {
if (!this.isOptionSelectable(option)) {
continue;
}
const li = document.createElement('li');
this.options.append(li);
li.id = this.options.id + '-' + option.index;
li.dataset.index = option.index;
li.textContent = option.textContent;
li.addEventListener('click', () => this.selectOption(option, true));
if (highlight && (option.index >= this.highlighted)) {
highlight = false;
this.highlight(null, li);
}
}
if (highlight) {
this.highlight(null, this.options.querySelector('li:last-child'));
}
}
highlightNext() {
const current = this.currentHighlighted();
if (!current) {
return;
}
this.highlight(current, current.nextElementSibling);
}
highlightPrev() {
const current = this.currentHighlighted();
if (!current) {
return;
}
this.highlight(current, current.previousElementSibling);
}
highlight(current, upcoming) {
if (!upcoming) {
return;
}
if (current) {
current.classList.remove('highlight');
}
upcoming.classList.remove('highlight');
this.search.setAttribute('aria-activedescendant', upcoming.id);
this.highlighted = upcoming.dataset.index;
upcoming.classList.add('highlight');
}
currentHighlighted() {
return document.getElementById(this.search.getAttribute('aria-activedescendant'));
}
isOptionSelectable(option) {
if (!option) {
return false;
}
if (option.selected) {
return false;
}
@ -104,20 +174,21 @@ class Multiselect extends HTMLDivElement {
}
selectOption(option, selected) {
if (!option) {
return;
}
option.selected = selected;
this.toSearch = this.search.value = '';
this.rebuild();
}
selectFirst() {
selectHighlighted() {
if (!this.isOpen()) {
return;
}
for (const option of this.select.options) {
const option = this.select.options[this.highlighted];
if (this.isOptionSelectable(option)) {
this.selectOption(option, true);
break;
}
}
}
@ -172,10 +243,12 @@ class Multiselect extends HTMLDivElement {
close() {
this.options.style.display = 'none';
this.search.setAttribute('aria-expanded', 'false');
}
open() {
this.options.removeAttribute('style');
this.search.setAttribute('aria-expanded', 'true');
}
toggle() {