@rpcbase/client 0.261.0 → 0.262.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.261.0",
3
+ "version": "0.262.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "scripts": {
@@ -1 +1,2 @@
1
1
  export * from "./useThrottledMeasure"
2
+ export * from "./useApplyScroll"
@@ -0,0 +1,145 @@
1
+ import { useEffect, useRef } from "react"
2
+ import { useLocation, useNavigate } from "@rpcbase/router"
3
+
4
+
5
+ function throttle(callback: (...args: any[]) => void, limit: number) {
6
+ let wait = false
7
+ return (...args: any[]) => {
8
+ if (!wait) {
9
+ callback(...args)
10
+ wait = true
11
+ setTimeout(() => {
12
+ wait = false
13
+ }, limit)
14
+ }
15
+ }
16
+ }
17
+
18
+ export function useApplyScroll() {
19
+ const location = useLocation()
20
+ const navigate = useNavigate()
21
+ const locationRef = useRef(location)
22
+ const previousPathRef = useRef(location.pathname)
23
+
24
+ // Keep a ref to the latest location object to use in callbacks without re-binding them
25
+ useEffect(() => {
26
+ locationRef.current = location
27
+ }, [location])
28
+
29
+ const isScrollingProgrammatically = useRef(false)
30
+ const scrollTimeoutRef = useRef<NodeJS.Timeout>(null)
31
+ const isUpdatingHashFromSpy = useRef(false)
32
+
33
+ // Effect for scrolling on path changes or hash changes
34
+ useEffect(() => {
35
+ // If path has changed, scroll to top, or to new hash target
36
+ if (previousPathRef.current !== location.pathname) {
37
+ previousPathRef.current = location.pathname
38
+ if (!location.hash) {
39
+ window.scrollTo({ top: 0, left: 0, behavior: "auto" })
40
+ return
41
+ }
42
+
43
+ setTimeout(() => {
44
+ if (!location.hash) return
45
+ const id = location.hash.substring(1)
46
+ const el = document.getElementById(id)
47
+ if (el) el.scrollIntoView({ behavior: "smooth" })
48
+ }, 100)
49
+
50
+ return
51
+ }
52
+
53
+ // Don't scroll if the hash was just updated by our scroll spy
54
+ if (isUpdatingHashFromSpy.current) {
55
+ isUpdatingHashFromSpy.current = false
56
+ return
57
+ }
58
+
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
+ }
73
+ }
74
+ }, [location.pathname, location.hash])
75
+
76
+ // Effect for the scroll spy
77
+ useEffect(() => {
78
+ const handleScroll = throttle(() => {
79
+ if (isScrollingProgrammatically.current) {
80
+ return
81
+ }
82
+
83
+ const sections = Array.from(document.querySelectorAll("section[id]"))
84
+ const scrollPosition = window.scrollY
85
+ const viewportHeight = window.innerHeight
86
+ // Determine the "active" section based on what's in the upper third of the viewport
87
+ const checkPoint = scrollPosition + viewportHeight / 3
88
+
89
+ let activeSectionId: string | null = null
90
+ for (const section of sections) {
91
+ if (section.offsetTop <= checkPoint && section.offsetTop + section.offsetHeight > checkPoint) {
92
+ activeSectionId = section.id
93
+ break
94
+ }
95
+ }
96
+
97
+ 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
+ }
105
+ }, 150)
106
+
107
+ document.addEventListener("scroll", handleScroll)
108
+ return () => {
109
+ document.removeEventListener("scroll", handleScroll)
110
+ if (scrollTimeoutRef.current) {
111
+ clearTimeout(scrollTimeoutRef.current)
112
+ }
113
+ }
114
+ }, [navigate])
115
+
116
+ // Effect to handle re-clicking on a link that is already in the hash
117
+ 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" })
131
+
132
+ if (scrollTimeoutRef.current) {
133
+ clearTimeout(scrollTimeoutRef.current)
134
+ }
135
+ scrollTimeoutRef.current = setTimeout(() => {
136
+ isScrollingProgrammatically.current = false
137
+ }, 1000)
138
+ }
139
+ }
140
+ }
141
+
142
+ document.addEventListener("click", handleClick, true)
143
+ return () => document.removeEventListener("click", handleClick, true)
144
+ }, [location.hash, location.pathname])
145
+ }