Skip to content

Instantly share code, notes, and snippets.

@wiegertschouten
Last active October 17, 2024 08:57
Show Gist options
  • Save wiegertschouten/3d9e139424c8b5b8313110cf3e71cdd8 to your computer and use it in GitHub Desktop.
Save wiegertschouten/3d9e139424c8b5b8313110cf3e71cdd8 to your computer and use it in GitHub Desktop.
A simple JavaScript class to trap focus within a given element.
import { FocusTrap } from './focus-trap.js';
// Select a menu and its toggle button
const menuToggle = document.getElementById('menu-toggle');
const menu = document.getElementById('menu');
// Create a new FocusTrap instance on the menu
const focusTrap = new FocusTrap(menu);
// Prepend the menu toggle button to the focusable elements
focusTrap.prepend(menuToggle);
// Add a click event listener to the menu toggle button
menuToggle.addEventListener('click', () => {
if (menu.hasAttribute('hidden')) {
menuToggle.setAttribute('aria-expanded', 'true');
menu.removeAttribute('hidden');
// Enable the focus trap
focusTrap.enable();
} else {
menuToggle.setAttribute('aria-expanded', 'false');
menu.setAttribute('hidden', '');
// Disable the focus trap
focusTrap.disable();
}
});
/**
* A simple JavaScript class to trap focus within a given element.
*
* Inspired by / based on:
* https://hidde.blog/using-javascript-to-trap-focus-in-an-element/
* https://zellwk.com/blog/keyboard-focusable-elements/
*/
export class FocusTrap {
#handler = this.#handleKeydown.bind(this);
#lastFocusedElement;
#focusableElements;
constructor(element) {
this.#focusableElements = this.#getFocusableElements(element);
}
#getFocusableElements(container) {
return [...container.querySelectorAll(
'a, button, input:not([type="hidden"]), textarea, select, details, iframe, embed, object, summary, dialog, audio[controls], video[controls], [contenteditable], [tabindex]'
)].filter(element => {
if (window.getComputedStyle(element).display === 'none') return false;
if (element.hasAttribute('disabled')) return false;
if (element.hasAttribute('hidden')) return false;
return true;
});
}
#handleKeydown(e) {
if (e.key !== 'Tab') {
return;
}
if (e.shiftKey && document.activeElement === this.#firstFocusableElement) {
this.#lastFocusableElement.focus();
e.preventDefault();
} else if (!e.shiftKey && document.activeElement === this.#lastFocusableElement) {
this.#firstFocusableElement.focus();
e.preventDefault();
}
}
get #firstFocusableElement() {
return this.#focusableElements[0];
}
get #lastFocusableElement() {
return this.#focusableElements[this.#focusableElements.length - 1];
}
enable() {
this.#lastFocusedElement = document.activeElement;
this.#firstFocusableElement.focus();
document.addEventListener('keydown', this.#handler);
}
disable() {
document.removeEventListener('keydown', this.#handler);
this.#lastFocusedElement.focus();
}
prepend(...elements) {
this.#focusableElements = [...elements, ...this.#focusableElements];
}
append(...elements) {
this.#focusableElements = [...this.#focusableElements, ...elements];
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment