function createFocusWalker(root: HTMLElement) {
return document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
acceptNode: (node: HTMLElement) =>
// `.tabIndex` is not the same as the `tabindex` attribute. It works on the
// runtime's understanding of tabbability, so this automatically accounts
// for any kind of element that could be tabbed to.
// @ts-ignore
node.tabIndex >= 0 && !node.disabled ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP,
});
}
/**
* Given a `root` container element and a `target` that is outside of that
* container and intended to receive focus, force the DOM focus to wrap around
* such that it remains within `root`.
*
* If `forceFirst` is set to `true`, the wrap will always attempt to focus the
* first viable element in `root`, rather than wrapping to the end.
*/
export default function wrapFocus(root: HTMLElement, target: Element, forceFirst: boolean = false) {
const walker = createFocusWalker(root);
const position = target.compareDocumentPosition(root);
let wrappedTarget: HTMLElement | null = null;
if (position & Node.DOCUMENT_POSITION_PRECEDING || forceFirst) {
wrappedTarget = walker.firstChild() as HTMLElement | null;
} else if (position & Node.DOCUMENT_POSITION_FOLLOWING) {
wrappedTarget = walker.lastChild() as HTMLElement | null;
}
const newFocus = wrappedTarget != null ? wrappedTarget : root;
newFocus.focus();
}