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:
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user