fix(a11y): add focus trap, Escape key, and dialog role to mobile sidebar

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jason Staack
2026-03-15 21:10:32 -05:00
parent 42216ea326
commit 409fb000b5

View File

@@ -53,6 +53,39 @@ export function Sidebar() {
const routerState = useRouterState() const routerState = useRouterState()
const currentPath = routerState.location.pathname 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<HTMLElement>('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<HTMLElement>('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 // Keyboard toggle: [ key collapses/expands sidebar
useEffect(() => { useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
@@ -336,7 +369,13 @@ export function Sidebar() {
className="lg:hidden fixed inset-0 z-40 bg-black/50" className="lg:hidden fixed inset-0 z-40 bg-black/50"
onClick={() => setMobileSidebarOpen(false)} onClick={() => setMobileSidebarOpen(false)}
/> />
<aside className="lg:hidden fixed inset-y-0 left-0 z-50 w-60 flex flex-col bg-sidebar border-r border-border shadow-xl"> <aside
id="mobile-sidebar"
role="dialog"
aria-modal="true"
aria-label="Navigation"
className="lg:hidden fixed inset-y-0 left-0 z-50 w-60 flex flex-col bg-sidebar border-r border-border shadow-xl"
>
{sidebarContent(false)} {sidebarContent(false)}
</aside> </aside>
</> </>