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:
@@ -23,7 +23,7 @@ export function Card({ children, full, className, tileId }: CardProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<article
|
||||||
style={baseStyles}
|
style={baseStyles}
|
||||||
className={className}
|
className={className}
|
||||||
data-tile-id={tileId}
|
data-tile-id={tileId}
|
||||||
@@ -31,7 +31,7 @@ export function Card({ children, full, className, tileId }: CardProps) {
|
|||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</article>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ export function CardHeader({ dotColor, title, rightText }: CardHeaderProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={headerStyles}>
|
<div style={headerStyles}>
|
||||||
<div style={dotStyles} />
|
<div style={dotStyles} aria-hidden="true" />
|
||||||
<span style={titleStyles}>{title}</span>
|
<span style={titleStyles}>{title}</span>
|
||||||
{rightText && <span style={rightTextStyles}>{rightText}</span>}
|
{rightText && <span style={rightTextStyles}>{rightText}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ export function DashboardLayout() {
|
|||||||
|
|
||||||
{/* Main content — scrollable card grid */}
|
{/* Main content — scrollable card grid */}
|
||||||
<motion.main
|
<motion.main
|
||||||
|
id="main-content"
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate="visible"
|
animate="visible"
|
||||||
variants={contentVariants}
|
variants={contentVariants}
|
||||||
|
|||||||
@@ -219,6 +219,7 @@ export default function Sidebar() {
|
|||||||
background: 'var(--success)',
|
background: 'var(--success)',
|
||||||
animation: 'pulse 2s infinite',
|
animation: 'pulse 2s infinite',
|
||||||
}}
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<span>{patient.badge}</span>
|
<span>{patient.badge}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,6 +26,32 @@ export function TopBar({ onSearchClick }: TopBarProps) {
|
|||||||
zIndex: 100,
|
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 */}
|
{/* Brand */}
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
<Home
|
<Home
|
||||||
@@ -131,7 +157,10 @@ export function TopBar({ onSearchClick }: TopBarProps) {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Session info (right) */}
|
{/* 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
|
<span
|
||||||
className="hidden sm:inline"
|
className="hidden sm:inline"
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -218,6 +218,7 @@ const ActivityItem: React.FC<ActivityItemProps> = ({ entry, isExpanded, onToggle
|
|||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
marginTop: '2px',
|
marginTop: '2px',
|
||||||
}}
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ function ProjectItem({ project, isExpanded, onToggle }: ProjectItemProps) {
|
|||||||
marginTop: '4px',
|
marginTop: '4px',
|
||||||
animation: isLive ? 'pulse 2s infinite' : undefined,
|
animation: isLive ? 'pulse 2s infinite' : undefined,
|
||||||
}}
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<span style={{ flex: 1 }}>{project.name}</span>
|
<span style={{ flex: 1 }}>{project.name}</span>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -371,3 +371,39 @@ html {
|
|||||||
to { transform: none; opacity: 1; }
|
to { transform: none; opacity: 1; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== FOCUS VISIBLE STYLES (WCAG Compliance) ===== */
|
||||||
|
/* Default focus ring for all focusable elements */
|
||||||
|
*:focus-visible {
|
||||||
|
outline: 2px solid rgba(13, 110, 110, 0.4);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button-like interactive elements */
|
||||||
|
button:focus-visible,
|
||||||
|
[role="button"]:focus-visible,
|
||||||
|
[role="option"]:focus-visible {
|
||||||
|
outline: 2px solid rgba(13, 110, 110, 0.4);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Links */
|
||||||
|
a:focus-visible {
|
||||||
|
outline: 2px solid rgba(13, 110, 110, 0.4);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inputs and textareas */
|
||||||
|
input:focus-visible,
|
||||||
|
textarea:focus-visible {
|
||||||
|
outline: 2px solid rgba(13, 110, 110, 0.6);
|
||||||
|
outline-offset: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
/* Disable pulse animation on status badge dot */
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user