diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 071770e..aa0e614 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -208,10 +208,47 @@ export const App: React.FC = () => { // Desktop: the window does not scroll; this area under the header does (phones keep the window scroll) const scrollAreaRef = useRef(null); - // A new page starts at its top (the desktop scroll area would otherwise keep the old page's position) + // Each page keeps its scroll position when switching pages. The new page is often still loading and + // shorter than before: the position comes back as soon as the content is high enough (or the user + // scrolls first, or after 3 s). Scroll events meanwhile do not count as the page's position. + const scrollPositionsRef = useRef>>({}); useEffect(() => { - scrollAreaRef.current?.scrollTo?.({ top: 0 }); - }, [activeTab]); + const area = scrollAreaRef.current; + if (!area) return; + const target = scrollPositionsRef.current[activeTab] ?? 0; + let restoring = true; + let observer: ResizeObserver | null = null; + const finish = () => { + if (!restoring) return; + restoring = false; + observer?.disconnect(); + window.clearTimeout(timeout); + area.removeEventListener('wheel', finish); + area.removeEventListener('pointerdown', finish); + area.removeEventListener('keydown', finish); + }; + const tryRestore = () => { + area.scrollTop = target; + if (Math.abs(area.scrollTop - target) < 2) finish(); + }; + const timeout = window.setTimeout(finish, 3000); + area.addEventListener('wheel', finish, { passive: true }); + area.addEventListener('pointerdown', finish); + area.addEventListener('keydown', finish); + tryRestore(); + if (restoring && typeof ResizeObserver !== 'undefined' && area.firstElementChild) { + observer = new ResizeObserver(tryRestore); + observer.observe(area.firstElementChild); + } + const onScroll = () => { + if (!restoring) scrollPositionsRef.current[activeTab] = area.scrollTop; + }; + area.addEventListener('scroll', onScroll, { passive: true }); + return () => { + finish(); + area.removeEventListener('scroll', onScroll); + }; + }, [activeTab, user?.id]); const [showAbout, setShowAbout] = useState(false); const [showTriageTrash, setShowTriageTrash] = useState(false); const { update: availableUpdate, dismiss: dismissUpdate } = useUpdateCheck(); @@ -1413,7 +1450,7 @@ export const App: React.FC = () => { /> {/* MOBILE BOTTOM NAVIGATION BAR (5 equal columns -> + FAB is dead-center) */} -