Přístupný combobox a autocomplete
Combobox, který při psaní nabízí a filtruje možnosti, je nejsložitější rozbalovací prvek, co ARIA nabízí. Kombinuje textové pole, seznam návrhů a několik možných typů automatického doplňování. Bez správné obsluhy nejde jen o to, že čtečka neřekne, co se děje. Bez klávesnice může být prvek úplně nepoužitelný.
1. Základní kostra: tři role a jejich propojení
<label for="mesto">Město</label>
<input
id="mesto"
role="combobox"
aria-expanded="false"
aria-controls="mesto-listbox"
aria-autocomplete="list"
autocomplete="off"
>
<ul id="mesto-listbox" role="listbox" aria-label="Návrhy měst" hidden></ul>
autocomplete="off" na vstupu vypíná prohlížečovo vlastní (nesouvisející) automatické doplňování formulářů, ať nekoliduje s vlastním seznamem návrhů.
2. Otevření popupu a filtrování při psaní
const input = document.getElementById('mesto');
const listbox = document.getElementById('mesto-listbox');
const mesta = ['Praha', 'Plzeň', 'Brno', 'Ostrava', 'Olomouc'];
input.addEventListener('input', () => {
const dotaz = input.value.trim().toLowerCase();
const shoda = dotaz ? mesta.filter(m => m.toLowerCase().startsWith(dotaz)) : [];
listbox.innerHTML = shoda.map((m, i) =>
`<li role="option" id="mesto-opt-${i}">${m}</li>`
).join('');
const otevrit = shoda.length > 0;
input.setAttribute('aria-expanded', String(otevrit));
listbox.hidden = !otevrit;
input.removeAttribute('aria-activedescendant');
});
3. Obsluha klávesnicí: šipky, Enter, Escape
Šipky nepřesouvají DOM fokus na jednotlivé volby. Ten zůstává celou dobu na textovém poli, aby šlo dál psát. Místo toho se při každém stisku šipky jen přepíše aria-activedescendant na input a vizuálně zvýrazní odpovídající <li>. Čtečka díky tomu ohlásí, jako by byl fokus na té volbě, i když se DOM fokus nikam skutečně nepřesunul:
let aktivniIndex = -1;
input.addEventListener('keydown', (e) => {
const volby = listbox.querySelectorAll('[role="option"]');
if (!volby.length) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
aktivniIndex = Math.min(aktivniIndex + 1, volby.length - 1);
nastavAktivni(volby, aktivniIndex);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
aktivniIndex = Math.max(aktivniIndex - 1, 0);
nastavAktivni(volby, aktivniIndex);
} else if (e.key === 'Enter' && aktivniIndex >= 0) {
e.preventDefault();
vyberVolbu(volby[aktivniIndex]);
} else if (e.key === 'Escape') {
zavriPopup();
}
});
function nastavAktivni(volby, index) {
volby.forEach(v => v.setAttribute('aria-selected', 'false'));
volby[index].setAttribute('aria-selected', 'true');
input.setAttribute('aria-activedescendant', volby[index].id);
volby[index].scrollIntoView({ block: 'nearest' });
}
function vyberVolbu(volba) {
input.value = volba.textContent;
zavriPopup();
}
function zavriPopup() {
listbox.hidden = true;
input.setAttribute('aria-expanded', 'false');
input.removeAttribute('aria-activedescendant');
aktivniIndex = -1;
}
Klik myší na volbu musí vést ke stejné funkci jako Enter na klávesnici:
listbox.addEventListener('click', (e) => {
const volba = e.target.closest('[role="option"]');
if (volba) vyberVolbu(volba);
});
4. Vícenásobný výběr (rozšíření)
Pokud má combobox dovolovat výběr víc hodnot najednou, třeba výběr víc štítků do jednoho pole, listbox dostane navíc aria-multiselectable="true" a výběr se řídí jinak: potvrzení volby jen přepne její aria-selected, popup zůstane otevřený a vybrané hodnoty se sbírají do vlastního seznamu vedle textového pole. Textové pole samo pak „hodnotu“ v obvyklém smyslu nenese.
<ul id="mesto-listbox" role="listbox" aria-multiselectable="true" aria-label="Návrhy měst" hidden></ul>
function prepniVolbu(volba) {
const vybrano = volba.getAttribute('aria-selected') === 'true';
volba.setAttribute('aria-selected', String(!vybrano));
// popup zůstává otevřený, textové pole se nemaže
}
Proč to takhle je
aria-expanded,aria-controlsaaria-autocompletepatří na textové pole od prvního vykreslení, i ve výchozím sbaleném stavu bez návrhů, ať čtečka dopředu ví, co pole umí.aria-activedescendantse aktualizuje při každém pohybu šipkami, ne až při potvrzení výběru. Jinak by čtečka hlásila starou, neplatnou volbu.- Klik myší na volbu musí vyvolat stejnou funkci jako Enter na klávesnici: dvě cesty ke stejnému výsledku, ne dvě různé implementace, které se časem rozejdou.
Pravidla
- WCAG 4.1.2 (Název, role, hodnota) —
aria-expandedaaria-selectedmusí vždy odpovídat skutečnému stavu. - WCAG 1.3.1 (Informace a vztahy) — vztah pole a popupu (
aria-controls) i fokusu a volby (aria-activedescendant). - WCAG 2.1.1 (Klávesnice) — šipky, Enter a Escape musí fungovat bez myši.
- WCAG 2.4.3 (Pořadí zaměření) — fokus po výběru volby nebo po Escape se vrací zpátky do textového pole, ne nikam jinam.
To je celé: tři role dají comboboxu tvar, input/keydown obsluha řídí filtrování i pohyb po návrzích, aria-expanded a aria-activedescendant drží čtečku v obraze o obojím.