@tanstack/react-router 0.0.1-beta.162 → 0.0.1-beta.163

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tanstack/react-router",
3
3
  "author": "Tanner Linsley",
4
- "version": "0.0.1-beta.162",
4
+ "version": "0.0.1-beta.163",
5
5
  "license": "MIT",
6
6
  "repository": "tanstack/router",
7
7
  "homepage": "https://tanstack.com/router",
@@ -43,8 +43,8 @@
43
43
  "tiny-invariant": "^1.3.1",
44
44
  "tiny-warning": "^1.0.3",
45
45
  "@gisatcz/cross-package-react-context": "^0.2.0",
46
- "@tanstack/router-core": "0.0.1-beta.162",
47
- "@tanstack/react-store": "0.0.1-beta.162"
46
+ "@tanstack/router-core": "0.0.1-beta.163",
47
+ "@tanstack/react-store": "0.0.1-beta.163"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "rollup --config rollup.config.js"
package/src/index.tsx CHANGED
@@ -40,6 +40,8 @@ import {
40
40
  //
41
41
 
42
42
  export * from '@tanstack/router-core'
43
+ export * from './scroll-restoration'
44
+
43
45
  export { useStore }
44
46
 
45
47
  declare module '@tanstack/router-core' {
@@ -0,0 +1,163 @@
1
+ import * as React from 'react'
2
+ import { ParsedLocation, useRouter } from '.'
3
+
4
+ const useLayoutEffect =
5
+ typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect
6
+
7
+ const windowKey = 'window'
8
+ const delimiter = '___'
9
+
10
+ let weakScrolledElementsByRestoreKey: Record<string, WeakSet<any>> = {}
11
+
12
+ const cache = (() => {
13
+ if (typeof window === 'undefined') {
14
+ return {
15
+ set: () => {},
16
+ get: () => {},
17
+ } as never
18
+ }
19
+
20
+ const storageKey = 'tsr-scroll-restoration-v1'
21
+
22
+ let cache = JSON.parse(window.sessionStorage.getItem(storageKey) || '{}')
23
+
24
+ return {
25
+ current: cache,
26
+ set: (key: string, value: any) => {
27
+ cache[key] = value
28
+ window.sessionStorage.setItem(storageKey, JSON.stringify(cache))
29
+ },
30
+ }
31
+ })()
32
+
33
+ export function ScrollRestoration() {
34
+ const router = useRouter()
35
+ const getKey = (location: ParsedLocation) => location.key as string
36
+
37
+ const pathDidChangeRef = React.useRef(false)
38
+
39
+ const restoreScrollPositions = () => {
40
+ const restoreKey = getKey(router.state.location)
41
+ let windowRestored = false
42
+
43
+ for (const cacheKey in cache.current) {
44
+ const entry = cache.current[cacheKey]!
45
+ const [key, elementSelector] = cacheKey.split(delimiter)
46
+ if (key === restoreKey) {
47
+ if (elementSelector === windowKey) {
48
+ windowRestored = true
49
+ window.scrollTo(entry.scrollX, entry.scrollY)
50
+ } else if (elementSelector) {
51
+ const element = document.querySelector(elementSelector)
52
+ if (element) {
53
+ element.scrollLeft = entry.scrollX
54
+ element.scrollTop = entry.scrollY
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ if (!windowRestored) {
61
+ window.scrollTo(0, 0)
62
+ }
63
+ }
64
+
65
+ useLayoutEffect(() => {
66
+ const { history } = window
67
+ if (history.scrollRestoration) {
68
+ history.scrollRestoration = 'manual'
69
+ }
70
+
71
+ const onScroll = (event: Event) => {
72
+ const restoreKey = getKey(router.state.resolvedLocation)
73
+
74
+ if (!weakScrolledElementsByRestoreKey[restoreKey]) {
75
+ weakScrolledElementsByRestoreKey[restoreKey] = new WeakSet()
76
+ }
77
+
78
+ const set = weakScrolledElementsByRestoreKey[restoreKey]!
79
+
80
+ if (set.has(event.target)) return
81
+ set.add(event.target)
82
+
83
+ const cacheKey = [
84
+ restoreKey,
85
+ event.target === document || event.target === window
86
+ ? windowKey
87
+ : getCssSelector(event.target),
88
+ ].join(delimiter)
89
+
90
+ if (!cache.current[cacheKey]) {
91
+ cache.set(cacheKey, {
92
+ scrollX: NaN,
93
+ scrollY: NaN,
94
+ })
95
+ }
96
+ }
97
+
98
+ const getCssSelector = (el: any): string => {
99
+ let path = [],
100
+ parent
101
+ while ((parent = el.parentNode)) {
102
+ path.unshift(
103
+ `${el.tagName}:nth-child(${
104
+ ([].indexOf as any).call(parent.children, el) + 1
105
+ })`,
106
+ )
107
+ el = parent
108
+ }
109
+ return `${path.join(' > ')}`.toLowerCase()
110
+ }
111
+
112
+ const onPathWillChange = (from: ParsedLocation) => {
113
+ const restoreKey = getKey(from)
114
+ for (const cacheKey in cache.current) {
115
+ const entry = cache.current[cacheKey]!
116
+ const [key, elementSelector] = cacheKey.split(delimiter)
117
+ if (restoreKey === key) {
118
+ if (elementSelector === windowKey) {
119
+ entry.scrollX = window.scrollX || 0
120
+ entry.scrollY = window.scrollY || 0
121
+ } else if (elementSelector) {
122
+ const element = document.querySelector(elementSelector)
123
+ entry.scrollX = element?.scrollLeft || 0
124
+ entry.scrollY = element?.scrollTop || 0
125
+ }
126
+
127
+ cache.set(cacheKey, entry)
128
+ }
129
+ }
130
+ }
131
+
132
+ const onPathChange = () => {
133
+ pathDidChangeRef.current = true
134
+ }
135
+
136
+ if (typeof document !== 'undefined') {
137
+ document.addEventListener('scroll', onScroll, true)
138
+ }
139
+
140
+ const unsubOnBeforeLoad = router.subscribe('onBeforeLoad', (event) => {
141
+ if (event.pathChanged) onPathWillChange(event.from)
142
+ })
143
+
144
+ const unsubOnLoad = router.subscribe('onLoad', (event) => {
145
+ if (event.pathChanged) onPathChange()
146
+ })
147
+
148
+ return () => {
149
+ document.removeEventListener('scroll', onScroll)
150
+ unsubOnBeforeLoad()
151
+ unsubOnLoad()
152
+ }
153
+ }, [])
154
+
155
+ useLayoutEffect(() => {
156
+ if (pathDidChangeRef.current) {
157
+ pathDidChangeRef.current = false
158
+ restoreScrollPositions()
159
+ }
160
+ })
161
+
162
+ return null
163
+ }