Skip to content

Adding keyboard interaction

Jetzt wollen wir

  • die Accordeons öffnen
  • die Accordeons schließen
  • mit den Pfeiltasten zwischen den Accordeons wechseln
  • verhindern, dass man ein geschlossenes Accordeon mit der Tab-Taste erreicht

Das Öffnen des Accordeons mit der Tastatur übernimmt der Button im Accordeon Header für uns. Wenn wir zum Header tabben, können wir mit Enter oder der Leertaste einen Mausklick triggern. Der Klick auf den Button bzw. auf eines der Elemente im Button (Überschrift, Icon) wird über die Event Delegation im Accordeon Header “aufgefangen”.

Wir können in das geöffnete Accordeon tabben, darin enthaltene Links werden fokussiert, ohne dass wir irgendein Fokusmanagement betreiben müssten.

Preventing people from tabbing into a closed accordion

Section titled “Preventing people from tabbing into a closed accordion”

Wenn das Accordeon geschlossen ist, sollte man allerdings nicht reintabben können. Das ist im Moment noch möglich, daher fügen wir visibility: hidden dazu, wenn das Accordeon geschlossen ist. Wenn es geöffnet wird, wechseln wir zu visibility: visible.

.accordion-content {
visibility: hidden;
.accordion.is-open & {
visibility: visible;
}
}

Jetzt passiert aber etwas komisches: wenn wir das Accordeon öffnen, ist alles gut, aber wenn wir den Schließen-Button drücken, verschwindet der Inhalt sofort (also bevor das Accordeon tatsächlich geschlossen wird). Warum ist das so?
visibility hat keine Transition. Um die Animation zu reparieren, müssen wir ein transition-delay für die Sichtbarkeit ergänzen, wenn das Accordeon geschlossen wird.

// Animation when closing
.accordion-content {
transition:
height 0.3s ease-out,
visibility 0s 0.3s;
}
// Animation when opening
.accordion.is-open .accordion-content {
transition:
height 0.3s ease-out,
visibility 0s 0s;
}

Das Schließen des Accordeons mit der Tastatur geht genauso out of the box wie das Öffnen.

Möglicherweise möchten User das Accordeon mit der <kbd>Escape</kbd> Taste schließen (es muss aber nicht sein). Sicherheitshalber schauen wir uns das einmal an.

Was muss passieren?

  1. Wir müssen auf einen keydown Event lauschen
  2. Wir müssen prüfen, ob es sich beim keydown Event um die Escape-Taste gehandelt at
  3. Wir müssen prüfen, ob sich der User innerhalb eines Accordeons befindet
  4. Wir müssen prüfen, ob das Accordeon geöffnet ist
  5. Wenn die Punkte 1-4 erfüllt sind, müssen wir das Accordeon schließen

Warum keydown? Weil wir wollen, dass das Accordeon sofort geschlossen wird, wenn der User die Taste drückt (wir wollen nicht warten, bis er sie wieder loslässt):

document.addEventListener('keydown', e => {
// Do sth.
});

Jetzt prüfen wir, ob die Escape-Taste gedrückt wurde und machen gleich einen Early Return, wenn es nicht die Escape-Taste war:

document.addEventListener('keydown', e => {
if (e.key !== 'Escape') return;
});

Im dritten Schritt prüfen wir, ob der User ein Element innerhalb des Accordeons fokussiert hat. Wenn kein Fokus im Accordeon ist, wissen wir nicht, welches Accordeon fokussiert wurde und wissen deshalb auch nicht, welches wir schließen müssen (und machen daher gar nichts). Das aktuell fokussierte Element erhalten wir mit document.activeElement.

document.addEventListener('keydown', e => {
const accordion = e.target.closest('.accordion');
if (e.key !== 'Escape') return;
if (!accordion) return;
});

Im vierten Schritt prüfen wir, ob das Accordeon geöffnet ist. Wenn es nicht geöffnet ist, gibt es nichts zu tun:

document.addEventListener('keydown', e => {
const accordion = e.target.closest('.accordion');
if (e.key !== 'Escape') return;
if (!accordion) return;
if (accordion.classList.contains('is-open')) {
const accordionContent = accordion.querySelector('.accordion-content');
accordion.classList.remove('is-open');
accordionContent.style.height = 0;
}
});

Wenn der User das Accordeon schließt, gehen wir davon aus, dass er den Inhalt nicht mehr lesen will. Deshalb sollten wir den Fokus auch wieder aus dem Inhalt herausnehmen und auf den Accordeon-Header setzen. Der Accordeon-Header selbst ist zwar kein fokussierbares Element, aber der darin enthaltene Button schon. Daher setzen wir den Fokus auf den Button (den wir zuerst aber einmal selektieren müssen):

document.addEventListener('keydown', e => {
// ...
if (accordion.classList.contains('is-open')) {
const accordionHeaderButton = accordion
.querySelector('.accordion-header')
.querySelector('button');
const accordionContent = accordion.querySelector('.accordion-content');
accordion.classList.remove('is-open');
accordionContent.style.height = 0;
accordionHeaderButton.focus();
}
});

Functions for opening and closing the accordion

Section titled “Functions for opening and closing the accordion”

Wir haben jetzt zwei Codesets zum Schließen des Accordeons: einmal in der Funktion updateAccordion() und einmal im keydown Event-Listener:

// From updateAccordion
function updateAccordion(accordion, height) {
// ...
accordionContent.style.height = `${height}px`;
accordion.classList.toggle('is-open');
}
// From the keydown event listener
document.addEventListener('keydown', e => {
// ...
if (!accordion.classList.contains('is-open')) return;
accordion.classList.remove('.is-open');
accordionContent.style.height = 0;
accordionHeaderButton.focus();
})

Das macht irgendwie keinen Sinn, daher wollen wir eine Funktion closeAccordion machen:

function closeAccordion(accordion) {
const accordionContent = accordion.querySelector('.accordion-content');
const accordionHeaderButton = accordion
.querySelector('.accordion-header')
.querySelector('button');
accordion.classList.remove('is-open');
accordionContent.style.height = 0;
accordionHeaderButton.focus();
}

Konsequenterweise erstellen wir uns jetzt auch eine Funktion openAccordion:

function openAccordion(accordion) {
const accordionContent = accordion.querySelector('.accordion-content');
const height = getContentHeight(accordion);
accordion.classList.add('is-open');
accordionContent.style.height = `${height}px`;
}

Sobald wir die Funktionen openAccordion() und closeAccordion() haben, muss getContentHeight nicht mehr vom Status des Accordeons abhängig sein. Wir können einfach immer die Höhe des geöffneten Accordeons abfragen, weil wir sie beim Schließen eh immer auf Null setzen.

function getContentHeight(accordion) {
const accordionInner = accordion.querySelector('.accordion-inner');
return accordionInner.getBoundingClientsRect().height;
}

Die Funktion openAccordion() lässt sich auch ein bisschen einkürzen:

function openAccordion(accordion) {
const accordionContent = accordion.querySelector('.accordion-content');
accordion.classList.add('is-open');
accordionContent.style.height = `${getContentHeight(accordion)}px`;
}

Schließlich macht es noch Sinn, eine Funktion zu haben, die prüft, ob das Accordeon geöffnet ist:

function isAccordionOpen(accordion) {
return accordion.classList.contains('is-open');
}

Verwenden von openAccordion() und closeAccordion():

// im Click-Event
accordionContainer.addEventListener('click', e => {
// ...
isAccordionOpen(accordion) {
? closeAccordion(accordion)
: openAccordion(accordion);
}
});
// im Keydown-Event
document.addEventListener('keydown', e => {
// ...
if (isAccordionOpen(accordion)) {
closeAccordion(accordion);
}
});

Zuletzt wollen wir noch mit den Pfeiltasten durch die Accordeons tabben:

  • Wenn der User einen Accordeon-Header fokussiert hat und die Pfeil-nach-unten-Taste drückt, wechseln wir zum nächsten Accordeon.
  • Wenn der User einen Accordeon-Header fokussiert hat und die Pfeil-nach-oben-Taste drückt, welchseln wir zum vorhergehenden Accordeon.

Was ist also zu tun?

  1. Auf ein keydown Event lauschen
  2. Prüfen, ob der Fokus gerade auf einem Accordeon-Header liegt
  3. Prüfen, ob der User die Pfeil-nach-oben oder die Pfeil-nach-unten-Taste gedrückt hat
  4. Wenn die Pfeil-nach-oben-Taste gedrückt wurde, fokussieren wir das vorhergehende Accordeon
  5. Wenn die Pfeil-nach-unten-Taste gedrückt wurde, fokussieren wir das nächste Accordeon
document.addEventListener('keydown', e => {
if (!e.target.closest('.accordion-header')) return;
const key = e.key;
const accordion = e.target.closest('.accordion');
const accordions = [...accordionContainer.querySelectorAll('.accordion')];
const index = accordions.findIndex(el => el === accordion);
if (key === 'ArrowUp' && accordions[index + 1]) {
accordions[index + 1].querySelector('button').focus();
}
if (key === 'ArrowDown' && accordions[index - 1]) {
accordions[index - 1].querySelector('button').focus();
}
});

Wir können diesen Code noch ein bisschen zusammenräumen, indem wir eine Variable targetAccordion deklarieren. Mit einem if/else-Statement finden wir das targetAccordion, bevor wir es fokussieren.

document.addEventListener('keydown', e => {
// ...
let targetAccordion;
if (key === 'ArrowDown') {
targetAccordion = accordions[index + 1];
} else if (key === 'ArrowUp') {
targetAccordion = accordions[index - 1];
}
if (targetAccordion) {
targetAccordion.querySelector('button').focus;
}
});