Skip to content

Instantly share code, notes, and snippets.

@cedmandocdoc
Created May 20, 2021 01:11
Show Gist options
  • Save cedmandocdoc/65365840ad58cf7c4e5172ebdb6d16be to your computer and use it in GitHub Desktop.
Save cedmandocdoc/65365840ad58cf7c4e5172ebdb6d16be to your computer and use it in GitHub Desktop.
Inert focus trapping
const Inert = {
enable(element) {
element.inert = false;
},
disable(element) {
element.inert = true;
},
// set the inert of all the siblings of trapped element to true
// and recursively run up to the root, body element, then return
// untrap function to undo the changes
trap(...elements) {
const excluded = [];
let firstElement;
let lastElement;
let keydownListener;
elements.forEach((element, index) => {
if (index === 0) {
const focusableElements = this.getAllFocusableElements(element);
firstElement = focusableElements.length === 0 ? element : focusableElements[0];
if (elements.length === 1) {
lastElement = focusableElements.length === 0 ? element : focusableElements[focusableElements.length - 1];
}
} else if (index === elements.length - 1) {
const focusableElements = this.getAllFocusableElements(element);
lastElement = focusableElements.length === 0 ? element : focusableElements[focusableElements.length - 1];
}
const roots = this.getRoots(element).filter(element => excluded.indexOf(element) === -1);
excluded.push(...roots);
});
const siblings = [];
const hideSiblingsAccessibility = element => {
const children = Array.from(element.children);
children.forEach(child => {
if (excluded.indexOf(child) !== -1) {
if (elements.indexOf(child) === -1) {
hideSiblingsAccessibility(child);
}
} else {
const inert = child.inert;
siblings.push({ element: child, inert });
child.inert = true;
}
})
};
hideSiblingsAccessibility(document.body);
elements.forEach(element => {
this.enable(element);
});
if (firstElement && lastElement) {
keydownListener = e => {
const { target, key, shiftKey } = e;
if (key === 'Tab') {
if (!shiftKey && target === lastElement) {
e.preventDefault();
firstElement.focus();
} else if (shiftKey && target === firstElement) {
e.preventDefault();
lastElement.focus();
}
}
}
window.addEventListener('keydown', keydownListener);
}
return () => {
siblings.forEach(sibling => {
if (sibling.inert === null) {
sibling.element.inert = false;
} else {
sibling.element.inert = sibling.inert;
}
});
if (keydownListener) window.removeEventListener('keydown', keydownListener);
};
},
// utils
getRoots(element) {
const roots = [];
let parent = element.parentElement;
while (parent.tagName !== 'BODY') {
roots.push(parent);
parent = parent.parentElement;
}
return roots;
},
getAllFocusableElements(element) {
return Array.from(element.querySelectorAll('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]')).filter(e => getComputedStyle(e).display !== 'none');
},
}
export default Inert;
@cedmandocdoc
Copy link
Author

Description:
It traps all the focusable elements and all the children inside it regardless of the structure of the DOM.

Usage:

const undo = Inert.trap(...elements);

undo(); // undo the trapping

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment