fix(keys): epic-end review round 1 — Back, menus and dialogs keep the focus where it belongs
Deep-review of E11 (19 raised, 14 confirmed; 2 refuted, 3 latent test-hardening notes dropped). - Back from a sub-view (a Plex show page, a Messages thread) restores the item it was opened from even when focus was still inside the view: the first attempt now waits one task, past the sub-view's own popstate listener (measured in Chrome: it left focus on <body>). - The restore listener is installed at load, so the first Back after a reload finds it; a push cancels a restore still waiting for the last landing's lists. - Navigating over an overlay asks it again, a render apart, while its entry stays: the Plex player peels one panel per Back step, so Shift+W over info + stats closed one panel and dropped the navigation. - j/k: a native <button> item keeps its own Tab stop; landing twice on the same item no longer leaks its tabindex; after the focused item is removed (a hidden card) the next j goes on from its slot when that slot is on screen. - An abandoned `g` no longer swallows a modifier chord (g, then Mod+K opened the browser search). - A hover card above a dialog no longer switches its Tab trap off (player, Modal, wizard, Lightbox): new layerStack `anyBlockingLayerAbove`. - "Manage views…" closes its menu before opening the dialog, through a shared `closeThenAct` that ActionMenu now uses too. - Copy: the EN welcome says "Feed"; the EN unreachable reason no longer claims a change; stale comments about PlayerModal's scroll lock and dialog contract.
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
// stamped onto the entry being left (`rememberNavPosition`) and a popstate landing on it focuses the item
|
||||
// once the page's lists are back. A landing is simulated the way the browser delivers one: the entry's
|
||||
// state is current and a popstate carries it. Its own file: history.ts and listNav keep module state.
|
||||
import { useRef } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { act, cleanup, render, waitFor } from "@testing-library/react";
|
||||
import { rememberNavPosition } from "./history";
|
||||
@@ -92,6 +92,44 @@ describe("rememberNavPosition · Back focuses the item a view was left from", ()
|
||||
await waitFor(() => expect(focused()).toBe("a1"));
|
||||
});
|
||||
|
||||
// A sub-view (a Plex show page) switches back in its OWN popstate listener, registered after this
|
||||
// module's: while ours runs, focus still sits in the view being left. Measured in Chrome (deep-review
|
||||
// E11 round 1) — Discover card → show → `j` on the show page → Back left focus on <body>.
|
||||
it("restores when a later popstate listener switches the view the focus was still in", async () => {
|
||||
const { unmount } = render(<Page />);
|
||||
const stamped = leaveFrom("b1", 4);
|
||||
unmount();
|
||||
function Views() {
|
||||
const [view, setView] = useState<"sub" | "page">("sub");
|
||||
useEffect(() => {
|
||||
const onPop = () => setView("page");
|
||||
window.addEventListener("popstate", onPop);
|
||||
return () => window.removeEventListener("popstate", onPop);
|
||||
}, []);
|
||||
return view === "page" ? <Page /> : <List id="s" count={2} />;
|
||||
}
|
||||
render(<Views />);
|
||||
press("j");
|
||||
expect(focused()).toBe("s0");
|
||||
landOn(stamped);
|
||||
await waitFor(() => expect(focused()).toBe("b1"));
|
||||
});
|
||||
|
||||
// A restore still waiting for the landed page's lists must not outlive the next push: it would
|
||||
// focus "list 1, item 1" of whatever page that push opened.
|
||||
it("is cancelled by the next push", async () => {
|
||||
const { unmount } = render(<Page />);
|
||||
const stamped = leaveFrom("b1", 4);
|
||||
unmount();
|
||||
landOn(stamped);
|
||||
render(<List id="b" count={3} />); // one list of the two: the restore keeps waiting
|
||||
await sleep(120);
|
||||
window.history.pushState({ ...rememberNavPosition(), view: "elsewhere" }, "");
|
||||
render(<Page />); // enough lists now, on the page the push opened
|
||||
await sleep(150);
|
||||
expect(focused()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("leaves focus where it is when something already has it", async () => {
|
||||
const { unmount } = render(<Page />);
|
||||
const stamped = leaveFrom("b1", 4);
|
||||
|
||||
Reference in New Issue
Block a user