diff --git a/src/components/layout/__tests__/auth-shell-scroll.browser.test.tsx b/src/components/layout/__tests__/auth-shell-scroll.browser.test.tsx index ec8a64d4b..70760cefa 100644 --- a/src/components/layout/__tests__/auth-shell-scroll.browser.test.tsx +++ b/src/components/layout/__tests__/auth-shell-scroll.browser.test.tsx @@ -113,4 +113,34 @@ describe("AuthShell scroll position", () => { .toBeTruthy(); await expect.poll(() => main().scrollTop).toBe(1500); }); + + it("stops pulling back once the person scrolls on a list that is still loading", async () => { + stubFetch(); + window.history.replaceState(null, "", "/labs"); + await renderWithApp( + + + , + { user: { id: "signed-in" } }, + ); + await expect + .poll(() => document.querySelector("[data-loaded]")) + .toBeTruthy(); + main().scrollTop = 1500; + await nextFrames(); + await openPage("/labs/biomarker-1"); + + window.history.back(); + await expect + .poll(() => document.querySelector('[data-path="/labs"]')) + .toBeTruthy(); + // While the list is still its short skeleton, the person scrolls. + window.dispatchEvent(new WheelEvent("wheel", { deltaY: -100 })); + await expect + .poll(() => document.querySelector("[data-loaded]")) + .toBeTruthy(); + await nextFrames(); + + expect(main().scrollTop).toBe(0); + }); }); diff --git a/src/components/layout/auth-shell.tsx b/src/components/layout/auth-shell.tsx index a53ea8b43..df9bc0f95 100644 --- a/src/components/layout/auth-shell.tsx +++ b/src/components/layout/auth-shell.tsx @@ -129,17 +129,24 @@ function useShellScrollMemory(pathname: string): void { } if (!main) return; // A list still painting its skeleton is shorter than the offset; keep - // trying until it has the height, for at most a second. - const until = performance.now() + 1000; + // trying until it has the height, for at most three seconds, and stop as + // soon as the person scrolls themselves. + const until = performance.now() + 3000; let frame = 0; + const stop = () => cancelAnimationFrame(frame); + const inputs = ["wheel", "touchstart", "keydown", "pointerdown"]; const apply = () => { main.scrollTop = target.top; if (main.scrollTop < target.top - 1 && performance.now() < until) { frame = requestAnimationFrame(apply); } }; + for (const type of inputs) window.addEventListener(type, stop, true); apply(); - return () => cancelAnimationFrame(frame); + return () => { + stop(); + for (const type of inputs) window.removeEventListener(type, stop, true); + }; }, [pathname]); }