domet 1.0.0 → 1.0.2
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/README.md +6 -4
- package/dist/{useDomet.d.ts → cjs/index.d.ts} +14 -12
- package/dist/{useDomet.js → cjs/index.js} +172 -156
- package/dist/{useScrowl.d.ts → es/index.d.mts} +14 -12
- package/dist/es/index.d.ts +62 -0
- package/dist/{useScrowl.js → es/index.js} +172 -156
- package/dist/es/index.mjs +504 -0
- package/package.json +22 -13
- package/LICENSE +0 -21
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -1
- package/dist/useDomet.d.ts.map +0 -1
- package/dist/useScrowl.d.ts.map +0 -1
|
@@ -1,17 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo, useState, useRef, useCallback, useLayoutEffect, useEffect } from 'react';
|
|
2
|
+
|
|
2
3
|
const DEFAULT_VISIBILITY_THRESHOLD = 0.6;
|
|
3
4
|
const DEFAULT_HYSTERESIS_MARGIN = 150;
|
|
4
5
|
const SCROLL_IDLE_MS = 100;
|
|
5
6
|
const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
|
|
6
|
-
|
|
7
|
-
const { offset = 0, offsetRatio = 0.08, debounceMs = 10, visibilityThreshold = DEFAULT_VISIBILITY_THRESHOLD, hysteresisMargin = DEFAULT_HYSTERESIS_MARGIN, behavior = "auto", onActiveChange, onSectionEnter, onSectionLeave, onScrollStart, onScrollEnd
|
|
8
|
-
|
|
9
|
-
const stableSectionIds = useMemo(()
|
|
10
|
-
|
|
7
|
+
function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
8
|
+
const { offset = 0, offsetRatio = 0.08, debounceMs = 10, visibilityThreshold = DEFAULT_VISIBILITY_THRESHOLD, hysteresisMargin = DEFAULT_HYSTERESIS_MARGIN, behavior = "auto", onActiveChange, onSectionEnter, onSectionLeave, onScrollStart, onScrollEnd } = options;
|
|
9
|
+
JSON.stringify(sectionIds);
|
|
10
|
+
const stableSectionIds = useMemo(()=>sectionIds, [
|
|
11
|
+
sectionIds
|
|
12
|
+
]);
|
|
13
|
+
const sectionIndexMap = useMemo(()=>{
|
|
11
14
|
const map = new Map();
|
|
12
|
-
|
|
15
|
+
for(let i = 0; i < stableSectionIds.length; i++){
|
|
16
|
+
map.set(stableSectionIds[i], i);
|
|
17
|
+
}
|
|
13
18
|
return map;
|
|
14
|
-
}, [
|
|
19
|
+
}, [
|
|
20
|
+
stableSectionIds
|
|
21
|
+
]);
|
|
15
22
|
const [activeId, setActiveId] = useState(stableSectionIds[0] || null);
|
|
16
23
|
const [scroll, setScroll] = useState({
|
|
17
24
|
y: 0,
|
|
@@ -21,7 +28,7 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
21
28
|
isScrolling: false,
|
|
22
29
|
maxScroll: 0,
|
|
23
30
|
viewportHeight: 0,
|
|
24
|
-
offset: 0
|
|
31
|
+
offset: 0
|
|
25
32
|
});
|
|
26
33
|
const [sections, setSections] = useState({});
|
|
27
34
|
const [containerElement, setContainerElement] = useState(null);
|
|
@@ -39,56 +46,61 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
39
46
|
const isScrollingRef = useRef(false);
|
|
40
47
|
const scrollIdleTimeoutRef = useRef(null);
|
|
41
48
|
const prevSectionsInViewport = useRef(new Set());
|
|
42
|
-
const recalculateRef = useRef(()
|
|
49
|
+
const recalculateRef = useRef(()=>{});
|
|
43
50
|
const scrollCleanupRef = useRef(null);
|
|
44
51
|
const callbackRefs = useRef({
|
|
45
52
|
onActiveChange,
|
|
46
53
|
onSectionEnter,
|
|
47
54
|
onSectionLeave,
|
|
48
55
|
onScrollStart,
|
|
49
|
-
onScrollEnd
|
|
56
|
+
onScrollEnd
|
|
50
57
|
});
|
|
51
58
|
callbackRefs.current = {
|
|
52
59
|
onActiveChange,
|
|
53
60
|
onSectionEnter,
|
|
54
61
|
onSectionLeave,
|
|
55
62
|
onScrollStart,
|
|
56
|
-
onScrollEnd
|
|
63
|
+
onScrollEnd
|
|
57
64
|
};
|
|
58
|
-
const getEffectiveOffset = useCallback(()
|
|
65
|
+
const getEffectiveOffset = useCallback(()=>{
|
|
59
66
|
return offset;
|
|
60
|
-
}, [
|
|
61
|
-
|
|
67
|
+
}, [
|
|
68
|
+
offset
|
|
69
|
+
]);
|
|
70
|
+
const getScrollBehavior = useCallback(()=>{
|
|
62
71
|
if (behavior === "auto") {
|
|
63
|
-
if (typeof window === "undefined")
|
|
64
|
-
return "instant";
|
|
72
|
+
if (typeof window === "undefined") return "instant";
|
|
65
73
|
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
66
74
|
return prefersReducedMotion ? "instant" : "smooth";
|
|
67
75
|
}
|
|
68
76
|
return behavior;
|
|
69
|
-
}, [
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
}, [
|
|
78
|
+
behavior
|
|
79
|
+
]);
|
|
80
|
+
useIsomorphicLayoutEffect(()=>{
|
|
81
|
+
var _ref;
|
|
82
|
+
const nextContainer = (_ref = containerRef == null ? void 0 : containerRef.current) != null ? _ref : null;
|
|
72
83
|
if (nextContainer !== containerElement) {
|
|
73
84
|
setContainerElement(nextContainer);
|
|
74
85
|
}
|
|
75
|
-
}, [
|
|
76
|
-
|
|
86
|
+
}, [
|
|
87
|
+
containerRef,
|
|
88
|
+
containerElement
|
|
89
|
+
]);
|
|
90
|
+
const registerRef = useCallback((id)=>{
|
|
77
91
|
const existing = refCallbacks.current[id];
|
|
78
|
-
if (existing)
|
|
79
|
-
|
|
80
|
-
const callback = (el) => {
|
|
92
|
+
if (existing) return existing;
|
|
93
|
+
const callback = (el)=>{
|
|
81
94
|
if (el) {
|
|
82
95
|
refs.current[id] = el;
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
96
|
+
} else {
|
|
85
97
|
delete refs.current[id];
|
|
86
98
|
}
|
|
87
99
|
};
|
|
88
100
|
refCallbacks.current[id] = callback;
|
|
89
101
|
return callback;
|
|
90
102
|
}, []);
|
|
91
|
-
const scrollToSection = useCallback((id)
|
|
103
|
+
const scrollToSection = useCallback((id)=>{
|
|
92
104
|
if (!stableSectionIds.includes(id)) {
|
|
93
105
|
if (process.env.NODE_ENV !== "production") {
|
|
94
106
|
console.warn(`[domet] scrollToSection: id "${id}" not in sectionIds`);
|
|
@@ -96,12 +108,11 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
96
108
|
return;
|
|
97
109
|
}
|
|
98
110
|
const element = refs.current[id];
|
|
99
|
-
if (!element)
|
|
100
|
-
return;
|
|
111
|
+
if (!element) return;
|
|
101
112
|
if (programmaticScrollTimeoutId.current) {
|
|
102
113
|
clearTimeout(programmaticScrollTimeoutId.current);
|
|
103
114
|
}
|
|
104
|
-
scrollCleanupRef.current
|
|
115
|
+
scrollCleanupRef.current == null ? void 0 : scrollCleanupRef.current.call(scrollCleanupRef);
|
|
105
116
|
isProgrammaticScrolling.current = true;
|
|
106
117
|
activeIdRef.current = id;
|
|
107
118
|
setActiveId(id);
|
|
@@ -109,19 +120,19 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
109
120
|
const elementRect = element.getBoundingClientRect();
|
|
110
121
|
const effectiveOffset = getEffectiveOffset() + 10;
|
|
111
122
|
const scrollTarget = container || window;
|
|
112
|
-
const unlockScroll = ()
|
|
123
|
+
const unlockScroll = ()=>{
|
|
113
124
|
isProgrammaticScrolling.current = false;
|
|
114
125
|
if (programmaticScrollTimeoutId.current) {
|
|
115
126
|
clearTimeout(programmaticScrollTimeoutId.current);
|
|
116
127
|
programmaticScrollTimeoutId.current = null;
|
|
117
128
|
}
|
|
118
|
-
requestAnimationFrame(()
|
|
129
|
+
requestAnimationFrame(()=>{
|
|
119
130
|
recalculateRef.current();
|
|
120
131
|
});
|
|
121
132
|
};
|
|
122
133
|
let debounceTimer = null;
|
|
123
134
|
let isUnlocked = false;
|
|
124
|
-
const cleanup = ()
|
|
135
|
+
const cleanup = ()=>{
|
|
125
136
|
if (debounceTimer) {
|
|
126
137
|
clearTimeout(debounceTimer);
|
|
127
138
|
debounceTimer = null;
|
|
@@ -132,31 +143,30 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
132
143
|
}
|
|
133
144
|
scrollCleanupRef.current = null;
|
|
134
145
|
};
|
|
135
|
-
const doUnlock = ()
|
|
136
|
-
if (isUnlocked)
|
|
137
|
-
return;
|
|
146
|
+
const doUnlock = ()=>{
|
|
147
|
+
if (isUnlocked) return;
|
|
138
148
|
isUnlocked = true;
|
|
139
149
|
cleanup();
|
|
140
150
|
unlockScroll();
|
|
141
151
|
};
|
|
142
|
-
const resetDebounce = ()
|
|
152
|
+
const resetDebounce = ()=>{
|
|
143
153
|
if (debounceTimer) {
|
|
144
154
|
clearTimeout(debounceTimer);
|
|
145
155
|
}
|
|
146
156
|
debounceTimer = setTimeout(doUnlock, SCROLL_IDLE_MS);
|
|
147
157
|
};
|
|
148
|
-
const handleScrollActivity = ()
|
|
158
|
+
const handleScrollActivity = ()=>{
|
|
149
159
|
resetDebounce();
|
|
150
160
|
};
|
|
151
|
-
const handleScrollEnd = ()
|
|
161
|
+
const handleScrollEnd = ()=>{
|
|
152
162
|
doUnlock();
|
|
153
163
|
};
|
|
154
164
|
scrollTarget.addEventListener("scroll", handleScrollActivity, {
|
|
155
|
-
passive: true
|
|
165
|
+
passive: true
|
|
156
166
|
});
|
|
157
167
|
if ("onscrollend" in scrollTarget) {
|
|
158
168
|
scrollTarget.addEventListener("scrollend", handleScrollEnd, {
|
|
159
|
-
once: true
|
|
169
|
+
once: true
|
|
160
170
|
});
|
|
161
171
|
}
|
|
162
172
|
scrollCleanupRef.current = cleanup;
|
|
@@ -166,121 +176,117 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
166
176
|
const relativeTop = elementRect.top - containerRect.top + container.scrollTop;
|
|
167
177
|
container.scrollTo({
|
|
168
178
|
top: relativeTop - effectiveOffset,
|
|
169
|
-
behavior: scrollBehavior
|
|
179
|
+
behavior: scrollBehavior
|
|
170
180
|
});
|
|
171
|
-
}
|
|
172
|
-
else {
|
|
181
|
+
} else {
|
|
173
182
|
const absoluteTop = elementRect.top + window.scrollY;
|
|
174
183
|
window.scrollTo({
|
|
175
184
|
top: absoluteTop - effectiveOffset,
|
|
176
|
-
behavior: scrollBehavior
|
|
185
|
+
behavior: scrollBehavior
|
|
177
186
|
});
|
|
178
187
|
}
|
|
179
188
|
if (scrollBehavior === "instant") {
|
|
180
189
|
doUnlock();
|
|
181
|
-
}
|
|
182
|
-
else {
|
|
190
|
+
} else {
|
|
183
191
|
resetDebounce();
|
|
184
192
|
}
|
|
185
|
-
}, [
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
193
|
+
}, [
|
|
194
|
+
stableSectionIds,
|
|
195
|
+
containerElement,
|
|
196
|
+
getEffectiveOffset,
|
|
197
|
+
getScrollBehavior
|
|
198
|
+
]);
|
|
199
|
+
const sectionProps = useCallback((id)=>({
|
|
200
|
+
id,
|
|
201
|
+
ref: registerRef(id),
|
|
202
|
+
"data-domet": id
|
|
203
|
+
}), [
|
|
204
|
+
registerRef
|
|
205
|
+
]);
|
|
206
|
+
const navProps = useCallback((id)=>({
|
|
207
|
+
onClick: ()=>scrollToSection(id),
|
|
208
|
+
"aria-current": activeId === id ? "page" : undefined,
|
|
209
|
+
"data-active": activeId === id
|
|
210
|
+
}), [
|
|
211
|
+
activeId,
|
|
212
|
+
scrollToSection
|
|
213
|
+
]);
|
|
214
|
+
useEffect(()=>{
|
|
215
|
+
var _stableSectionIds_;
|
|
197
216
|
const idsSet = new Set(stableSectionIds);
|
|
198
|
-
for (const id of Object.keys(refs.current))
|
|
217
|
+
for (const id of Object.keys(refs.current)){
|
|
199
218
|
if (!idsSet.has(id)) {
|
|
200
219
|
delete refs.current[id];
|
|
201
220
|
}
|
|
202
221
|
}
|
|
203
|
-
for (const id of Object.keys(refCallbacks.current))
|
|
222
|
+
for (const id of Object.keys(refCallbacks.current)){
|
|
204
223
|
if (!idsSet.has(id)) {
|
|
205
224
|
delete refCallbacks.current[id];
|
|
206
225
|
}
|
|
207
226
|
}
|
|
208
227
|
const currentActive = activeIdRef.current;
|
|
209
|
-
const nextActive = currentActive && idsSet.has(currentActive)
|
|
210
|
-
? currentActive
|
|
211
|
-
: (stableSectionIds[0] ?? null);
|
|
228
|
+
const nextActive = currentActive && idsSet.has(currentActive) ? currentActive : (_stableSectionIds_ = stableSectionIds[0]) != null ? _stableSectionIds_ : null;
|
|
212
229
|
if (nextActive !== currentActive) {
|
|
213
230
|
activeIdRef.current = nextActive;
|
|
214
231
|
}
|
|
215
|
-
setActiveId((prev)
|
|
216
|
-
}, [
|
|
217
|
-
|
|
232
|
+
setActiveId((prev)=>prev !== nextActive ? nextActive : prev);
|
|
233
|
+
}, [
|
|
234
|
+
stableSectionIds
|
|
235
|
+
]);
|
|
236
|
+
const getSectionBounds = useCallback(()=>{
|
|
218
237
|
const container = containerElement;
|
|
219
238
|
const scrollTop = container ? container.scrollTop : window.scrollY;
|
|
220
239
|
const containerTop = container ? container.getBoundingClientRect().top : 0;
|
|
221
|
-
return stableSectionIds
|
|
222
|
-
.map((id) => {
|
|
240
|
+
return stableSectionIds.map((id)=>{
|
|
223
241
|
const el = refs.current[id];
|
|
224
|
-
if (!el)
|
|
225
|
-
return null;
|
|
242
|
+
if (!el) return null;
|
|
226
243
|
const rect = el.getBoundingClientRect();
|
|
227
|
-
const relativeTop = container
|
|
228
|
-
? rect.top - containerTop + scrollTop
|
|
229
|
-
: rect.top + window.scrollY;
|
|
244
|
+
const relativeTop = container ? rect.top - containerTop + scrollTop : rect.top + window.scrollY;
|
|
230
245
|
return {
|
|
231
246
|
id,
|
|
232
247
|
top: relativeTop,
|
|
233
248
|
bottom: relativeTop + rect.height,
|
|
234
|
-
height: rect.height
|
|
249
|
+
height: rect.height
|
|
235
250
|
};
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
251
|
+
}).filter((bounds)=>bounds !== null);
|
|
252
|
+
}, [
|
|
253
|
+
stableSectionIds,
|
|
254
|
+
containerElement
|
|
255
|
+
]);
|
|
256
|
+
const calculateActiveSection = useCallback(()=>{
|
|
257
|
+
if (isProgrammaticScrolling.current) return;
|
|
242
258
|
const container = containerElement;
|
|
243
259
|
const currentActiveId = activeIdRef.current;
|
|
244
260
|
const now = Date.now();
|
|
245
261
|
const scrollY = container ? container.scrollTop : window.scrollY;
|
|
246
|
-
const viewportHeight = container
|
|
247
|
-
|
|
248
|
-
: window.innerHeight;
|
|
249
|
-
const scrollHeight = container
|
|
250
|
-
? container.scrollHeight
|
|
251
|
-
: document.documentElement.scrollHeight;
|
|
262
|
+
const viewportHeight = container ? container.clientHeight : window.innerHeight;
|
|
263
|
+
const scrollHeight = container ? container.scrollHeight : document.documentElement.scrollHeight;
|
|
252
264
|
const maxScroll = Math.max(0, scrollHeight - viewportHeight);
|
|
253
265
|
const scrollProgress = maxScroll > 0 ? scrollY / maxScroll : 0;
|
|
254
|
-
const scrollDirection = scrollY === lastScrollY.current
|
|
255
|
-
? null
|
|
256
|
-
: scrollY > lastScrollY.current
|
|
257
|
-
? "down"
|
|
258
|
-
: "up";
|
|
266
|
+
const scrollDirection = scrollY === lastScrollY.current ? null : scrollY > lastScrollY.current ? "down" : "up";
|
|
259
267
|
const deltaTime = now - lastScrollTime.current;
|
|
260
268
|
const deltaY = scrollY - lastScrollY.current;
|
|
261
269
|
const velocity = deltaTime > 0 ? Math.abs(deltaY) / deltaTime : 0;
|
|
262
270
|
lastScrollY.current = scrollY;
|
|
263
271
|
lastScrollTime.current = now;
|
|
264
272
|
const sectionBounds = getSectionBounds();
|
|
265
|
-
if (sectionBounds.length === 0)
|
|
266
|
-
return;
|
|
273
|
+
if (sectionBounds.length === 0) return;
|
|
267
274
|
const baseOffset = getEffectiveOffset();
|
|
268
275
|
const effectiveOffset = Math.max(baseOffset, viewportHeight * offsetRatio);
|
|
269
276
|
const triggerLine = scrollY + effectiveOffset;
|
|
270
277
|
const viewportTop = scrollY;
|
|
271
278
|
const viewportBottom = scrollY + viewportHeight;
|
|
272
|
-
const scores = sectionBounds.map((section)
|
|
279
|
+
const scores = sectionBounds.map((section)=>{
|
|
280
|
+
var _sectionIndexMap_get;
|
|
273
281
|
const visibleTop = Math.max(section.top, viewportTop);
|
|
274
282
|
const visibleBottom = Math.min(section.bottom, viewportBottom);
|
|
275
283
|
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
|
|
276
284
|
const visibilityRatio = section.height > 0 ? visibleHeight / section.height : 0;
|
|
277
285
|
const visibleInViewportRatio = viewportHeight > 0 ? visibleHeight / viewportHeight : 0;
|
|
278
286
|
const isInViewport = section.bottom > viewportTop && section.top < viewportBottom;
|
|
279
|
-
const sectionProgress = (()
|
|
280
|
-
if (section.height === 0)
|
|
281
|
-
return 0;
|
|
287
|
+
const sectionProgress = (()=>{
|
|
288
|
+
if (section.height === 0) return 0;
|
|
282
289
|
const entryPoint = viewportBottom;
|
|
283
|
-
const _exitPoint = viewportTop;
|
|
284
290
|
const totalTravel = viewportHeight + section.height;
|
|
285
291
|
const traveled = entryPoint - section.top;
|
|
286
292
|
return Math.max(0, Math.min(1, traveled / totalTravel));
|
|
@@ -288,15 +294,11 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
288
294
|
let score = 0;
|
|
289
295
|
if (visibilityRatio >= visibilityThreshold) {
|
|
290
296
|
score += 1000 + visibilityRatio * 500;
|
|
291
|
-
}
|
|
292
|
-
else if (isInViewport) {
|
|
297
|
+
} else if (isInViewport) {
|
|
293
298
|
score += visibleInViewportRatio * 800;
|
|
294
299
|
}
|
|
295
|
-
const sectionIndex = sectionIndexMap.get(section.id)
|
|
296
|
-
if (scrollDirection &&
|
|
297
|
-
isInViewport &&
|
|
298
|
-
section.top <= triggerLine &&
|
|
299
|
-
section.bottom > triggerLine) {
|
|
300
|
+
const sectionIndex = (_sectionIndexMap_get = sectionIndexMap.get(section.id)) != null ? _sectionIndexMap_get : 0;
|
|
301
|
+
if (scrollDirection && isInViewport && section.top <= triggerLine && section.bottom > triggerLine) {
|
|
300
302
|
score += 200;
|
|
301
303
|
}
|
|
302
304
|
score -= sectionIndex * 0.1;
|
|
@@ -306,47 +308,43 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
306
308
|
visibilityRatio,
|
|
307
309
|
isInViewport,
|
|
308
310
|
bounds: section,
|
|
309
|
-
progress: sectionProgress
|
|
311
|
+
progress: sectionProgress
|
|
310
312
|
};
|
|
311
313
|
});
|
|
312
|
-
const
|
|
313
|
-
const
|
|
314
|
+
const hasScroll = maxScroll > 10;
|
|
315
|
+
const isAtBottom = hasScroll && scrollY + viewportHeight >= scrollHeight - 5;
|
|
316
|
+
const isAtTop = hasScroll && scrollY <= 5;
|
|
314
317
|
let newActiveId = null;
|
|
315
318
|
if (isAtBottom && stableSectionIds.length > 0) {
|
|
316
319
|
newActiveId = stableSectionIds[stableSectionIds.length - 1];
|
|
317
|
-
}
|
|
318
|
-
else if (isAtTop && stableSectionIds.length > 0) {
|
|
320
|
+
} else if (isAtTop && stableSectionIds.length > 0) {
|
|
319
321
|
newActiveId = stableSectionIds[0];
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const visibleScores = scores.filter((s) => s.isInViewport);
|
|
322
|
+
} else {
|
|
323
|
+
const visibleScores = scores.filter((s)=>s.isInViewport);
|
|
323
324
|
const candidates = visibleScores.length > 0 ? visibleScores : scores;
|
|
324
|
-
candidates.sort((a, b)
|
|
325
|
+
candidates.sort((a, b)=>b.score - a.score);
|
|
325
326
|
if (candidates.length > 0) {
|
|
326
327
|
const bestCandidate = candidates[0];
|
|
327
|
-
const currentScore = scores.find((s)
|
|
328
|
-
const shouldSwitch = !currentScore ||
|
|
329
|
-
!currentScore.isInViewport ||
|
|
330
|
-
bestCandidate.score > currentScore.score + hysteresisMargin ||
|
|
331
|
-
bestCandidate.id === currentActiveId;
|
|
328
|
+
const currentScore = scores.find((s)=>s.id === currentActiveId);
|
|
329
|
+
const shouldSwitch = !currentScore || !currentScore.isInViewport || bestCandidate.score > currentScore.score + hysteresisMargin || bestCandidate.id === currentActiveId;
|
|
332
330
|
newActiveId = shouldSwitch ? bestCandidate.id : currentActiveId;
|
|
333
331
|
}
|
|
334
332
|
}
|
|
335
333
|
if (newActiveId !== currentActiveId) {
|
|
336
334
|
activeIdRef.current = newActiveId;
|
|
337
335
|
setActiveId(newActiveId);
|
|
338
|
-
callbackRefs.current.onActiveChange
|
|
336
|
+
callbackRefs.current.onActiveChange == null ? void 0 : callbackRefs.current.onActiveChange.call(callbackRefs.current, newActiveId, currentActiveId);
|
|
339
337
|
}
|
|
340
|
-
const currentInViewport = new Set(scores.filter((s)
|
|
338
|
+
const currentInViewport = new Set(scores.filter((s)=>s.isInViewport).map((s)=>s.id));
|
|
341
339
|
const prevInViewport = prevSectionsInViewport.current;
|
|
342
|
-
for (const id of currentInViewport)
|
|
340
|
+
for (const id of currentInViewport){
|
|
343
341
|
if (!prevInViewport.has(id)) {
|
|
344
|
-
callbackRefs.current.onSectionEnter
|
|
342
|
+
callbackRefs.current.onSectionEnter == null ? void 0 : callbackRefs.current.onSectionEnter.call(callbackRefs.current, id);
|
|
345
343
|
}
|
|
346
344
|
}
|
|
347
|
-
for (const id of prevInViewport)
|
|
345
|
+
for (const id of prevInViewport){
|
|
348
346
|
if (!currentInViewport.has(id)) {
|
|
349
|
-
callbackRefs.current.onSectionLeave
|
|
347
|
+
callbackRefs.current.onSectionLeave == null ? void 0 : callbackRefs.current.onSectionLeave.call(callbackRefs.current, id);
|
|
350
348
|
}
|
|
351
349
|
}
|
|
352
350
|
prevSectionsInViewport.current = currentInViewport;
|
|
@@ -358,20 +356,20 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
358
356
|
isScrolling: isScrollingRef.current,
|
|
359
357
|
maxScroll,
|
|
360
358
|
viewportHeight,
|
|
361
|
-
offset: effectiveOffset
|
|
359
|
+
offset: effectiveOffset
|
|
362
360
|
};
|
|
363
361
|
const newSections = {};
|
|
364
|
-
for (const s of scores)
|
|
362
|
+
for (const s of scores){
|
|
365
363
|
newSections[s.id] = {
|
|
366
364
|
bounds: {
|
|
367
365
|
top: s.bounds.top,
|
|
368
366
|
bottom: s.bounds.bottom,
|
|
369
|
-
height: s.bounds.height
|
|
367
|
+
height: s.bounds.height
|
|
370
368
|
},
|
|
371
369
|
visibility: Math.round(s.visibilityRatio * 100) / 100,
|
|
372
370
|
progress: Math.round(s.progress * 100) / 100,
|
|
373
371
|
isInViewport: s.isInViewport,
|
|
374
|
-
isActive: s.id === newActiveId
|
|
372
|
+
isActive: s.id === newActiveId
|
|
375
373
|
};
|
|
376
374
|
}
|
|
377
375
|
setScroll(newScrollState);
|
|
@@ -384,31 +382,37 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
384
382
|
visibilityThreshold,
|
|
385
383
|
hysteresisMargin,
|
|
386
384
|
getSectionBounds,
|
|
387
|
-
containerElement
|
|
385
|
+
containerElement
|
|
388
386
|
]);
|
|
389
387
|
recalculateRef.current = calculateActiveSection;
|
|
390
|
-
useEffect(()
|
|
388
|
+
useEffect(()=>{
|
|
391
389
|
const container = containerElement;
|
|
392
390
|
const scrollTarget = container || window;
|
|
393
|
-
const scheduleCalculate = ()
|
|
391
|
+
const scheduleCalculate = ()=>{
|
|
394
392
|
if (rafId.current) {
|
|
395
393
|
cancelAnimationFrame(rafId.current);
|
|
396
394
|
}
|
|
397
|
-
rafId.current = requestAnimationFrame(()
|
|
395
|
+
rafId.current = requestAnimationFrame(()=>{
|
|
398
396
|
rafId.current = null;
|
|
399
397
|
calculateActiveSection();
|
|
400
398
|
});
|
|
401
399
|
};
|
|
402
|
-
const handleScrollEnd = ()
|
|
400
|
+
const handleScrollEnd = ()=>{
|
|
403
401
|
isScrollingRef.current = false;
|
|
404
|
-
setScroll((prev)
|
|
405
|
-
|
|
402
|
+
setScroll((prev)=>({
|
|
403
|
+
...prev,
|
|
404
|
+
isScrolling: false
|
|
405
|
+
}));
|
|
406
|
+
callbackRefs.current.onScrollEnd == null ? void 0 : callbackRefs.current.onScrollEnd.call(callbackRefs.current);
|
|
406
407
|
};
|
|
407
|
-
const handleScroll = ()
|
|
408
|
+
const handleScroll = ()=>{
|
|
408
409
|
if (!isScrollingRef.current) {
|
|
409
410
|
isScrollingRef.current = true;
|
|
410
|
-
setScroll((prev)
|
|
411
|
-
|
|
411
|
+
setScroll((prev)=>({
|
|
412
|
+
...prev,
|
|
413
|
+
isScrolling: true
|
|
414
|
+
}));
|
|
415
|
+
callbackRefs.current.onScrollStart == null ? void 0 : callbackRefs.current.onScrollStart.call(callbackRefs.current);
|
|
412
416
|
}
|
|
413
417
|
if (scrollIdleTimeoutRef.current) {
|
|
414
418
|
clearTimeout(scrollIdleTimeoutRef.current);
|
|
@@ -424,7 +428,7 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
424
428
|
clearTimeout(throttleTimeoutId.current);
|
|
425
429
|
}
|
|
426
430
|
scheduleCalculate();
|
|
427
|
-
throttleTimeoutId.current = setTimeout(()
|
|
431
|
+
throttleTimeoutId.current = setTimeout(()=>{
|
|
428
432
|
isThrottled.current = false;
|
|
429
433
|
throttleTimeoutId.current = null;
|
|
430
434
|
if (hasPendingScroll.current) {
|
|
@@ -433,16 +437,20 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
433
437
|
}
|
|
434
438
|
}, debounceMs);
|
|
435
439
|
};
|
|
436
|
-
const handleResize = ()
|
|
440
|
+
const handleResize = ()=>{
|
|
437
441
|
scheduleCalculate();
|
|
438
442
|
};
|
|
439
443
|
calculateActiveSection();
|
|
440
|
-
const deferredRecalcId = setTimeout(()
|
|
444
|
+
const deferredRecalcId = setTimeout(()=>{
|
|
441
445
|
calculateActiveSection();
|
|
442
446
|
}, 0);
|
|
443
|
-
scrollTarget.addEventListener("scroll", handleScroll, {
|
|
444
|
-
|
|
445
|
-
|
|
447
|
+
scrollTarget.addEventListener("scroll", handleScroll, {
|
|
448
|
+
passive: true
|
|
449
|
+
});
|
|
450
|
+
window.addEventListener("resize", handleResize, {
|
|
451
|
+
passive: true
|
|
452
|
+
});
|
|
453
|
+
return ()=>{
|
|
446
454
|
clearTimeout(deferredRecalcId);
|
|
447
455
|
scrollTarget.removeEventListener("scroll", handleScroll);
|
|
448
456
|
window.removeEventListener("resize", handleResize);
|
|
@@ -462,18 +470,25 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
462
470
|
clearTimeout(scrollIdleTimeoutRef.current);
|
|
463
471
|
scrollIdleTimeoutRef.current = null;
|
|
464
472
|
}
|
|
465
|
-
scrollCleanupRef.current
|
|
473
|
+
scrollCleanupRef.current == null ? void 0 : scrollCleanupRef.current.call(scrollCleanupRef);
|
|
466
474
|
isThrottled.current = false;
|
|
467
475
|
hasPendingScroll.current = false;
|
|
468
476
|
isProgrammaticScrolling.current = false;
|
|
469
477
|
isScrollingRef.current = false;
|
|
470
478
|
};
|
|
471
|
-
}, [
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
479
|
+
}, [
|
|
480
|
+
calculateActiveSection,
|
|
481
|
+
debounceMs,
|
|
482
|
+
containerElement
|
|
483
|
+
]);
|
|
484
|
+
const activeIndex = useMemo(()=>{
|
|
485
|
+
var _sectionIndexMap_get;
|
|
486
|
+
if (!activeId) return -1;
|
|
487
|
+
return (_sectionIndexMap_get = sectionIndexMap.get(activeId)) != null ? _sectionIndexMap_get : -1;
|
|
488
|
+
}, [
|
|
489
|
+
activeId,
|
|
490
|
+
sectionIndexMap
|
|
491
|
+
]);
|
|
477
492
|
return {
|
|
478
493
|
activeId,
|
|
479
494
|
activeIndex,
|
|
@@ -482,7 +497,8 @@ export function useDomet(sectionIds, containerRef = null, options = {}) {
|
|
|
482
497
|
registerRef,
|
|
483
498
|
scrollToSection,
|
|
484
499
|
sectionProps,
|
|
485
|
-
navProps
|
|
500
|
+
navProps
|
|
486
501
|
};
|
|
487
502
|
}
|
|
488
|
-
|
|
503
|
+
|
|
504
|
+
export { useDomet as default, useDomet };
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
|
|
3
|
+
type SectionBounds = {
|
|
3
4
|
top: number;
|
|
4
5
|
bottom: number;
|
|
5
6
|
height: number;
|
|
6
7
|
};
|
|
7
|
-
|
|
8
|
+
type ScrollState = {
|
|
8
9
|
y: number;
|
|
9
10
|
progress: number;
|
|
10
11
|
direction: "up" | "down" | null;
|
|
@@ -14,15 +15,15 @@ export type ScrollState = {
|
|
|
14
15
|
viewportHeight: number;
|
|
15
16
|
offset: number;
|
|
16
17
|
};
|
|
17
|
-
|
|
18
|
+
type SectionState = {
|
|
18
19
|
bounds: SectionBounds;
|
|
19
20
|
visibility: number;
|
|
20
21
|
progress: number;
|
|
21
22
|
isInViewport: boolean;
|
|
22
23
|
isActive: boolean;
|
|
23
24
|
};
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
type ScrollBehavior = "smooth" | "instant" | "auto";
|
|
26
|
+
type DometOptions = {
|
|
26
27
|
offset?: number;
|
|
27
28
|
offsetRatio?: number;
|
|
28
29
|
debounceMs?: number;
|
|
@@ -35,17 +36,17 @@ export type DometOptions = {
|
|
|
35
36
|
onScrollStart?: () => void;
|
|
36
37
|
onScrollEnd?: () => void;
|
|
37
38
|
};
|
|
38
|
-
|
|
39
|
+
type SectionProps = {
|
|
39
40
|
id: string;
|
|
40
41
|
ref: (el: HTMLElement | null) => void;
|
|
41
42
|
"data-domet": string;
|
|
42
43
|
};
|
|
43
|
-
|
|
44
|
+
type NavProps = {
|
|
44
45
|
onClick: () => void;
|
|
45
46
|
"aria-current": "page" | undefined;
|
|
46
47
|
"data-active": boolean;
|
|
47
48
|
};
|
|
48
|
-
|
|
49
|
+
type UseDometReturn = {
|
|
49
50
|
activeId: string | null;
|
|
50
51
|
activeIndex: number;
|
|
51
52
|
scroll: ScrollState;
|
|
@@ -55,6 +56,7 @@ export type UseDometReturn = {
|
|
|
55
56
|
sectionProps: (id: string) => SectionProps;
|
|
56
57
|
navProps: (id: string) => NavProps;
|
|
57
58
|
};
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
declare function useDomet(sectionIds: string[], containerRef?: RefObject<HTMLElement> | null, options?: DometOptions): UseDometReturn;
|
|
60
|
+
|
|
61
|
+
export { useDomet as default, useDomet };
|
|
62
|
+
export type { DometOptions, NavProps, ScrollBehavior, ScrollState, SectionBounds, SectionProps, SectionState, UseDometReturn };
|