Skip to content

Commit c240df0

Browse files
authored
[backport] fix: aliased navigations should apply scroll handling (#82900) (#83015)
Backports: - #82900
1 parent e61151e commit c240df0

File tree

4 files changed

+62
-3
lines changed

4 files changed

+62
-3
lines changed

packages/next/src/client/components/router-reducer/aliased-prefetch-navigations.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import type {
22
CacheNodeSeedData,
33
FlightRouterState,
4+
FlightSegmentPath,
45
} from '../../../server/app-render/types'
56
import type { CacheNode } from '../../../shared/lib/app-router-context.shared-runtime'
67
import {
78
addSearchParamsIfPageSegment,
9+
DEFAULT_SEGMENT_KEY,
810
PAGE_SEGMENT_KEY,
911
} from '../../../shared/lib/segment'
1012
import type { NormalizedFlightData } from '../../flight-data-helpers'
@@ -14,6 +16,7 @@ import { createHrefFromUrl } from './create-href-from-url'
1416
import { createRouterCacheKey } from './create-router-cache-key'
1517
import { fillCacheWithNewSubTreeDataButOnlyLoading } from './fill-cache-with-new-subtree-data'
1618
import { handleMutable } from './handle-mutable'
19+
import { generateSegmentsFromPatch } from './reducers/navigate-reducer'
1720
import type { Mutable, ReadonlyReducerState } from './router-reducer-types'
1821

1922
/**
@@ -34,6 +37,7 @@ export function handleAliasedPrefetchEntry(
3437
let currentCache = state.cache
3538
const href = createHrefFromUrl(url)
3639
let applied
40+
let scrollableSegments: FlightSegmentPath[] = []
3741

3842
if (typeof flightData === 'string') {
3943
return false
@@ -115,6 +119,20 @@ export function handleAliasedPrefetchEntry(
115119
currentCache = newCache
116120
applied = true
117121
}
122+
123+
for (const subSegment of generateSegmentsFromPatch(treePatch)) {
124+
const scrollableSegmentPath = [
125+
...normalizedFlightData.pathToSegment,
126+
...subSegment,
127+
]
128+
// Filter out the __DEFAULT__ paths as they shouldn't be scrolled to in this case.
129+
if (
130+
scrollableSegmentPath[scrollableSegmentPath.length - 1] !==
131+
DEFAULT_SEGMENT_KEY
132+
) {
133+
scrollableSegments.push(scrollableSegmentPath)
134+
}
135+
}
118136
}
119137

120138
if (!applied) {
@@ -125,6 +143,7 @@ export function handleAliasedPrefetchEntry(
125143
mutable.cache = currentCache
126144
mutable.canonicalUrl = href
127145
mutable.hashFragment = url.hash
146+
mutable.scrollableSegments = scrollableSegments
128147

129148
return handleMutable(state, mutable)
130149
}

packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export function handleExternalUrl(
4848
return handleMutable(state, mutable)
4949
}
5050

51-
function generateSegmentsFromPatch(
51+
export function generateSegmentsFromPatch(
5252
flightRouterPatch: FlightRouterState
5353
): FlightSegmentPath[] {
5454
const segments: FlightSegmentPath[] = []
Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1+
import Link from 'next/link'
2+
13
export const dynamic = 'force-dynamic'
24

3-
export default async function Page() {
4-
await new Promise((resolve) => setTimeout(resolve, 5000))
5+
export default async function Page(props: PageProps<'/loading-scroll'>) {
6+
const search = await props.searchParams
7+
const skipSleep = !!search.skipSleep
8+
if (!skipSleep) {
9+
await new Promise((resolve) => setTimeout(resolve, 5000))
10+
}
511
return (
612
<>
13+
{search.page ? <div id="current-page">{search.page}</div> : null}
714
<div style={{ display: 'none' }}>Content that is hidden.</div>
815
<div id="content-that-is-visible">Content which is not hidden.</div>
916
{
@@ -12,6 +19,13 @@ export default async function Page() {
1219
<div key={i}>{i}</div>
1320
))
1421
}
22+
<div id="pages">
23+
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item) => (
24+
<Link key={item} href={`?page=${item}&skipSleep=1`}>
25+
{item}
26+
</Link>
27+
))}
28+
</div>
1529
</>
1630
)
1731
}

test/e2e/app-dir/router-autoscroll/router-autoscroll.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,5 +248,31 @@ describe('router autoscrolling on navigation', () => {
248248
await browser.waitForElementByCss('#content-that-is-visible')
249249
await check(() => browser.eval('window.scrollY'), 0)
250250
})
251+
252+
it('should scroll to top when navigating to same page with different search params', async () => {
253+
const browser = await next.browser('/loading-scroll?skipSleep=1')
254+
255+
await retry(async () => {
256+
// scroll to the links at the bottom of the page
257+
await browser.eval(`document.getElementById("pages").scrollIntoView()`)
258+
259+
// grab the current scroll position
260+
const scrollY = await browser.eval(`window.scrollY`)
261+
262+
// sanity check: we should not be scrolled to the top
263+
expect(scrollY).not.toBe(0)
264+
})
265+
266+
// click a link
267+
await browser.elementByCss("a[href='?page=2&skipSleep=1']").click()
268+
269+
// assert the new page id has been committed
270+
expect(await browser.elementById('current-page').text()).toBe('2')
271+
272+
await retry(async () => {
273+
// we should have scrolled to the top
274+
expect(await browser.eval(`window.scrollY`)).toBe(0)
275+
})
276+
})
251277
})
252278
})

0 commit comments

Comments
 (0)