Skip to content

Commit ea6e02b

Browse files
committed
accessibility: add ARIA labels, table semantics, and skip navigation links
Comprehensive accessibility pass covering the dashboard, navbar, and secondary pages. Zero new dependencies, no feature regressions. Changes by file: web/src/app/dashboard/page.tsx - Add <a href="#main-content"> skip link (keyboard users can bypass nav) - Add id="main-content" to <main> anchor target - Wrap stats card grid in role="region" aria-label for screen readers - Add aria-label to each stat card with full readable description - Mark decorative icons with aria-hidden="true" - Add role="img" + aria-label to both chart containers describing data - Add aria-label to <table> with visible result count - Add scope="col" to all <th> elements (required for table semantics) - Wrap result timestamps in <time dateTime={...}> for machine readability - Add aria-label to ping and packet-loss <td> cells (color is not the only indicator of quality — text label now read aloud by screen readers) web/src/components/Navbar.tsx - Add aria-label="Main navigation" to <nav> (landmark is now named) - Add aria-haspopup="true" to mobile menu toggle button - Add role="menu" + aria-label to mobile menu container - Improve mobile toggle aria-label wording to be more descriptive web/src/app/community/page.tsx web/src/app/download/page.tsx - Add skip-to-content link and id="main-content" (these pages were missing both, making keyboard-only navigation significantly harder)
1 parent c856806 commit ea6e02b

4 files changed

Lines changed: 64 additions & 30 deletions

File tree

web/src/app/community/page.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ import { Footer } from "@/components/Footer";
77
export default function CommunityPage() {
88
return (
99
<div className="min-h-screen">
10+
<a href="#main-content" className="skip-to-content focus-ring">
11+
Skip to main content
12+
</a>
1013
<Navbar />
1114

12-
<main className="max-w-6xl mx-auto px-4 py-16">
15+
<main id="main-content" className="max-w-6xl mx-auto px-4 py-16">
1316
{/* Coming Soon Banner */}
1417
<div className="text-center mb-16">
1518
<div className="inline-flex items-center justify-center w-20 h-20 bg-yellow-500/20 rounded-2xl mb-6">

web/src/app/dashboard/page.tsx

Lines changed: 51 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -226,10 +226,13 @@ export default function DashboardPage() {
226226

227227
return (
228228
<div className="min-h-screen">
229+
<a href="#main-content" className="skip-to-content focus-ring">
230+
Skip to main content
231+
</a>
229232
<Navbar />
230233

231234
{/* Main Content */}
232-
<main className="max-w-6xl mx-auto px-4 py-8">
235+
<main id="main-content" className="max-w-6xl mx-auto px-4 py-8">
233236
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-8">
234237
<div>
235238
<h1 className="text-3xl font-bold">Dashboard</h1>
@@ -341,23 +344,33 @@ export default function DashboardPage() {
341344
) : (
342345
<>
343346
{/* Stats Cards */}
344-
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
345-
<div className="bg-zinc-900 border border-zinc-800 rounded-xl p-6">
347+
<div
348+
className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8"
349+
role="region"
350+
aria-label="Connection statistics summary"
351+
>
352+
<div
353+
className="bg-zinc-900 border border-zinc-800 rounded-xl p-6"
354+
aria-label={`Average ping: ${avgPing} milliseconds — ${getQualityLabel(avgPing)}`}
355+
>
346356
<div className="flex items-center gap-3 mb-2">
347-
<Wifi className="w-5 h-5 text-blue-500" />
357+
<Wifi className="w-5 h-5 text-blue-500" aria-hidden="true" />
348358
<span className="text-zinc-400 text-sm">Average Ping</span>
349359
</div>
350-
<div className={`text-3xl font-bold ${getQualityColor(avgPing)}`}>
360+
<div className={`text-3xl font-bold ${getQualityColor(avgPing)}`} aria-hidden="true">
351361
{avgPing}ms
352362
</div>
353363
<div className="text-sm text-zinc-500">
354364
{getQualityLabel(avgPing)}
355365
</div>
356366
</div>
357367

358-
<div className="bg-zinc-900 border border-zinc-800 rounded-xl p-6">
368+
<div
369+
className="bg-zinc-900 border border-zinc-800 rounded-xl p-6"
370+
aria-label={`Average packet loss: ${avgPacketLoss} percent — ${parseFloat(avgPacketLoss) === 0 ? "No loss" : "Some loss"}`}
371+
>
359372
<div className="flex items-center gap-3 mb-2">
360-
<AlertTriangle className="w-5 h-5 text-orange-500" />
373+
<AlertTriangle className="w-5 h-5 text-orange-500" aria-hidden="true" />
361374
<span className="text-zinc-400 text-sm">Packet Loss</span>
362375
</div>
363376
<div
@@ -366,6 +379,7 @@ export default function DashboardPage() {
366379
? "text-green-500"
367380
: "text-orange-500"
368381
}`}
382+
aria-hidden="true"
369383
>
370384
{avgPacketLoss}%
371385
</div>
@@ -374,23 +388,29 @@ export default function DashboardPage() {
374388
</div>
375389
</div>
376390

377-
<div className="bg-zinc-900 border border-zinc-800 rounded-xl p-6">
391+
<div
392+
className="bg-zinc-900 border border-zinc-800 rounded-xl p-6"
393+
aria-label={`Average jitter: ${avgJitter} milliseconds`}
394+
>
378395
<div className="flex items-center gap-3 mb-2">
379-
<TrendingDown className="w-5 h-5 text-purple-500" />
396+
<TrendingDown className="w-5 h-5 text-purple-500" aria-hidden="true" />
380397
<span className="text-zinc-400 text-sm">Jitter</span>
381398
</div>
382-
<div className="text-3xl font-bold text-purple-500">
399+
<div className="text-3xl font-bold text-purple-500" aria-hidden="true">
383400
{avgJitter}ms
384401
</div>
385402
<div className="text-sm text-zinc-500">Variation</div>
386403
</div>
387404

388-
<div className="bg-zinc-900 border border-zinc-800 rounded-xl p-6">
405+
<div
406+
className="bg-zinc-900 border border-zinc-800 rounded-xl p-6"
407+
aria-label={`Total tests run: ${filteredResults.length}`}
408+
>
389409
<div className="flex items-center gap-3 mb-2">
390-
<Clock className="w-5 h-5 text-green-500" />
410+
<Clock className="w-5 h-5 text-green-500" aria-hidden="true" />
391411
<span className="text-zinc-400 text-sm">Tests Run</span>
392412
</div>
393-
<div className="text-3xl font-bold text-green-500">
413+
<div className="text-3xl font-bold text-green-500" aria-hidden="true">
394414
{filteredResults.length}
395415
</div>
396416
<div className="text-sm text-zinc-500">Total tests</div>
@@ -402,7 +422,7 @@ export default function DashboardPage() {
402422
{/* Ping History Chart */}
403423
<div className="bg-zinc-900 border border-zinc-800 rounded-xl p-6">
404424
<h3 className="text-lg font-semibold mb-4">Ping History</h3>
405-
<div className="h-64">
425+
<div className="h-64" role="img" aria-label={`Line chart showing ping history across ${chartData.length} recent tests. Latest ping: ${chartData.at(-1)?.ping ?? 0}ms`}>
406426
<ResponsiveContainer width="100%" height="100%">
407427
<LineChart data={chartData}>
408428
<CartesianGrid strokeDasharray="3 3" stroke="#333" />
@@ -430,7 +450,7 @@ export default function DashboardPage() {
430450
{/* Server Comparison Chart */}
431451
<div className="bg-zinc-900 border border-zinc-800 rounded-xl p-6">
432452
<h3 className="text-lg font-semibold mb-4">Server Comparison</h3>
433-
<div className="h-64">
453+
<div className="h-64" role="img" aria-label={`Bar chart comparing average ping across ${serverChartData.length} servers. Best server: ${serverChartData[0]?.name ?? "N/A"} at ${serverChartData[0]?.ping ?? 0}ms`}>
434454
<ResponsiveContainer width="100%" height="100%">
435455
<BarChart data={serverChartData}>
436456
<CartesianGrid strokeDasharray="3 3" stroke="#333" />
@@ -459,15 +479,18 @@ export default function DashboardPage() {
459479
</span>
460480
</div>
461481
<div className="overflow-x-auto">
462-
<table className="w-full">
482+
<table
483+
className="w-full"
484+
aria-label={`Recent test results — showing ${Math.min(filteredResults.length, 10)} of ${filteredResults.length}`}
485+
>
463486
<thead>
464487
<tr className="text-left text-zinc-400 text-sm">
465-
<th className="pb-4">Server</th>
466-
<th className="pb-4">Ping</th>
467-
<th className="pb-4">Jitter</th>
468-
<th className="pb-4">Loss</th>
469-
<th className="pb-4">ISP</th>
470-
<th className="pb-4">Time</th>
488+
<th scope="col" className="pb-4 font-medium">Server</th>
489+
<th scope="col" className="pb-4 font-medium">Ping</th>
490+
<th scope="col" className="pb-4 font-medium">Jitter</th>
491+
<th scope="col" className="pb-4 font-medium">Loss</th>
492+
<th scope="col" className="pb-4 font-medium">ISP</th>
493+
<th scope="col" className="pb-4 font-medium">Time</th>
471494
</tr>
472495
</thead>
473496
<tbody>
@@ -482,9 +505,8 @@ export default function DashboardPage() {
482505
</div>
483506
</td>
484507
<td
485-
className={`py-4 font-semibold ${getQualityColor(
486-
result.ping_avg
487-
)}`}
508+
className={`py-4 font-semibold ${getQualityColor(result.ping_avg)}`}
509+
aria-label={`${result.ping_avg} milliseconds — ${getQualityLabel(result.ping_avg)}`}
488510
>
489511
{result.ping_avg}ms
490512
</td>
@@ -497,14 +519,17 @@ export default function DashboardPage() {
497519
? "text-green-500"
498520
: "text-orange-500"
499521
}`}
522+
aria-label={`${result.packet_loss} percent packet loss${result.packet_loss === 0 ? " — no loss" : ""}`}
500523
>
501524
{result.packet_loss}%
502525
</td>
503526
<td className="py-4 text-zinc-400">
504527
{result.isp || "Unknown"}
505528
</td>
506529
<td className="py-4 text-zinc-500 text-sm">
507-
{new Date(result.created_at).toLocaleDateString()}
530+
<time dateTime={result.created_at}>
531+
{new Date(result.created_at).toLocaleDateString()}
532+
</time>
508533
</td>
509534
</tr>
510535
))}

web/src/app/download/page.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,13 @@ export default function DownloadPage() {
6565

6666
return (
6767
<div className="min-h-screen">
68+
<a href="#main-content" className="skip-to-content focus-ring">
69+
Skip to main content
70+
</a>
6871
<Navbar />
6972

7073
{/* Main Content */}
71-
<main className="max-w-4xl mx-auto px-4 py-16">
74+
<main id="main-content" className="max-w-4xl mx-auto px-4 py-16">
7275
<div className="text-center mb-12">
7376
<h1 className="text-4xl font-bold mb-4">Download PingDiff</h1>
7477
<p className="text-zinc-400 text-lg">

web/src/components/Navbar.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export function Navbar() {
2424
const isDownload = (href: string) => href === "/download";
2525

2626
return (
27-
<nav className="border-b border-zinc-800 bg-zinc-950/80 backdrop-blur-sm sticky top-0 z-50">
27+
<nav className="border-b border-zinc-800 bg-zinc-950/80 backdrop-blur-sm sticky top-0 z-50" aria-label="Main navigation">
2828
<div className="max-w-6xl mx-auto px-4 py-4 flex justify-between items-center">
2929
<Link href="/" className="flex items-center gap-2 focus-ring rounded-lg">
3030
<Activity className="w-7 h-7 md:w-8 md:h-8 text-blue-500" />
@@ -35,9 +35,10 @@ export function Navbar() {
3535
<button
3636
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
3737
className="md:hidden p-2 text-zinc-400 hover:text-white transition focus-ring rounded-lg"
38-
aria-label={mobileMenuOpen ? "Close menu" : "Open menu"}
38+
aria-label={mobileMenuOpen ? "Close navigation menu" : "Open navigation menu"}
3939
aria-expanded={mobileMenuOpen}
4040
aria-controls="mobile-menu"
41+
aria-haspopup="true"
4142
>
4243
{mobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
4344
</button>
@@ -72,6 +73,8 @@ export function Navbar() {
7273
{mobileMenuOpen && (
7374
<div
7475
id="mobile-menu"
76+
role="menu"
77+
aria-label="Navigation menu"
7578
className="md:hidden border-t border-zinc-800 bg-zinc-950 fade-in"
7679
>
7780
<div className="px-4 py-4 flex flex-col gap-4">

0 commit comments

Comments
 (0)