Last active
June 12, 2025 13:56
-
-
Save guelzow/a580d23bd2079dccd4d85647a18cffb9 to your computer and use it in GitHub Desktop.
Focus Trap
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Focus Trap | |
* | |
* This script provides a focus trap functionality for modal dialogs. | |
* | |
* @usage | |
* To use this focus trap, call `focusTrap.init()` with the selector of the modal dialog element. | |
* You can also provide an optional selector for a close button within the modal dialog. | |
* This will ensure that when the modal dialog is open, the focus remains within the dialog, | |
* cycling through the focusable elements and preventing focus from leaving the dialog. | |
* | |
* Example 01: | |
* focusTrap.init('#myModal', '.close-button'); | |
* This will initialize the focus trap for the modal dialog with the ID `myModal` | |
* and the close button with the class `close-button`. | |
* | |
* Example 02: | |
* focusTrap.init('#myModal'); | |
* Init the focus trap for the modal dialog with the ID `myModal` | |
* without a close button. | |
* gallery.listen('destroy', focusTrap.destroy); | |
* This will destroy the focus trap when the modal dialog is closed. | |
* Keep in mind, that there are a lot of different modal dialog implementations, | |
* so you might need to adapt the event registration. | |
* This example is for PhotoSwipe. | |
* | |
* Example 03: | |
* focusTrap.init('#headerSearch'); | |
* This will initialize the focus trap for the modal dialog with the ID `headerSearch`. | |
* focusTrap.setEscapeKeyListener(function (event) { | |
* $headersearch.collapse('hide'); | |
* }); | |
* This will set a custom listener for the Escape key, | |
* which will collapse the header search when the Escape key is pressed. | |
* | |
* Example 04: | |
* focusTrap.updateFocusableElements(); | |
* This will update the list of focusable elements within the modal dialog. | |
* Normally, you do not need to call this function, but it can be useful if the modal dialog content changes | |
* dynamically or if new focusable elements are added or removed. | |
* | |
*/ | |
(function () { | |
'use strict'; | |
const focusTrap = {}; | |
focusTrap.modalElement = null; | |
focusTrap.focusableElements = null; | |
focusTrap.firstElement = null; | |
focusTrap.lastElement = null; | |
focusTrap.closeButton = null; | |
focusTrap.escapeKeyListener = function () {}; | |
focusTrap.element = null; | |
/** | |
* Initializes the focus trap for a modal dialog. | |
* | |
* This function sets up the focus trap by | |
* finding all focusable elements within the modal dialog | |
* and adding an event listener for the keydown event. | |
* It ensures that when the Tab key is pressed, | |
* the focus remains within the modal dialog, | |
* cycling through the focusable elements. | |
* | |
* @param {String} elementSelector | |
* The selector for the modal dialog element. | |
* This should be a valid CSS selector | |
* that matches the modal dialog you want to trap focus within. | |
* @param {String} closeButtonSelector | |
* Optional selector for a close button within the modal dialog. | |
* If provided, clicking this button will destroy the focus trap. | |
* If you do not provide a close button selector, | |
* you will have to call `focusTrap.destroy()` manually. | |
* | |
* @returns {void} | |
*/ | |
focusTrap.init = function (elementSelector, closeButtonSelector) { | |
focusTrap.modalElement = document.querySelector(elementSelector); | |
focusTrap.modalElement.addEventListener('keydown', focusTrap.handleTabKeyPress); | |
focusTrap.updateFocusableElements(); | |
if (closeButtonSelector) { | |
focusTrap.closeButton = focusTrap.modalElement.querySelector(closeButtonSelector); | |
if (focusTrap.closeButton) { | |
focusTrap.closeButton.addEventListener('click', focusTrap.destroy); | |
focusTrap.closeButton.focus(); | |
} | |
} | |
if (focusTrap.closeButton) { | |
focusTrap.closeButton.focus(); | |
} else { | |
focusTrap.firstElement.focus(); | |
} | |
} | |
/** | |
* Updates the list of focusable elements within the modal dialog. | |
* | |
* This function scans the modal dialog for all focusable elements and updates the `focusableElements`, | |
* `firstElement`, and `lastElement` properties. | |
* It should be called whenever the modal dialog content changes or when the focusable elements need to be refreshed. | |
* This is useful if the modal dialog content is dynamic or if new focusable elements are added or removed. | |
* | |
* @returns {void} | |
*/ | |
focusTrap.updateFocusableElements = function () { | |
focusTrap.focusableElements = []; | |
let el, parentEl, isFocusable; | |
let possibleElements = focusTrap.modalElement.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); | |
for (let i = 0; i < possibleElements.length; i++) { | |
el = possibleElements[i]; | |
parentEl = el; | |
isFocusable = true; | |
while (parentEl) { | |
// If the element is inside a hidden parent, skip it | |
if ( | |
window.getComputedStyle(parentEl, null).display === 'none' | |
|| parentEl.style.display === 'none' | |
|| parentEl.disabled | |
|| window.getComputedStyle(parentEl, null).visibility === 'hidden' | |
|| parentEl.style.visibility === 'hidden' | |
) { | |
isFocusable = false; | |
break; | |
} | |
parentEl = parentEl.parentElement; | |
} | |
// Check if the element is visible and not disabled | |
if (isFocusable) { | |
focusTrap.focusableElements.push(el); | |
} | |
} | |
focusTrap.firstElement = focusTrap.focusableElements[0]; | |
focusTrap.lastElement = focusTrap.focusableElements[focusTrap.focusableElements.length - 1]; | |
} | |
/** | |
* Destroys the focus trap by removing the event listener and resetting the focus trap properties. | |
* | |
* This function should be called when the modal dialog is closed | |
* to clean up the focus trap state. | |
*/ | |
focusTrap.destroy = function () { | |
if (focusTrap.modalElement) { | |
focusTrap.modalElement.removeEventListener('keydown', focusTrap.handleTabKeyPress); | |
} | |
if (focusTrap.closeButton) { | |
focusTrap.closeButton.removeEventListener('click', focusTrap.destroy); | |
} | |
focusTrap.closeButton = null; | |
focusTrap.modalElement = null; | |
focusTrap.focusableElements = null; | |
focusTrap.firstElement = null; | |
focusTrap.lastElement = null; | |
focusTrap.element = null; | |
focusTrap.escapeKeyListener = function () {}; | |
} | |
/** | |
* Handles the Tab key press to trap focus within the modal dialog. | |
* | |
* This function prevents the focus from leaving the modal dialog | |
* and cycles through the focusable elements. | |
* | |
* @param {KeyboardEvent} event - The keydown event triggered by pressing the Tab key. | |
* | |
* @returns {void} | |
*/ | |
focusTrap.handleTabKeyPress = function (event) { | |
focusTrap.updateFocusableElements(); | |
if (event.key === 'Tab') { | |
let activeElem = document.activeElement; | |
if (event.shiftKey && activeElem.isEqualNode(focusTrap.firstElement)) { | |
event.preventDefault(); | |
focusTrap.lastElement.focus(); | |
return; | |
} | |
if (!event.shiftKey && activeElem.isEqualNode(focusTrap.lastElement)) { | |
event.preventDefault(); | |
focusTrap.firstElement.focus(); | |
} | |
} | |
if (event.key === 'Escape') { | |
event.preventDefault(); | |
focusTrap.escapeKeyListener(event); | |
} | |
} | |
/** | |
* Sets a custom listener for the Escape key. | |
* | |
* This function allows you to define a custom action | |
* when the Escape key is pressed while the focus trap is active. | |
* You can use this to close the modal dialog or perform any other action. | |
* | |
* @param {Function} callback | |
* The callback function to be executed when the Escape key is pressed. | |
* Takes the {KeyboardEvent} event as a parameter. | |
* | |
* @returns {void} | |
*/ | |
focusTrap.setEscapeKeyListener = function (callback) { | |
if (typeof callback !== 'function') { | |
throw new Error('focusTrap.setEscapeKeyListener expects a function as an argument.'); | |
} | |
focusTrap.escapeKeyListener = callback; | |
}; | |
// Export the focusTrap object | |
window.focusTrap = focusTrap; | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment