fix(08-02): scroll restoration — save/restore both window and <main> scrollTop, defer to rAF after rows render

This commit is contained in:
lorentz 2026-05-07 22:23:48 -04:00
parent 6bdc937861
commit 8834db981d

View file

@ -172,35 +172,48 @@ export default function MobileEngagementPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sortKey]);
// D-04 fallback: the mobile shell's <main> is overflow-y-auto, so Next.js's
// built-in scrollRestoration (which targets window) doesn't help. Persist the
// inner scroll position across navigations via sessionStorage so back from
// /mobile/engagement/[userId] restores the row the user tapped.
// D-04 fallback: scroll restoration via sessionStorage. The actual scroll
// container differs by viewport — `<main>` has overflow-y-auto, but in some
// configurations the document scrolls instead. Listen on both, save the one
// that's non-zero, and restore both.
const restoredScrollRef = useRef(false);
useEffect(() => {
const main = document.querySelector('main');
if (!main) return;
let raf = 0;
const save = () => {
const main = document.querySelector('main');
const mainTop = main?.scrollTop ?? 0;
const winTop = window.scrollY || document.documentElement.scrollTop || 0;
sessionStorage.setItem('mobile-engagement-scroll', JSON.stringify({ main: mainTop, win: winTop }));
};
const onScroll = () => {
if (raf) return;
raf = requestAnimationFrame(() => {
raf = 0;
sessionStorage.setItem('mobile-engagement-list-scroll', String(main.scrollTop));
});
raf = requestAnimationFrame(() => { raf = 0; save(); });
};
main.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('scroll', onScroll, { passive: true });
const main = document.querySelector('main');
main?.addEventListener('scroll', onScroll, { passive: true });
return () => {
main.removeEventListener('scroll', onScroll);
window.removeEventListener('scroll', onScroll);
main?.removeEventListener('scroll', onScroll);
if (raf) cancelAnimationFrame(raf);
};
}, []);
useEffect(() => {
if (restoredScrollRef.current) return;
if (usersLoading || users.length === 0) return;
const main = document.querySelector('main');
if (!main) return;
const saved = sessionStorage.getItem('mobile-engagement-list-scroll');
if (saved) main.scrollTop = parseInt(saved, 10);
const raw = sessionStorage.getItem('mobile-engagement-scroll');
if (!raw) { restoredScrollRef.current = true; return; }
try {
const { main: mainTop, win: winTop } = JSON.parse(raw) as { main: number; win: number };
// Defer one frame so layout has settled with the new rows in place.
requestAnimationFrame(() => {
const main = document.querySelector('main');
if (main && mainTop) main.scrollTop = mainTop;
if (winTop) window.scrollTo(0, winTop);
});
} catch {
/* corrupt entry — ignore */
}
restoredScrollRef.current = true;
}, [usersLoading, users.length]);