Fokus in bestimmtem Website-Bereich fangen

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.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

To respond on your own website, enter the URL of your response which should contain a link to this post's permalink URL. Your response will then appear (possibly after moderation) on this page. Want to update or remove your response? Update or delete your post and re-enter your post's URL again. (Find out more about Webmentions.)