diff --git a/docs/docs/router.md b/docs/docs/router.md index 0e8dd94ced8c..9a2b14ae51b8 100644 --- a/docs/docs/router.md +++ b/docs/docs/router.md @@ -614,6 +614,9 @@ const SomePage = () => { 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`. +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: +`navigate(routes.home(), { scroll: false })` + ### back Going back is as easy as using the `back()` function that's exported from the router. @@ -675,6 +678,9 @@ const SomePage = () => In addition to the `to` prop, `` 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: ``. +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: +`` + ## Code-splitting 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. diff --git a/packages/router/src/__tests__/routeScrollReset.test.tsx b/packages/router/src/__tests__/routeScrollReset.test.tsx index 723a9251b2ae..7d1482e055fc 100644 --- a/packages/router/src/__tests__/routeScrollReset.test.tsx +++ b/packages/router/src/__tests__/routeScrollReset.test.tsx @@ -89,4 +89,58 @@ describe('Router scroll reset', () => { expect(globalThis.scrollTo).not.toHaveBeenCalled() }) + + + it('when scroll option is false, does NOT reset on location/path change', async () => { + act(() => + navigate( + // @ts-expect-error - AvailableRoutes built in project only + routes.page2(), + { + scroll: false + } + ), + ) + + screen.getByText('Page 2') + + expect(globalThis.scrollTo).toHaveBeenCalledTimes(0) + }) + + it('when scroll option is false, does NOT reset on location/path and queryChange change', async () => { + act(() => + navigate( + // @ts-expect-error - AvailableRoutes built in project only + routes.page2({ + tab: 'three', + }), + { + scroll: false, + } + ), + ) + + screen.getByText('Page 2') + + expect(globalThis.scrollTo).toHaveBeenCalledTimes(0) + }) + + it('when scroll option is false, does NOT reset scroll on query params (search) change on the same page', async () => { + act(() => + // We're staying on page 1, but changing the query params + navigate( + // @ts-expect-error - AvailableRoutes built in project only + routes.page1({ + queryParam1: 'foo', + }), + { + scroll: false + } + ), + ) + + screen.getByText('Page 1') + + expect(globalThis.scrollTo).toHaveBeenCalledTimes(0) + }) }) diff --git a/packages/router/src/history.tsx b/packages/router/src/history.tsx index ba04716b8ace..231ea1f59d9d 100644 --- a/packages/router/src/history.tsx +++ b/packages/router/src/history.tsx @@ -1,8 +1,9 @@ export interface NavigateOptions { replace?: boolean + scroll?: boolean } -export type Listener = (ev?: PopStateEvent) => any +export type Listener = (ev?: PopStateEvent, options?: NavigateOptions) => any export type BeforeUnloadListener = (ev: BeforeUnloadEvent) => any export type BlockerCallback = (tx: { retry: () => void }) => void export type Blocker = { id: string; callback: BlockerCallback } @@ -19,7 +20,9 @@ const createHistory = () => { globalThis.addEventListener('popstate', listener) return listenerId }, - navigate: (to: string, options?: NavigateOptions) => { + navigate: (to: string, options: NavigateOptions = { + scroll: true + }) => { const performNavigation = () => { const { pathname, search, hash } = new URL( globalThis?.location?.origin + to, @@ -38,7 +41,7 @@ const createHistory = () => { } for (const listener of Object.values(listeners)) { - listener() + listener(undefined, options) } } diff --git a/packages/router/src/location.tsx b/packages/router/src/location.tsx index ce37e79df678..f10f5040bf99 100644 --- a/packages/router/src/location.tsx +++ b/packages/router/src/location.tsx @@ -82,12 +82,12 @@ class LocationProvider extends React.Component< // componentDidMount() is not called during server rendering (aka SSR and // prerendering) componentDidMount() { - this.HISTORY_LISTENER_ID = gHistory.listen(() => { + this.HISTORY_LISTENER_ID = gHistory.listen((_, options) => { const context = this.getContext() this.setState((lastState) => { if ( - context?.pathname !== lastState?.context?.pathname || - context?.search !== lastState?.context?.search + (context?.pathname !== lastState?.context?.pathname || + context?.search !== lastState?.context?.search) && options?.scroll === true ) { globalThis?.scrollTo(0, 0) }