feat(a11y): keyboard navigation for ARIA tab panels (ArrowLeft/Right/Home/End)

Both validera and katalog/[id] tab lists now follow the ARIA Authoring
Practices Guide tabs pattern: arrow keys move focus + activate adjacent
tabs, Home/End jump to first/last tab.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-25 18:18:48 +02:00
co-authored by Claude Sonnet 4.6
parent c95cc44a11
commit 369bc11281
2 changed files with 28 additions and 0 deletions
+14
View File
@@ -231,6 +231,19 @@ function DatasetView({ d, tab, setTab }: { d: Dataset; tab: TabKey; setTab: (k:
const deposit = Math.round(d.price / 2);
const remainder = d.price - deposit;
function handleTabKey(e: React.KeyboardEvent, idx: number) {
let next = -1;
if (e.key === "ArrowRight") next = (idx + 1) % TABS.length;
else if (e.key === "ArrowLeft") next = (idx - 1 + TABS.length) % TABS.length;
else if (e.key === "Home") next = 0;
else if (e.key === "End") next = TABS.length - 1;
if (next !== -1) {
e.preventDefault();
setTab(TABS[next].key);
document.getElementById(`katalog-tab-${TABS[next].key}`)?.focus();
}
}
return (
<>
{/* breadcrumb */}
@@ -279,6 +292,7 @@ function DatasetView({ d, tab, setTab }: { d: Dataset; tab: TabKey; setTab: (k:
aria-selected={active}
aria-controls={`katalog-panel-${tb.key}`}
onClick={() => setTab(tb.key)}
onKeyDown={(e) => handleTabKey(e, TABS.findIndex((t) => t.key === tb.key))}
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)",
+14
View File
@@ -77,6 +77,19 @@ export default function ValidationPage() {
const toggleDest = (d: string) => setDests((p) => (p.includes(d) ? p.filter((x) => x !== d) : [...p, d]));
function handleTabKey(e: React.KeyboardEvent, idx: number) {
let next = -1;
if (e.key === "ArrowRight") next = (idx + 1) % TABS.length;
else if (e.key === "ArrowLeft") next = (idx - 1 + TABS.length) % TABS.length;
else if (e.key === "Home") next = 0;
else if (e.key === "End") next = TABS.length - 1;
if (next !== -1) {
e.preventDefault();
setTab(TABS[next].key);
document.getElementById(`validera-tab-${TABS[next].key}`)?.focus();
}
}
async function approve() {
try {
const r = await fetch(`/api/orders/${id}`, {
@@ -130,6 +143,7 @@ export default function ValidationPage() {
aria-selected={tab === tb.key}
aria-controls={`validera-panel-${tb.key}`}
onClick={() => setTab(tb.key)}
onKeyDown={(e) => handleTabKey(e, i)}
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)" }}
>