@rpcbase/client 0.282.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpcbase/client",
3
- "version": "0.282.0",
3
+ "version": "0.283.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "scripts": {
@@ -1,5 +1,5 @@
1
- import { useEffect, useRef } from "react"
2
- import { useLocation, useNavigate } from "@rpcbase/router"
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
- locationRef.current = location
27
- }, [location])
26
+ if (typeof window !== "undefined") {
27
+ lastAppliedHashRef.current = window.location.hash || ""
28
+ }
29
+ }, [])
28
30
 
29
- const isScrollingProgrammatically = useRef(false)
30
- const scrollTimeoutRef = useRef<NodeJS.Timeout>(null)
31
- const isUpdatingHashFromSpy = useRef(false)
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
- // If path has changed, scroll to top, or to new hash target
36
- if (previousPathRef.current !== location.pathname) {
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 el = document.getElementById(id)
47
- if (el) el.scrollIntoView({ behavior: "smooth" })
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
- // Don't scroll if the hash was just updated by our scroll spy
54
- if (isUpdatingHashFromSpy.current) {
55
- isUpdatingHashFromSpy.current = false
80
+ if (!location.hash) {
56
81
  return
57
82
  }
58
83
 
59
- if (location.hash) {
60
- const id = location.hash.substring(1)
61
- const element = document.getElementById(id)
62
- if (element) {
63
- isScrollingProgrammatically.current = true
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.pathname, location.hash])
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 (section.offsetTop <= checkPoint && section.offsetTop + section.offsetHeight > checkPoint) {
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
- const { pathname, search, hash } = locationRef.current
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
- }, [navigate])
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 = (e: MouseEvent) => {
119
- const target = e.target as HTMLElement
120
- const link = target.closest("a")
121
-
122
- if (link && link.hash && link.pathname === location.pathname && link.hash === location.hash) {
123
- const id = link.hash.substring(1)
124
- const element = document.getElementById(id)
125
- if (element) {
126
- e.preventDefault()
127
- e.stopPropagation()
128
-
129
- isScrollingProgrammatically.current = true
130
- element.scrollIntoView({ behavior: "smooth" })
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
- if (scrollTimeoutRef.current) {
133
- clearTimeout(scrollTimeoutRef.current)
134
- }
135
- scrollTimeoutRef.current = setTimeout(() => {
136
- isScrollingProgrammatically.current = false
137
- }, 1000)
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
  }