fix(shell): stop restoring the list's offset once the person scrolls

On Back, the shell set the list's saved offset again on every frame
while the list was still shorter than that offset. Someone who started
scrolling in that time was pulled back, and a list whose data took more
than a second ended up where the skeleton allowed. The restore now stops
at the first wheel, touch, key or pointer input and keeps trying for up
to three seconds.
This commit is contained in:
Marc-André Bombeck committed 2026-11-12 14:04:31 +01:00
1 parent 12a23bf779
commit b617c1f88e
2 files changed
+40 -3

No files matched your search

@@ -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(
<AuthShell>
<Page />
</AuthShell>,
{ 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);
});
});
+10 -3
View File
@@ -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]);
}