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:
@@ -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.
|
// 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(document.querySelector("[role=menu]")).not.toBeNull();
|
||||||
expect(alert.closest("[role=menu]")).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
|
// 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.
|
// on <body> with the menu open — it lands on the first row that came back.
|
||||||
const retry = (await screen.findByText("common.retry")).closest("button")!;
|
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);
|
fireEvent.click(retry);
|
||||||
const beta = await checkRow("Beta");
|
const beta = await checkRow("Beta");
|
||||||
await vi.waitFor(() => expect(document.activeElement).toBe(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 () => {
|
it("clears the name when Plex made the collection and only the read after it failed", async () => {
|
||||||
|
|||||||
@@ -79,18 +79,23 @@ export function MembershipMenu({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shownStatus.current === status) return;
|
if (shownStatus.current === status) return;
|
||||||
shownStatus.current = status;
|
shownStatus.current = status;
|
||||||
const root = rootRef.current;
|
// A frame later: rows that just mounted join the menu's list a render after this effect, and a
|
||||||
const at = document.activeElement;
|
// row focused before that is not known to it — the arrows then did nothing from it.
|
||||||
// …unless a name is being typed into it meanwhile.
|
const frame = requestAnimationFrame(() => {
|
||||||
const parked = at === placed.current && !(at instanceof HTMLInputElement && at.value);
|
const root = rootRef.current;
|
||||||
const lost = at == null || at === document.body || parked;
|
const at = document.activeElement;
|
||||||
if (!root || !lost) return;
|
// …unless a name is being typed into it meanwhile.
|
||||||
const target =
|
const parked = at === placed.current && !(at instanceof HTMLInputElement && at.value);
|
||||||
root.querySelector<HTMLElement>("[role=menuitemcheckbox]") ??
|
const lost = at == null || at === document.body || parked;
|
||||||
root.querySelector<HTMLElement>("input") ??
|
if (!root || !lost) return;
|
||||||
root.querySelector<HTMLElement>("button");
|
const target =
|
||||||
target?.focus();
|
root.querySelector<HTMLElement>("[role=menuitemcheckbox]") ??
|
||||||
placed.current = target;
|
root.querySelector<HTMLElement>("input") ??
|
||||||
|
root.querySelector<HTMLElement>("button");
|
||||||
|
target?.focus();
|
||||||
|
placed.current = target;
|
||||||
|
});
|
||||||
|
return () => cancelAnimationFrame(frame);
|
||||||
}, [status]);
|
}, [status]);
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
|
|||||||
Reference in New Issue
Block a user