fix(ui): Tabs is an ARIA tab list with roving focus and a wired panel

The rail was a row of aria-pressed toggles: every tab a Tab stop, no arrows,
nothing tying a tab to its content. tablist/tab/aria-selected, one Tab stop,
arrows on either axis, Home/End, and useTabPanel for aria-controls/labelledby.
This commit is contained in:
2026-09-27 14:22:37 +02:00
parent a975ab627a
commit 922d589b09
14 changed files with 311 additions and 139 deletions
+5 -1
View File
@@ -13,6 +13,7 @@ import Tooltip from "./ui/Tooltip";
import { LoadingState, StateMessage } from "./ui/QueryState";
import FilterChipGroup from "./ui/FilterChipGroup";
import Tabs from "./ui/Tabs";
import { useTabPanel } from "./ui/tabPanel";
import { PageToolbar } from "./ui/PageShell";
import { Section, SettingRow } from "./ui/form";
import { useMe } from "../lib/useMe";
@@ -34,15 +35,18 @@ export default function Stats() {
const [tabRaw, setTab] = useAccountPersistedState(LS.statsTab, "overview");
// Clamp at render: only admins may land on the System tab (covers a stale stored value).
const tab: StatsTab = tabRaw === "system" && isAdmin ? "system" : "overview";
const panel = useTabPanel();
return (
<div className="px-4 pb-4 pt-3 max-w-4xl mx-auto">
// A tab panel only where there are tabs: everyone but an admin sees the Overview alone.
<div {...(isAdmin ? panel.panelProps(tab) : {})} className="px-4 pb-4 pt-3 max-w-4xl mx-auto">
{/* Only admins have a second (System) tab; everyone else just sees the Overview. The tabs are
the page's band (B4), so they stay in reach on the long System tab. */}
{isAdmin && (
<PageToolbar>
<div className="px-4 pt-2 max-w-4xl w-full mx-auto">
<Tabs
idBase={panel.idBase}
active={tab}
onChange={setTab}
tabs={(["overview", "system"] as StatsTab[]).map((id) => ({