Skip to content

Commit 5c0c9f8

Browse files
midudevclaude
andcommitted
feat(a11y): improve accessibility across the app
- Add screen-reader <h1> for the selected algorithm view (only the welcome screen had a heading before) - Localize the main region aria-label via new visualizationLabel key - Fix 404 page lang attribute by passing locale to Layout - Respect prefers-reduced-motion to pause the auto-cycling showcase - Mark decorative kbd SVGs as aria-hidden in the welcome screen - Enrich complexity chart aria-labels with the actual Big-O values Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 6180389 commit 5c0c9f8

7 files changed

Lines changed: 44 additions & 8 deletions

File tree

src/components/AlgoViz.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
getAlgorithmDescription,
66
getAlgorithmMetaTitle,
77
getAlgorithmMetaDescription,
8+
getCategoryName,
89
defaultLocale,
910
locales,
1011
} from '@i18n/translations'
@@ -342,8 +343,15 @@ export default function AlgoViz({ locale = 'en', initialAlgorithmId }: AlgoVizPr
342343
<main
343344
id="main-content"
344345
className="flex-1 flex flex-col overflow-hidden min-w-0"
345-
aria-label="Algorithm visualization"
346+
aria-label={t.visualizationLabel}
346347
>
348+
{/* Screen-reader page heading — reflects the current algorithm.
349+
The WelcomeScreen renders its own visible <h1> when nothing is selected. */}
350+
{selectedAlgorithm && (
351+
<h1 className="sr-only">
352+
{selectedAlgorithm.name}{getCategoryName(locale, selectedAlgorithm.category)}
353+
</h1>
354+
)}
347355
<div className="flex-1 flex flex-col p-4 md:p-8 overflow-auto">
348356
{renderVisualization()}
349357
</div>

src/components/AlgorithmShowcase.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,17 @@ export default function AlgorithmShowcase({
6161
const pausingRef = useRef(false)
6262
const timersRef = useRef<ReturnType<typeof setTimeout>[]>([])
6363

64+
// Respect the user's reduced-motion preference: pause the auto-cycling demo
65+
// (WCAG 2.2.2 — give users control over moving / auto-updating content).
66+
const [reducedMotion, setReducedMotion] = useState(false)
67+
useEffect(() => {
68+
const mq = window.matchMedia('(prefers-reduced-motion: reduce)')
69+
const handler = (e: MediaQueryListEvent) => setReducedMotion(e.matches)
70+
setReducedMotion(mq.matches)
71+
mq.addEventListener('change', handler)
72+
return () => mq.removeEventListener('change', handler)
73+
}, [])
74+
6475
const current = items[algoIdx]
6576

6677
const clearTimers = useCallback(() => {
@@ -69,6 +80,7 @@ export default function AlgorithmShowcase({
6980
}, [])
7081

7182
useEffect(() => {
83+
if (reducedMotion) return
7284
pausingRef.current = false
7385

7486
const interval = setInterval(() => {
@@ -97,7 +109,7 @@ export default function AlgorithmShowcase({
97109
clearInterval(interval)
98110
clearTimers()
99111
}
100-
}, [algoIdx, current.steps.length, items.length, clearTimers])
112+
}, [algoIdx, current.steps.length, items.length, clearTimers, reducedMotion])
101113

102114
const goToAlgo = useCallback(
103115
(idx: number) => {

src/components/ComplexityChart.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,11 @@ export default function ComplexityChart({ description, locale = 'en' }: { descri
192192
}
193193
const resolvedLabels = resolveOverlaps(labels, 11)
194194

195+
const chartTitle = locale === 'es' ? 'Complejidad temporal' : 'Time complexity'
196+
const chartLabel = `${chartTitle}: ${entries
197+
.map((e) => (e.label ? `${e.label} ${e.raw}` : e.raw))
198+
.join(', ')}`
199+
195200
return (
196201
<div className="mt-5 mb-3">
197202
<div className="text-[10px] text-neutral-500 uppercase tracking-wider font-semibold mb-2">
@@ -203,7 +208,7 @@ export default function ComplexityChart({ description, locale = 'en' }: { descri
203208
className="w-full"
204209
style={{ maxHeight: 150 }}
205210
role="img"
206-
aria-label="Time complexity growth chart"
211+
aria-label={chartLabel}
207212
>
208213
{/* Gradient definitions for area fills */}
209214
<defs>

src/components/ConceptVisualizer.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,11 @@ function BigOChart({ state }: { state: BigOState }) {
111111
className="w-full max-w-2xl"
112112
style={{ maxHeight: '340px' }}
113113
role="img"
114-
aria-label="Big O complexity chart"
114+
aria-label={`Big O complexity chart${
115+
visibleCurves.length > 0
116+
? `: ${visibleCurves.map((c) => c.name).join(', ')}`
117+
: ''
118+
}`}
115119
>
116120
{/* Background */}
117121
<rect x={PAD.left} y={PAD.top} width={chartW} height={chartH} fill="rgba(255,255,255,0.02)" rx="4" />

src/components/WelcomeScreen.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export default function WelcomeScreen({ t, locale, onSelectAlgorithm }: WelcomeS
5353
strokeWidth={1}
5454
strokeLinecap="round"
5555
strokeLinejoin="round"
56+
aria-hidden="true"
5657
>
5758
<path d="M4 10v3a1 1 0 0 0 1 1h14a1 1 0 0 0 1 -1v-3" />
5859
</svg>
@@ -68,6 +69,7 @@ export default function WelcomeScreen({ t, locale, onSelectAlgorithm }: WelcomeS
6869
strokeLinejoin="round"
6970
viewBox="0 0 16 16"
7071
style={{ color: 'currentcolor' }}
72+
aria-hidden="true"
7173
>
7274
<path
7375
fillRule="evenodd"
@@ -84,6 +86,7 @@ export default function WelcomeScreen({ t, locale, onSelectAlgorithm }: WelcomeS
8486
strokeLinejoin="round"
8587
viewBox="0 0 16 16"
8688
style={{ color: 'currentcolor' }}
89+
aria-hidden="true"
8790
>
8891
<path
8992
fillRule="evenodd"

src/i18n/translations.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export interface Translations {
5353
// Mobile / aria labels
5454
sidebarAriaLabel: string
5555
codePanelAriaLabel: string
56+
visualizationLabel: string
5657
mobileMenuTitle: string
5758
openMenu: string
5859
closeMenu: string
@@ -119,6 +120,7 @@ export const translations: Record<Locale, Translations> = {
119120

120121
sidebarAriaLabel: 'Algorithm categories',
121122
codePanelAriaLabel: 'Code and details panel',
123+
visualizationLabel: 'Algorithm visualization',
122124
mobileMenuTitle: 'Algorithms',
123125
openMenu: 'Open menu',
124126
closeMenu: 'Close menu',
@@ -1091,6 +1093,7 @@ Named after the Greek mathematician Eratosthenes of Cyrene (~276–194 BCE), thi
10911093

10921094
sidebarAriaLabel: 'Categorías de algoritmos',
10931095
codePanelAriaLabel: 'Panel de código y detalles',
1096+
visualizationLabel: 'Visualización del algoritmo',
10941097
mobileMenuTitle: 'Algoritmos',
10951098
openMenu: 'Abrir menú',
10961099
closeMenu: 'Cerrar menú',

src/pages/404.astro

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import Layout from '@layouts/Layout.astro'
33
import { translations, defaultLocale, type Locale, locales } from '@i18n/translations'
44
55
const pathName = Astro.url.pathname
6-
const locale = pathName.split('/')[1] as Locale
7-
const t = locales.includes(locale) ? translations[locale] : translations[defaultLocale]
8-
const pathnameToRedirect = locales.includes(locale) && locale !== defaultLocale ? `/${locale}` : `/`
6+
const maybeLocale = pathName.split('/')[1] as Locale
7+
const locale = locales.includes(maybeLocale) ? maybeLocale : defaultLocale
8+
const t = translations[locale]
9+
const pathnameToRedirect = locale === defaultLocale ? `/` : `/${locale}`
910
---
1011

11-
<Layout title="404 — Page not found">
12+
<Layout locale={locale} title={t.notFoundTitle}>
1213
<div class="h-screen flex flex-col items-center justify-center gap-6 bg-black text-white px-4">
1314
<div
1415
class="w-12 h-12 rounded-xl bg-white/4 flex items-center justify-center border border-white/8"

0 commit comments

Comments
 (0)