feat(design): the channel menu's Tags and Priority submenus (C1, E28 S3e3b)

Tags A-Z as in-place checks (the menu stays open for the next); Priority
as the fixed steps 10 5 3 2 1 0, an off-scale current value ticked in its
place. A pick re-sorts the list at once, moving only that row
(placeByPriority, the server's order), and the table / card pager and
the focus follow it (DataTable and useCardPager take a reveal seq,
listNav.focusNavItem lands on the row's own "…", a virtualized card
revealed first). ActionMenu rows take check: many | one. The menu reads
the row fresh from the cache and keys its target by id.
This commit is contained in:
2026-09-26 17:34:47 +02:00
parent 2b3094ffc5
commit 5e5c8a42a9
15 changed files with 467 additions and 28 deletions
@@ -84,6 +84,37 @@ describe("ActionMenu", () => {
await vi.waitFor(() => expect(onSelect).toHaveBeenCalledTimes(1));
});
it("a 'many' check row toggles in place: the menu stays open and the focus stays on it", async () => {
const onSelect = vi.fn();
renderMenu([
{ id: "rust", label: "rust", check: { kind: "many", checked: true }, onSelect },
{ id: "go", label: "go", check: { kind: "many", checked: false }, onSelect: vi.fn() },
]);
fireEvent.click(screen.getByRole("button", { name: "Catalog actions" }));
const rows = screen.getAllByRole("menuitemcheckbox", { hidden: true });
expect(rows.map((r) => r.getAttribute("aria-checked"))).toEqual(["true", "false"]);
fireEvent.click(rows[0]!);
// Synchronously — no close, no deferred act — and nothing closed around it.
expect(onSelect).toHaveBeenCalledTimes(1);
expect(screen.getAllByRole("menuitemcheckbox", { hidden: true })).toHaveLength(2);
await new Promise((r) => setTimeout(r, 0));
expect(screen.getAllByRole("menuitemcheckbox", { hidden: true })).toHaveLength(2);
});
it("a 'one' check row is a radio that picks and closes like any row", async () => {
const onSelect = vi.fn();
renderMenu([
{ id: "10", label: "10", check: { kind: "one", checked: false }, onSelect },
{ id: "5", label: "5", check: { kind: "one", checked: true }, onSelect: vi.fn() },
]);
fireEvent.click(screen.getByRole("button", { name: "Catalog actions" }));
const rows = screen.getAllByRole("menuitemradio", { hidden: true });
expect(rows.map((r) => r.getAttribute("aria-checked"))).toEqual(["false", "true"]);
fireEvent.click(rows[0]!);
expect(screen.queryByRole("menuitemradio", { hidden: true })).toBeNull();
await vi.waitFor(() => expect(onSelect).toHaveBeenCalledTimes(1));
});
it("a disabled item is inert — rendered disabled and never fires onSelect", () => {
const onSelect = vi.fn();
renderMenu([{ id: "sync", label: "Sync", onSelect, disabled: true }]);