Skip to content

Commit 3dfa9af

Browse files
GatsbyJS Botmicha149
andauthored
fix(gatsby-react-router-scroll): scroll restoration for layout components (#26861) (#31079)
* add generic type to useScrollRestoration hook this is required to match the type of `useRef` which requires the target element type to be specified. The hook can be used like the following: ```typescript const MyComponent: React.FunctionComponent = () => { const scrollRestorationProps = useScrollRestoration<HTMLDivElement>(`some-key`) return ( <div {...scrollRestorationProps} style={{ overflow: `auto` }} > Test </div> ) } ``` Fixes: #26458 * add tests for useScrollRestoration hook * fix useScrollResotration to update position on location change Previously, the scroll position was only updated when the using component was re-rendered. This did not work when the hook is used in a wrapping Layout component, becaus its persitent over location changes. Adding the location key to useEffect will cause the scroll position is updated every time the key changes. * lint/ts fixes Co-authored-by: Vladimir Razuvaev <[email protected]> (cherry picked from commit f57efab) Co-authored-by: Michael van Engelshoven <[email protected]>
1 parent e56f544 commit 3dfa9af

File tree

2 files changed

+177
-6
lines changed

2 files changed

+177
-6
lines changed
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import React from "react"
2+
import {
3+
LocationProvider,
4+
History,
5+
createMemorySource,
6+
createHistory,
7+
} from "@reach/router"
8+
import { render, fireEvent } from "@testing-library/react"
9+
import { useScrollRestoration } from "../use-scroll-restoration"
10+
import { ScrollHandler } from "../scroll-handler"
11+
import { SessionStorage } from "../session-storage"
12+
13+
const TRUE = (): boolean => true
14+
15+
const Fixture: React.FunctionComponent = () => {
16+
const scrollRestorationProps = useScrollRestoration<HTMLDivElement>(`test`)
17+
return (
18+
<div
19+
{...scrollRestorationProps}
20+
style={{ overflow: `auto` }}
21+
data-testid="scrollfixture"
22+
>
23+
Test
24+
</div>
25+
)
26+
}
27+
28+
describe(`useScrollRestoration`, () => {
29+
let history: History
30+
const session = new SessionStorage()
31+
let htmlElementPrototype: HTMLElement
32+
let fakedScrollTo = false
33+
34+
beforeAll(() => {
35+
const wrapper = render(<div>hello</div>)
36+
htmlElementPrototype = wrapper.container.constructor.prototype
37+
38+
// jsdom doesn't support .scrollTo(), lets fix this temporarily
39+
if (typeof htmlElementPrototype.scrollTo === `undefined`) {
40+
htmlElementPrototype.scrollTo = function scrollTo(
41+
optionsOrX?: ScrollToOptions | number,
42+
y?: number
43+
): void {
44+
if (typeof optionsOrX === `number`) {
45+
this.scrollLeft = optionsOrX
46+
}
47+
if (typeof y === `number`) {
48+
this.scrollTop = y
49+
}
50+
}
51+
fakedScrollTo = true
52+
}
53+
})
54+
55+
beforeEach(() => {
56+
history = createHistory(createMemorySource(`/`))
57+
sessionStorage.clear()
58+
})
59+
60+
afterAll(() => {
61+
if (fakedScrollTo && htmlElementPrototype.scrollTo) {
62+
// @ts-ignore
63+
delete htmlElementPrototype.scrollTo
64+
}
65+
})
66+
67+
it(`stores current scroll position in storage`, () => {
68+
const wrapper = render(
69+
<LocationProvider history={history}>
70+
<ScrollHandler
71+
navigate={history.navigate}
72+
location={history.location}
73+
shouldUpdateScroll={TRUE}
74+
>
75+
<Fixture />
76+
</ScrollHandler>
77+
</LocationProvider>
78+
)
79+
80+
fireEvent.scroll(wrapper.getByTestId(`scrollfixture`), {
81+
target: { scrollTop: 123 },
82+
})
83+
84+
expect(session.read(history.location, `test`)).toBe(123)
85+
})
86+
87+
it(`scrolls to stored offset on render`, () => {
88+
session.save(history.location, `test`, 684)
89+
90+
const wrapper = render(
91+
<LocationProvider history={history}>
92+
<ScrollHandler
93+
navigate={history.navigate}
94+
location={history.location}
95+
shouldUpdateScroll={TRUE}
96+
>
97+
<Fixture />
98+
</ScrollHandler>
99+
</LocationProvider>
100+
)
101+
102+
expect(wrapper.getByTestId(`scrollfixture`)).toHaveProperty(
103+
`scrollTop`,
104+
684
105+
)
106+
})
107+
108+
it(`scrolls to 0 on render when session has no entry`, () => {
109+
const wrapper = render(
110+
<LocationProvider history={history}>
111+
<ScrollHandler
112+
navigate={history.navigate}
113+
location={history.location}
114+
shouldUpdateScroll={TRUE}
115+
>
116+
<Fixture />
117+
</ScrollHandler>
118+
</LocationProvider>
119+
)
120+
121+
expect(wrapper.getByTestId(`scrollfixture`)).toHaveProperty(`scrollTop`, 0)
122+
})
123+
124+
it(`updates scroll position on location change`, async () => {
125+
const wrapper = render(
126+
<LocationProvider history={history}>
127+
<ScrollHandler
128+
navigate={history.navigate}
129+
location={history.location}
130+
shouldUpdateScroll={TRUE}
131+
>
132+
<Fixture />
133+
</ScrollHandler>
134+
</LocationProvider>
135+
)
136+
137+
fireEvent.scroll(wrapper.getByTestId(`scrollfixture`), {
138+
target: { scrollTop: 356 },
139+
})
140+
141+
await history.navigate(`/another-location`)
142+
143+
expect(wrapper.getByTestId(`scrollfixture`)).toHaveProperty(`scrollTop`, 0)
144+
})
145+
146+
it(`restores scroll position when navigating back`, async () => {
147+
const wrapper = render(
148+
<LocationProvider history={history}>
149+
<ScrollHandler
150+
navigate={history.navigate}
151+
location={history.location}
152+
shouldUpdateScroll={TRUE}
153+
>
154+
<Fixture />
155+
</ScrollHandler>
156+
</LocationProvider>
157+
)
158+
159+
fireEvent.scroll(wrapper.getByTestId(`scrollfixture`), {
160+
target: { scrollTop: 356 },
161+
})
162+
163+
await history.navigate(`/another-location`)
164+
await history.navigate(-1)
165+
166+
expect(wrapper.getByTestId(`scrollfixture`)).toHaveProperty(
167+
`scrollTop`,
168+
356
169+
)
170+
})
171+
})

packages/gatsby-react-router-scroll/src/use-scroll-restoration.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,24 @@ import { ScrollContext } from "./scroll-handler"
22
import { useRef, useContext, useLayoutEffect, MutableRefObject } from "react"
33
import { useLocation } from "@gatsbyjs/reach-router"
44

5-
interface IScrollRestorationProps {
6-
ref: MutableRefObject<HTMLElement | undefined>
5+
interface IScrollRestorationProps<T extends HTMLElement> {
6+
ref: MutableRefObject<T | null>
77
onScroll(): void
88
}
99

10-
export function useScrollRestoration(
10+
export function useScrollRestoration<T extends HTMLElement>(
1111
identifier: string
12-
): IScrollRestorationProps {
12+
): IScrollRestorationProps<T> {
1313
const location = useLocation()
1414
const state = useContext(ScrollContext)
15-
const ref = useRef<HTMLElement>()
15+
const ref = useRef<T>(null)
1616

1717
useLayoutEffect((): void => {
1818
if (ref.current) {
1919
const position = state.read(location, identifier)
2020
ref.current.scrollTo(0, position || 0)
2121
}
22-
}, [])
22+
}, [location.key])
2323

2424
return {
2525
ref,

0 commit comments

Comments
 (0)