Přístupný modální dialog

Modální dialog přebírá celou obrazovku, ale bez správné obsluhy o tom čtečka obrazovky ani uživatel klávesnice 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>

Nejjednodušší cesta je použít vestavěný prvek <dialog>, který prohlížeč podporuje sám o sobě. HTML dialogu vypadá takto:

<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>

Otevření a zavření pak zajistí pár řádků JavaScriptu:

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

Metoda showModal() automaticky řeší past na fokus i skrytí zbytku stránky před čtečkou. Nemusí se to tedy 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)

Nativní <dialog> ale nejde použít vždycky, třeba když modál staví knihovna třetí strany nebo vlastní komponenta postavená na obyčejném <div>. V tom případě si všechno, co <dialog> dělá automaticky, musí obsluhovat kód ručně: past na fokus, skrytí pozadí i to, kam se fokus vrátí po zavření.

Nejdřív HTML kontejner s ARIA atributy, které dialogovou roli a modalitu popíšou čtečce obrazovky:

<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>

A k němu JavaScript, který dialog otevře a zavře, ošetří klávesu Esc a udrží past na fokus, aby se Tab a Shift+Tab točily jen uvnitř dialogu a neutekly do zbytku stránky:

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();
    }
}

Shrnutí toho, co musí být u ručně stavěného modálu v HTML a v kódu, ať už ho píšete sami nebo ho staví knihovna či sada 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á

Oba přístupy z předchozích částí se tu skutečně používají, ne jen jako teoretická ukázka:

  • 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 jsem přesně tohle přehlédl. Past na fokus, Esc i návrat fokusu fungovaly, ale aria-modal chybě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. Podle W3C ARIA APG (Dialog Modal Pattern) se má napsat, jen když jsou splněné obě podmínky zároveň: kód opravdu znemožňuje jakoukoliv interakci s obsahem mimo dialog a vizuálně je zbytek stránky obsahem dialogu zakrytý. Napsat ho „na sucho“ bez skutečného zablokování je vůči čtečce lež.
  • Tlačítko zavření má popisný text, ne jen „X“ nebo obecné „Close“, například „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 přes 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.

Zdroje

Štítky

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“