feat(design): a page's background menu on the five list pages (E28 S3e4c)
A right-click on the EMPTY area of a page opens a menu of that page's own toolbar: the feed (sort, reshuffle, view, watch state, content, source, clear filters), the Plex library grid (sort, watch state, clear), the channel manager (layout, the cards' sort, sync status, edit tags, clear the tag filter, read subscriptions), a playlist (play all, group by, reset view, save order, undo/redo, the list's own menu) and the notification center (mark all read). No destructive row and no backfill. PageShell listens on the page area; lib/backgroundTarget decides empty (not text, a control, a link, media, or a row/card with its own menu). Shift+right- click, a touch long-press and a right-click on selected words keep the browser's menu. ui/PageMenu registers while mounted, so a loading or error state keeps the browser's menu too. The rows mirror each control through ui/pageMenuItems (sortSubmenu via pickSort, choiceSubmenu, multiSubmenu). pageMenuScope.contract.test.ts pins the five pages.
This commit is contained in:
@@ -37,14 +37,16 @@ vi.mock("../lib/notifications", async (importOriginal) => ({
|
||||
}));
|
||||
vi.mock("./playlistsContext", () => ({ usePlaylists: () => ({ selectedId: h.selectedId }) }));
|
||||
vi.mock("./confirmContext", () => ({ useConfirm: () => h.confirm }));
|
||||
// The list's own menu is usePlaylistActions' (pinned in its own test); here only its rename row,
|
||||
// which opens THIS page's title field.
|
||||
// The list's own menu is usePlaylistActions' (pinned in its own test); here its rename row, which
|
||||
// opens THIS page's title field, and its two destructive rows as that hook marks them (danger).
|
||||
vi.mock("./usePlaylistActions", () => ({
|
||||
usePlaylistActions: () => ({
|
||||
remove: vi.fn(),
|
||||
rename: vi.fn(),
|
||||
items: (_pl: unknown, o: { onRename: () => void }) => [
|
||||
{ id: "rename", label: "playlists.rename", onSelect: o.onRename },
|
||||
{ id: "revert", label: "playlists.revert", danger: true, onSelect: vi.fn() },
|
||||
{ id: "delete", label: "playlists.delete", danger: true, onSelect: vi.fn() },
|
||||
],
|
||||
}),
|
||||
}));
|
||||
@@ -67,6 +69,7 @@ vi.mock("./PlayerModal", async () => {
|
||||
|
||||
import Playlists from "./Playlists";
|
||||
import { PageToolbarSlot } from "./ui/pageToolbarSlot";
|
||||
import { PageMenuSlot, type PageMenuOpener } from "./ui/pageMenuSlot";
|
||||
import { api, HttpError, type Video } from "../lib/api";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { patchCachedProgress } from "../lib/progressCache";
|
||||
@@ -633,3 +636,46 @@ describe("Playlists · the header", () => {
|
||||
expect(await screen.findByText("playlists.groupByChannel")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
// DESIGN "Background menu": the page's blank area mirrors the band (Play all, the view controls) and
|
||||
// the list's own "…" — without its destructive rows, which stay one deliberate "…" away.
|
||||
describe("Playlists · the page's background menu", () => {
|
||||
it("offers Play all, Group by and the list's menu, never Delete", async () => {
|
||||
let open: PageMenuOpener | null = null;
|
||||
vi.spyOn(api, "playlists").mockResolvedValue([list(1, [vid("A", 0), vid("B", 0)])] as never);
|
||||
vi.spyOn(api, "playlist").mockImplementation(
|
||||
async () => list(1, [vid("A", 0), vid("B", 0)]) as never,
|
||||
);
|
||||
qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
qc.setQueryData(qk.me(), { id: 1, can_write: true, prefs: {}, preferences: {} });
|
||||
render(
|
||||
<QueryClientProvider client={qc}>
|
||||
<PageMenuSlot.Provider
|
||||
value={(o) => {
|
||||
open = o;
|
||||
return () => {
|
||||
if (open === o) open = null;
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Playlists />
|
||||
</PageMenuSlot.Provider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
await screen.findByRole("button", { name: "card.play — Video A" });
|
||||
expect(open, "registered once a list is on screen").not.toBeNull();
|
||||
act(() => open!(5, 5, document.body));
|
||||
const rows = await waitFor(() => {
|
||||
const r = screen
|
||||
.getAllByRole("menu", { hidden: true })
|
||||
.find((m) => m.getAttribute("aria-label") === "common.pageMenu");
|
||||
expect(r).toBeTruthy();
|
||||
return [...r!.querySelectorAll("[role^=menuitem]")].map((b) => b.textContent);
|
||||
});
|
||||
expect(rows).toEqual(
|
||||
expect.arrayContaining(["playlists.playAll", "playlists.groupByChannel", "playlists.rename"]),
|
||||
);
|
||||
expect(rows).not.toContain("playlists.delete");
|
||||
expect(rows).not.toContain("playlists.revert");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user