fix(nav): Back looks for a moved item only in the page's lists, once its own list has had its chance, and its reveal stops the scroll restore
The cross-list match took a same-named row of the app-wide download tray, and took it at the first look while a virtualized list had yet to render the item. The reveal of a moved item ran before the scroll restore's first frame, which put the old offset back over it. The pending restore's name is readable, for a page that folds rows away.
This commit is contained in:
@@ -9,13 +9,20 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { act, cleanup, render } from "@testing-library/react";
|
||||
import { useRef } from "react";
|
||||
|
||||
import { navItemAttrs, navPosition, restoreNavPosition, useListNav } from "./listNav";
|
||||
import {
|
||||
NAV_REVEAL_EVENT,
|
||||
navItemAttrs,
|
||||
navPosition,
|
||||
restoreNavPosition,
|
||||
useListNav,
|
||||
} from "./listNav";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
function List({ items }: { items: string[] }) {
|
||||
// `count`: a virtualized list's, which counts rows it has yet to render.
|
||||
function List({ items, count }: { items: string[]; count?: number }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useListNav(ref, { enabled: true });
|
||||
useListNav(ref, { enabled: true, count });
|
||||
return (
|
||||
<div ref={ref}>
|
||||
{items.map((t, i) => (
|
||||
@@ -27,12 +34,25 @@ function List({ items }: { items: string[] }) {
|
||||
);
|
||||
}
|
||||
|
||||
const Page = ({ lists }: { lists: string[][] }) => (
|
||||
<main>
|
||||
{lists.map((items, i) => (
|
||||
<List key={i} items={items} />
|
||||
))}
|
||||
</main>
|
||||
// `tray`: a list of the app's own, outside the page (the running downloads), whose rows can share a
|
||||
// name with the page's.
|
||||
const Page = ({
|
||||
lists,
|
||||
tray,
|
||||
counts = [],
|
||||
}: {
|
||||
lists: string[][];
|
||||
tray?: string[];
|
||||
counts?: number[];
|
||||
}) => (
|
||||
<>
|
||||
<main>
|
||||
{lists.map((items, i) => (
|
||||
<List key={i} items={items} count={counts[i]} />
|
||||
))}
|
||||
</main>
|
||||
{tray && <List items={tray} />}
|
||||
</>
|
||||
);
|
||||
|
||||
const button = (name: string) =>
|
||||
@@ -40,12 +60,17 @@ const button = (name: string) =>
|
||||
const focused = () => document.activeElement?.closest("[data-nav-index]")?.textContent;
|
||||
const wait = (ms: number) => act(() => new Promise((r) => setTimeout(r, ms)));
|
||||
|
||||
async function leaveAndReturn(before: string[][], from: string, after: string[][]) {
|
||||
const { unmount } = render(<Page lists={before} />);
|
||||
async function leaveAndReturn(
|
||||
before: string[][],
|
||||
from: string,
|
||||
after: string[][],
|
||||
tray?: string[],
|
||||
) {
|
||||
const { unmount } = render(<Page lists={before} tray={tray} />);
|
||||
button(from).focus();
|
||||
const pos = navPosition()!;
|
||||
unmount(); // the page switch
|
||||
render(<Page lists={after} />); // Back
|
||||
render(<Page lists={after} tray={tray} />); // Back
|
||||
const cancel = restoreNavPosition(pos);
|
||||
await wait(120);
|
||||
cancel();
|
||||
@@ -78,15 +103,51 @@ describe("Back to an item that moved to another list", () => {
|
||||
// off screen.
|
||||
const reveal = vi.fn();
|
||||
Element.prototype.scrollIntoView = reveal;
|
||||
// ...and tells the page's scroll restore, which would otherwise put the old offset back over it.
|
||||
const told = vi.fn();
|
||||
document.addEventListener(NAV_REVEAL_EVENT, told);
|
||||
await leaveAndReturn([["subscribed", "download"], ["older"]], "subscribed", [
|
||||
["download"],
|
||||
["subscribed", "older"],
|
||||
]);
|
||||
expect(reveal).toHaveBeenCalledTimes(1);
|
||||
expect(told).toHaveBeenCalledTimes(1);
|
||||
cleanup();
|
||||
reveal.mockClear();
|
||||
told.mockClear();
|
||||
await leaveAndReturn([["subscribed", "download"]], "download", [["download", "subscribed"]]);
|
||||
expect(focused()).toBe("download");
|
||||
expect(reveal).not.toHaveBeenCalled();
|
||||
expect(told).not.toHaveBeenCalled();
|
||||
document.removeEventListener(NAV_REVEAL_EVENT, told);
|
||||
});
|
||||
|
||||
it("never takes it from a list outside the page: the download tray's row of that name", async () => {
|
||||
// A feed card and its download in the tray are both named by the video's title.
|
||||
await leaveAndReturn(
|
||||
[["subscribed", "download"]],
|
||||
"subscribed",
|
||||
[["download"]],
|
||||
["subscribed"],
|
||||
);
|
||||
expect(focused()).toBe("download");
|
||||
});
|
||||
|
||||
it("waits for its own list to render the place before looking in another one", async () => {
|
||||
// A virtualized list puts its rows back a frame after the page: the item is still to come at
|
||||
// the first look, and the one of that name in the rail below is not it.
|
||||
const { unmount } = render(<Page lists={[["older", "subscribed"], ["subscribed-2"]]} />);
|
||||
button("subscribed").focus();
|
||||
const pos = navPosition()!;
|
||||
unmount();
|
||||
const { rerender } = render(<Page lists={[["older"], ["subscribed"]]} counts={[2]} />);
|
||||
const cancel = restoreNavPosition(pos);
|
||||
await wait(60);
|
||||
rerender(<Page lists={[["older", "subscribed"], ["subscribed"]]} counts={[2]} />);
|
||||
await wait(120);
|
||||
cancel();
|
||||
expect(document.activeElement?.closest("main > div")).toBe(
|
||||
document.querySelector("main")!.firstElementChild,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user