@rpcbase/client 0.281.0 → 0.283.0
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 +1 -1
- package/src/utils/useApplyScroll.ts +91 -67
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useEffect, useRef } from "react"
|
|
2
|
-
import { useLocation
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react"
|
|
2
|
+
import { useLocation } from "@rpcbase/router"
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
function throttle(callback: (...args: any[]) => void, limit: number) {
|
|
@@ -17,91 +17,112 @@ function throttle(callback: (...args: any[]) => void, limit: number) {
|
|
|
17
17
|
|
|
18
18
|
export function useApplyScroll() {
|
|
19
19
|
const location = useLocation()
|
|
20
|
-
const navigate = useNavigate()
|
|
21
|
-
const locationRef = useRef(location)
|
|
22
20
|
const previousPathRef = useRef(location.pathname)
|
|
21
|
+
const isScrollingProgrammatically = useRef(false)
|
|
22
|
+
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
|
23
|
+
const lastAppliedHashRef = useRef("")
|
|
23
24
|
|
|
24
|
-
// Keep a ref to the latest location object to use in callbacks without re-binding them
|
|
25
25
|
useEffect(() => {
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
if (typeof window !== "undefined") {
|
|
27
|
+
lastAppliedHashRef.current = window.location.hash || ""
|
|
28
|
+
}
|
|
29
|
+
}, [])
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
lastAppliedHashRef.current = location.hash || ""
|
|
33
|
+
}, [location.hash])
|
|
34
|
+
|
|
35
|
+
const replaceHashSilently = useCallback((hash: string) => {
|
|
36
|
+
if (typeof window === "undefined") {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
if (lastAppliedHashRef.current === hash) {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
const base = `${window.location.pathname}${window.location.search}`
|
|
43
|
+
window.history.replaceState(window.history.state, "", `${base}${hash}`)
|
|
44
|
+
lastAppliedHashRef.current = hash
|
|
45
|
+
}, [])
|
|
46
|
+
|
|
47
|
+
const markProgrammaticScroll = useCallback(() => {
|
|
48
|
+
isScrollingProgrammatically.current = true
|
|
49
|
+
if (scrollTimeoutRef.current) {
|
|
50
|
+
clearTimeout(scrollTimeoutRef.current)
|
|
51
|
+
}
|
|
52
|
+
scrollTimeoutRef.current = setTimeout(() => {
|
|
53
|
+
isScrollingProgrammatically.current = false
|
|
54
|
+
}, 1000)
|
|
55
|
+
}, [])
|
|
32
56
|
|
|
33
|
-
// Effect for scrolling on path changes or hash changes
|
|
34
57
|
useEffect(() => {
|
|
35
|
-
|
|
36
|
-
|
|
58
|
+
const pathChanged = previousPathRef.current !== location.pathname
|
|
59
|
+
|
|
60
|
+
if (pathChanged) {
|
|
37
61
|
previousPathRef.current = location.pathname
|
|
62
|
+
|
|
38
63
|
if (!location.hash) {
|
|
39
64
|
window.scrollTo({ top: 0, left: 0, behavior: "auto" })
|
|
40
65
|
return
|
|
41
66
|
}
|
|
42
67
|
|
|
43
68
|
setTimeout(() => {
|
|
44
|
-
if (!location.hash) return
|
|
45
69
|
const id = location.hash.substring(1)
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
70
|
+
const element = document.getElementById(id)
|
|
71
|
+
if (element) {
|
|
72
|
+
markProgrammaticScroll()
|
|
73
|
+
element.scrollIntoView({ behavior: "smooth" })
|
|
74
|
+
}
|
|
48
75
|
}, 100)
|
|
49
76
|
|
|
50
77
|
return
|
|
51
78
|
}
|
|
52
79
|
|
|
53
|
-
|
|
54
|
-
if (isUpdatingHashFromSpy.current) {
|
|
55
|
-
isUpdatingHashFromSpy.current = false
|
|
80
|
+
if (!location.hash) {
|
|
56
81
|
return
|
|
57
82
|
}
|
|
58
83
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
element.scrollIntoView({ behavior: "smooth" })
|
|
65
|
-
|
|
66
|
-
if (scrollTimeoutRef.current) {
|
|
67
|
-
clearTimeout(scrollTimeoutRef.current)
|
|
68
|
-
}
|
|
69
|
-
scrollTimeoutRef.current = setTimeout(() => {
|
|
70
|
-
isScrollingProgrammatically.current = false
|
|
71
|
-
}, 1000) // Block scroll spy for 1s after programmatic scroll
|
|
72
|
-
}
|
|
84
|
+
const id = location.hash.substring(1)
|
|
85
|
+
const element = document.getElementById(id)
|
|
86
|
+
if (element) {
|
|
87
|
+
markProgrammaticScroll()
|
|
88
|
+
element.scrollIntoView({ behavior: "smooth" })
|
|
73
89
|
}
|
|
74
|
-
}, [location.
|
|
90
|
+
}, [location.hash, location.pathname, markProgrammaticScroll])
|
|
75
91
|
|
|
76
|
-
// Effect for the scroll spy
|
|
77
92
|
useEffect(() => {
|
|
93
|
+
if (typeof window === "undefined") {
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
78
97
|
const handleScroll = throttle(() => {
|
|
79
98
|
if (isScrollingProgrammatically.current) {
|
|
80
99
|
return
|
|
81
100
|
}
|
|
82
101
|
|
|
83
102
|
const sections = Array.from(document.querySelectorAll("section[id]"))
|
|
103
|
+
|
|
104
|
+
if (sections.length === 0) {
|
|
105
|
+
replaceHashSilently("")
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
84
109
|
const scrollPosition = window.scrollY
|
|
85
110
|
const viewportHeight = window.innerHeight
|
|
86
|
-
// Determine the "active" section based on what's in the upper third of the viewport
|
|
87
111
|
const checkPoint = scrollPosition + viewportHeight / 3
|
|
88
112
|
|
|
89
113
|
let activeSectionId: string | null = null
|
|
90
114
|
for (const section of sections) {
|
|
91
|
-
if (
|
|
115
|
+
if (
|
|
116
|
+
section.offsetTop <= checkPoint &&
|
|
117
|
+
section.offsetTop + section.offsetHeight > checkPoint
|
|
118
|
+
) {
|
|
92
119
|
activeSectionId = section.id
|
|
93
120
|
break
|
|
94
121
|
}
|
|
95
122
|
}
|
|
96
123
|
|
|
97
124
|
const newHash = activeSectionId ? `#${activeSectionId}` : ""
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (hash !== newHash) {
|
|
101
|
-
isUpdatingHashFromSpy.current = true
|
|
102
|
-
// Use replace to not pollute browser history
|
|
103
|
-
navigate(pathname + search + newHash, { replace: true })
|
|
104
|
-
}
|
|
125
|
+
replaceHashSilently(newHash)
|
|
105
126
|
}, 150)
|
|
106
127
|
|
|
107
128
|
document.addEventListener("scroll", handleScroll)
|
|
@@ -109,37 +130,40 @@ export function useApplyScroll() {
|
|
|
109
130
|
document.removeEventListener("scroll", handleScroll)
|
|
110
131
|
if (scrollTimeoutRef.current) {
|
|
111
132
|
clearTimeout(scrollTimeoutRef.current)
|
|
133
|
+
scrollTimeoutRef.current = null
|
|
112
134
|
}
|
|
113
135
|
}
|
|
114
|
-
}, [
|
|
136
|
+
}, [replaceHashSilently])
|
|
115
137
|
|
|
116
|
-
// Effect to handle re-clicking on a link that is already in the hash
|
|
117
138
|
useEffect(() => {
|
|
118
|
-
const handleClick = (
|
|
119
|
-
const target =
|
|
120
|
-
const link = target
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
139
|
+
const handleClick = (event: MouseEvent) => {
|
|
140
|
+
const target = event.target as HTMLElement | null
|
|
141
|
+
const link = target?.closest("a")
|
|
142
|
+
const currentHash =
|
|
143
|
+
typeof window !== "undefined"
|
|
144
|
+
? window.location.hash
|
|
145
|
+
: location.hash || ""
|
|
146
|
+
|
|
147
|
+
if (
|
|
148
|
+
!link ||
|
|
149
|
+
!link.hash ||
|
|
150
|
+
link.pathname !== location.pathname ||
|
|
151
|
+
link.hash !== currentHash
|
|
152
|
+
) {
|
|
153
|
+
return
|
|
154
|
+
}
|
|
131
155
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
156
|
+
const id = link.hash.substring(1)
|
|
157
|
+
const element = document.getElementById(id)
|
|
158
|
+
if (element) {
|
|
159
|
+
event.preventDefault()
|
|
160
|
+
event.stopPropagation()
|
|
161
|
+
markProgrammaticScroll()
|
|
162
|
+
element.scrollIntoView({ behavior: "smooth" })
|
|
139
163
|
}
|
|
140
164
|
}
|
|
141
165
|
|
|
142
166
|
document.addEventListener("click", handleClick, true)
|
|
143
167
|
return () => document.removeEventListener("click", handleClick, true)
|
|
144
|
-
}, [location.hash, location.pathname])
|
|
168
|
+
}, [location.hash, location.pathname, markProgrammaticScroll])
|
|
145
169
|
}
|