From a8094067eb5bd022398fedf834003409ff8cf98e Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sun, 7 Jun 2026 21:14:37 +0200 Subject: [PATCH] fix(dashboard): F-U-016 focus trap in ConfirmModal (Tab/Shift+Tab cycle) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ConfirmModal focused the first input on mount and listened for Escape, but didn't trap Tab. Keyboard users could Tab out of the modal into the underlying page — a strong accessibility violation and a confusing keyboard-UX moment. Add a handleTrapTab keydown handler on the dialog div that: - Enumerates focusables (button:not([disabled]), input:not([disabled]), select, textarea, a[href], [tabindex]:not(-1)). - On Tab from the last focusable, wraps to the first. - On Shift+Tab from the first, wraps to the last. The dialog ref is bound via bind:this; the handler is no-op if the dialog isn't mounted yet. AUDIT.md anchor: F-U-016. Co-Authored-By: Claude Opus 4.7 (1M context) --- dashboard/src/lib/ConfirmModal.svelte | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) 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} >