Skip to content

Adding keyboard interaction

Was müssen wir bei der Interaktion mit dem Keyboard alles beachten? Wir müssen

  • das Modal öffnen
  • das Modal schließen
  • Trap Focus innerhalb des Modals (siehe auch Hinweis unten!)
  • verhindern, dass User in das versteckte Modal reintabben

Um das Modal mit der Tastatur zu öffen, tabben wir zum Button (und fokussieren ihn damit) und drücken dann die Space oder Enter Taste. Wie wir schon wissen, funktioniert das, weil Space und Enter auf einem fokussierten Button ein click Event triggern.

Wenn sich das Modal öffnet, soll der User Benutzername und Passwort eingeben. Praktisch wäre, wenn der Fokus gleich nach dem Öffnen auf das Input-Feld gelegt wird.

const modal = document.querySelector('.modal');
const modalButton = modal.querySelector('button');
modalButton.addEventListener('click', e => {
document.body.classList.add('modal-is-open');
const input = modal.querySelector('input');
// Focus on input
input.focus();
});

Die letzte der drei Methoden, das Modal zu schließen, ist der Klick auf Escape. Dazu legen wir einen EventListener auf das Dokument und prüfen, ob die Escape-Taste gedrückt wurde. Wenn ja, und wenn das Modal offen ist, entfernen wir die Klasse .modal-is-open.

Damit der User dort weitertabben kann, wo er vorher aufgehört hat, legen wir den Fokus beim Schließen auf den ModalButton.

document.addEventListener('keydown', e => {
if ((e.key === 'Escape') && body.classList.contains('modal-is-open')) {
document.body.classList.remove('modal-is-open');
modalButton.focus();
}
});

Jetzt macht es Sinn, eine Funktion zum Schließen des Modals zu schreiben:

function closeModal() {
document.body.classList.remove('modal-is-open');
modalButton.focus();
}

Damit vereinfachen wir den EventListener:

document.addEventListener('keydown', e => {
if ((e.key === 'Escape') && body.classList.contains('modal-is-open')) {
closeModal();
}
});

Jetzt auch noch eine Funktion zum Öffnen des Modals:

function openModal() {
document.body.classList.add('modal-is-open');
const input = modal.querySelector('input');
input.focus();
}

Schließlich macht es noch Sinn zu fragen, ob das Modal geöffnet oder geschlossen ist:

/**
* Checks if modal is open
*/
function isModalOpen() {
return document.body.classList.contains('modal-is-open');
}

Damit vereinfachen wir den EventListener weiter:

document.addEventListener('keydown', e => {
if (isModalOpen() && e.key === 'Escape') {
closeModal();
}
});

Es ist wichtig, zwischen einem Dialog und einem Modal zu unterscheiden:

  • Bei einem Dialog kann der User weiter mit der übrigen Seite interagieren. Er kann das Dialogfenster herumschieben, in die Taskleiste schicken oder es auch maximieren. Wir nennen solche Elemente non-modal dialogs.
  • Ein Modal benötigt Aufmerksamkeit und muss “abgearbeitet” werden, bevor der User die Seite weiter benutzen kann. Solche Elemente nennen wir modal dialogs.

Wir gehen jetzt davon aus, dass wir es mit einem modal dialog zu tun haben, das wirklich wichtig ist. Hier müssen wir sicherstellen, dass der User nur mit Elementen innerhalb des Modals interagieren kann.

Mausbenutzer werden durch das Overlay an der Interaktion mit den dahinterliegenden Inhalten gehindert.
Für Tastaturbenutzer müssen wir eine Fokusfalle einbauen.

Wie geht das?
Dazu müssen wir alle fokussierbaren Elemente im Modal erfassen und sicherstellen, dass der Fokus bei Tab bzw. Shift und Tab innerhalb des Modals gehalten wird.

  • Wenn der User das letzte fokussierbare Element auf der Seite erreicht hat und die Tab-Taste drückt, setzen wir den Fokus auf das erste fokussierbare Element.
  • Wenn der User das erste fokussierbare Element auf der Seite erreicht hat und Tab und Shift drückt, setzen wir den Fokus auf das letzte fokussierbare Element.

Zuerst müssen wir die fokussierbaren Elemente rausfiltern und davon das erste und das letzte fokussierbare Element in einer Variablen speichern:

function openModal() {
//
// Trap focus
const focusables = modal.querySelectorAll('input, button');
const firstFocusable = focusables[0];
const lastFocusable = focusables[focusables.length - 1];
}

Wenn der User die Tab-Taste drückt (ohne Shift), prüfen wir, ob sie auf dem letzten fokussierbaren Element sind. Dafür verwenden wir document.activeElement. Wenn der User auf dem letzten fokussierbaren Element ist, legen wir den Fokus auf das erste fokussierbare Element.

function openModal() {
// ...
document.addEventListener('keydown', e => {
if (
document.activeElement === lastFocusable &&
e.key === 'Tab' &&
!e.shiftKey
) {
firstFocusable.focus();
}
});
}

Jetzt gibt es noch ein Problem: tatsächlich wird nicht das erste fokussierbare Element fokussiert, sondern das Input-Feld, das eigentlich erst an zweiter Stelle kommt. Das passiert deshalb, weil die Tab-Taste per default den Fokus auf das nächste fokussierbare Element legt. (Der Fokus rutscht also automatisch um ein Feld weiter?). Verhindern können wir das mit preventDefault(). Gleiches gilt natürlich auch im umgekehrten Fall:

function openModal() {
// ...
document.addEventListener('keydown', e => {
// directs to first focusable
if (
document.activeElement === lastFocusable &&
e.key === 'Tab' &&
!e.shiftKey
) {
e.preventDefault();
firstFocusable.focus();
}
// directs to the last focusable
if (
document.activeElement === firstFocusable &&
e.key === 'Tab' &&
e.shiftKey
) {
e.preventDefault();
lastFocusable.focus();
}
});
}

Wenn der User das Modal schließt und es aber trotzdem irgendwie schafft, ins Modal zu tabben (weil es nicht richtig ausgeblendet ist, siehe unten), gerät er in die Fokusfalle und kommt nicht mehr raus. Damit das nicht passieren kann, müssen wir den EventListener beim Schließen des Modals wieder entfernen. Und um das zu machen, müssen wir die gleiche Callback-Referenz an addEventListener und removeEventListener übergeben. Das bedeutet weiter, dass wir keine anonyme Arrow Function verwenden können, sondern eine eigene Funktion dafür schreiben müssen:

// Callback für die event listener
function trapFocus(e) {
const focusables = modal.querySelectorAll('input, button');
const firstFocusable = focusables[0];
const lastFocusable = focusables[focusables.length - 1];
if ((document.activeElement === lastFocusable) &&
(e.key === 'Tab') &&
(!e.shiftKey)) {
e.preventDefault();
firstFocusable.focus();
}
if ((document.activeElement === firstFocusable) &&
(e.key === 'Tab') &&
(e.shiftKey)) {
e.preventDefault();
lastFocusable.focus();
}
}

Die Funktionen zum Öffnen und Schließen des Modals bzw. die EventListener werden damit sehr übersichtlich:

// Traps focus
function openModal(e) {
// Adding an event listener
document.addEventListener('keydown', trapFocus);
}
// Removes focus trap
function closeModal(e) {
// Removing an event listener
document.removeEventListener('keydown', trapFocus);
}

Selbst wenn es jemand schaffen sollte, ins versteckte Modal zu tabben, kommt er problemlos wieder raus. Aber besser ist es, diese Möglichkeit ganz auszuschließen:

Preventing users from tabbing into a hidden modal

Section titled “Preventing users from tabbing into a hidden modal”

Wenn User nicht mit einem Modal interagieren sollen, sollen sie auch keine Elemente darin fokussieren können. Wir verstecken das Modal also wieder mit visibility: hidden und machen es mit visibility: visible wieder sichtbar, wenn <body> die Klasse .modal-is-open hat.
Und wieder geht die Transition kaputt, daher bekommt sie im geöffneten Zustand ein transition-delay.

.modal-overlay {
transition:
opacity 0.3s ease-out,
z-index 0s 0.3s,
visibility: 0s 0.3s;
visibility: hidden;
}
.modal-is-open .modal-overlay {
transition-delay: 0s;
visibility: visible;
}