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
@@ -136,3 +136,27 @@ describe("DataTable · keyboardNav", () => {
expect(document.activeElement).toBe(document.body);
});
});
describe("DataTable · reveal", () => {
const many: Row[] = Array.from({ length: 25 }, (_, i) => ({ id: `r${i}` }));
const cols: Column<Row>[] = [
{ key: "name", header: "Name", cardPrimary: true, render: (r) => r.id },
];
const shown = () => [...document.querySelectorAll("tbody tr")].map((tr) => tr.textContent);
it("turns to the page that now holds a moved row on each new seq", () => {
viewport(true);
const view = (list: Row[], reveal: { key: string; seq: number } | null) => (
<DataTable rows={list} columns={cols} rowKey={(r) => r.id} reveal={reveal} />
);
const { rerender } = render(view(many, { key: "r22", seq: 0 }));
// The mount's seq is not a request: the first page stays.
expect(shown()[0]).toBe("r0");
rerender(view(many, { key: "r22", seq: 1 }));
expect(shown()).toContain("r22");
expect(shown()[0]).toBe("r20");
// r22 bubbled up to the top: the next seq follows it back to page one.
rerender(view([many[22]!, ...many.filter((r) => r.id !== "r22")], { key: "r22", seq: 2 }));
expect(shown()[0]).toBe("r22");
});
});