Skip to content
Open
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
48 changes: 33 additions & 15 deletions e2e/react-router/basic-scroll-restoration/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,39 @@ const rootRoute = createRootRoute({
function RootComponent() {
return (
<>
<div className="p-2 flex gap-2 sticky top-0 border-b bg-gray-100 dark:bg-gray-900">
<Link to="/" className="[&.active]:font-bold">
Home
</Link>{' '}
<Link to="/about" className="[&.active]:font-bold">
About
</Link>
<Link to="/about" resetScroll={false}>
About (No Reset)
</Link>
<Link to="/by-element" className="[&.active]:font-bold">
By-Element
</Link>
<div
id="sidebar"
className="fixed left-0 top-0 w-48 h-screen overflow-auto border-r bg-gray-50 dark:bg-gray-800 z-10"
>
<div className="p-2 space-y-2">
{Array.from({ length: 30 }).map((_, i) => (
<div
key={i}
className="h-[50px] p-2 rounded bg-gray-200 dark:bg-gray-700 text-sm"
>
Sidebar Item {i + 1}
</div>
))}
</div>
</div>
<div className="ml-48">
<div className="p-2 flex gap-2 sticky top-0 border-b bg-gray-100 dark:bg-gray-900 z-20">
<Link to="/" className="[&.active]:font-bold">
Home
</Link>{' '}
<Link to="/about" className="[&.active]:font-bold">
About
</Link>
<Link to="/about" resetScroll={false}>
About (No Reset)
</Link>
<Link to="/by-element" className="[&.active]:font-bold">
By-Element
</Link>
</div>
<Outlet />
<TanStackRouterDevtools />
</div>
<Outlet />
<TanStackRouterDevtools />
</>
)
}
Expand Down Expand Up @@ -268,6 +285,7 @@ const router = createRouter({
defaultPreload: 'intent',
scrollRestoration: true,
getScrollRestorationKey: (location) => location.pathname,
scrollToTopSelectors: ['#sidebar'],
})

declare global {
Expand Down
104 changes: 104 additions & 0 deletions e2e/react-router/basic-scroll-restoration/tests/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,107 @@ test('scroll to top when not scrolled, regression test for #4782', async ({
const restoredScrollPosition = await page.evaluate(() => window.scrollY)
expect(restoredScrollPosition).toBe(0)
})

test('resetScroll=false saves scroll position for back navigation without scroll event, regression test for #6595', async ({
page,
}) => {
const storageKey = 'tsr-scroll-restoration-v1_3'
const targetScrollPosition = 1000

await page.goto('/')
await expect(page.locator('#greeting')).toContainText('Welcome Home!')

await page.evaluate(
(scrollPos: number) => window.scrollTo(0, scrollPos),
targetScrollPosition,
)

await page.waitForFunction(
([key, path, expectedY]) => {
const cache = sessionStorage.getItem(key)
if (!cache) return false
const parsed = JSON.parse(cache)
return parsed[path]?.window?.scrollY === expectedY
},
[storageKey, '/', targetScrollPosition] as const,
)

const scrollBeforeNav = await page.evaluate(() => window.scrollY)
expect(scrollBeforeNav).toBe(targetScrollPosition)

await page.getByRole('link', { name: 'About (No Reset)' }).click()
await expect(page.locator('#greeting')).toContainText('Hello from About!')

await page.waitForFunction(
([key, path, expectedY]) => {
const cache = sessionStorage.getItem(key)
if (!cache) return false
const parsed = JSON.parse(cache)
return parsed[path]?.window?.scrollY === expectedY
},
[storageKey, '/about', targetScrollPosition] as const,
)

await page.goto('/foo')
await expect(page.getByTestId('foo-route-component')).toBeVisible()

await page.goBack()
await expect(page.locator('#greeting')).toContainText('Hello from About!')

await page.waitForFunction(
(expectedY) => window.scrollY === expectedY,
targetScrollPosition,
)

const restoredScrollPosition = await page.evaluate(() => window.scrollY)
expect(restoredScrollPosition).toBe(targetScrollPosition)
})

test('resetScroll=false preserves scrollToTopSelectors element scroll position, extension of #6595', async ({
page,
}) => {
const storageKey = 'tsr-scroll-restoration-v1_3'
const sidebarScrollPosition = 500

await page.goto('/')
await expect(page.locator('#greeting')).toContainText('Welcome Home!')

await page.evaluate((scrollPos: number) => {
const sidebar = document.querySelector('#sidebar')
if (sidebar) sidebar.scrollTo(0, scrollPos)
}, sidebarScrollPosition)

const sidebarScrollBeforeNav = await page.evaluate(
() => document.querySelector('#sidebar')?.scrollTop,
)
expect(sidebarScrollBeforeNav).toBe(sidebarScrollPosition)

await page.getByRole('link', { name: 'About (No Reset)' }).click()
await expect(page.locator('#greeting')).toContainText('Hello from About!')

await page.waitForFunction(
([key, path, expectedY]) => {
const cache = sessionStorage.getItem(key)
if (!cache) return false
const parsed = JSON.parse(cache)
return parsed[path]?.['#sidebar']?.scrollY === expectedY
},
[storageKey, '/about', sidebarScrollPosition] as const,
)

await page.goto('/foo')
await expect(page.getByTestId('foo-route-component')).toBeVisible()

await page.goBack()
await expect(page.locator('#greeting')).toContainText('Hello from About!')

await page.waitForFunction(
(expectedY) => document.querySelector('#sidebar')?.scrollTop === expectedY,
sidebarScrollPosition,
)

const restoredSidebarScroll = await page.evaluate(
() => document.querySelector('#sidebar')?.scrollTop,
)
expect(restoredSidebarScroll).toBe(sidebarScrollPosition)
})
44 changes: 43 additions & 1 deletion packages/router-core/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ import {
} from './path'
import { createLRUCache } from './lru-cache'
import { isNotFound } from './not-found'
import { setupScrollRestoration } from './scroll-restoration'
import {
defaultGetScrollRestorationKey,
getCssSelector,
scrollRestorationCache,
setupScrollRestoration,
} from './scroll-restoration'
import { defaultParseSearch, defaultStringifySearch } from './searchParams'
import { rootRouteId } from './root'
import { isRedirect, redirect } from './redirect'
Expand All @@ -41,6 +46,7 @@ import {
executeRewriteOutput,
rewriteBasepath,
} from './rewrite'
import type { ScrollRestorationByElement } from './scroll-restoration'
import type { LRUCache } from './lru-cache'
import type {
ProcessRouteTreeResult,
Expand Down Expand Up @@ -2127,6 +2133,42 @@ export class RouterCore<

this.shouldViewTransition = viewTransition

if (
next.resetScroll === false &&
this.isScrollRestoring &&
scrollRestorationCache
) {
const getKey =
this.options.getScrollRestorationKey || defaultGetScrollRestorationKey
const toKey = getKey(next as unknown as ParsedLocation)
scrollRestorationCache.set((state) => {
const keyEntry = (state[toKey] ||= {} as ScrollRestorationByElement)
keyEntry['window'] = {
scrollX: window.scrollX || 0,
scrollY: window.scrollY || 0,
}
if (this.options.scrollToTopSelectors) {
for (const selector of this.options.scrollToTopSelectors) {
const element =
typeof selector === 'function'
? selector()
: document.querySelector(selector)
if (element) {
const elementSelector =
typeof selector === 'string'
? selector
: getCssSelector(element)
keyEntry[elementSelector] = {
scrollX: element.scrollLeft || 0,
scrollY: element.scrollTop || 0,
}
}
}
}
return state
})
}

this.history[next.replace ? 'replace' : 'push'](
nextHistory.publicHref,
nextHistory.state,
Expand Down
Loading