48 lines
1.3 KiB
TypeScript
48 lines
1.3 KiB
TypeScript
// Focus trap utility for modals
|
|
export function trapFocus(node: HTMLElement) {
|
|
const focusableSelectors = [
|
|
'button:not([disabled])',
|
|
'input:not([disabled])',
|
|
'select:not([disabled])',
|
|
'textarea:not([disabled])',
|
|
'a[href]',
|
|
'[tabindex]:not([tabindex="-1"])',
|
|
].join(', ');
|
|
|
|
function getFocusableElements() {
|
|
return Array.from(node.querySelectorAll<HTMLElement>(focusableSelectors));
|
|
}
|
|
|
|
function handleKeydown(e: KeyboardEvent) {
|
|
if (e.key !== 'Tab') return;
|
|
|
|
const focusable = getFocusableElements();
|
|
if (focusable.length === 0) return;
|
|
|
|
const first = focusable[0];
|
|
const last = focusable[focusable.length - 1];
|
|
|
|
if (e.shiftKey && document.activeElement === first) {
|
|
e.preventDefault();
|
|
last.focus();
|
|
} else if (!e.shiftKey && document.activeElement === last) {
|
|
e.preventDefault();
|
|
first.focus();
|
|
}
|
|
}
|
|
|
|
// Focus first element on mount
|
|
const focusable = getFocusableElements();
|
|
if (focusable.length > 0) {
|
|
focusable[0].focus();
|
|
}
|
|
|
|
node.addEventListener('keydown', handleKeydown);
|
|
|
|
return {
|
|
destroy() {
|
|
node.removeEventListener('keydown', handleKeydown);
|
|
}
|
|
};
|
|
}
|