From 409fb000b5ff7e856a89eb31dbbbd259fe2fb3c2 Mon Sep 17 00:00:00 2001 From: Jason Staack Date: Sun, 15 Mar 2026 21:10:32 -0500 Subject: [PATCH] fix(a11y): add focus trap, Escape key, and dialog role to mobile sidebar Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/components/layout/Sidebar.tsx | 41 +++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index 6b4d20b..ba020c3 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -53,6 +53,39 @@ export function Sidebar() { const routerState = useRouterState() const currentPath = routerState.location.pathname + // Mobile sidebar focus trap + useEffect(() => { + if (!mobileSidebarOpen) return + + const sidebar = document.getElementById('mobile-sidebar') + if (!sidebar) return + + const focusable = sidebar.querySelectorAll('a, button, input') + if (focusable.length) focusable[0].focus() + + function handleKeyDown(e: KeyboardEvent) { + if (e.key === 'Escape') { + setMobileSidebarOpen(false) + return + } + if (e.key === 'Tab') { + const els = sidebar!.querySelectorAll('a, button, input') + const first = els[0] + const last = els[els.length - 1] + if (e.shiftKey && document.activeElement === first) { + e.preventDefault() + last.focus() + } else if (!e.shiftKey && document.activeElement === last) { + e.preventDefault() + first.focus() + } + } + } + + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, [mobileSidebarOpen, setMobileSidebarOpen]) + // Keyboard toggle: [ key collapses/expands sidebar useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -336,7 +369,13 @@ export function Sidebar() { className="lg:hidden fixed inset-0 z-40 bg-black/50" onClick={() => setMobileSidebarOpen(false)} /> -