Skip to content

Commit 2315047

Browse files
ifer47posva
andauthored
fix(router): skip scroll saving for unknown pop direction (fix #1431) (#2780)
* fix(router): skip scroll saving for unknown pop direction * test: no internals * style: format Fix #1431 --------- Co-authored-by: Eduardo San Martin Morote <posva13@gmail.com>
1 parent d2b87d5 commit 2315047

5 files changed

Lines changed: 115 additions & 2 deletions

File tree

packages/router/__tests__/router.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,7 @@ describe('Router', () => {
698698

699699
beforeEach(() => {
700700
scrollTo.mockClear()
701+
window.history.replaceState(null, '', '/')
701702
})
702703

703704
afterAll(() => {
@@ -713,6 +714,46 @@ describe('Router', () => {
713714
'/p/a': 2000,
714715
}
715716

717+
it('does not restore scroll positions for pop navigations with unknown direction', async () => {
718+
const scrollBehavior = vi.fn()
719+
const { router } = await newRouter({
720+
history: createWebHashHistory(),
721+
scrollBehavior,
722+
})
723+
scrollBehavior.mockClear()
724+
725+
// Plain `<a href="#...">` links and manual `location.hash` writes create
726+
// a history entry with no state, so the popstate fires with
727+
// `state: null` and the router cannot compute a direction (delta 0).
728+
// happy-dom does not fire popstate on hash changes, so dispatch it like
729+
// a browser would.
730+
function changeHash(hash: string) {
731+
window.location.hash = hash
732+
window.dispatchEvent(new PopStateEvent('popstate', { state: null }))
733+
}
734+
735+
changeHash('#/foo')
736+
await nextNavigation(router)
737+
changeHash('#/')
738+
await nextNavigation(router)
739+
740+
expect(scrollBehavior).toHaveBeenCalledTimes(2)
741+
expect(scrollBehavior).toHaveBeenNthCalledWith(
742+
1,
743+
expect.objectContaining({ path: '/foo' }),
744+
expect.objectContaining({ path: '/' }),
745+
null
746+
)
747+
// a stale position saved under the unknown-direction key would show up
748+
// here as a non-null savedPosition and override the target anchor
749+
expect(scrollBehavior).toHaveBeenNthCalledWith(
750+
2,
751+
expect.objectContaining({ path: '/' }),
752+
expect.objectContaining({ path: '/foo' }),
753+
null
754+
)
755+
})
756+
716757
it('ignores the scroll of navigations superseded before they finish', async () => {
717758
// scrollBehavior resolves asynchronously, simulating waiting for the DOM
718759
const scrollBehavior = vi.fn((to: { path: string }) => {

packages/router/e2e/specs/scroll-behavior.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,35 @@ const anchorTop = (page: Page, id: string) =>
2323
)
2424

2525
test.describe('scroll-behavior', () => {
26+
test('does not restore saved positions for manual hash changes', async ({
27+
page,
28+
}) => {
29+
await page.goto('/scroll-behavior/bar')
30+
await expect(page.locator('.view.bar')).toBeVisible()
31+
32+
await page.evaluate(() => {
33+
window.location.hash = '#anchor'
34+
})
35+
await expect.poll(() => anchorTop(page, 'anchor')).toBeLessThan(1)
36+
37+
await page.evaluate(() => {
38+
window.location.hash = '#anchor2'
39+
})
40+
await expect.poll(() => anchorTop(page, 'anchor2')).toBeLessThan(101)
41+
42+
// Make a stale saved position visibly different from the anchor target.
43+
await page.evaluate(() => {
44+
window.scrollTo(0, 150)
45+
window.location.hash = '#anchor'
46+
})
47+
await expect.poll(() => anchorTop(page, 'anchor')).toBeLessThan(1)
48+
49+
await page.evaluate(() => {
50+
window.location.hash = '#anchor2'
51+
})
52+
await expect.poll(() => anchorTop(page, 'anchor2')).toBeLessThan(101)
53+
})
54+
2655
test('scroll behavior', async ({ page }) => {
2756
await page.goto('/scroll-behavior/')
2857
await expect(

packages/router/src/experimental/router.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,6 +1002,7 @@ describe('Experimental Router', () => {
10021002

10031003
beforeEach(() => {
10041004
scrollTo.mockClear()
1005+
window.history.replaceState(null, '', '/')
10051006
})
10061007

10071008
afterAll(() => {
@@ -1017,6 +1018,46 @@ describe('Experimental Router', () => {
10171018
'/p/a': 2000,
10181019
}
10191020

1021+
it('does not restore scroll positions for pop navigations with unknown direction', async () => {
1022+
const scrollBehavior = vi.fn()
1023+
const { router } = await newRouter({
1024+
history: createWebHashHistory(),
1025+
scrollBehavior,
1026+
})
1027+
scrollBehavior.mockClear()
1028+
1029+
// Plain `<a href="#...">` links and manual `location.hash` writes create
1030+
// a history entry with no state, so the popstate fires with
1031+
// `state: null` and the router cannot compute a direction (delta 0).
1032+
// happy-dom does not fire popstate on hash changes, so dispatch it like
1033+
// a browser would.
1034+
function changeHash(hash: string) {
1035+
window.location.hash = hash
1036+
window.dispatchEvent(new PopStateEvent('popstate', { state: null }))
1037+
}
1038+
1039+
changeHash('#/foo')
1040+
await nextNavigation(router)
1041+
changeHash('#/')
1042+
await nextNavigation(router)
1043+
1044+
expect(scrollBehavior).toHaveBeenCalledTimes(2)
1045+
expect(scrollBehavior).toHaveBeenNthCalledWith(
1046+
1,
1047+
expect.objectContaining({ path: '/foo' }),
1048+
expect.objectContaining({ path: '/' }),
1049+
null
1050+
)
1051+
// a stale position saved under the unknown-direction key would show up
1052+
// here as a non-null savedPosition and override the target anchor
1053+
expect(scrollBehavior).toHaveBeenNthCalledWith(
1054+
2,
1055+
expect.objectContaining({ path: '/' }),
1056+
expect.objectContaining({ path: '/foo' }),
1057+
null
1058+
)
1059+
})
1060+
10201061
it('ignores the scroll of navigations superseded before they finish', async () => {
10211062
// scrollBehavior resolves asynchronously, simulating waiting for the DOM
10221063
const scrollBehavior = vi.fn((to: { path: string }) => {

packages/router/src/experimental/router.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1107,8 +1107,9 @@ export function experimental_createRouter(
11071107
pendingLocation = toLocation
11081108
const from = currentRoute.value
11091109

1110+
// Unknown-direction navigations cannot be tied to a history entry.
11101111
// TODO: should be moved to web history?
1111-
if (isBrowser) {
1112+
if (isBrowser && info.delta) {
11121113
saveScrollPosition(getScrollKey(from.fullPath, info.delta))
11131114
}
11141115

packages/router/src/router.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -814,8 +814,9 @@ export function createRouter(options: RouterOptions): Router {
814814
pendingLocation = toLocation
815815
const from = currentRoute.value
816816

817+
// Unknown-direction navigations cannot be tied to a history entry.
817818
// TODO: should be moved to web history?
818-
if (isBrowser) {
819+
if (isBrowser && info.delta) {
819820
saveScrollPosition(getScrollKey(from.fullPath, info.delta))
820821
}
821822

0 commit comments

Comments
 (0)