Skip to content

Commit 1555713

Browse files
feat(router): Add option to not reset scroll to the top on navigate/link (#11380)
Co-authored-by: Tobbe Lundberg <[email protected]>
1 parent b144c6f commit 1555713

File tree

5 files changed

+75
-6
lines changed

5 files changed

+75
-6
lines changed

.changesets/11380.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
- feat(router): Add option to not reset scroll to the top on navigate/link (#11380) by @guitheengineer
2+
3+
You can now do ``navigate(`?id=${id}`, { scroll: false })`` and ``<Link to={`?id=${id}`} options={{ scroll: false }} />`` to not reset the scroll to the top when navigating.

docs/docs/router.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,9 @@ const SomePage = () => {
614614

615615
The browser keeps track of the browsing history in a stack. By default when you navigate to a new page a new item is pushed to the history stack. But sometimes you want to replace the top item on the stack instead of appending to the stack. This is how you do that in Redwood: `navigate(routes.home(), { replace: true })`. As you can see you need to pass an options object as the second parameter to `navigate` with the option `replace` set to `true`.
616616

617+
By default `navigate` will scroll to the top after navigating to a new route (except for hash param changes), we can prevent this behavior by setting the `scroll` option to false:
618+
`navigate(routes.home(), { scroll: false })`
619+
617620
### back
618621

619622
Going back is as easy as using the `back()` function that's exported from the router.
@@ -675,6 +678,9 @@ const SomePage = () => <Redirect to={routes.home()} />
675678

676679
In addition to the `to` prop, `<Redirect />` also takes an `options` prop. This is the same as [`navigate()`](#navigate)'s second argument: `navigate(_, { replace: true })`. We can use it to _replace_ the top item of the browser history stack (instead of pushing a new one). This is how you use it to have this effect: `<Redirect to={routes.home()} options={{ replace: true }}/>`.
677680

681+
By default redirect will scroll to the top after navigating to a new route (except for hash param changes), we can prevent this behavior by setting the `scroll` option to false:
682+
`<Redirect to={routes.home()} options={{ scroll: false }}/>`
683+
678684
## Code-splitting
679685

680686
By default, the router will code-split on every Page, creating a separate lazy-loaded bundle for each. When navigating from page to page, the router will wait until the new Page module is loaded before re-rendering, thus preventing the "white-flash" effect.

packages/router/src/__tests__/routeScrollReset.test.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,57 @@ describe('Router scroll reset', () => {
8989

9090
expect(globalThis.scrollTo).not.toHaveBeenCalled()
9191
})
92+
93+
it('when scroll option is false, does NOT reset on location/path change', async () => {
94+
act(() =>
95+
navigate(
96+
// @ts-expect-error - AvailableRoutes built in project only
97+
routes.page2(),
98+
{
99+
scroll: false,
100+
},
101+
),
102+
)
103+
104+
screen.getByText('Page 2')
105+
106+
expect(globalThis.scrollTo).toHaveBeenCalledTimes(0)
107+
})
108+
109+
it('when scroll option is false, does NOT reset on location/path and queryChange change', async () => {
110+
act(() =>
111+
navigate(
112+
// @ts-expect-error - AvailableRoutes built in project only
113+
routes.page2({
114+
tab: 'three',
115+
}),
116+
{
117+
scroll: false,
118+
},
119+
),
120+
)
121+
122+
screen.getByText('Page 2')
123+
124+
expect(globalThis.scrollTo).toHaveBeenCalledTimes(0)
125+
})
126+
127+
it('when scroll option is false, does NOT reset scroll on query params (search) change on the same page', async () => {
128+
act(() =>
129+
// We're staying on page 1, but changing the query params
130+
navigate(
131+
// @ts-expect-error - AvailableRoutes built in project only
132+
routes.page1({
133+
queryParam1: 'foo',
134+
}),
135+
{
136+
scroll: false,
137+
},
138+
),
139+
)
140+
141+
screen.getByText('Page 1')
142+
143+
expect(globalThis.scrollTo).toHaveBeenCalledTimes(0)
144+
})
92145
})

packages/router/src/history.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
export interface NavigateOptions {
22
replace?: boolean
3+
scroll?: boolean
34
}
45

5-
export type Listener = (ev?: PopStateEvent) => any
6+
export type Listener = (ev?: PopStateEvent, options?: NavigateOptions) => any
67
export type BeforeUnloadListener = (ev: BeforeUnloadEvent) => any
78
export type BlockerCallback = (tx: { retry: () => void }) => void
89
export type Blocker = { id: string; callback: BlockerCallback }
@@ -19,7 +20,12 @@ const createHistory = () => {
1920
globalThis.addEventListener('popstate', listener)
2021
return listenerId
2122
},
22-
navigate: (to: string, options?: NavigateOptions) => {
23+
navigate: (
24+
to: string,
25+
options: NavigateOptions = {
26+
scroll: true,
27+
},
28+
) => {
2329
const performNavigation = () => {
2430
const { pathname, search, hash } = new URL(
2531
globalThis?.location?.origin + to,
@@ -38,7 +44,7 @@ const createHistory = () => {
3844
}
3945

4046
for (const listener of Object.values(listeners)) {
41-
listener()
47+
listener(undefined, options)
4248
}
4349
}
4450

packages/router/src/location.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,13 @@ class LocationProvider extends React.Component<
8282
// componentDidMount() is not called during server rendering (aka SSR and
8383
// prerendering)
8484
componentDidMount() {
85-
this.HISTORY_LISTENER_ID = gHistory.listen(() => {
85+
this.HISTORY_LISTENER_ID = gHistory.listen((_, options) => {
8686
const context = this.getContext()
8787
this.setState((lastState) => {
8888
if (
89-
context?.pathname !== lastState?.context?.pathname ||
90-
context?.search !== lastState?.context?.search
89+
(context?.pathname !== lastState?.context?.pathname ||
90+
context?.search !== lastState?.context?.search) &&
91+
options?.scroll === true
9192
) {
9293
globalThis?.scrollTo(0, 0)
9394
}

0 commit comments

Comments
 (0)