66 lines
2.5 KiB
JavaScript
66 lines
2.5 KiB
JavaScript
// Action: Focus Trap
|
|
export function focusTrap(node, enabled) {
|
|
const elemWhitelist = 'a[href]:not([tabindex="-1"]), button:not([tabindex="-1"]), input:not([tabindex="-1"]), textarea:not([tabindex="-1"]), select:not([tabindex="-1"]), details:not([tabindex="-1"]), [tabindex]:not([tabindex="-1"])';
|
|
let elemFirst;
|
|
let elemLast;
|
|
// When the first element is selected, shift+tab pressed, jump to the last selectable item.
|
|
function onFirstElemKeydown(e) {
|
|
if (e.shiftKey && e.code === 'Tab') {
|
|
e.preventDefault();
|
|
elemLast.focus();
|
|
}
|
|
}
|
|
// When the last item selected, tab pressed, jump to the first selectable item.
|
|
function onLastElemKeydown(e) {
|
|
if (!e.shiftKey && e.code === 'Tab') {
|
|
e.preventDefault();
|
|
elemFirst.focus();
|
|
}
|
|
}
|
|
const onScanElements = (fromObserver) => {
|
|
if (enabled === false)
|
|
return;
|
|
// Gather all focusable elements
|
|
const focusableElems = Array.from(node.querySelectorAll(elemWhitelist));
|
|
if (focusableElems.length) {
|
|
// Set first/last focusable elements
|
|
elemFirst = focusableElems[0];
|
|
elemLast = focusableElems[focusableElems.length - 1];
|
|
// Auto-focus first focusable element only when not called from observer
|
|
if (!fromObserver)
|
|
elemFirst.focus();
|
|
// Listen for keydown on first & last element
|
|
elemFirst.addEventListener('keydown', onFirstElemKeydown);
|
|
elemLast.addEventListener('keydown', onLastElemKeydown);
|
|
}
|
|
};
|
|
onScanElements(false);
|
|
function onCleanUp() {
|
|
if (elemFirst)
|
|
elemFirst.removeEventListener('keydown', onFirstElemKeydown);
|
|
if (elemLast)
|
|
elemLast.removeEventListener('keydown', onLastElemKeydown);
|
|
}
|
|
// When children of node are changed (added or removed)
|
|
const onObservationChange = (mutationRecords, observer) => {
|
|
if (mutationRecords.length) {
|
|
onCleanUp();
|
|
onScanElements(true);
|
|
}
|
|
return observer;
|
|
};
|
|
const observer = new MutationObserver(onObservationChange);
|
|
observer.observe(node, { childList: true, subtree: true });
|
|
// Lifecycle
|
|
return {
|
|
update(newArgs) {
|
|
enabled = newArgs;
|
|
newArgs ? onScanElements(false) : onCleanUp();
|
|
},
|
|
destroy() {
|
|
onCleanUp();
|
|
observer.disconnect();
|
|
}
|
|
};
|
|
}
|