fix(ui): focus, paging and state that a key or a finger can reach

- A key landing on a side panel's tab steps into the card it peeks open, not onto the hidden tab.
- The Channels cards keep their status / pager row at the list's end while the band is folded.
- A menu row's note shows on an enabled row too: the download state under 'Download…', not a title.
- A removal chip hands the focus to its neighbour before it goes, for every caller (the feed's and
  Plex's active filters); the hidden-filter cue's own copy is gone.
- Back to a spot within a page's end spacer lands at the page's end, not its top.
- A running push / revert is tracked per list: another list's push is no longer refused unseen.
This commit is contained in:
2026-09-27 19:43:04 +02:00
parent e31a588f19
commit 740602b9bf
15 changed files with 231 additions and 48 deletions
@@ -42,16 +42,20 @@ describe("ActionMenu", () => {
// floating-ui never computes a position in jsdom, so the surface stays visibility:hidden; the items
// are in the DOM but the a11y tree treats them as hidden — hence `{ hidden: true }` on role queries.
it("says why a disabled row is off, under its label — and only on a disabled row", () => {
it("says under its label why a disabled row is off, and what an enabled one opens stands at", () => {
// A `title` on a disabled row reached nobody: a disabled button fires no pointer events and
// roving focus skips it. The reason is visible text now, undimmed so it can be read.
// roving focus skips it. The reason is visible text now, undimmed so it can be read. An enabled
// row's state ("Downloaded" under "Download…") is the same line: a `title` shows to a resting
// mouse only, never to a key or a finger (round 2, F5).
renderMenu([
{ id: "move", label: "Move to", onSelect: vi.fn(), disabled: true, note: "No other list" },
{ id: "sync", label: "Sync", onSelect: vi.fn(), note: "never shown" },
{ id: "dl", label: "Download…", onSelect: vi.fn(), note: "Downloaded" },
{ id: "sync", label: "Sync", onSelect: vi.fn() },
]);
fireEvent.click(screen.getByRole("button", { name: "Catalog actions" }));
const [move, sync] = screen.getAllByRole("menuitem", { hidden: true });
const [move, dl, sync] = screen.getAllByRole("menuitem", { hidden: true });
expect(move!.textContent).toBe("Move toNo other list");
expect(dl!.textContent).toBe("Download…Downloaded");
expect(sync!.textContent).toBe("Sync");
const note = screen.getByText("No other list");
expect(note.closest(".opacity-50")).toBeNull();