Ein Menü mit mehr Accessibility

In den letzten Tagen habe ich immer mal wieder an meinem Hamburger-Menü auf kleinen Devices gearbeitet und es zugänglicher gemacht. Zunächst einmal war es ja sowieso schon eine Checkbox, was den einfachen Vorteil hat, dass das Menü auch dann noch funktioniert, wenn JavaScript einmal deaktiviert oder kaputt ist: Ich prüfe im CSS einfach auf die :checked-Pseudoklasse des input und zeige abhängig davon mittels general sibling combinator das Menü an oder eben nicht:

nav input:checked ~ ul {
  display: block;
}

So etwas mit einem button umzusetzen ist ohne JavaScript nicht möglich.

Das Label

Im Anschluss habe ich nun das Ganze mal mit Voiceover getestet und weiter angepasst. Das Label meiner Checkbox erhielt zuerst einmal zwei zusätzliche Attribute:

<label
  for="toggle"
  aria-label="Menü anzeigen"
  data-alternative="Menü ausblenden"
>

Mit aria-label wird ein „richtiger Text“ bei Fokus des Hamburgers verlesen; der Inhalt von data-alternative wird mittels JavaScript in das Label geschrieben, wenn die Checkbox aktiviert wurde:

document.querySelector('nav input').addEventListener('change', () => {
  const 🍔 = document.querySelector('nav label')
  const 💬 = 🍔.getAttribute('aria-label')
  const 💭 = 🍔.dataset.alternative
  🍔.setAttribute('aria-label', 💭)
  🍔.dataset.alternative = 💬
})

Tastaturfokus

Soweit so gut. Im nächsten Schritt musste ich mich an den Tastaturfokus wagen, denn wenn man munter durch’s geöffnete Menü tabbt, landet man nach dem letzten Link im Dokument – und das ist nicht clever. Tatsächlich soll sich das Menü wie ein Modal verhalten.

The common way to move around controls is via the tab key. So you’ll have to make sure your keyboard focus never leaves the dialog. — Marco Zehe

Also bemühte ich erneut JavaScript und erweiterte den onChange-Listener:

// Setup menu items and tab handling
const ☑️ = document.querySelector('nav input')
const 🔗 = document.querySelector('nav li:last-of-type a')
const 🔜 = event => {
  const {keyCode, shiftKey} = event
  if (keyCode === 9 && !shiftKey) {
    event.preventDefault()
    ☑️.focus()
  }
}
const 🔚 = event => {
  const {keyCode, shiftKey} = event
  if (keyCode === 9 && shiftKey) {
    event.preventDefault()
    🔗.focus()
  }
}

// Handle menu
☑️.addEventListener('change', event => {
  const 🍔 = document.querySelector('nav label')
  const 💬 = 🍔.getAttribute('aria-label')
  const 💭 = 🍔.dataset.alternative
  🍔.setAttribute('aria-label', 💭)
  🍔.dataset.alternative = 💬
  if (event.target.checked) {
    ☑️.addEventListener('keydown', 🔚, false)
    🔗.addEventListener('keydown', 🔜, false)
  } else {
    ☑️.removeEventListener('keydown', 🔚, false)
    🔗.removeEventListener('keydown', 🔜, false)
  }
}, false)

Auf diese Weise konnte ich sicherstellen, dass ein TAB nach dem letzten Menülink wieder den Schließen-Button fokussiert, ein SHIFT+TAB auf dem Button hingegen zurück auf den letzten Link führt.

Eine Fluchtroute

As is customary in dialogs, there is usually a way to escape out of it without making changes. Often, this does the same as whatever the Cancel button does. You’ll have to provide the same functionality to your dialog. So, make sure pressing Escape will close your dialog, and usually discard any changes. — Marco Zehe

Für das grande finale fehlte noch ein Fluchtweg mittels ESC. Via onKeyUp-Event horche ich auf den zugehörigen Key-Code, schaue ob das Menü geöffnet ist, und falls ja, deaktiviere ich die Checkbox, triggere das onChange-Event zum Zurücksetzen des Hamburger-Buttons und fokussiere den Hamburger – denn:

In any case, after you close the dialog, you must!!!, you absolutely must!!! then set focus back to the element that opened the dialog […] — Marco Zehe

Der Code dazu:

// Close menu when hitting ESC key
document.addEventListener('keyup', event => {
  if (event.keyCode === 27 && ☑️.checked) {
    ☑️.checked = false
    ☑️.dispatchEvent(new Event('change'))
    ☑️.focus()
  }
}, false)

Das ist eigentlich auch schon die ganze Magie. Man mag JavaScript auch heutzutage immer noch gerne verteufeln, aber für die Accessibility ist und bleibt es ein unverzichtbares Werkzeug.