fix(keys): a list inside the topmost layer walks by keyboard

The list keys are the page's and an open layer silences them, so the
player's related videos could only be reached by Tab. A layer now names
its root element; with the focus on a row inside the topmost one, j/k,
the arrows and Enter walk that list, the walk stays inside the layer,
and the player's seek and volume arrows leave a focused row alone.
This commit is contained in:
2026-10-24 22:45:26 +02:00
parent 5205091116
commit 7d45c4f59f
8 changed files with 194 additions and 42 deletions
+29
View File
@@ -202,6 +202,35 @@ describe("useListNav", () => {
removeLayer(tooltip);
});
// A list INSIDE the topmost layer (the player's related videos) is the one the user is in.
it("walks a list inside the topmost layer, and only inside it", () => {
render(
<>
<List id="a" count={2} />
<div data-testid="layer">
<List id="b" count={2} />
</div>
</>,
);
const dialog = pushLayer({ root: () => screen.getByTestId("layer") });
act(() => screen.getByTestId("b0-action").focus());
press("j");
expect(focused()).toBe("b1");
press("k");
press("k"); // its first row: the walk stops there, not on the page's list behind
expect(focused()).toBe("b0");
// A menu opened over it moves with its own keys: the list stays still.
const menu = pushLayer();
press("j");
expect(focused()).toBe("b0");
removeLayer(menu);
// Focus left behind on the page's list does not walk it while the layer is up.
act(() => screen.getByTestId("a0-action").focus());
press("j");
expect(focused()).toBe("a0-action");
removeLayer(dialog);
});
// A virtualized grid only mounts a screen of rows: the next item may not exist yet.
it("asks the list to reveal an unmounted item, then focuses it once it renders", async () => {
const reveal = vi.fn();