From aa2c73c35079783ee9169a9b4caaf5a93576fc6a Mon Sep 17 00:00:00 2001 From: Guilherme Samuel Date: Tue, 27 Aug 2024 23:12:48 +0000 Subject: [PATCH 1/4] feat: add scroll option --- .../src/__tests__/routeScrollReset.test.tsx | 54 +++++++++++++++++++ packages/router/src/history.tsx | 9 ++-- packages/router/src/location.tsx | 6 +-- 3 files changed, 63 insertions(+), 6 deletions(-) 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) } From 8199e21eb335993c98308860bde5c87fbd258006 Mon Sep 17 00:00:00 2001 From: Guilherme Samuel Date: Wed, 28 Aug 2024 05:45:11 +0000 Subject: [PATCH 2/4] docs: add navigate option --- docs/docs/router.md | 6 ++++++ 1 file changed, 6 insertions(+) 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. From a8fae0079820d719b4c362b684ad61ae5381bcb5 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sat, 25 Jan 2025 18:27:12 +0100 Subject: [PATCH 3/4] Fix formatting --- .../router/src/__tests__/routeScrollReset.test.tsx | 11 +++++------ packages/router/src/history.tsx | 9 ++++++--- packages/router/src/location.tsx | 3 ++- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/router/src/__tests__/routeScrollReset.test.tsx b/packages/router/src/__tests__/routeScrollReset.test.tsx index 7d1482e055fc..87926439e526 100644 --- a/packages/router/src/__tests__/routeScrollReset.test.tsx +++ b/packages/router/src/__tests__/routeScrollReset.test.tsx @@ -90,15 +90,14 @@ 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 - } + scroll: false, + }, ), ) @@ -116,7 +115,7 @@ describe('Router scroll reset', () => { }), { scroll: false, - } + }, ), ) @@ -134,8 +133,8 @@ describe('Router scroll reset', () => { queryParam1: 'foo', }), { - scroll: false - } + scroll: false, + }, ), ) diff --git a/packages/router/src/history.tsx b/packages/router/src/history.tsx index 231ea1f59d9d..bbb542f7dea0 100644 --- a/packages/router/src/history.tsx +++ b/packages/router/src/history.tsx @@ -20,9 +20,12 @@ const createHistory = () => { globalThis.addEventListener('popstate', listener) return listenerId }, - navigate: (to: string, options: NavigateOptions = { - scroll: true - }) => { + navigate: ( + to: string, + options: NavigateOptions = { + scroll: true, + }, + ) => { const performNavigation = () => { const { pathname, search, hash } = new URL( globalThis?.location?.origin + to, diff --git a/packages/router/src/location.tsx b/packages/router/src/location.tsx index f10f5040bf99..074d48ffcc66 100644 --- a/packages/router/src/location.tsx +++ b/packages/router/src/location.tsx @@ -87,7 +87,8 @@ class LocationProvider extends React.Component< this.setState((lastState) => { if ( (context?.pathname !== lastState?.context?.pathname || - context?.search !== lastState?.context?.search) && options?.scroll === true + context?.search !== lastState?.context?.search) && + options?.scroll === true ) { globalThis?.scrollTo(0, 0) } From 512f2686987e635972eefe0aab28a5d2fdcd8f35 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sat, 25 Jan 2025 18:30:56 +0100 Subject: [PATCH 4/4] changeset --- .changesets/11380.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changesets/11380.md diff --git a/.changesets/11380.md b/.changesets/11380.md new file mode 100644 index 000000000000..6a92d5802e9b --- /dev/null +++ b/.changesets/11380.md @@ -0,0 +1,3 @@ +- feat(router): Add option to not reset scroll to the top on navigate/link (#11380) by @guitheengineer + +You can now do ``navigate(`?id=${id}`, { scroll: false })`` and ```` to not reset the scroll to the top when navigating.