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
+30 -1
View File
@@ -3,7 +3,8 @@
// E28 S3b: the chip family (docs/DESIGN.md "One primitive per button family"). One geometry for a
// filter, an applied narrowing, a tag and a suggestion, where five files had five.
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
import { useState } from "react";
import { Chip, DismissibleChip } from "./Chip";
import { chipClass } from "./chipStyles";
@@ -41,6 +42,34 @@ describe("Chip", () => {
expect(onClick).toHaveBeenCalledOnce();
});
// Round 2 (F6): the feed's and Plex's active-filter chips unmount with the filter they remove,
// and a keyboard press left the focus on <body>. The chip hands it on itself, for every caller.
it("a removing chip pressed from the keyboard hands the focus to its neighbour", () => {
function Row() {
const [chips, setChips] = useState(["Music", "News"]);
return (
<div>
{chips.map((c) => (
<Chip
key={c}
variant="token"
removeLabel={`Remove ${c}`}
onClick={() => setChips((all) => all.filter((x) => x !== c))}
>
{c}
</Chip>
))}
</div>
);
}
render(<Row />);
const music = screen.getByRole("button", { name: "Remove Music" });
act(() => music.focus());
fireEvent.click(music, { detail: 0 });
expect(screen.queryByRole("button", { name: "Remove Music" })).toBeNull();
expect(document.activeElement).toBe(screen.getByRole("button", { name: "Remove News" }));
});
it("the measuring mirror lays out exactly like the chip it measures", () => {
render(
<Chip variant="tag" size="xs">