fix(nav): Back's focus keeps its place among items of the same name
Restoring by name picked the first item of that name, so Back on the log console (repeated http.request lines) or two same-titled downloads landed on the wrong one. The saved place wins while it still holds the name; otherwise the nearest item of that name.
This commit is contained in:
@@ -194,6 +194,37 @@ describe("rememberNavPosition · the list it comes back to (deep-review E11 roun
|
||||
await waitFor(() => expect(focused()).toBe("I Am Mother"));
|
||||
});
|
||||
|
||||
// Names repeat — a log console's `http.request GET /api/feed 200` lines. The first of them is not
|
||||
// the one left: the saved place still holding the name wins, and when the list moved, the nearest.
|
||||
it("among items of one name, takes the saved place or the nearest", async () => {
|
||||
const Lines = ({ names }: { names: string[] }) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useListNav(ref, { enabled: true });
|
||||
return (
|
||||
<div ref={ref} aria-label="Lines">
|
||||
{names.map((name, i) => (
|
||||
<div key={i} data-testid={`l${i}`} data-nav-name={name} {...navItemAttrs(i)}>
|
||||
{name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const names = ["GET 200", "POST 200", "POST 200", "GET 200", "POST 200"];
|
||||
const first = render(<Lines names={names} />);
|
||||
const stamped = leaveFrom("l3", 4);
|
||||
first.unmount();
|
||||
landOn(stamped);
|
||||
const back = render(<Lines names={names} />);
|
||||
await waitFor(() => expect(focused()).toBe("l3"));
|
||||
back.unmount();
|
||||
(document.activeElement as HTMLElement | null)?.blur?.();
|
||||
landOn(stamped);
|
||||
// A line arrived above: the left one is l4 now, a GET two places up is l1.
|
||||
render(<Lines names={["new", ...names]} />);
|
||||
await waitFor(() => expect(focused()).toBe("l4"));
|
||||
});
|
||||
|
||||
it("gives up on an unnamed list when the page came back with another number of lists", async () => {
|
||||
const { unmount } = render(<Page />);
|
||||
const stamped = leaveFrom("a1", 2);
|
||||
|
||||
@@ -366,6 +366,16 @@ const RESTORE_POLL_MS = 50;
|
||||
* list N may be another list, and <body> is the honest outcome. Gives up when the time is up, and at
|
||||
* once when focus is already somewhere (the reader moved on, a dialog took it). The item is focused
|
||||
* where the scroll restore put it, never scrolled to. Returns a cancel. */
|
||||
function restoreTarget(list: NavList, pos: NavPosition): HTMLElement | null {
|
||||
const at = itemAt(list, pos.index);
|
||||
if (!pos.name || (at && itemName(at) === pos.name)) return at;
|
||||
const distance = (el: HTMLElement) => Math.abs(indexOf(el) - pos.index);
|
||||
const named = ownItems(list.el)
|
||||
.filter((it) => itemName(it) === pos.name)
|
||||
.sort((a, b) => distance(a) - distance(b));
|
||||
return named[0] ?? at;
|
||||
}
|
||||
|
||||
export function restoreNavPosition(pos: NavPosition): () => void {
|
||||
const deadline = Date.now() + RESTORE_MS;
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
@@ -381,9 +391,10 @@ export function restoreNavPosition(pos: NavPosition): () => void {
|
||||
// The item by its NAME first, its place second: a rail's contents can shift while the page was
|
||||
// away — Home's Continue rail leaves out whatever the hero shows, so a different hero moved every
|
||||
// card after it one place and the focus landed on the neighbour (prod UAT 2026-09-19).
|
||||
const named =
|
||||
list && pos.name ? ownItems(list.el).find((it) => itemName(it) === pos.name) : null;
|
||||
const el = named ?? (list ? itemAt(list, pos.index) : null);
|
||||
// The saved place still holding the saved name wins, and among several items of that name the
|
||||
// nearest one: names repeat (a log console's `http.request GET /api/feed 200` lines, two
|
||||
// downloads of one video), and the first of them is rarely the one left behind.
|
||||
const el = list ? restoreTarget(list, pos) : null;
|
||||
if (el) return focusItem(el, false);
|
||||
if (Date.now() < deadline) timer = setTimeout(attempt, RESTORE_POLL_MS);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user