112 lines
3.0 KiB
Plaintext
112 lines
3.0 KiB
Plaintext
import Mousetrap from 'mousetrap';
|
|
import {
|
|
runInSeries,
|
|
elementIsVisible,
|
|
FOCUSABLE_ELEMENTS,
|
|
} from './utils.js';
|
|
|
|
export function focusTrap(node) {
|
|
const keyboardShortcuts = {
|
|
'alt+tab': previous,
|
|
'end': focusLastItem,
|
|
'home': focusFirstItem,
|
|
'shift+tab': previous,
|
|
down: next,
|
|
tab: next,
|
|
up: previous,
|
|
};
|
|
|
|
Object.entries(keyboardShortcuts).forEach(([keys, handler]) => {
|
|
Mousetrap.bind(keys, runInSeries([
|
|
(event) => ({ event }),
|
|
preventDefault,
|
|
stopPropagation,
|
|
getAllFocusableChildren,
|
|
getCurrentlyFocusedItem,
|
|
handler,
|
|
]));
|
|
});
|
|
|
|
function preventDefault(context) {
|
|
context.event.preventDefault();
|
|
return context;
|
|
}
|
|
|
|
function stopPropagation(context) {
|
|
context.event.stopPropagation();
|
|
return context;
|
|
}
|
|
|
|
function getAllFocusableChildren(context) {
|
|
let focusables = [...node.querySelectorAll(FOCUSABLE_ELEMENTS)]; // NodeList to Array
|
|
return {
|
|
...context,
|
|
allFocusableItems: focusables.filter(elementIsVisible),
|
|
};
|
|
}
|
|
|
|
function getCurrentlyFocusedItem(context) {
|
|
let currentlyFocusedItem = document.activeElement;
|
|
|
|
if (currentlyFocusedItem && !node.contains(currentlyFocusedItem)) {
|
|
return context;
|
|
}
|
|
|
|
return {
|
|
...context,
|
|
currentlyFocusedItem,
|
|
};
|
|
}
|
|
|
|
function next({ allFocusableItems, currentlyFocusedItem }) {
|
|
// if focus is not within the focuables, focus the first one.
|
|
if (!currentlyFocusedItem) {
|
|
allFocusableItems[0] && allFocusableItems[0].focus();
|
|
return;
|
|
}
|
|
|
|
let currentlyFocusedIndex = allFocusableItems.indexOf(currentlyFocusedItem);
|
|
|
|
// If we have focus on the last one, give focus on the first.
|
|
if ((allFocusableItems.length - 1) === currentlyFocusedIndex) {
|
|
allFocusableItems[0] && allFocusableItems[0].focus();
|
|
return;
|
|
}
|
|
|
|
// Focus the next one.
|
|
allFocusableItems[currentlyFocusedIndex + 1] && allFocusableItems[currentlyFocusedIndex + 1].focus();
|
|
}
|
|
|
|
function previous({ allFocusableItems, currentlyFocusedItem }) {
|
|
// If focus is not within the focusables, focus the last one
|
|
if (!currentlyFocusedItem) {
|
|
allFocusableItems[allFocusableItems.length - 1].focus();
|
|
return;
|
|
}
|
|
|
|
let currentlyFocusedIndex = allFocusableItems.indexOf(currentlyFocusedItem);
|
|
|
|
// If we have focus on the first one, wrap to the end one.
|
|
if (currentlyFocusedIndex === 0) {
|
|
allFocusableItems[allFocusableItems.length - 1] && allFocusableItems[allFocusableItems.length - 1].focus();
|
|
return;
|
|
}
|
|
|
|
// Focus the previous one.
|
|
allFocusableItems[currentlyFocusedIndex - 1] && allFocusableItems[currentlyFocusedIndex - 1].focus();
|
|
}
|
|
|
|
function focusFirstItem({ allFocusableItems }) {
|
|
allFocusableItems[0] && allFocusableItems[0].focus();
|
|
}
|
|
|
|
function focusLastItem({ allFocusableItems }) {
|
|
allFocusableItems[allFocusableItems.length - 1].focus();
|
|
}
|
|
|
|
return {
|
|
destroy() {
|
|
Object.keys(keyboardShortcuts).forEach((key) => Mousetrap.unbind(key));
|
|
}
|
|
};
|
|
} |