fix(plex): the player's ⋯ and tracks lists are real menus, and every close returns the focus
They were hand-built divs: no menu roles, no arrow keys, and Escape left the focus on <body>. Both run on the popover engine now (media-tone rows), and the tuning panel is a labelled dialog that takes the focus and hands it back to ⋯.
This commit is contained in:
@@ -183,6 +183,17 @@ function pressPicture(pointerType: "touch" | "mouse", x = 450) {
|
||||
fireEvent.click(v, { clientX: x, clientY: 250 });
|
||||
}
|
||||
|
||||
// A menu's rows are named by their text: the surface is `visibility: hidden` until floating-ui has
|
||||
// measured it (never, under jsdom), and a hidden element's accessible name is empty.
|
||||
const menuItem = (label: string) =>
|
||||
waitFor(() => {
|
||||
const row = screen
|
||||
.queryAllByRole("menuitem", { hidden: true })
|
||||
.find((b) => b.textContent === label);
|
||||
expect(row).toBeDefined();
|
||||
return row!;
|
||||
});
|
||||
|
||||
describe("a tap on the picture (C3)", () => {
|
||||
it("a mouse click plays or pauses; a finger's tap only shows or hides the controls", async () => {
|
||||
mount({ details: { A: item("A") } });
|
||||
@@ -277,13 +288,58 @@ describe("the C5 row", () => {
|
||||
"plex.player.help",
|
||||
"plex.player.download",
|
||||
];
|
||||
for (const r of rows) expect(screen.getByRole("button", { name: r }), r).toBeTruthy();
|
||||
const menu = await screen.findByRole("menu", { hidden: true });
|
||||
expect(
|
||||
[...menu.querySelectorAll('[role="menuitem"]')].map(
|
||||
(b) => b.getAttribute("aria-label") ?? b.textContent,
|
||||
),
|
||||
).toEqual(rows);
|
||||
// A row closes the menu and opens its tool: Settings → the tuning panel.
|
||||
fireEvent.click(screen.getByRole("button", { name: "plex.player.settings" }));
|
||||
expect(screen.queryByRole("button", { name: "plex.player.stats.title" })).toBeNull();
|
||||
fireEvent.click(await menuItem("plex.player.settings"));
|
||||
await screen.findByRole("dialog", { name: "plex.player.settings" });
|
||||
expect(
|
||||
screen
|
||||
.queryAllByRole("menuitem", { hidden: true })
|
||||
.find((b) => b.textContent === "plex.player.stats.title") ?? null,
|
||||
).toBeNull();
|
||||
expect(screen.getByLabelText("plex.player.more").getAttribute("aria-expanded")).toBe("true");
|
||||
});
|
||||
|
||||
// E28 review D3: the ⋯ was a hand-built <div> of buttons — no menu role, no arrow keys, and Escape
|
||||
// closed it with the focus left on <body>. It is a menu on the popover engine now.
|
||||
it("walks the ⋯ with the arrows, and Escape gives the focus back to the ⋯", async () => {
|
||||
mount({ details: { A: item("A") } });
|
||||
await playingAt("/media/A", 100);
|
||||
const more = screen.getByLabelText("plex.player.more");
|
||||
more.focus();
|
||||
fireEvent.click(more);
|
||||
const menu = await screen.findByRole("menu", { hidden: true });
|
||||
const rows = [...menu.querySelectorAll<HTMLElement>('[role="menuitem"]')];
|
||||
await waitFor(() => expect(document.activeElement).toBe(rows[0]));
|
||||
fireEvent.keyDown(document.activeElement!, { key: "ArrowDown" });
|
||||
expect(document.activeElement).toBe(rows[1]);
|
||||
fireEvent.keyDown(document.activeElement!, { key: "End" });
|
||||
expect(document.activeElement).toBe(rows[rows.length - 1]);
|
||||
fireEvent.keyDown(document.activeElement!, { key: "Escape" });
|
||||
await waitFor(() => expect(screen.queryByRole("menu")).toBeNull());
|
||||
await waitFor(() => expect(document.activeElement).toBe(more));
|
||||
});
|
||||
|
||||
it("gives the focus back to the ⋯ when the tuning panel closes from inside", async () => {
|
||||
mount({ details: { A: item("A") } });
|
||||
await playingAt("/media/A", 100);
|
||||
const more = screen.getByLabelText("plex.player.more");
|
||||
fireEvent.click(more);
|
||||
fireEvent.click(await menuItem("plex.player.settings"));
|
||||
const panel = await screen.findByRole("dialog", { name: "plex.player.settings" });
|
||||
// Into the panel on open…
|
||||
await waitFor(() => expect(panel.contains(document.activeElement)).toBe(true));
|
||||
fireEvent.keyDown(document.activeElement!, { key: "Escape" });
|
||||
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
||||
// …and back on the ⋯ when it goes, not on <body>.
|
||||
await waitFor(() => expect(document.activeElement).toBe(more));
|
||||
});
|
||||
|
||||
it("Next shows only when there is a next item", async () => {
|
||||
mount({ details: { A: item("A") } });
|
||||
await playingAt("/media/A", 100);
|
||||
|
||||
Reference in New Issue
Block a user