Manchmal kann es sinnvoll sein, den Fokus in einem bestimmten Website-Bereich zu fangen, sodass kein fokussierbares Element außerhalb dieses Bereichs angesteuert werden kann (zum Beispiel über die Tab-Taste der Tastatur). Hier zeige ich, wie sich das umsetzen lässt.
Die Ausgangslage
Ich habe nach einer Lösung für das angesprochene Problem gesucht, als ich mein Fanoe-Theme überarbeitet habe. Das Theme kommt mit einer Off-Canvas-Sidebar, die seit der Überarbeitung nicht mehr den Inhalt mit nach links verschiebt, sondern darüber eingeblendet wird, wie in folgendem GIF zu sehen:
Wenn die Sidebar eingeblendet ist, soll der Fokus in der Sidebar gefangen sein, sodass ein Tastatur-User andere Fokus-Elemente außerhalb des Sidebar-Elements nicht ansteuern kann, bevor die Sidebar über den Button oder die Esc-Taste wieder ausgeblendet wird.
Umsetzung der Lösung mit ally.js
Die JavaScript-Bibliothek ally.js bietet eine Lösung für das Problem an und veranschaulicht die Funktionsweise in einem Tutorial. In meinem Theme sieht das entscheidende Markup so aus:
<button class="sidebar-button -open">
<span class="screen-reader-text">Show Sidebar</span>
<span aria-hidden="true">≡</span>
</button>
<aside class="sidebar" role="sidebar">
<button class="sidebar-button -close">
<span class="screen-reader-text">Close Sidebar</span>
<span aria-hidden="true">≡</span>
</button>
<div class="sidebar-content">
<!-- The widgets -->
</div>
</aside>
Code-Sprache: HTML, XML (xml)
Und das ist der wichtige JS-Teil (ich nutze Webpack, um den import
aufzulösen, und Babel, um ES6-Syntax in ES5 umzuwandeln – ich habe einen kleinen Beitrag zur Einrichtung davon geschrieben):
import ally from 'ally.js/ally';
/**
* Custom JavaScript functions.
*
* @version 2.0.3
*
* @package Fanoe
*/
{
/**
* Get the html element.
*
* @type {Element}
*/
const root = document.documentElement;
/**
* Get the elements for sidebar handling.
*/
const openButton = document.querySelector('.sidebar-button.-open');
const closeButton = document.querySelector('.sidebar-button.-close');
const sidebarElem = document.querySelector('.sidebar');
let disabledHandle;
let tabHandle;
let keyHandle;
/**
* Call sidebar function sidebar button click.
*/
openButton.addEventListener('click', sidebar, false);
/**
* Call sidebar function sidebar button click.
*/
closeButton.addEventListener('click', sidebar, false);
/**
* Catch clicks on the document to close sidebar on mouse click
* outside the open sidebar.
*/
document.body.addEventListener('click', function (e) {
/**
* Check if the sidebar is visible.
*/
if (root.classList.contains('active-sidebar')) {
/**
* Check if the click was made on the .sidebar element, not the .sidebar-content.
*/
if (e.explicitOriginalTarget.classList.contains('sidebar')) {
disabledHandle.disengage();
tabHandle.disengage();
root.classList.remove('active-sidebar');
}
}
}, false);
/**
* Function to display and hide sidebar.
*
* @link https://allyjs.io/tutorials/accessible-dialog.html
*/
function sidebar() {
/**
* Toggle .active-sidebar class.
*/
root.classList.toggle('active-sidebar');
/**
* Check for class name to know if the
* sidebar is currently visible or not.
*/
if (root.classList.contains('active-sidebar')) {
disabledHandle = ally.maintain.disabled({
filter: sidebarElem,
});
tabHandle = ally.maintain.tabFocus({
context: sidebarElem,
});
keyHandle = ally.when.key({
escape: closeSidebarByKey,
});
} else {
disabledHandle.disengage();
tabHandle.disengage();
}
}
}
Code-Sprache: JavaScript (javascript)
Der Code orientiert sich stark an dem oben verlinkten Tutorial. Zunächst importieren wir die ally-Bibliothek, holen das Root-Element und speichern die Buttons und das Sidebar-Element. Danach fügen wir die benötigten EventListener hinzu:
- Klick auf die beiden Buttons. Davon wird die
sidebar()
-Funktion aufgerufen, um die Sidebar anzuzeigen oder auszublenden. - Klick auf das
body
-Element. Nach einem Klick wird geprüft, ob die Sidebar gerade angezeigt wird. In dem Fall wird danach geguckt, ob der Klick auf das.sidebar
-Element ausgeführt wurde. Wenn das so ist, wurde der Klick auf den ausgegrauten Bereich außerhalb des Sidebar-Inhalts ausgeführt (das liegt daran, weil der graue Bereich über dem Rest der Website durch eine.sidebar::before
-Regel erstellt wurde).
In sidebar()
wird zunächst für das Root-Element die Klasse active-sidebar
hinzugefügt oder entfernt – je nachdem, ob sie vorhanden ist oder nicht. Anschließend wird geprüft, ob die Klasse gerade vorhanden ist (die Sidebar also durch den Klick auf den Anzeigen-Button eingeblendet wurde) und in diesem Fall über ally.maintain.disabled
allen Elementen außer denen innerhalb des Sidebar-Elements die Fokus-Möglichkeit entzogen. Über ally.maintain.tabFocus
wird danach noch der Tab-Fokus auf die Sidebar gesetzt, damit nach dem letzten fokussierbaren Element der Sidebar nicht zum Browser-UI gesprungen wird, sondern zum ersten Fokus-Element der Sidebar.
Anschließend legen wir fest, dass bei Aufruf der Escape-Taste sidebar()
aufgerufen werden soll, um die Sidebar zu schließen. Wenn active-sidebar
nicht gesetzt ist, werden die ganzen Beschränkungen wieder rückgängig gemacht.