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
+4 -2
View File
@@ -40,6 +40,7 @@ import { LoadingState, StateMessage } from "./ui/QueryState";
import AccountPanel from "./AccountPanel";
import { useConfirm } from "./confirmContext";
import Tabs from "./ui/Tabs";
import { useTabPanel } from "./ui/tabPanel";
import { useAccountPersistedState } from "../lib/storage";
import { PageToolbar } from "./ui/PageShell";
import { useMe } from "../lib/useMe";
@@ -65,6 +66,7 @@ export default function AdminUsers() {
const me = useMe();
const { t } = useTranslation();
const [tab, setTab] = useAccountPersistedState(ADMIN_USERS_TAB_KEY, "roles");
const panel = useTabPanel();
const tabs = [
{ id: "roles", label: t("users.tabs.roles") },
// Both kinds of "waiting on the admin": a request needing a decision, and a sign-up whose
@@ -83,10 +85,10 @@ export default function AdminUsers() {
{/* The tab bar is fixed chrome; the tab content scrolls under it. */}
<PageToolbar>
<div className="px-4 pt-2 max-w-3xl w-full mx-auto">
<Tabs tabs={tabs} active={active} onChange={setTab} />
<Tabs idBase={panel.idBase} tabs={tabs} active={active} onChange={setTab} />
</div>
</PageToolbar>
<div className="px-4 pb-4 pt-3 max-w-3xl w-full mx-auto">
<div {...panel.panelProps(active)} className="px-4 pb-4 pt-3 max-w-3xl w-full mx-auto">
{active === "roles" && <UsersRoles me={me} />}
{active === "access" && (
<>