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