Created
May 20, 2021 01:11
-
-
Save cedmandocdoc/65365840ad58cf7c4e5172ebdb6d16be to your computer and use it in GitHub Desktop.
Inert focus trapping
This file contains 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 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; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Description:
It traps all the focusable elements and all the children inside it regardless of the structure of the DOM.
Usage: