Task 20: Accessibility audit improvements

Semantic HTML:
- Changed Card component from div to article element
- Added id="main-content" to main element for skip link target

Keyboard Navigation & ARIA:
- Added skip link to TopBar (visible only on focus, navigates to #main-content)
- Added aria-label="Active session information" to session info container
- Added aria-hidden="true" to all decorative colored dots (CardHeader, CareerActivity, Projects, Sidebar status badge)
- All expandable items already have role="button", tabIndex={0}, aria-expanded
- All KPI cards already have proper aria-label describing flip state
- Command palette already has full ARIA implementation (combobox, listbox, dialog)

Focus Management:
- Added global focus-visible styles in index.css (2px accent outline, 2px offset)
- Buttons, links, inputs all have proper focus rings with accent color
- Command palette focus trap already implemented

Reduced Motion:
- All components already check prefers-reduced-motion at module scope
- Dashboard entrance, tile expansion, KPI flip, palette animations respect reduced motion
- Added reduced motion override for pulse animation (disables pulse, keeps static dot)

Color Contrast:
- All color tokens already meet WCAG AA standards per ref spec
- Tertiary text (#8DA8A5) used only for supplementary labels where information is conveyed elsewhere

Quality checks: typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
This commit is contained in:
2026-02-13 18:04:52 +00:00
parent 8dc27ff8a9
commit 6a4fc86387
7 changed files with 73 additions and 4 deletions
+3 -3
View File
@@ -23,7 +23,7 @@ export function Card({ children, full, className, tileId }: CardProps) {
}
return (
<div
<article
style={baseStyles}
className={className}
data-tile-id={tileId}
@@ -31,7 +31,7 @@ export function Card({ children, full, className, tileId }: CardProps) {
onMouseLeave={() => setIsHovered(false)}
>
{children}
</div>
</article>
)
}
@@ -85,7 +85,7 @@ export function CardHeader({ dotColor, title, rightText }: CardHeaderProps) {
return (
<div style={headerStyles}>
<div style={dotStyles} />
<div style={dotStyles} aria-hidden="true" />
<span style={titleStyles}>{title}</span>
{rightText && <span style={rightTextStyles}>{rightText}</span>}
</div>
+1
View File
@@ -135,6 +135,7 @@ export function DashboardLayout() {
{/* Main content — scrollable card grid */}
<motion.main
id="main-content"
initial="hidden"
animate="visible"
variants={contentVariants}
+1
View File
@@ -219,6 +219,7 @@ export default function Sidebar() {
background: 'var(--success)',
animation: 'pulse 2s infinite',
}}
aria-hidden="true"
/>
<span>{patient.badge}</span>
</div>
+30 -1
View File
@@ -26,6 +26,32 @@ export function TopBar({ onSearchClick }: TopBarProps) {
zIndex: 100,
}}
>
{/* Skip to main content link (only visible on focus) */}
<a
href="#main-content"
className="skip-link"
style={{
position: 'absolute',
top: '-40px',
left: '0',
background: 'var(--accent)',
color: '#FFFFFF',
padding: '8px 16px',
textDecoration: 'none',
zIndex: 101,
borderRadius: '0 0 4px 0',
fontSize: '13px',
fontWeight: 600,
}}
onFocus={(e) => {
e.currentTarget.style.top = '0'
}}
onBlur={(e) => {
e.currentTarget.style.top = '-40px'
}}
>
Skip to main content
</a>
{/* Brand */}
<div className="flex items-center gap-2 shrink-0">
<Home
@@ -131,7 +157,10 @@ export function TopBar({ onSearchClick }: TopBarProps) {
</button>
{/* Session info (right) */}
<div className="flex items-center gap-2 sm:gap-3 shrink-0">
<div
className="flex items-center gap-2 sm:gap-3 shrink-0"
aria-label="Active session information"
>
<span
className="hidden sm:inline"
style={{
@@ -218,6 +218,7 @@ const ActivityItem: React.FC<ActivityItemProps> = ({ entry, isExpanded, onToggle
flexShrink: 0,
marginTop: '2px',
}}
aria-hidden="true"
/>
<div style={{ flex: 1, minWidth: 0 }}>
<div
+1
View File
@@ -85,6 +85,7 @@ function ProjectItem({ project, isExpanded, onToggle }: ProjectItemProps) {
marginTop: '4px',
animation: isLive ? 'pulse 2s infinite' : undefined,
}}
aria-hidden="true"
/>
<span style={{ flex: 1 }}>{project.name}</span>
<span