feat(ui): PageUp/PageDown page through every dropdown list, not just the palette
The palette got PageUp/PageDown, and the same crawl stayed everywhere else a list opens under a control: the sidebar's channel filter, the Plex genre/studio/collection pickers, the filter and sort menus, submenus of every playlist you own. floating-ui's list navigation has only the arrows and Home/End. - `lib/listPaging`: the one definition of a page (the rows wholly in view minus one, a fixed 10 with no layout) and of where it lands (stops at both ends, never on a row it may not take, looking back toward the start rather than past it). The palette now uses it instead of its own copy, and `listNav` takes its scroll-parent helper from here. - `AnchoredPopover`: one keydown handler on the reference AND the surface, so a combobox (keys on its input, `aria-activedescendant`) and a roving menu (keys on the focused row) both page. It runs after floating-ui's own, which has by then switched to keyboard modality and scrolls the new row in. A disabled row is never the target; a submenu's key does not also page the menu around it; with no mounted rows (closed, a hover card, a dialog) the key keeps its native meaning. Measured in the browser: the channel filter shows 8 rows and pages by 7 with the active row in view after the scroll; a menu pages to its last row and back, the page behind it does not scroll; the palette still pages by 11 of its 12 visible rows.
This commit is contained in:
@@ -130,3 +130,67 @@ describe("ActionMenu", () => {
|
||||
expect(screen.getByTestId("custom-trigger")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
// PageUp/PageDown in a roving menu (E11, owner request) — the same page as the palette's and the
|
||||
// combobox's (lib/listPaging); here the keyboard's row holds real DOM focus (the `tabindex=0` rover).
|
||||
describe("ActionMenu · paging", () => {
|
||||
it("moves the roving focus a page at a time, over disabled rows, stopping at the ends", () => {
|
||||
renderMenu(
|
||||
Array.from({ length: 25 }, (_, i) => ({
|
||||
id: `r${i}`,
|
||||
label: `Row ${i}`,
|
||||
onSelect: vi.fn(),
|
||||
disabled: i === 20,
|
||||
})),
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Catalog actions" }));
|
||||
const menu = screen.getByRole("menu", { hidden: true });
|
||||
const rover = () =>
|
||||
screen
|
||||
.getAllByRole("menuitem", { hidden: true })
|
||||
.find((el) => el.getAttribute("tabindex") === "0")?.textContent;
|
||||
|
||||
expect(fireEvent.keyDown(menu, { key: "PageDown" })).toBe(false);
|
||||
expect(rover()).toBe("Row 10");
|
||||
fireEvent.keyDown(menu, { key: "PageDown" });
|
||||
expect(rover()).toBe("Row 21"); // Row 20 is disabled
|
||||
fireEvent.keyDown(menu, { key: "PageDown" });
|
||||
expect(rover()).toBe("Row 24");
|
||||
fireEvent.keyDown(menu, { key: "PageUp" });
|
||||
expect(rover()).toBe("Row 14");
|
||||
});
|
||||
|
||||
it("pages only the submenu the key was pressed in, not the menu around it", () => {
|
||||
// A submenu's surface sits inside its parent's in the React tree, so the key bubbles through the
|
||||
// parent's handler too; paging both would drag the focus back out to the parent's row.
|
||||
renderMenu([
|
||||
{
|
||||
id: "move",
|
||||
label: "Move to",
|
||||
items: Array.from({ length: 25 }, (_, i) => ({
|
||||
id: `s${i}`,
|
||||
label: `Sub ${i}`,
|
||||
onSelect: vi.fn(),
|
||||
})),
|
||||
},
|
||||
...Array.from({ length: 12 }, (_, i) => ({
|
||||
id: `p${i}`,
|
||||
label: `Top ${i}`,
|
||||
onSelect: vi.fn(),
|
||||
})),
|
||||
]);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Catalog actions" }));
|
||||
fireEvent.click(screen.getByText("Move to"));
|
||||
const rovers = () =>
|
||||
screen
|
||||
.getAllByRole("menuitem", { hidden: true })
|
||||
.filter((el) => el.getAttribute("tabindex") === "0")
|
||||
.map((el) => el.textContent ?? "");
|
||||
const outer = () => rovers().filter((label) => !label.startsWith("Sub"));
|
||||
const before = outer();
|
||||
|
||||
fireEvent.keyDown(screen.getByText("Sub 0").closest("button")!, { key: "PageDown" });
|
||||
expect(rovers()).toContain("Sub 10");
|
||||
expect(outer()).toEqual(before);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user