Description
src/components/voice/VoiceConfirmModal.tsx declares full modal-dialog semantics and auto-focuses its primary action on open:
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="voice-confirm-heading"
aria-describedby="voice-confirm-desc"
className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in"
>
with an Escape key handler to cancel and a focus-on-open effect for the Confirm button — but there is no Tab/Shift+Tab keydown handling anywhere in the file to keep focus cycling within the dialog's two buttons (Confirm / Cancel). A keyboard user pressing Tab repeatedly from either button will tab straight out of the modal into the underlying page content while the modal overlay is still visually blocking interaction, violating the implicit contract of aria-modal="true" and WCAG 2.4.3 (Focus Order). This is a distinct file/component from any other modal focus-trap issue already tracked elsewhere in the codebase (e.g. InfoTooltip's popover), since no shared focus-trap utility exists for either to reuse.
Requirements
VoiceConfirmModal must trap keyboard focus within its two interactive elements while open — Tab from the last focusable element wraps to the first, and Shift+Tab from the first wraps to the last.
- Existing
Escape-to-cancel and auto-focus-on-open behavior must be preserved.
Suggested execution
- Add a
Tab/Shift+Tab keydown handler in VoiceConfirmModal.tsx's existing useEffect (alongside the current Escape handling) that queries the dialog's focusable elements (Confirm and Cancel buttons, plus the close X button) and wraps focus at the boundaries, mirroring the focus-cycling logic already implemented in CreateStreamFab.tsx's menu (handleMenuKeyDown) for a similar pattern in this same codebase.
- Alternatively, extract a small shared
useFocusTrap hook from this implementation so future modals (and CreateStreamFab's menu) can share it instead of duplicating the cycling logic.
- Add a test that opens the modal, tabs past the last focusable element, and asserts focus wraps back to the first rather than leaving the dialog.
Acceptance criteria
Security notes
None; accessibility correctness fix.
Guidelines
- Minimum 95% test coverage
- Timeframe: 96 hours
Description
src/components/voice/VoiceConfirmModal.tsxdeclares full modal-dialog semantics and auto-focuses its primary action on open:with an
Escapekey handler to cancel and a focus-on-open effect for the Confirm button — but there is noTab/Shift+Tabkeydown handling anywhere in the file to keep focus cycling within the dialog's two buttons (Confirm / Cancel). A keyboard user pressingTabrepeatedly from either button will tab straight out of the modal into the underlying page content while the modal overlay is still visually blocking interaction, violating the implicit contract ofaria-modal="true"and WCAG 2.4.3 (Focus Order). This is a distinct file/component from any other modal focus-trap issue already tracked elsewhere in the codebase (e.g.InfoTooltip's popover), since no shared focus-trap utility exists for either to reuse.Requirements
VoiceConfirmModalmust trap keyboard focus within its two interactive elements while open —Tabfrom the last focusable element wraps to the first, andShift+Tabfrom the first wraps to the last.Escape-to-cancel and auto-focus-on-open behavior must be preserved.Suggested execution
Tab/Shift+Tabkeydown handler inVoiceConfirmModal.tsx's existinguseEffect(alongside the currentEscapehandling) that queries the dialog's focusable elements (Confirm and Cancel buttons, plus the closeXbutton) and wraps focus at the boundaries, mirroring the focus-cycling logic already implemented inCreateStreamFab.tsx's menu (handleMenuKeyDown) for a similar pattern in this same codebase.useFocusTraphook from this implementation so future modals (andCreateStreamFab's menu) can share it instead of duplicating the cycling logic.Acceptance criteria
VoiceConfirmModalwhile it is open.Security notes
None; accessibility correctness fix.
Guidelines