feat(design): a Plex title's one action list (C1) — the poster's "…", details and the pin in its menu (E28 S3e2)

Every Plex poster list (library, Discover rails, person, show, season, collection, Home's rails)
now opens one menu per list from the right-click, the long-press, Shift+F10 and the card's "…":
play/open, watched, add to playlist, media info, copy title, the pin (usePlexCardActions +
PlexMenu, replacing the pin-only PlexPinMenu). The card keeps its state at rest and shows
watched / add to playlist / "…" on its picture on hover (ui/reveal.ts, shared with the video
card); on touch the toggles are gone and a 44px "…" sits beside the title, while the watched pill
stays. The hover ⓘ left the card; an episode's add button moved from its caption to the hover
row. Seasons get their own short menu (open, watched, add, copy), labelled "Open season" (the
card's overlay said "Open show"). Related shows are ordinary poster cards now. A collection's
admin "Remove" X moved into its menu, still behind the confirmation. The collection grid is two
columns on a phone. The unused .reveal-idle rule is gone.
This commit is contained in:
2026-09-26 01:51:53 +02:00
parent 75ae810866
commit a4737b92e4
17 changed files with 1003 additions and 465 deletions
+16 -17
View File
@@ -1,7 +1,7 @@
// @vitest-environment jsdom
//
// Plex card a11y: locks the accessible names on the icon-only card controls (the E4 a11y sweep). These
// buttons are icon-only (WatchedIcon / Info / ListPlus, no text), so their name comes solely from the
// buttons are icon-only (WatchedIcon / ListPlus / the "…", no text), so their name comes solely from the
// aria-label — a dropped label silently reintroduces an unnamed button for screen-reader users. i18n is
// key-passthrough, so the asserted names are the i18n keys.
import { describe, it, expect, afterEach, vi } from "vitest";
@@ -42,23 +42,26 @@ const season = (status = "new"): PlexSeasonDetail => ({
afterEach(cleanup);
describe("PlexPosterCard a11y", () => {
it("names the watched-toggle (mark-watched when unwatched) and the info button", () => {
it("names the hover row: the watched-toggle, add to playlist and the menu's '…'", () => {
render(
<PlexPosterCard card={movie()} onOpen={vi.fn()} onInfo={vi.fn()} onToggleWatched={vi.fn()} />,
<PlexPosterCard
card={movie()}
onOpen={vi.fn()}
onToggleWatched={vi.fn()}
onAdd={vi.fn()}
menuButtonProps={() => ({})}
/>,
);
expect(screen.getByRole("button", { name: "plex.markWatched" })).toBeTruthy();
expect(screen.getByRole("button", { name: "plex.info.title" })).toBeTruthy();
expect(screen.getByRole("button", { name: "plex.playlistAdd.manage" })).toBeTruthy();
// Two: the hover row's and its touch twin beside the title (CSS shows one per device).
expect(screen.getAllByRole("button", { name: "card.menu.more" })).toHaveLength(2);
// Details moved into the menu (E28 C1): the card has no ⓘ of its own any more.
expect(screen.queryByRole("button", { name: "plex.info.title" })).toBeNull();
});
it("names the watched-toggle as mark-unwatched when the movie is watched", () => {
render(
<PlexPosterCard
card={movie("watched")}
onOpen={vi.fn()}
onInfo={vi.fn()}
onToggleWatched={vi.fn()}
/>,
);
render(<PlexPosterCard card={movie("watched")} onOpen={vi.fn()} onToggleWatched={vi.fn()} />);
expect(screen.getByRole("button", { name: "plex.markUnwatched" })).toBeTruthy();
});
@@ -67,7 +70,6 @@ describe("PlexPosterCard a11y", () => {
<PlexPosterCard
card={{ ...movie(), year: 2023, rating: 7.5, summary: "A quiet synopsis." }}
onOpen={vi.fn()}
onInfo={vi.fn()}
onToggleWatched={vi.fn()}
/>,
);
@@ -76,9 +78,7 @@ describe("PlexPosterCard a11y", () => {
});
it("omits the scrim when the card has no synopsis or rating", () => {
render(
<PlexPosterCard card={movie()} onOpen={vi.fn()} onInfo={vi.fn()} onToggleWatched={vi.fn()} />,
);
render(<PlexPosterCard card={movie()} onOpen={vi.fn()} onToggleWatched={vi.fn()} />);
expect(screen.queryByText(/synopsis/i)).toBeNull();
});
});
@@ -111,7 +111,6 @@ describe("Plex cards in a keyboard-walkable list", () => {
<PlexPosterCard
card={movie()}
onOpen={onOpen}
onInfo={vi.fn()}
onToggleWatched={vi.fn()}
navIndex={navIndex}
/>