feat(a11y): add ARIA tablist/tab/tabpanel roles + i18n error page

- validera: role=tablist + role=tab + aria-selected + aria-controls/id
  on tab buttons; role=tabpanel + id + aria-labelledby on content div
- katalog/[id]: same ARIA tab pattern; replaced incorrect aria-current=page
  on tab buttons with role=tab + aria-selected
- error.tsx: i18n all hardcoded English strings via useT()

All three changes pass tsc 0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-25 18:17:19 +02:00
co-authored by Claude Sonnet 4.6
parent 37f3c9e6b8
commit c95cc44a11
3 changed files with 33 additions and 12 deletions
+7 -5
View File
@@ -3,6 +3,7 @@
import Link from "next/link";
import { useEffect } from "react";
import { Header } from "@/components/header";
import { useT } from "@/components/locale-provider";
export default function Error({
error,
@@ -11,6 +12,7 @@ export default function Error({
error: Error & { digest?: string };
reset: () => void;
}) {
const t = useT();
useEffect(() => {
console.error(error);
}, [error]);
@@ -27,26 +29,26 @@ export default function Error({
500
</span>
<p className="font-mono text-[11px] uppercase tracking-[0.18em] text-muted-foreground">
Error
{t({ en: "Error", sv: "Fel" })}
</p>
<h1 className="mt-5 max-w-[14ch] text-[clamp(2.6rem,7vw,5.5rem)] font-medium leading-[0.95] tracking-[-0.035em]">
Something went wrong.
{t({ en: "Something went wrong.", sv: "Något gick fel." })}
</h1>
<p className="mt-6 max-w-[40ch] font-mono text-sm text-muted-foreground">
An unexpected error occurred. Nothing was guessed.{error.digest ? ` (${error.digest})` : ""}
{t({ en: "An unexpected error occurred. Nothing was guessed.", sv: "Ett oväntat fel inträffade. Inget gissades." })}{error.digest ? ` (${error.digest})` : ""}
</p>
<div className="mt-12 flex flex-wrap gap-3">
<button
onClick={reset}
className="bg-foreground px-7 py-4 font-mono text-[12px] uppercase tracking-[0.14em] text-background transition-colors hover:bg-[var(--accent-red)]"
>
Try again →
{t({ en: "Try again →", sv: "Försök igen →" })}
</button>
<Link
href="/"
className="border border-foreground px-7 py-4 font-mono text-[12px] uppercase tracking-[0.14em] transition-colors hover:bg-foreground hover:text-background"
>
Home
{t({ en: "Home", sv: "Hem" })}
</Link>
</div>
</div>
+13 -4
View File
@@ -267,15 +267,18 @@ function DatasetView({ d, tab, setTab }: { d: Dataset; tab: TabKey; setTab: (k:
<div className="grid grid-cols-1 lg:grid-cols-[1fr_340px]">
<div className="reveal-left delay-1 lg:border-r lg:border-border">
{/* tabs */}
<nav aria-label={t({ en: "Views", sv: "Vyer" })} className="sticky top-16 z-30 -mx-px flex flex-wrap gap-px border-b border-border bg-border">
<div role="tablist" aria-label={t({ en: "Dataset views", sv: "Datasetvyer" })} className="sticky top-16 z-30 -mx-px flex flex-wrap gap-px border-b border-border bg-border">
{TABS.map((tb) => {
const active = tb.key === tab;
return (
<button
key={tb.key}
id={`katalog-tab-${tb.key}`}
role="tab"
type="button"
aria-selected={active}
aria-controls={`katalog-panel-${tb.key}`}
onClick={() => setTab(tb.key)}
aria-current={active ? "page" : undefined}
className="flex-1 bg-background px-4 py-3.5 font-mono text-[11px] uppercase tracking-[0.1em] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
style={{
backgroundColor: active ? "var(--foreground)" : "var(--background)",
@@ -286,9 +289,15 @@ function DatasetView({ d, tab, setTab }: { d: Dataset; tab: TabKey; setTab: (k:
</button>
);
})}
</nav>
</div>
<div className="px-1 py-10 lg:pr-8">
<div
className="px-1 py-10 lg:pr-8"
role="tabpanel"
id={`katalog-panel-${tab}`}
aria-labelledby={`katalog-tab-${tab}`}
tabIndex={0}
>
{tab === "overview" ? <OverviewTab d={d} /> : null}
{tab === "samples" ? <SampleTab d={d} /> : null}
{tab === "quality" ? <QualityTab d={d} /> : null}
+13 -3
View File
@@ -121,12 +121,16 @@ export default function ValidationPage() {
<div className="mx-auto max-w-[80rem] px-6 py-12">
{/* tabs */}
<div className="reveal flex flex-wrap border-b border-border">
<div role="tablist" aria-label={t({ en: "Result views", sv: "Resultatvyer" })} className="reveal flex flex-wrap border-b border-border">
{TABS.map((tb, i) => (
<button
key={tb.key}
id={`validera-tab-${tb.key}`}
role="tab"
aria-selected={tab === tb.key}
aria-controls={`validera-panel-${tb.key}`}
onClick={() => setTab(tb.key)}
className="group relative -mb-px px-5 py-3.5 font-mono text-[11px] uppercase tracking-[0.12em] transition-colors"
className="group relative -mb-px px-5 py-3.5 font-mono text-[11px] uppercase tracking-[0.12em] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
style={{ color: tab === tb.key ? "var(--foreground)" : "var(--muted-foreground)" }}
>
<span className="mr-2 text-muted-foreground">{String(i + 1).padStart(2, "0")}</span>
@@ -140,7 +144,13 @@ export default function ValidationPage() {
))}
</div>
<div className="mt-10">
<div
className="mt-10"
role="tabpanel"
id={`validera-panel-${tab}`}
aria-labelledby={`validera-tab-${tab}`}
tabIndex={0}
>
{/* Table tab */}
{tab === "table" ? (
<div className="reveal-fade">