feat(t5): ein Tabs-Baustein für Listen, Einstellungen und Statistik (T5a A8)
- ui/tabs.tsx nur noch Variante segment: Leiste auf --muted, aktiver Tab mit Kartenfläche, fetter Schrift und 3-px-Balken in --primary-ink; 44 px bei Touch, kompakt mit Maus; inaktive Schrift ≥ 6,27:1. - ActiveHistoryTabs als Vorlage auf Tabs für 10 Listen (Pfeiltasten schalten direkt, Daten sind geladen), mit toolbar und heading. - Einstellungen (Automatisierung, Benutzer & Rollen) und Statistik mit manueller Aktivierung: Pfeil bewegt den Fokus, Enter/Leertaste wählt; ?tab= über router.replace. TabSwitcher entfernt. - Menüeinträge (Dropdown, Select) mit innerem Fokusring ≥ 3:1. - Aufgaben: der Prioritätsfilter lädt die Liste jetzt neu (queryKey). - Wächter: keine eigenen tablist/tab/tabpanel außerhalb ui/tabs.tsx. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
759d85dc67
commit
fce9d10192
26 files changed
+1137
-606
No files matched your search
@@ -0,0 +1,115 @@
|
||||
import { useState } from "react";
|
||||
import { render, screen, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { ActiveHistoryTabs, type EntityLifecycleTab } from "./active-history-tabs";
|
||||
|
||||
function Harness({ onValueChange }: { onValueChange?: (value: EntityLifecycleTab) => void }) {
|
||||
const [value, setValue] = useState<EntityLifecycleTab>("active");
|
||||
return (
|
||||
<ActiveHistoryTabs
|
||||
value={value}
|
||||
onValueChange={(next) => {
|
||||
setValue(next);
|
||||
onValueChange?.(next);
|
||||
}}
|
||||
activeCount={1}
|
||||
historyCount={0}
|
||||
toolbar={<button type="button">Filter</button>}
|
||||
>
|
||||
{{ active: <p>Liste aktiv</p>, history: <p>Liste Historie</p> }}
|
||||
</ActiveHistoryTabs>
|
||||
);
|
||||
}
|
||||
|
||||
describe("ActiveHistoryTabs (spec 10.4)", () => {
|
||||
it("ActiveHistoryTabs shows the counts and renders the matching panel", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onValueChange = vi.fn();
|
||||
render(<Harness onValueChange={onValueChange} />);
|
||||
|
||||
const active = screen.getByRole("tab", { name: "Aktiv (1)" });
|
||||
const history = screen.getByRole("tab", { name: "Historie (0)" });
|
||||
expect(active).toHaveAttribute("aria-selected", "true");
|
||||
expect(screen.getByRole("tabpanel", { name: "Aktiv (1)" })).toHaveTextContent("Liste aktiv");
|
||||
expect(screen.queryByText("Liste Historie")).not.toBeInTheDocument();
|
||||
|
||||
await user.click(history);
|
||||
expect(onValueChange).toHaveBeenLastCalledWith("history");
|
||||
expect(history).toHaveAttribute("aria-selected", "true");
|
||||
expect(screen.getByRole("tabpanel", { name: "Historie (0)" })).toHaveTextContent("Liste Historie");
|
||||
expect(screen.queryByText("Liste aktiv")).not.toBeInTheDocument();
|
||||
|
||||
await user.keyboard("{ArrowLeft}");
|
||||
expect(onValueChange).toHaveBeenLastCalledWith("active");
|
||||
expect(screen.getByRole("tabpanel")).toHaveTextContent("Liste aktiv");
|
||||
});
|
||||
|
||||
it("leaves the counts out while they are unknown", () => {
|
||||
render(
|
||||
<ActiveHistoryTabs value="active" onValueChange={() => {}}>
|
||||
{{ active: null, history: null }}
|
||||
</ActiveHistoryTabs>,
|
||||
);
|
||||
expect(screen.getByRole("tab", { name: "Aktiv" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("tab", { name: "Historie" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("puts the toolbar next to the tabs, outside the panels", () => {
|
||||
render(<Harness />);
|
||||
const filter = screen.getByRole("button", { name: "Filter" });
|
||||
expect(within(screen.getByRole("tabpanel")).queryByRole("button", { name: "Filter" })).toBeNull();
|
||||
expect(screen.getByRole("tablist").parentElement).toContainElement(filter);
|
||||
});
|
||||
|
||||
it("arrow keys switch at once here, because both panels filter data that is already loaded", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Harness />);
|
||||
await user.click(screen.getByRole("tab", { name: "Aktiv (1)" }));
|
||||
await user.keyboard("{ArrowRight}");
|
||||
expect(screen.getByRole("tab", { name: "Historie (0)" })).toHaveAttribute("aria-selected", "true");
|
||||
expect(screen.getByRole("tabpanel")).toHaveTextContent("Liste Historie");
|
||||
});
|
||||
|
||||
it("toolbarAlign end pushes the toolbar to the far right of the row", () => {
|
||||
render(
|
||||
<ActiveHistoryTabs
|
||||
value="active"
|
||||
onValueChange={() => {}}
|
||||
toolbar={<button type="button">Neue Rolle</button>}
|
||||
toolbarAlign="end"
|
||||
>
|
||||
{{ active: null, history: null }}
|
||||
</ActiveHistoryTabs>,
|
||||
);
|
||||
const wrapper = screen.getByRole("button", { name: "Neue Rolle" }).parentElement as HTMLElement;
|
||||
expect(wrapper.className.split(/\s+/)).toContain("ml-auto");
|
||||
expect(screen.getByRole("tablist").parentElement).toContainElement(wrapper);
|
||||
});
|
||||
|
||||
it("a heading opens the row before the tabs, the tabs sit on the right", () => {
|
||||
render(
|
||||
<ActiveHistoryTabs
|
||||
value="active"
|
||||
onValueChange={() => {}}
|
||||
heading={<h2>Lagerbehälter</h2>}
|
||||
>
|
||||
{{ active: null, history: null }}
|
||||
</ActiveHistoryTabs>,
|
||||
);
|
||||
const heading = screen.getByRole("heading", { name: "Lagerbehälter" });
|
||||
const list = screen.getByRole("tablist");
|
||||
expect(heading.compareDocumentPosition(list) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
expect(list.parentElement).toContainElement(heading);
|
||||
expect(list.className.split(/\s+/)).toContain("ml-auto");
|
||||
});
|
||||
|
||||
it("the chips are 44 px high on touch and compact only with a fine pointer", () => {
|
||||
render(<Harness />);
|
||||
for (const tab of screen.getAllByRole("tab")) {
|
||||
const classes = tab.className.split(/\s+/);
|
||||
expect(classes).toContain("h-11");
|
||||
expect(classes).toContain("pointer-fine:h-9");
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user