-
Notifications
You must be signed in to change notification settings - Fork 585
/
Copy pathuse-scroll-restoration.ts
92 lines (84 loc) · 3.08 KB
/
use-scroll-restoration.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import {useEffect} from 'react'
import {NavigationType, useLocation, useNavigation, useNavigationType} from 'react-router-dom'
import {usePrevious} from 'react-use'
/** Whether to restore, reset to zero or ignore scroll position. */
export type ScrollRestorationAction = 'restore' | 'reset' | 'ignore'
/**
* Handler function testing if scroll position should be restored, reset to zero
* or ignored.
*/
export type ScrollRestorationHandler = (
thisPathname: string,
prevPathname: string,
navigationType: NavigationType,
) => ScrollRestorationAction
/**
* Given a ref to a scrolling container element, keep track of its scroll
* position before navigation and restore it on return (e.g., back/forward nav).
* Behavior is determined by the provided {@link ScrollRestorationHandler}.
*/
export function useScrollRestoration(container: React.RefObject<HTMLElement>, handler: ScrollRestorationHandler) {
const location = useLocation()
const thisPathname = location.pathname
const prevPathname = usePrevious(thisPathname)
// `location.pathname` is used in the cache key, not `location.key`. This
// means that query strings do not affect scroll restoration. This is mainly
// to avoid scrolling for the `dialog` query param.
const cacheKey = `scroll-position-${thisPathname}`
const {state} = useNavigation()
const navigationType = useNavigationType()
useEffect(() => {
const scrollElement = container.current
if (state === 'idle') {
if (!prevPathname) {
// Clear cache when first entering a scroll restoration context
clearScrollPositions()
} else if (thisPathname !== prevPathname) {
// Restore or reset cached scroll position where applicable
const action = handler(thisPathname, prevPathname, navigationType)
if (action === 'restore') {
const y = getScrollPosition(cacheKey)
scrollElement?.scrollTo(0, y)
setScrollPosition(cacheKey, y)
} else if (action === 'reset') {
scrollElement?.scrollTo(0, 0)
setScrollPosition(cacheKey, 0)
} else {
// ignore
}
}
}
// Cache last known scroll position. TODO: Use 'scrollend' listener when
// supported in Safari: https://caniuse.com/?search=scrollend
const handleScrollEnd = () => {
const y = Math.round(scrollElement?.scrollTop ?? 0)
setScrollPosition(cacheKey, y ?? 0)
}
scrollElement?.addEventListener('scroll' /*end*/, handleScrollEnd)
return () => {
scrollElement?.removeEventListener('scroll' /*end*/, handleScrollEnd)
}
}, [cacheKey, state, container, thisPathname, prevPathname, navigationType])
}
function getScrollPosition(key: string) {
const pos = window.sessionStorage.getItem(key)
return pos && /^[0-9]+$/.test(pos) ? parseInt(pos, 10) : 0
}
function setScrollPosition(key: string, pos: number) {
if (pos) {
window.sessionStorage.setItem(key, pos.toString())
} else {
window.sessionStorage.removeItem(key)
}
}
function clearScrollPositions() {
let index = 0
while (index < window.sessionStorage.length) {
const key = window.sessionStorage.key(index)
if (key?.startsWith('scroll-position-')) {
window.sessionStorage.removeItem(key)
} else {
index++
}
}
}