feat(ui): add trapFocus action for modal dialogs (T7.5, FR-UX-4)
Moves focus into the dialog on mount, cycles Tab/Shift+Tab within its focusable elements, and restores focus to whatever had it before the dialog opened once it closes.
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
// Svelte action for modal dialogs (T7.5, FR-UX-4): moves focus into the node
|
||||
// on mount, cycles Tab/Shift+Tab within its focusable elements, and restores
|
||||
// focus to whatever had it before the dialog opened once it closes.
|
||||
|
||||
const FOCUSABLE =
|
||||
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
||||
|
||||
export function trapFocus(node: HTMLElement) {
|
||||
const previouslyFocused = document.activeElement as HTMLElement | null;
|
||||
|
||||
function focusables(): HTMLElement[] {
|
||||
return Array.from(node.querySelectorAll<HTMLElement>(FOCUSABLE));
|
||||
}
|
||||
|
||||
(focusables()[0] ?? node).focus();
|
||||
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
if (e.key !== "Tab") return;
|
||||
const items = focusables();
|
||||
if (items.length === 0) return;
|
||||
const first = items[0];
|
||||
const last = items[items.length - 1];
|
||||
if (e.shiftKey && document.activeElement === first) {
|
||||
e.preventDefault();
|
||||
last.focus();
|
||||
} else if (!e.shiftKey && document.activeElement === last) {
|
||||
e.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
}
|
||||
|
||||
node.addEventListener("keydown", onKeydown);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
node.removeEventListener("keydown", onKeydown);
|
||||
previouslyFocused?.focus();
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user