feat(a11y): Implement keyboard shortcuts and accessibility (Task 13)

- Create AccessibilityContext for global focus management and expanded state
- Add roving tabindex to sidebar with Up/Down/Enter/Home/End navigation
- Focus management: after login, after view change, after item expansion
- Global Escape closes expanded items across all views
- Add scope='col' to SummaryView table headers
- Add focus-after-expand to ConsultationsView
- Update ARIA roles: role='menu', role='menuitem', aria-current
This commit is contained in:
2026-02-11 02:49:51 +00:00
parent fc3c0659b2
commit f7f7e0db8c
9 changed files with 258 additions and 40 deletions
+13 -2
View File
@@ -58,6 +58,7 @@ function ConsultationEntry({
prefersReducedMotion,
}: ConsultationEntryProps) {
const contentRef = useRef<HTMLDivElement>(null)
const expandedContentRef = useRef<HTMLDivElement>(null)
const [height, setHeight] = useState<number | undefined>(isExpanded ? undefined : 0)
useEffect(() => {
@@ -75,6 +76,12 @@ function ConsultationEntry({
setHeight(0)
}, [isExpanded, prefersReducedMotion])
useEffect(() => {
if (isExpanded && expandedContentRef.current) {
expandedContentRef.current.focus()
}
}, [isExpanded])
const keyCodedEntry = consultation.codedEntries[0]
return (
@@ -134,6 +141,7 @@ function ConsultationEntry({
<ExpandedContent
consultation={consultation}
prefersReducedMotion={prefersReducedMotion}
contentRef={expandedContentRef}
/>
)}
</div>
@@ -162,15 +170,18 @@ function StatusDot({ isCurrent }: StatusDotProps) {
interface ExpandedContentProps {
consultation: Consultation
prefersReducedMotion: boolean
contentRef: React.RefObject<HTMLDivElement>
}
function ExpandedContent({ consultation, prefersReducedMotion }: ExpandedContentProps) {
function ExpandedContent({ consultation, prefersReducedMotion, contentRef }: ExpandedContentProps) {
const opacity = prefersReducedMotion ? 1 : undefined
const transition = prefersReducedMotion ? 'none' : 'opacity 150ms ease-out'
return (
<div
className="px-4 pb-4"
ref={contentRef}
tabIndex={-1}
className="px-4 pb-4 outline-none"
style={{ opacity, transition }}
>
<div className="pl-5 border-l border-gray-200 ml-1">