Last active
October 23, 2020 00:15
-
-
Save hagata/62948ea351668b26976619f1f0b5fa85 to your computer and use it in GitHub Desktop.
Object based accessibility-utils file for managing and traping focus within a given dialog or modal box. Also includes a handy escape handler.
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
| const accessibilityUtils = { | |
| /** | |
| * string list of selectors that are valid types that can be focused on. | |
| * Used in querySelectorAll to create a NodeList of child elements within | |
| * intended element to trap focus (the parent) | |
| */ | |
| focusableElements: 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])', | |
| /** | |
| * Local context for mounting Nodes to be referenced when the handler is | |
| * called. | |
| */ | |
| trappedElements: {}, | |
| /** | |
| * The handler method that is called then a `focusout` event is fired and | |
| * called, after being registered with this.trapFocus. | |
| * | |
| * @private | |
| * @param {FocusEvent} e the focusout event pased from the Event Listener. | |
| */ | |
| focusHandler(e) { | |
| const { | |
| parent, | |
| firstFocusableElement, | |
| lastFocusableElement | |
| } = this.trappedElements; | |
| if (!parent.contains(e.relatedTarget)) { | |
| if (e.target === firstFocusableElement) { | |
| e.preventDefault(); | |
| lastFocusableElement.focus(); | |
| } else if (e.target === lastFocusableElement) { | |
| e.preventDefault(); | |
| firstFocusableElement.focus(); | |
| } | |
| } | |
| }, | |
| /** | |
| * Accessibility helper function to trap keyboard focus, TAB and SHIFT+TAB | |
| * navigation within a given parent element. | |
| * | |
| * @param {Node} parent Element to trap focus within. | |
| * @param {Node*} setFocusItem optional item to set focus to when this | |
| * method is called. | |
| * @param {Node*} returnNode Optional node to return focus to when untrapping. | |
| */ | |
| trapFocus(parent, setFocusItem, returnNode) { | |
| // Setup: bind the focus handler so it has access to _this_ objects scope. | |
| this.focusHandler = this.focusHandler.bind(this); | |
| // add all the elements inside modal which you want to make focusable | |
| this.returnNode = returnNode || null; | |
| // Trapping divs | |
| // Bracket the dialog node with two invisible, focusable nodes. | |
| // While this dialog is open, we use these to make sure that focus never | |
| // leaves the document even if dialogNode is the first or last node. | |
| if (!this.preNode) { | |
| const preDiv = document.createElement('div'); | |
| preDiv.classList.add('tray-focus__trap'); | |
| this.preNode = parent.parentNode.insertBefore(preDiv, parent); | |
| this.preNode.tabIndex = 0; | |
| } | |
| if (!this.postNode) { | |
| const postDiv = document.createElement('div'); | |
| postDiv.classList.add('tray-focus__trap'); | |
| this.postNode = parent.parentNode.insertBefore(postDiv, parent.nextSibling); | |
| this.postNode.tabIndex = 0; | |
| } | |
| // get the first element to be focused inside parent container | |
| const firstFocusableElement = parent.querySelectorAll( | |
| this.focusableElements | |
| )[0]; | |
| const focusableContent = parent.querySelectorAll(this.focusableElements); | |
| // get last element to be focused inside parent container | |
| const lastFocusableElement = focusableContent[focusableContent.length - 1]; | |
| this.trappedElements = { | |
| parent, | |
| setFocusItem, | |
| firstFocusableElement, | |
| focusableContent, | |
| lastFocusableElement | |
| }; | |
| const focusItem = setFocusItem || firstFocusableElement; | |
| focusItem.focus(); | |
| document.addEventListener('focusout', this.focusHandler); | |
| }, | |
| /** | |
| * Removes the focusout event listener to untrap focus on the *current* element | |
| * and prevent 're-binding" if /when the component is reopened and the trapFocus() menthod | |
| * is called repetively. | |
| */ | |
| unTrapFocus() { | |
| // Teardown trapping divs | |
| this.preNode.remove(); | |
| delete this.preNode; | |
| this.postNode.remove(); | |
| delete this.postNode; | |
| document.removeEventListener('focusout', this.focusHandler); | |
| if (this.returnNode) { | |
| this.returnNode.focus(); | |
| } | |
| }, | |
| /** | |
| * Handles Escape keydown events for any given element and fires the | |
| * provided callback. Self cleaning and removes it's own listener | |
| * when called. | |
| * | |
| * @param {Element} bind any element to listen for Esc key | |
| * @param {*} callback Function to call when escape key is pressed | |
| */ | |
| handleEscape(bind, callback) { | |
| const escapeHandler = { | |
| handleEvent(e) { | |
| switch (e.key) { | |
| case 'Escape': | |
| // remove listener and call the callback | |
| bind.removeEventListener('keydown', escapeHandler); | |
| callback(); | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| }; | |
| bind.addEventListener('keydown', escapeHandler); | |
| } | |
| }; | |
| export default accessibilityUtils; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment