Kürzlich habe ich diesen Artikel von Keith Grant gelesen, der das neu erschienene Element <dialog> vorstellte. . Begeistert von diesem neuen UI-Element habe ich mich sofort daran gemacht, damit zu experimentieren, um zu sehen, wie es effektiv als Modal verwendet werden kann – was die häufigste Anwendung dafür ist. Bei meinen Experimenten habe ich einen netten CSS-Trick entdeckt, um den Fokus innerhalb des Elements einzuschließen, eine gängige Anforderung an die Zugänglichkeit für Modals und eine bekanntermaßen schwierige.
Haftungsausschluss: Die <dialog>-Demos in diesem Artikel werden nur in Chrome und Firefox getestet. Safari hat ein merkwürdiges Problem, bei dem nicht alle Elemente bei der normalen Tastaturnavigation mit der Tab-Taste fokussiert werden!
Was ist Fokus-Falle?
Zuerst ein Zitat aus der W3C-Dokumentation darüber, was nach einem Tastendruck innerhalb eines Dialogs geschehen soll
Tab
- Verschiebt den Fokus auf das nächste fokussierbare Element im Dialog.
- Wenn sich der Fokus auf dem letzten fokussierbaren Element im Dialog befindet, wird der Fokus auf das erste fokussierbare Element im Dialog verschoben.
Umschalttaste + Tab
- Verschiebt den Fokus auf das vorherige fokussierbare Element im Dialog.
- Wenn sich der Fokus auf dem ersten fokussierbaren Element im Dialog befindet, wird der Fokus auf das letzte fokussierbare Element im Dialog verschoben.
Zusammenfassend lässt sich sagen, dass beim Drücken von Tab oder Umschalttaste+Tab innerhalb eines Dialogs der Fokus nur innerhalb des Dialogs gewechselt werden sollte – zwischen den fokussierbaren Elementen innerhalb des Dialogs.
Das ergibt Sinn, denn wenn ein Dialog geöffnet ist, interagiert ein Benutzer nur innerhalb dieses Dialogs, und wenn der Fokus nach außen entweichen könnte, würde dies im Wesentlichen Kontexte vermischen und möglicherweise einen Zustand schaffen, in dem der Benutzer nicht weiß, welches Element den Fokus hat.
Wenn wir also zurück zur Idee eines Modals gehen, wäre unsere Erwartung, dass das Tabbing innerhalb des Modals nur Elemente innerhalb des Modals fokussiert. Alles außerhalb des Kontexts des Modals wäre außer Reichweite, da sich die Tabulatortaste nur auf das konzentriert, was sich darin befindet. Das ist es, was wir unter Fokus-Falle verstehen.
Eine Implementierung mit JavaScript
Wenn wir eine Fokus-Falle innerhalb eines <dialog>-Elements implementieren wollten, wäre der üblichste Ansatz, Folgendes zu tun, wenn der Dialog geöffnet wird:
1. Alle fokussierbaren/tab-fähigen Elemente innerhalb des Dialogs abrufen.
2. Auf Tab- und Umschalttaste+Tab-Tastendrücke reagieren und manuell zum nächsten bzw. vorherigen Element springen.
3. Wenn die Tastendruck auf dem ersten fokussierbaren Element erfolgt, dann auf das letzte fokussierbare Element in der Kette springen und umgekehrt.
Auf diese Weise erzeugen wir eine Fokus-Schleife, wenn der Benutzer Tab oder Umschalttaste+Tab drückt. Sehen Sie sich diese W3C-Code-Snippet als Beispiel dafür an, wie dies mit JavaScript angegangen werden könnte. Sie werden sehen, dass es *ziemlich viel* JavaScript ist.
Einführung in :focus-within
Zurück zu meinem Experiment mit dem neuen <dialog>-Element. Wenn ich über Fokus-Falle nachdachte, fiel mir sofort eine CSS-Pseudoklasse ein (ebenfalls sehr neu in Browsern): :focus-within.
Wenn Sie davon noch nichts gehört haben, repräsentiert es ein Element, das den Fokus erhalten hat oder ein Element enthält, das den Fokus erhalten hat. Wenn Sie also beispielsweise ein <div> haben und darin ein input-Element, können Sie dieses <div> wie folgt gestalten, wenn das enthaltene input den Fokus hat:
div:focus-within {
border: 2px solid red;
}
Der CSS-Trick für Fokus-Falle
Nutzen wir :focus-within und CSS-Übergänge, um eine einfache Fokus-Falle innerhalb eines <dialog>-Elements zu implementieren.
Zusammenfassend lässt sich sagen, dass der Trick so funktioniert. Wenn der Fokus nicht im Dialog liegt (und der Dialog geöffnet ist), lösen wir Folgendes aus:
- eine CSS-Übergang auslösen
- den Abschluss des Übergangs in JavaScript erkennen
- das erste Element im Dialog fokussieren
Aber zuerst richten wir alles ein. Hier ist der grundlegende Dialog und die Öffnungsfunktionalität
<button id="button">Open dialog</button>
<dialog id="modal">
<form action="">
<label>
<input type="text" /> Username
</label>
<label>
<input type="password" /> Password
</label>
<input type="submit" value="Submit" />
</form>
</dialog>
button.onclick = () => {
modal.showModal();
}
Da haben wir es. Das Klicken auf den Button sollte unseren Dialog öffnen. Nur dieser Code ist erforderlich, um ein grundlegendes funktionierendes Modal mit dem neuen <dialog> zu erstellen.
Hinweis: Wie im obigen Beispiel-Demo des Dialogs sehen Sie einige zusätzliche Polyfill-Codes, um <dialog> in Browsern, in denen es nicht unterstützt wird, zum Laufen zu bringen.
Wenn Sie den Dialog im obigen Beispiel geöffnet und mehrmals mit Tabulator navigiert haben, haben Sie vielleicht schon das Problem bemerkt: Der Fokus beginnt bei Elementen im Dialog, verlässt ihn aber, sobald das letzte Element im Dialog passiert wurde.
Das ist der Kern unseres Tricks. Wir müssen irgendwie den verlorenen Fokus, der mit :focus-within erkannt wird, an JavaScript senden, damit wir den Fokus zurück zum Dialog senden können. Hier kommen CSS-Übergänge ins Spiel. Ein CSS-Übergang ist etwas, das über CSS geschieht, aber auch Ereignisse in JavaScript ausgibt. In unserem Fall können wir einen Übergang für eine beliebige Eigenschaft mit einem vernachlässigbaren (da er in unserem Fall keine Rolle spielt) visuellen Unterschied auslösen und auf den Abschluss des Übergangs in JavaScript warten.
Beachten Sie, dass wir diesen Übergang auslösen müssen, wenn der Dialog geöffnet ist, aber keinen Fokus darin hat.
dialog {
background-color: rgb(255, 255, 255);
}
dialog[open]:not(:focus-within) {
background-color: rgb(255, 255, 254);
transition: background-color 0.01s;
}
Sehen wir uns an, was dieses CSS tut.
- Wir weisen dem Dialog eine
background-colorunserer Wahl zu. Dies ist nicht notwendig, stellt aber sicher, dass wir in allen Browsern die gleiche Hintergrundfarbe haben. - Der Selektor
dialog[open]:not(:focus-within)wird angewendet, wenn der Dialog geöffnet ist, aber keinen Fokus darauf oder darin hat. Das funktioniert, weil das native Element einopen-Attribut setzt, wenn es geöffnet ist. - Innerhalb dieser Regel ändern wir die
background-colorum einen minimalen Betrag. Dies ist die geringste Änderung, die erforderlich ist, um eine CSS-Animation auszulösen und gleichzeitig keine visuellen Unterschiede für den Benutzer zu verursachen (denken Sie daran, dass dies ein Dummy-Übergang ist). Außerdem setzen wir dietransition-Eigenschaft mit einer sehr kurzen Dauer, da wir möchten, dass sie so schnell wie möglich abgeschlossen wird und in JavaScript erkannt wird.
Ein Hauch von JavaScript
Jetzt müssen wir nur noch das Ende unseres ausgelösten CSS-Übergangs erkennen und das erste Element im Modal wieder fokussieren, wie folgt:
modal.addEventListener('transitionend', (e) => {
modal.querySelector('input').focus();
});
Wir fügen dem Modal einen transitionend-Listener hinzu und fokussieren im Callback das erste Eingabeelement im Modal. Fertig!
Einschränkungen
Dies ist ein kurzes Experiment, das ich durchgeführt habe, um einen funktionierenden Proof-of-Concept für Fokus-Falle mit der :focus-within-Pseudoklasse zu erstellen. Es hat mehrere Einschränkungen im Vergleich zu dedizierten JavaScript-Lösungen, um dies zu erreichen. Dennoch ist etwas besser als nichts!
Hier sind ein paar Dinge, die dieser Implementierung fehlen
- Laut W3C-Richtlinien sollte der Fokus auf dem fokussierbaren Element zirkulieren. Wir fokussieren aber immer auf das erste
input-Element. Das liegt daran, dass wir ohne mehr JavaScript nicht wissen können, ob der Fokus vom ersten oder vom letzten Element verloren gegangen ist. - Wir fokussieren immer wieder auf das erste
input-Element. Aber es gibt viele weitere fokussierbare HTML-Elemente, die vor deminputvorhanden sein könnten, oder vielleicht gibt es gar keininput-Element im Modal. Auch hier erkennen vollwertige JavaScript-Lösungen und pflegen eine Liste aller fokussierbaren Elemente und fokussieren das richtige.
Bessere (JavaScript) Implementierungen von Fokus-Falle
- Wie bereits erwähnt, ist ein funktionierendes Beispiel in der W3C-Dokumentation selbst verfügbar.
- Hier ist eine weitere Implementierung von Rodney Rehm, die ebenfalls auf Tabulator reagiert.
- Greg Kraus hat eine Bibliothek, die dies erreicht. Seine Implementierung pflegt eine Liste von Selektoren für alle gültigen fokussierbaren Elemente.
- Eine weitere leichtgewichtige Bibliothek zur Erstellung zugänglicher Modals.
Das war's für dieses Experiment. Wenn Ihnen dieser Trick gefallen hat, können Sie mir auf Twitter folgen, wo ich weitere Artikel und meine Nebenprojekte teile.
Das Dialog-Element funktioniert nicht in Firefox, Safari oder Edge, sodass die Demo dort nicht funktionsfähig ist. Dies ist eine Abspaltung, die nahe herankommt
Ich bin mir nicht einmal sicher, warum es nicht vollständig funktioniert, vielleicht kann sich jemand damit beschäftigen.
Hallo Chris, ich habe die Demos mit einem Polyfill für Dialog behoben. Und einen Haftungsausschluss für Safari hinzugefügt, wo selbst die normale Tabulator-Navigation nicht funktioniert!
Das
focusin-Ereignis blubbert, sodass Sie einfach so etwas tun können.Interessant! Dieses Ereignis (focusin) ist im Grunde die JS-Alternative für meinen CSS
focus-within-Ansatz.Haben Sie bei den Problemen mit der normalen Tastaturnavigation in Safari sichergestellt, dass unter Systemeinstellungen -> Tastatur -> Kurzbefehle die vollständige Tastaturzugriff auf "Alle Steuerelemente" eingestellt ist?
Ah, das war also das Problem. Das wusste ich nicht. Ich werde den Artikel aktualisieren. Danke :)
Schöne Arbeit und sehr interessantes Experiment.
Ich hätte nicht gedacht, dass
:focus-withinfür die Fokus-Falle verwendet werden kann.Wie Sie richtig sagten, ist der richtige Weg gemäß den W3C-Anforderungen, dies mit JS zu implementieren.
Daher hatte ich keine Wahl, als es für meine aktuellen und zukünftigen Projekte mit JS zu implementieren.
https://goo.gl/g1A958
Schöner Artikel, sieht nach einer guten Lösung für die Zukunft aus. Derzeit verwende ich 2 data-* Attribute, data-first und data-last, um Start- und Endpunkte zu definieren und höre nur auf Input und verschiebe das Letzte zum Ersten und umgekehrt für die Rückwärtsnavigation. Benötigt nur ein kleines bisschen JS und gibt die volle Kontrolle über die Reihenfolge.
Das Blur-Ereignis kann abgefangen werden, das einzige Problem ist, darauf zu warten, bis es vollständig verarbeitet ist, und den Fokus erst danach zurückzusetzen.
Hier ist die Beispiel-Fork: https://codepen.io/anon/pen/MVVqrq
Leider ist die Fokus-Falle nur ein Teil der Lösung, wenn es darum geht, Modals barrierefrei zu machen. Ein modales Dialogfeld sollte den Benutzer daran hindern, mit Dingen außerhalb des Modals zu interagieren – daher der Zweck Ihres Artikels. Benutzer von Screenreadern können immer noch auf die darunter liegenden Inhalte zugreifen, indem sie spezielle Tastenkombinationen verwenden, die den Zugriff auf diese Elemente ermöglichen. Abhängig von der Marke und Version ihres Betriebssystems/Browsers/Screenreaders könnten sie leicht Tastenkombinationen aktivieren, um auf darunter liegende Formularfelder, Überschriften usw. zuzugreifen, was Sie je nach Anwendungsfall verhindern möchten (sonst gäbe es ja gar keinen modalen Dialog, oder?). Die Lösung ist recht einfach: Wenden Sie
aria-hidden="true"auf den Inhalt außerhalb des Dialogs an.Das ist tatsächlich ein sehr guter Punkt, etwas, das ich vorher noch nie bedacht hatte. Ich habe mich im letzten Jahr viel damit beschäftigt, zugängliche Modals zu erstellen, bin aber nie auf diesen Punkt gestoßen. Toller Tipp!
Ich empfehle a11y-dialog 1, eine browserübergreifende Lösung für modale Dialoge, die Fokus-Falle standardmäßig unterstützt.
Das ist ein ziemlich cooles Experiment, obwohl Karl in Bezug auf die Barrierefreiheit recht hat.
Für mich ist die Inert-Eigenschaft (mit einem Polyfill) meine erste Wahl für die Handhabung von Modal-Interaktionen.