Skip to content

Commit 8060b23

Browse files
committed
add scrollTo prop to <ScrollRestoration/> for custom scrolling behaviour
1 parent 25e706e commit 8060b23

File tree

6 files changed

+115
-4
lines changed

6 files changed

+115
-4
lines changed

.changeset/moody-hats-tickle.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router": minor
3+
---
4+
5+
add scrollTo prop to <ScrollRestoration/> for custom scrolling behaviour

contributors.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@
141141
- HelpMe-Pls
142142
- HenriqueLimas
143143
- hernanif1
144+
- herrwitzi
144145
- hi-ogawa
145146
- HK-SHAO
146147
- holynewbie

docs/api/components/ScrollRestoration.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,25 @@ element
8787
The key to use for storing scroll positions in [`sessionStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage).
8888
Defaults to `"react-router-scroll-positions"`.
8989

90+
### scrollTo
91+
92+
A function that will be called to scroll to a specific position.
93+
This is useful for custom scroll restoration logic, such as explicitly seting the scroll behaviour to "auto"
94+
so that CSS (scroll-behavior: smooth on the html tag) does not affect the call (the current behaviour jumps around
95+
when switching pages in a non-intuitive way).
96+
Defaults to `window.scrollTo(0, y)`.
97+
98+
```css
99+
html {
100+
scroll-behavior: smooth;
101+
}
102+
```
103+
```tsx
104+
<ScrollRestoration
105+
scrollTo={(y) => {
106+
document.documentElement.style.scrollBehavior = "auto"
107+
window.scrollTo(0, y)
108+
document.documentElement.style.scrollBehavior = ""
109+
}}
110+
/>
111+
```

packages/react-router/__tests__/dom/scroll-restoration-test.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,69 @@ describe(`ScrollRestoration`, () => {
319319
// Ensure that scroll position is restored
320320
expect(scrollToMock).toHaveBeenCalledWith(0, 20);
321321
});
322+
323+
it("should restore scroll position with custom scrollTo", () => {
324+
let scrollToMock = jest.spyOn(window, "scrollTo");
325+
let router = createMemoryRouter([
326+
{
327+
id: "root",
328+
path: "/",
329+
element: (
330+
<>
331+
<Outlet />
332+
<ScrollRestoration
333+
scrollTo={(y) => window.scrollTo(0, y)}
334+
/>
335+
<Scripts />
336+
</>
337+
),
338+
},
339+
]);
340+
router.state.restoreScrollPosition = 20;
341+
render(
342+
<FrameworkContext.Provider value={context}>
343+
<RouterProvider router={router} />
344+
</FrameworkContext.Provider>,
345+
);
346+
347+
expect(scrollToMock).toHaveBeenCalledWith(0, 20);
348+
});
349+
350+
it("should restore scroll position on navigation with custom scrollTo", () => {
351+
let scrollToMock = jest.spyOn(window, "scrollTo");
352+
let router = createMemoryRouter([
353+
{
354+
id: "root",
355+
path: "/",
356+
element: (
357+
<>
358+
<Outlet />
359+
<ScrollRestoration
360+
scrollTo={(y) => window.scrollTo(0, y)}
361+
/>
362+
<Scripts />
363+
</>
364+
),
365+
},
366+
]);
367+
render(
368+
<FrameworkContext.Provider value={context}>
369+
<RouterProvider router={router} />
370+
</FrameworkContext.Provider>,
371+
);
372+
// Always called when using <ScrollRestoration />
373+
expect(scrollToMock).toHaveBeenCalledWith(0, 0);
374+
// Mock user scroll
375+
window.scrollTo(0, 20);
376+
// Mock navigation
377+
redirect("/otherplace");
378+
// Mock return to original page where navigation had happened
379+
expect(scrollToMock).toHaveBeenCalledWith(0, 0);
380+
// Mock return to original page where navigation had happened
381+
redirect("/");
382+
// Ensure that scroll position is restored
383+
expect(scrollToMock).toHaveBeenCalledWith(0, 20);
384+
});
322385
});
323386
});
324387

packages/react-router/lib/dom/lib.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type {
2424
RelativeRoutingType,
2525
Router as DataRouter,
2626
RouterInit,
27+
ScrollToFunction,
2728
} from "../router/router";
2829
import { IDLE_FETCHER, createRouter } from "../router/router";
2930
import type {
@@ -1934,6 +1935,11 @@ export type ScrollRestorationProps = ScriptsProps & {
19341935
* Defaults to `"react-router-scroll-positions"`.
19351936
*/
19361937
storageKey?: string;
1938+
/**
1939+
* A function that will be called to scroll to a specific position. Defaults to
1940+
* `window.scrollTo(0, y)`. This is useful for custom scroll restoration.
1941+
*/
1942+
scrollTo?: ScrollToFunction;
19371943
};
19381944

19391945
/**
@@ -1968,19 +1974,21 @@ export type ScrollRestorationProps = ScriptsProps & {
19681974
* @param {ScrollRestorationProps.getKey} props.getKey n/a
19691975
* @param {ScriptsProps.nonce} props.nonce n/a
19701976
* @param {ScrollRestorationProps.storageKey} props.storageKey n/a
1977+
* @param {ScrollRestorationProps.scrollTo} props.scrollTo n/a
19711978
* @returns A [`<script>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script)
19721979
* tag that restores scroll positions on navigation.
19731980
*/
19741981
export function ScrollRestoration({
19751982
getKey,
19761983
storageKey,
1984+
scrollTo,
19771985
...props
19781986
}: ScrollRestorationProps) {
19791987
let remixContext = React.useContext(FrameworkContext);
19801988
let { basename } = React.useContext(NavigationContext);
19811989
let location = useLocation();
19821990
let matches = useMatches();
1983-
useScrollRestoration({ getKey, storageKey });
1991+
useScrollRestoration({ getKey, storageKey, scrollTo });
19841992

19851993
// In order to support `getKey`, we need to compute a "key" here so we can
19861994
// hydrate that up so that SSR scroll restoration isn't waiting on React to
@@ -2019,7 +2027,7 @@ export function ScrollRestoration({
20192027
let positions = JSON.parse(sessionStorage.getItem(storageKey) || "{}");
20202028
let storedY = positions[restoreKey || window.history.state.key];
20212029
if (typeof storedY === "number") {
2022-
window.scrollTo(0, storedY);
2030+
scrollTo ? scrollTo(storedY) : window.scrollTo(0, storedY);
20232031
}
20242032
} catch (error: unknown) {
20252033
console.error(error);
@@ -2911,14 +2919,19 @@ function getScrollRestorationKey(
29112919
* to `location.key`.
29122920
* @param options.storageKey The key to use for storing scroll positions in
29132921
* `sessionStorage`. Defaults to `"react-router-scroll-positions"`.
2922+
* @param options.scrollTo A function that will be called to scroll to a
2923+
* specific position. Defaults to `window.scrollTo(0, y)`. This is useful for custom
2924+
* scroll restoration.
29142925
* @returns {void}
29152926
*/
29162927
export function useScrollRestoration({
29172928
getKey,
29182929
storageKey,
2930+
scrollTo,
29192931
}: {
29202932
getKey?: GetScrollRestorationKeyFunction;
29212933
storageKey?: string;
2934+
scrollTo?: ScrollToFunction;
29222935
} = {}): void {
29232936
let { router } = useDataRouterContext(DataRouterHook.UseScrollRestoration);
29242937
let { restoreScrollPosition, preventScrollReset } = useDataRouterState(
@@ -2999,7 +3012,7 @@ export function useScrollRestoration({
29993012

30003013
// been here before, scroll to it
30013014
if (typeof restoreScrollPosition === "number") {
3002-
window.scrollTo(0, restoreScrollPosition);
3015+
scrollTo ? scrollTo(restoreScrollPosition) : window.scrollTo(0, restoreScrollPosition);
30033016
return;
30043017
}
30053018

@@ -3029,7 +3042,7 @@ export function useScrollRestoration({
30293042
}
30303043

30313044
// otherwise go to the top on new locations
3032-
window.scrollTo(0, 0);
3045+
scrollTo ? scrollTo(0) : window.scrollTo(0, 0);
30333046
}, [location, restoreScrollPosition, preventScrollReset]);
30343047
}
30353048
}

packages/react-router/lib/router/router.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,13 @@ export interface GetScrollPositionFunction {
485485
(): number;
486486
}
487487

488+
/**
489+
* Function signature for scrolling to a specific position
490+
*/
491+
export interface ScrollToFunction {
492+
(y: number): void;
493+
}
494+
488495
/**
489496
* - "route": relative to the route hierarchy so `..` means remove all segments
490497
* of the current route even if it has many. For example, a `route("posts/:id")`

0 commit comments

Comments
 (0)