diff --git a/dashboard/src/lib/ConfirmModal.svelte b/dashboard/src/lib/ConfirmModal.svelte index 9933fe7..e945c98 100644 --- a/dashboard/src/lib/ConfirmModal.svelte +++ b/dashboard/src/lib/ConfirmModal.svelte @@ -90,11 +90,34 @@ // doesn't auto-confirm a destructive action). let inputRef = $state(null); let cancelRef = $state(null); + let dialogRef = $state(null); $effect(() => { if (inputRef) inputRef.focus(); else if (cancelRef) cancelRef.focus(); }); + + // F-U-016: focus trap. The browser will otherwise Tab into the + // underlying page; for a modal that's a strong accessibility and + // keyboard-UX violation. Cycle focus within the dialog and reverse + // on Shift+Tab. + function handleTrapTab(event: KeyboardEvent) { + if (event.key !== 'Tab' || !dialogRef) return; + const focusables = dialogRef.querySelectorAll( + 'button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), a[href], [tabindex]:not([tabindex="-1"])' + ); + if (focusables.length === 0) return; + const first = focusables[0]!; + const last = focusables[focusables.length - 1]!; + const active = document.activeElement as HTMLElement | null; + if (event.shiftKey && active === first) { + event.preventDefault(); + last.focus(); + } else if (!event.shiftKey && active === last) { + event.preventDefault(); + first.focus(); + } + } @@ -105,11 +128,13 @@ onclick={handleBackdrop} >