Restore scroll position when navigating back to a list - #7143
tassilogravura wants to merge 2 commits into
Conversation
React Router v5 has no scroll restoration, so the app relied on the browser's native restoration. That doesn't work for the list views: at the point the history entry is popped, the list content hasn't been fetched and rendered yet, so the document is one viewport tall and the position is clamped to the top. Nothing scrolls back once the content arrives, leaving the user at the top of the list. Record the scroll position for each history entry and re-apply it on POP until the page is able to scroll there, holding it briefly while the list settles. Positions are kept in session storage so they survive a page reload. Coming back to a list that was showing a page other than the first re-parses the filter from the query string, which looks like a page change to useScrollToTopOnPageChange and scrolled over the restored position, so that hook now defers to an in-progress restoration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GBjwuhWgCvRbAvcYed3mfv
74a1992 to
dbea5e9
Compare
|
One concern: |
Restoring the scroll position gave up after three seconds and held the restored position for a fixed 750ms afterwards. Both numbers were guesses about how long a list takes to arrive, and on a slow query - a big library, an unindexed sort, a phone on a weak connection - the three seconds run out while the page is still one viewport tall. The restoration is abandoned and the viewer is left at the top, which is the whole thing this was meant to fix. The clock now only runs when nothing is loading. Lists report that they are waiting on their content, and a restoration in progress waits for them however long they take; the three seconds are left as a fallback for pages that report nothing, and a thirty second cap keeps a page that never loads from suppressing the automatic scrolling forever. PagedList is the one place every list view renders through, so the one call there covers all of them. Re-applying is driven by the document changing height rather than by a callback on every frame, so the position goes back as soon as the page is tall enough to take it. A slow interval alongside it watches the deadlines, which no resize reports, and covers the case of custom CSS pinning the height of the elements being watched. The first attempt is still made synchronously so that content that was already cached is restored before the page is painted. The hold afterwards now starts when the content is in rather than when the position first became reachable, which is what the 750ms was approximating - a quarter of a second is enough once it is measured from the right moment. Pressing something now abandons the restoration as scrolling already did, since with the longer cap a press on the pagination has to be able to get out of the way. Checked in a browser against the real hook: a load that takes eight seconds now lands at the right place where it previously landed at the top, cached content still restores before the first paint, a competing scroll to the top during the hold is overridden, a wheel event abandons the restoration, and a page that never loads gives up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0114kctDcLF4XiCKpaQt8Vz9
Good catch, the 3s is real. With an 8s query it gives up and you land at the Deadline is conditional now: lists report while loading (one call in PagedList, Took the ResizeObserver suggestion too, so re-applying is driven by the document Kept the settle hold but cut it to 250ms, measured from when loading finishes. |
Description
React Router v5 has no scroll restoration, so the app relied on the browser's native restoration. That doesn't work for the list views: at the point the history entry is popped, the list content hasn't been fetched and rendered yet, so the document is one viewport tall and the position is clamped to the top. Nothing scrolls back once the content arrives, leaving the user at the top of the list.
Record the scroll position for each history entry and re-apply it on POP until the page is able to scroll there, holding it briefly while the list settles. Positions are kept in session storage so they survive a page reload.
Coming back to a list that was showing a page other than the first re-parses the filter from the query string, which looks like a page change to useScrollToTopOnPageChange and scrolled over the restored position, so that hook now defers to an in-progress restoration.
Related Issue
Closes #7142
Testing
manual testing in my own browser
Screenshots
Checklist
AI Usage Disclosure
Additional Context