52 lines
1.5 KiB
TypeScript
52 lines
1.5 KiB
TypeScript
const focusableSelector = [
|
|
'a[href]',
|
|
'button:not([disabled])',
|
|
'input:not([disabled])',
|
|
'select:not([disabled])',
|
|
'textarea:not([disabled])',
|
|
'[tabindex]:not([tabindex="-1"])'
|
|
].join(', ');
|
|
|
|
function focusableElements(container: HTMLElement): HTMLElement[] {
|
|
return [...container.querySelectorAll<HTMLElement>(focusableSelector)].filter(
|
|
(element) => element.getClientRects().length > 0
|
|
);
|
|
}
|
|
|
|
export function focusTrap(container: HTMLElement) {
|
|
const previouslyFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
(focusableElements(container)[0] ?? container).focus({ preventScroll: true });
|
|
|
|
function handleKeydown(event: KeyboardEvent) {
|
|
if (event.key !== 'Tab') return;
|
|
|
|
const elements = focusableElements(container);
|
|
const first = elements[0];
|
|
const last = elements.at(-1);
|
|
const active = document.activeElement;
|
|
|
|
if (!first || !last) {
|
|
event.preventDefault();
|
|
container.focus({ preventScroll: true });
|
|
} else if (!container.contains(active)) {
|
|
event.preventDefault();
|
|
first.focus();
|
|
} else if (event.shiftKey && active === first) {
|
|
event.preventDefault();
|
|
last.focus();
|
|
} else if (!event.shiftKey && active === last) {
|
|
event.preventDefault();
|
|
first.focus();
|
|
}
|
|
}
|
|
|
|
document.addEventListener('keydown', handleKeydown);
|
|
|
|
return {
|
|
destroy() {
|
|
document.removeEventListener('keydown', handleKeydown);
|
|
if (previouslyFocused?.isConnected) previouslyFocused.focus({ preventScroll: true });
|
|
}
|
|
};
|
|
}
|