Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Scroll restoration bug caused by idx reset to 0 on reload #36861

Merged
merged 2 commits into from
May 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions packages/next/shared/lib/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ type PreflightEffect =
type HistoryState =
| null
| { __N: false }
| ({ __N: true; idx: number } & NextHistoryState)
| ({ __N: true; key: string } & NextHistoryState)

let detectDomainLocale: typeof import('../i18n/detect-domain-locale').detectDomainLocale

Expand Down Expand Up @@ -612,6 +612,10 @@ interface NextDataCache {
[asPath: string]: Promise<object>
}

export function createKey() {
return Math.random().toString(36).slice(2, 10)
}

export default class Router implements BaseRouter {
basePath: string

Expand Down Expand Up @@ -651,7 +655,7 @@ export default class Router implements BaseRouter {
isPreview: boolean
}>

private _idx: number = 0
private _key: string = createKey()

static events: MittEmitter<RouterEvent> = mitt()

Expand Down Expand Up @@ -821,29 +825,29 @@ export default class Router implements BaseRouter {
}

let forcedScroll: { x: number; y: number } | undefined
const { url, as, options, idx } = state
const { url, as, options, key } = state
if (process.env.__NEXT_SCROLL_RESTORATION) {
if (manualScrollRestoration) {
if (this._idx !== idx) {
if (this._key !== key) {
// Snapshot current scroll position:
try {
sessionStorage.setItem(
'__next_scroll_' + this._idx,
'__next_scroll_' + this._key,
JSON.stringify({ x: self.pageXOffset, y: self.pageYOffset })
)
} catch {}

// Restore old scroll position:
try {
const v = sessionStorage.getItem('__next_scroll_' + idx)
const v = sessionStorage.getItem('__next_scroll_' + key)
forcedScroll = JSON.parse(v!)
} catch {
forcedScroll = { x: 0, y: 0 }
}
}
}
}
this._idx = idx
this._key = key

const { pathname } = parseRelativeUrl(url)

Expand Down Expand Up @@ -900,7 +904,7 @@ export default class Router implements BaseRouter {
try {
// Snapshot scroll position right before navigating to a new page:
sessionStorage.setItem(
'__next_scroll_' + this._idx,
'__next_scroll_' + this._key,
JSON.stringify({ x: self.pageXOffset, y: self.pageYOffset })
)
} catch {}
Expand Down Expand Up @@ -1451,7 +1455,7 @@ export default class Router implements BaseRouter {
as,
options,
__N: true,
idx: (this._idx = method !== 'pushState' ? this._idx : this._idx + 1),
key: (this._key = method !== 'pushState' ? this._key : createKey()),
} as HistoryState,
// Most browsers currently ignores this parameter, although they may use it in the future.
// Passing the empty string here should be safe against future changes to the method.
Expand Down
145 changes: 145 additions & 0 deletions test/e2e/reload-scroll-backforward-restoration/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { createNext, FileRef } from 'e2e-utils'
import { NextInstance } from 'test/lib/next-modes/base'
import { check } from 'next-test-utils'
import { join } from 'path'
import webdriver from 'next-webdriver'

describe('reload-scroll-back-restoration', () => {
let next: NextInstance

beforeAll(async () => {
next = await createNext({
files: {
pages: new FileRef(join(__dirname, 'pages')),
'next.config.js': new FileRef(join(__dirname, 'next.config.js')),
},
dependencies: {},
})
})
afterAll(() => next.destroy())

it('should restore the scroll position on navigating back', async () => {
const browser = await webdriver(next.url, '/0')
await browser.eval(() => document.querySelector('#link').scrollIntoView())

// check browser restoration setting
const scrollRestoration = await browser.eval(
() => window.history.scrollRestoration
)
expect(scrollRestoration).toBe('manual')

const scrollPositionMemories: Array<{ x: number; y: number }> = []
scrollPositionMemories.push({
x: Math.floor(await browser.eval(() => window.scrollX)),
y: Math.floor(await browser.eval(() => window.scrollY)),
})

// check initial value
expect(scrollPositionMemories[0].x).not.toBe(0)
expect(scrollPositionMemories[0].y).not.toBe(0)

await browser.eval(`window.next.router.push('/1')`)
await browser.eval(() => document.querySelector('#link').scrollIntoView())
scrollPositionMemories.push({
x: Math.floor(await browser.eval(() => window.scrollX)),
y: Math.floor(await browser.eval(() => window.scrollY)),
})
await browser.eval(`window.next.router.push('/2')`)

// check restore value on history index: 1
await browser.back()
await check(
() => browser.eval(() => document.documentElement.innerHTML),
/routeChangeComplete/
)

expect(scrollPositionMemories[1].x).toBe(
Math.floor(await browser.eval(() => window.scrollX))
)
expect(scrollPositionMemories[1].y).toBe(
Math.floor(await browser.eval(() => window.scrollY))
)

await browser.refresh()

// check restore value on history index: 0
await browser.back()
await check(
() => browser.eval(() => document.documentElement.innerHTML),
/routeChangeComplete/
)

expect(scrollPositionMemories[0].x).toBe(
Math.floor(await browser.eval(() => window.scrollX))
)
expect(scrollPositionMemories[0].y).toBe(
Math.floor(await browser.eval(() => window.scrollY))
)
})

it('should restore the scroll position on navigating forward', async () => {
const browser = await webdriver(next.url, '/0')
await browser.eval(() => document.querySelector('#link').scrollIntoView())

// check browser restoration setting
const scrollRestoration = await browser.eval(
() => window.history.scrollRestoration
)
expect(scrollRestoration).toBe('manual')

const scrollPositionMemories: Array<{ x: number; y: number }> = []
scrollPositionMemories.push({
x: Math.floor(await browser.eval(() => window.scrollX)),
y: Math.floor(await browser.eval(() => window.scrollY)),
})

// check initial value
expect(scrollPositionMemories[0].x).not.toBe(0)
expect(scrollPositionMemories[0].y).not.toBe(0)

await browser.eval(`window.next.router.push('/1')`)
await browser.eval(() => document.querySelector('#link').scrollIntoView())
scrollPositionMemories.push({
x: Math.floor(await browser.eval(() => window.scrollX)),
y: Math.floor(await browser.eval(() => window.scrollY)),
})
await browser.eval(`window.next.router.push('/2')`)
await browser.eval(() => document.querySelector('#link').scrollIntoView())
scrollPositionMemories.push({
x: Math.floor(await browser.eval(() => window.scrollX)),
y: Math.floor(await browser.eval(() => window.scrollY)),
})

// check restore value on history index: 1
await browser.back()
await browser.back()
await browser.forward()
await check(
() => browser.eval(() => document.documentElement.innerHTML),
/routeChangeComplete/
)

expect(scrollPositionMemories[1].x).toBe(
Math.floor(await browser.eval(() => window.scrollX))
)
expect(scrollPositionMemories[1].y).toBe(
Math.floor(await browser.eval(() => window.scrollY))
)

await browser.refresh()

// check restore value on history index: 2
await browser.forward()
await check(
() => browser.eval(() => document.documentElement.innerHTML),
/routeChangeComplete/
)

expect(scrollPositionMemories[2].x).toBe(
Math.floor(await browser.eval(() => window.scrollX))
)
expect(scrollPositionMemories[2].y).toBe(
Math.floor(await browser.eval(() => window.scrollY))
)
})
})
5 changes: 5 additions & 0 deletions test/e2e/reload-scroll-backforward-restoration/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
experimental: {
scrollRestoration: true,
},
}
50 changes: 50 additions & 0 deletions test/e2e/reload-scroll-backforward-restoration/pages/[id].js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useState, useEffect } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'

const Page = ({ id }) => {
const router = useRouter()
const [ready, setReady] = useState(false)
useEffect(() => {
router.events.on('routeChangeComplete', () => {
setReady(true)
})
}, [router, ready, setReady])

return (
<>
<div
style={{
width: 10000,
height: 10000,
background: 'blue',
}}
/>
<p>{ready ? 'routeChangeComplete' : 'loading'}</p>
<Link href={`/${id + 1}`}>
<a
id="link"
style={{
marginLeft: 5000,
width: 95000,
display: 'block',
}}
>
next page
</a>
</Link>
<div id="end-el">hello, world</div>
</>
)
}

export default Page

export const getServerSideProps = (context) => {
const { id = 0 } = context.query
return {
props: {
id,
},
}
}