Don't scroll page to top when opening or closing a side modal - #3336
Don't scroll page to top when opening or closing a side modal#3336david-crespo wants to merge 1 commit into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| // same page, new location (opened a side modal): leave the scroll alone | ||
| // and record it under the new location's key so back/forward to it | ||
| // restores correctly | ||
| setScrollPosition(key, window.scrollY) |
There was a problem hiding this comment.
this branch isn't tested, strictly speaking. i could delete this line and pass
There was a problem hiding this comment.
Oh yeah. You have to open a model, then go to another page, and then go back to test it. I’ll add it to the tests.
| // ones like opening a side modal, so key says we just landed on a new | ||
| // location (or this is the initial render) and page decides whether the | ||
| // nav stayed within the same page | ||
| if (prev.current?.page === page) { |
There was a problem hiding this comment.
i had a good deal of trouble following the code paths here. mostly to do with reconciling the conditions and the comments around them, or rather, being certain this wasn't overlooking some unnamed conditions. i think we can handle that, add some clarity to the existing cases, and prevent future spaghettification by using a custom type to put explicit names on what the conditions are observing:
/**
* The type of navigation that occurred based on the navigation state and how
* the new location compares to the previous one.
*/
type NavigationKind =
/** A navigation is in flight; the old page is still on screen. */
| 'loading'
/** Landed on a new location, but the same page is underneath, e.g. a side
* modal opening/closing. */
| 'movedToSamePage'
/** Landed on a new location showing a different page. */
| 'movedToNewPage'
/** Re-rendered at the location we're already on; nothing to do. */
| 'noChange'
function getNavigationKind(
state: Navigation['state'],
key: string,
page: string,
prev: PrevLocation | null
): NavigationKind {
return match(state)
.with('loading', (): NavigationKind => 'loading')
.with('submitting', (): NavigationKind => 'noChange')
.with('idle', (): NavigationKind => {
// same key means we re-rendered at the location we're already on
if (prev?.key === key) return 'noChange'
return prev?.page === page ? 'movedToSamePage' : 'movedToNewPage'
})
.exhaustive()
}and then in place of the big if block:
match(getNavigationKind(state, key, page, prev.current))
.with('loading', () => setScrollPosition(key, window.scrollY))
// leave the scroll alone and record it under the new location's key so
// back/forward to it restores correctly
.with('movedToSamePage', () => setScrollPosition(key, window.scrollY))
.with('movedToNewPage', () => window.scrollTo(0, getScrollPosition(key)))
.with('noChange', () => {})
.exhaustive()
Closes #3321
Because side modals get their own routes, and the scroll height cache is keyed (in part) by route, opening a side modal counts as a new page with no saved scroll height, so it scrolls the document to top. The fix is to handle the modal scenario specially by opting out of resetting scroll when we switch to a new location that has the same underlying page path as the previous one.
2026-08-18-scroll-height-fix.mp4
Example flows
Each of these scenarios is covered by e2e tests.
The underlying "page path" for a location is the path of the last non-
titleOnlycrumb, i.e., where the nav breadcrumbs point: a regular page's own path, or for a side modal, the path of the page under it. When a nav starts, we save the current scroll under the outgoing location's key. When it completes, if the page path changed we scroll to the new location's cached position (0 if none); if it didn't (side modal open/close), we leave the scroll alone and just record it under the new key so back/forward still works.Normal page → page nav (
…/disks→…/snapshots, then back)…/disks(k1)…/disks…/disks(k1)…/disks…/snapshots(k2)…/snapshots…/snapshots(k2)…/snapshots…/disks(k1)…/disksPage → modal → page (open the New disk form, then dismiss it)
…/disks(k1)…/disks…/disks(k1)…/disks…/disks-new(k2)…/disks…/disks-new(k2)…/disks…/disks(k3)…/disksModal submit → new page (create a VPC, land on VPC detail page)
…/vpcs-new(k2)…/vpcs…/vpcs-new(k2)…/vpcs…/vpcs/my-vpc(k3)…/vpcs/my-vpc