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:
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import type { ViewId } from '../types/pmr'
|
||||
import { ClinicalSidebar } from './ClinicalSidebar'
|
||||
import { PatientBanner } from './PatientBanner'
|
||||
@@ -9,12 +9,13 @@ import { ProblemsView } from './views/ProblemsView'
|
||||
import { InvestigationsView } from './views/InvestigationsView'
|
||||
import { DocumentsView } from './views/DocumentsView'
|
||||
import { ReferralsView } from './views/ReferralsView'
|
||||
import { useAccessibility } from '../contexts/AccessibilityContext'
|
||||
|
||||
interface PMRInterfaceProps {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export function PMRInterface({ children }: PMRInterfaceProps) {
|
||||
function PMRContent({ children }: PMRInterfaceProps) {
|
||||
const [activeView, setActiveView] = useState<ViewId>(() => {
|
||||
const hash = window.location.hash.slice(1) as ViewId
|
||||
const validViews: ViewId[] = [
|
||||
@@ -28,15 +29,30 @@ export function PMRInterface({ children }: PMRInterfaceProps) {
|
||||
]
|
||||
return validViews.includes(hash) ? hash : 'summary'
|
||||
})
|
||||
|
||||
const viewHeadingRef = useRef<HTMLDivElement>(null)
|
||||
const { requestFocusAfterViewChange, expandedItemId, setExpandedItem } = useAccessibility()
|
||||
|
||||
useEffect(() => {
|
||||
requestFocusAfterViewChange()
|
||||
if (viewHeadingRef.current) {
|
||||
viewHeadingRef.current.focus()
|
||||
}
|
||||
}, [activeView, requestFocusAfterViewChange])
|
||||
|
||||
const handleViewChange = (view: ViewId) => {
|
||||
setActiveView(view)
|
||||
if (expandedItemId) {
|
||||
setExpandedItem(null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleNavigate = (view: ViewId, itemId?: string) => {
|
||||
void itemId
|
||||
const handleNavigate = (view: ViewId) => {
|
||||
setActiveView(view)
|
||||
window.location.hash = view
|
||||
if (expandedItemId) {
|
||||
setExpandedItem(null)
|
||||
}
|
||||
}
|
||||
|
||||
const renderView = () => {
|
||||
@@ -69,6 +85,16 @@ export function PMRInterface({ children }: PMRInterfaceProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const viewLabels: Record<ViewId, string> = {
|
||||
summary: 'Patient Summary',
|
||||
consultations: 'Consultation History',
|
||||
medications: 'Current Medications',
|
||||
problems: 'Problem List',
|
||||
investigations: 'Investigation Results',
|
||||
documents: 'Attached Documents',
|
||||
referrals: 'Referral Form',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-pmr-content">
|
||||
<PatientBanner />
|
||||
@@ -79,9 +105,21 @@ export function PMRInterface({ children }: PMRInterfaceProps) {
|
||||
aria-label={`${activeView} view`}
|
||||
className="flex-1 min-h-[calc(100vh-80px)] p-6"
|
||||
>
|
||||
<div
|
||||
ref={viewHeadingRef}
|
||||
tabIndex={-1}
|
||||
className="outline-none"
|
||||
aria-label={viewLabels[activeView]}
|
||||
>
|
||||
<h1 className="sr-only">{viewLabels[activeView]}</h1>
|
||||
</div>
|
||||
{children || renderView()}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function PMRInterface(props: PMRInterfaceProps) {
|
||||
return <PMRContent {...props} />
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user