Přístupný modální dialog
Modální dialog přebírá celou obrazovku, ale bez správné obsluhy o tom čtečka obrazovky ani klávesnicový uživatel neví — fokus zůstane za dialogem, Tab prochází i obsah pod ním a po zavření se nikdo nevrátí tam, odkud přišel.
1. HTML — nativní <dialog>
<dialog id="priklad-dialog" aria-labelledby="priklad-dialog-title">
<h1 id="priklad-dialog-title">Název dialogu</h1>
<p>Obsah dialogu…</p>
<button type="button" data-dialog-close="priklad-dialog">Zavřít</button>
</dialog>
const dialog = document.getElementById('priklad-dialog');
dialog.showModal(); // otevře dialog, sám vytvoří past na fokus a zpřístupní jen jeho obsah
dialog.close(); // zavře a vrátí fokus na prvek, který dialog otevřel
Nativní <dialog> (metoda showModal()) řeší past na fokus i skrytí zbytku stránky před čtečkou automaticky — nemusí se to programovat ručně. Klávesa Esc dialog zavírá bez dalšího kódu.
2. HTML a JS — ruční modál (<div> + ARIA atributy)
<div class="modal" id="priklad-modal" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="priklad-modal-title" aria-hidden="true">
<h1 id="priklad-modal-title">Název dialogu</h1>
<p>Obsah dialogu…</p>
<button type="button" data-dialog-close="priklad-modal">Zavřít</button>
</div>
let naposledyZaostreno = null;
function otevriModal(modal) {
naposledyZaostreno = document.activeElement; // kam se má fokus vrátit po zavření
modal.hidden = false;
modal.removeAttribute('aria-hidden');
modal.focus(); // vyžaduje tabindex="-1" na kontejneru, viz výše
document.addEventListener('keydown', obsluhaKlaves);
}
function zavriModal(modal) {
modal.hidden = true;
modal.setAttribute('aria-hidden', 'true');
document.removeEventListener('keydown', obsluhaKlaves);
naposledyZaostreno?.focus(); // vrátí fokus tam, odkud dialog vznikl
}
function obsluhaKlaves(event) {
const modal = document.getElementById('priklad-modal');
if (event.key === 'Escape') {
zavriModal(modal);
return;
}
if (event.key !== 'Tab') {
return;
}
// past na fokus: Tab na posledním fokusovatelném prvku skočí na první, Shift+Tab na prvním na poslední
const fokusovatelne = modal.querySelectorAll('a[href], button, input, textarea, select, [tabindex]:not([tabindex="-1"])');
const prvni = fokusovatelne[0];
const posledni = fokusovatelne[fokusovatelne.length - 1];
if (event.shiftKey && document.activeElement === prvni) {
event.preventDefault();
posledni.focus();
} else if (!event.shiftKey && document.activeElement === posledni) {
event.preventDefault();
prvni.focus();
}
}
Tohle je potřeba, když se modál staví „ručně" (obyčejný <div>), ať už úplně vlastním kódem, nebo pomocí libovolné knihovny či sady komponent:
role="dialog"a hlavněaria-modal="true"musí být v HTML napsané explicitně. U obyčejného<div>modalitu nejde odvodit z toho, jak dialog vypadá nebo jak se chová – musí to tam být zapsané jako atribut, jinak to čtečka nepozná.tabindex="-1"na kontejneru je potřeba, aby na něj vůbec šlo skriptem přesunout fokus po otevření.- Past na fokus (Tab/Shift+Tab neopustí dialog), zavření klávesou Esc a návrat fokusu na spouštěč po zavření musí něco zajistit – vlastní kód (ukázka výše), nebo zvolená knihovna/komponenta.
Které z těchhle věcí zajistí zvolená knihovna sama a které zůstávají na vás, se knihovna od knihovny liší – nedá se to paušálně předpokládat. aria-modal="true" je zrovna atribut, který se snadno přehlédne, protože dialog se navenek může chovat úplně správně (fokus viditelně uvnitř, klávesnice ho neopustí) i bez něj – rozdíl pozná až čtečka obrazovky ve výchozím prohlížecím režimu, která bez něj pořád „vidí" a umí projet obsah stránky za otevřeným dialogem. Ověřujte si přímo ve vygenerovaném HTML, že tam skutečně je – nespoléhejte na to jen podle toho, jak se dialog navenek chová.
3. Kde se to na tomto webu používá
- Přepis audio nahrávky ( marker) — nativní
<dialog>přesně podle vzoru v části 1. - Nápověda k formátování (Markdown) v administraci — ruční modál nad knihovnou třetí strany, kde jsme přesně tohle přehlédli: past na fokus, Esc i návrat fokusu fungovaly, ale
aria-modalchybělo. Objevilo se to až při ověřování proti obecnému vzoru, ne při běžném používání.
Proč to takhle je
- První nadpis uvnitř dialogu je vždy H1, bez ohledu na to, na jaké úrovni nadpisů byla stránka pod ním. Čtečka obrazovky bere dialog jako samostatný dokument/kontext, takže jeho vlastní nadpisová hierarchie začíná znovu od H1.
- aria-labelledby na dialogu míří na id toho H1 — díky tomu čtečka po otevření řekne rovnou název dialogu, ne jen "dialog".
- aria-modal="true" říká čtečce, že zbytek stránky je teď neaktivní (inert) — u nativního
<dialog>je to automatické, u ručně stavěného modálu je to atribut, který musí být v HTML napsaný explicitně; ze samotného chování dialogu (fokus uvnitř, klávesnice ho neopustí) se nedá poznat, jestli tam je. - Tlačítko zavření má popisný text, ne jen „X" nebo obecné „Close" — např. „Zavřít nápovědu", ne „Zavřít" bez kontextu, pokud je na stránce dialogů víc druhů.
- Fokus se po zavření vrátí na prvek, který dialog otevřel (u nativního <dialog> to řeší prohlížeč sám u showModal()/close(), u ručního modálu to řeší kód v části 2).
Pravidla
- WCAG 4.1.2 (Název, role, hodnota) — role="dialog" (u nativního <dialog> implicitní) + aria-labelledby.
- WCAG 2.4.3 (Pořadí zaměření) — fokus jde dovnitř dialogu při otevření a zpátky na spouštěč při zavření.
- WCAG 2.1.2 (Žádná past na klávesnici) — past na fokus uvnitř dialogu je v pořádku, pokud jde vždy odejít standardní klávesou (Esc) nebo tlačítkem zavřít.
To je celé — nativní <dialog> s showModal() zvládne past na fokus i skrytí pozadí sám. Bez něj (ruční modál) musí HTML mít navíc aria-modal="true" a tabindex="-1", a přesun fokusu dovnitř, past na fokus i návrat fokusu při zavření musí zajistit vlastní kód (viz ukázka v části 2) nebo zvolená knihovna – ověřeně, ne jen podle odhadu.
Komentáře
Zatím zde nejsou žádné komentáře. Buďte první, kdo napíše svůj názor.
Přidat komentář k „Přístupný modální dialog“