fix(ui): a membership menu re-homes focus a frame later, once its new rows are on the list

Rows that just mounted join the menu's navigation list a render after the status effect ran: a row
focused that early was unknown to it, and the arrows did nothing from it (seen live after a keyboard
Retry). The retry test now arrows on from the row it lands on.
This commit is contained in:
2026-10-08 22:16:47 +02:00
parent 4eaa55385f
commit 83886494ce
2 changed files with 23 additions and 13 deletions
@@ -364,7 +364,9 @@ describe("a Plex title's menu — Add to collection", () => {
// A menu owns its items only: the line about the list sits beside it, not in it.
expect(document.querySelector("[role=menu]")).not.toBeNull();
expect(alert.closest("[role=menu]")).toBeNull();
list.mockResolvedValue({ collections: [coll("cB", "Beta", false)] });
list.mockResolvedValue({
collections: [coll("cB", "Beta", false), coll("cC", "Gamma", false)],
});
// Retry from the keyboard: the focused button goes as the list loads, and focus must not be left
// on <body> with the menu open — it lands on the first row that came back.
const retry = (await screen.findByText("common.retry")).closest("button")!;
@@ -372,6 +374,9 @@ describe("a Plex title's menu — Add to collection", () => {
fireEvent.click(retry);
const beta = await checkRow("Beta");
await vi.waitFor(() => expect(document.activeElement).toBe(beta));
// …and the menu's own arrows carry on from there.
fireEvent.keyDown(beta, { key: "ArrowDown" });
expect(document.activeElement).toBe(await checkRow("Gamma"));
});
it("clears the name when Plex made the collection and only the read after it failed", async () => {
@@ -79,6 +79,9 @@ export function MembershipMenu({
useEffect(() => {
if (shownStatus.current === status) return;
shownStatus.current = status;
// A frame later: rows that just mounted join the menu's list a render after this effect, and a
// row focused before that is not known to it — the arrows then did nothing from it.
const frame = requestAnimationFrame(() => {
const root = rootRef.current;
const at = document.activeElement;
// …unless a name is being typed into it meanwhile.
@@ -91,6 +94,8 @@ export function MembershipMenu({
root.querySelector<HTMLElement>("button");
target?.focus();
placed.current = target;
});
return () => cancelAnimationFrame(frame);
}, [status]);
async function submit() {