Skip to content

Adding keyboard interaction

Nun wollen wir die Off-Canvas-Navigation mit dem Keyboard bedienbar machen.

Das Öffnen der OCN funktioniert schon out of the box: wir tabben zum Button und drücken dann die Space oder Enter Taste. Space und Enter triggern ein click Event auf einem fokussierten Button.

Wenn ein User die OCN mit einem Keyboard öffnet, erwartet er, dass der Fokus anschließend auf der Navigation liegt. Das passiert im Moment noch nicht. Der Fokus geht zum Browsertab (oder, je nach Browser, sonstwohin, aber jedenfalls raus aus dem <document>). Das ist so, weil der Button das letzte fokussierbare Element auf der Seite ist. Gäbe es danach noch eines (z.B. einen Link), würde der Fokus dorthin gelegt werden.

Um den Fokus zur Navigation zu bringen, müssen wir der Navigation ein tabindex="-1" mitgeben.

<div class="offsite-container">
<nav class="nav" tabindex="-1">
<!-- ... -->
</nav>
</div>

Jetzt können wir mit JavaScript den Fokus auf die Navigation legen, wenn sie geöffnet ist.

const nav = document.querySelector('nav');
button.addEventListener('click', e => {
if (body.classList.contains('offsite-is-open')) {
body.classList.remove('offsite-is-open');
} else {
body.classList.add('offsite-is-open');
nav.focus();
}
});

Die Navigation wird jetzt fokussiert – allerdings wollen wir den blauen Fokusring nicht anzeigen, da der User mit dem Element direkt ja nicht interagieren kann. Daher fügen wir folgendes CSS hinzu (am besten ins reset.scss):

[tabindex="-1"] {
outline: none !important;
}

Tabben wir weiter, wird der erste Navigationspunkt fokussiert.

Wir können die OCN schließen, indem wir zum Button tabben und wieder Space oder Enter drücken. Allerdings ist das ein bisschen mühsam. Zusätzlich wollen wir die Navigation noch mit der Enter Taste schließen. Dazu richten wir einen EventListener ein, der auf ein keydown Event lauscht. Wenn es ein keydown auf den key “Enter” ist und wenn die Navigation geöffnet ist, also offsite-is-open in <body> enthalten ist, entfernen wir die Klasse offsite-is-open.

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

Sobald die Navigation geschlossen ist, wollen wir den Fokus wieder dorthin zurücksetzen, wo er vorher war: auf den Button. Wir ergänzen also:

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

Nun haben wir ein paar Redundanzen im Code: wir checken zwei Mal, ob die Navigation geöffnet ist und wir haben das OCN zwei Mal geschlossen. Diese Doubletten können wir in eigene Funktionen auslagern. Und wenn wir schon dabei sind, schreiben wir auch eine Funktion zum Öffnen der Navigation:

// check whether the sidebar is open
function isOpenOffCanvasNavigation() {
return body.classList.contains('offsite-is-open');
}
// closes the sidebar
function closeOffCanvasNavigation() {
body.classList.remove('offsite-is-open');
button.focus();
}
// opens the sidebar
function openOffCanvasNavigation() {
body.classList.add('offsite-is-open');
nav.focus();
}

Wir können die neuen Funktionen folgendermaßen nutzen:

// in the click event
button.addEventListener('click', e => {
isOffcanvasNavigationOpen() ? closeOffCanvasNavigation() : openOffCanvasNavigation();
});
// in the keydown event
document.addEventListener('keydown', e => {
if (isOffCanvasNavigationOpen() && e.key === 'Escape') {
closeOffCanvasNavigation();
}
});

Einer der blödesten Fehler, den man hier machen kann, ist es, Elemente innerhalb der OCN im geschlossenen Zustand mit der Tabtaste erreichbar zu machen. Wenn User nicht mit einem Element interagieren können, dürfen sie es auch nicht fokussieren können. Wir setzen daher visibility auf hidden. Sobald die OCN eingeblendet wird, machen wir sie auch wieder sichtbar:

.offsite-container {
visibility: hidden;
}
.offsite-is-open .offsite-container {
visibility: visible;
}

Das macht dann leider die Transition kaputt, die OCN schließt sich abrupt. Daher müssen wir ein transition-delay für visibility ergänzen:

.offsite-container {
transition:
transform 0.3s ease-out,
visibility 0.3s;
visibility: hidden;
}
.offsite-is-open .offsite-container {
transition-delay: visibility 0.3s;
visibility: visible;
}