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:
+7
-5
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user