Skip to content

Instantly share code, notes, and snippets.

@hagata
Last active October 23, 2020 00:15
Show Gist options
  • Select an option

  • Save hagata/62948ea351668b26976619f1f0b5fa85 to your computer and use it in GitHub Desktop.

Select an option

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.
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