@donotdev/components 0.0.4 → 0.0.6
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/dist/atomic/Button/index.js +3 -3
- package/dist/atomic/CallToAction/index.d.ts +2 -2
- package/dist/atomic/CallToAction/index.d.ts.map +1 -1
- package/dist/atomic/CallToAction/index.js +1 -1
- package/dist/atomic/Card/index.d.ts.map +1 -1
- package/dist/atomic/Card/index.js +1 -1
- package/dist/atomic/List/index.d.ts +1 -1
- package/dist/atomic/List/index.js +1 -1
- package/dist/atomic/Tooltip/index.d.ts +14 -22
- package/dist/atomic/Tooltip/index.d.ts.map +1 -1
- package/dist/atomic/Tooltip/index.js +50 -55
- package/dist/atomic/VideoPlayer/index.d.ts +0 -67
- package/dist/atomic/VideoPlayer/index.d.ts.map +1 -1
- package/dist/atomic/VideoPlayer/index.js +26 -14
- package/dist/hooks/useIntersectionObserver.d.ts +63 -45
- package/dist/hooks/useIntersectionObserver.d.ts.map +1 -1
- package/dist/hooks/useIntersectionObserver.js +192 -49
- package/dist/index.js +4 -4
- package/dist/styles/index.css +360 -30
- package/package.json +1 -1
|
@@ -1,39 +1,92 @@
|
|
|
1
1
|
// packages/components/src/hooks/useIntersectionObserver.ts
|
|
2
2
|
/**
|
|
3
|
-
* @fileoverview Intersection
|
|
4
|
-
* @description
|
|
5
|
-
*
|
|
3
|
+
* @fileoverview useIntersectionObserver Hook - Basic Intersection Observation
|
|
4
|
+
* @description Lightweight hook for basic intersection observation with optimal performance
|
|
5
|
+
* @package @donotdev/components
|
|
6
6
|
* @version 0.0.1
|
|
7
7
|
* @since 0.0.1
|
|
8
8
|
* @author AMBROISE PARK Consulting
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - CSR/SSR safe with proper fallbacks
|
|
12
|
+
* - Intersection Observer API with automatic cleanup
|
|
13
|
+
* - Once-only triggering option
|
|
14
|
+
* - TypeScript support with full type safety
|
|
15
|
+
* - Zero dependencies beyond React and framework utils
|
|
16
|
+
*
|
|
17
|
+
* Performance:
|
|
18
|
+
* - Uses native IntersectionObserver API
|
|
19
|
+
* - React 19 useSyncExternalStore pattern for optimal performance
|
|
20
|
+
* - Prevents tearing in concurrent rendering
|
|
21
|
+
* - Automatic cleanup prevents memory leaks
|
|
22
|
+
* - Minimal state updates with proper change detection
|
|
9
23
|
*/
|
|
10
|
-
import { useRef, useEffect,
|
|
11
|
-
import { observeElement } from '../utils/intersectionObserver';
|
|
24
|
+
import { useRef, useEffect, useSyncExternalStore, useMemo } from 'react';
|
|
12
25
|
/**
|
|
13
|
-
* Hook
|
|
26
|
+
* useIntersectionObserver - Basic Intersection Observation Hook (React 19 Optimized)
|
|
27
|
+
*
|
|
28
|
+
* Lightweight hook for basic intersection observation with optimal performance.
|
|
29
|
+
* Uses React 19's useSyncExternalStore pattern for automatic cleanup, proper
|
|
30
|
+
* SSR handling, and tearing prevention in concurrent rendering.
|
|
31
|
+
*
|
|
32
|
+
* @param options - Configuration options for intersection observation
|
|
33
|
+
* @returns Object containing ref, isIntersecting state, and entry
|
|
34
|
+
*
|
|
35
|
+
* @example Basic intersection detection
|
|
36
|
+
* ```tsx
|
|
37
|
+
* function IntersectionComponent() {
|
|
38
|
+
* const { ref, isIntersecting } = useIntersectionObserver();
|
|
39
|
+
*
|
|
40
|
+
* return (
|
|
41
|
+
* <div ref={ref} className={isIntersecting ? 'visible' : 'hidden'}>
|
|
42
|
+
* Content that appears when intersecting
|
|
43
|
+
* </div>
|
|
44
|
+
* );
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
14
47
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
48
|
+
* @example With custom threshold
|
|
49
|
+
* ```tsx
|
|
50
|
+
* function ThresholdComponent() {
|
|
51
|
+
* const { ref, isIntersecting } = useIntersectionObserver({
|
|
52
|
+
* threshold: 0.5,
|
|
53
|
+
* rootMargin: '50px'
|
|
54
|
+
* });
|
|
55
|
+
*
|
|
56
|
+
* return (
|
|
57
|
+
* <div ref={ref}>
|
|
58
|
+
* {isIntersecting ? '50% visible' : 'Not visible enough'}
|
|
59
|
+
* </div>
|
|
60
|
+
* );
|
|
61
|
+
* }
|
|
62
|
+
* ```
|
|
17
63
|
*
|
|
18
|
-
* @example
|
|
64
|
+
* @example Once-only triggering (React 19 optimized)
|
|
19
65
|
* ```tsx
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* });
|
|
66
|
+
* function OnceComponent() {
|
|
67
|
+
* const { ref, isIntersecting } = useIntersectionObserver({ once: true });
|
|
23
68
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
69
|
+
* return (
|
|
70
|
+
* <div ref={ref}>
|
|
71
|
+
* {isIntersecting && <ExpensiveComponent />}
|
|
72
|
+
* </div>
|
|
73
|
+
* );
|
|
74
|
+
* }
|
|
29
75
|
* ```
|
|
30
76
|
*
|
|
31
|
-
* @example
|
|
77
|
+
* @example SSR-safe implementation
|
|
32
78
|
* ```tsx
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
79
|
+
* function SSRComponent() {
|
|
80
|
+
* const { ref, isIntersecting } = useIntersectionObserver({
|
|
81
|
+
* fallbackIntersecting: true // Shows content during SSR
|
|
82
|
+
* });
|
|
83
|
+
*
|
|
84
|
+
* return (
|
|
85
|
+
* <div ref={ref}>
|
|
86
|
+
* {isIntersecting ? 'Client-side' : 'SSR fallback'}
|
|
87
|
+
* </div>
|
|
88
|
+
* );
|
|
89
|
+
* }
|
|
37
90
|
* ```
|
|
38
91
|
*
|
|
39
92
|
* @version 0.0.1
|
|
@@ -41,38 +94,128 @@ import { observeElement } from '../utils/intersectionObserver';
|
|
|
41
94
|
* @author AMBROISE PARK Consulting
|
|
42
95
|
*/
|
|
43
96
|
export function useIntersectionObserver(options = {}) {
|
|
44
|
-
const { threshold = 0
|
|
97
|
+
const { threshold = 0, root = null, rootMargin = '0px', once = false, fallbackIntersecting = true, } = options;
|
|
45
98
|
const ref = useRef(null);
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
// Intersection observer callback
|
|
50
|
-
const handleIntersection = useCallback((intersectionEntry) => {
|
|
51
|
-
const isIntersectingNow = intersectionEntry.isIntersecting;
|
|
52
|
-
if (isIntersectingNow && (!hasTriggered || !once)) {
|
|
53
|
-
setIsIntersecting(true);
|
|
54
|
-
setHasTriggered(true);
|
|
55
|
-
}
|
|
56
|
-
else if (!isIntersectingNow && !once) {
|
|
57
|
-
setIsIntersecting(false);
|
|
58
|
-
}
|
|
59
|
-
setEntry(intersectionEntry);
|
|
60
|
-
}, [once, hasTriggered]);
|
|
61
|
-
// Set up intersection observer
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
const element = ref.current;
|
|
64
|
-
if (!element)
|
|
65
|
-
return;
|
|
66
|
-
const cleanup = observeElement(element, handleIntersection, {
|
|
99
|
+
// Create intersection observer using React 19 patterns
|
|
100
|
+
const observer = useMemo(() => {
|
|
101
|
+
return createIntersectionObserver({
|
|
67
102
|
threshold,
|
|
103
|
+
root,
|
|
68
104
|
rootMargin,
|
|
105
|
+
once,
|
|
106
|
+
fallbackIntersecting,
|
|
69
107
|
});
|
|
70
|
-
|
|
71
|
-
|
|
108
|
+
}, [threshold, root, rootMargin, once, fallbackIntersecting]);
|
|
109
|
+
// Use useSyncExternalStore for optimal performance and proper tearing prevention
|
|
110
|
+
const state = useSyncExternalStore(observer.subscribe, observer.getSnapshot, observer.getServerSnapshot);
|
|
111
|
+
// Update observer when ref element changes
|
|
112
|
+
// Empty deps intentional: observer is stable from useMemo, ref updates are tracked internally
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (ref.current) {
|
|
115
|
+
observer.setElement(ref.current);
|
|
116
|
+
}
|
|
117
|
+
return () => observer.setElement(null);
|
|
118
|
+
}, [observer]);
|
|
72
119
|
return {
|
|
73
120
|
ref,
|
|
74
|
-
isIntersecting,
|
|
75
|
-
hasTriggered,
|
|
76
|
-
entry,
|
|
121
|
+
isIntersecting: state.isIntersecting,
|
|
122
|
+
hasTriggered: state.hasTriggered,
|
|
123
|
+
entry: state.entry,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Create an intersection observer with React 19 patterns
|
|
128
|
+
* Follows same pattern as createLocalStorageObserver for consistency
|
|
129
|
+
*/
|
|
130
|
+
function createIntersectionObserver(options) {
|
|
131
|
+
const { threshold, root, rootMargin, once, fallbackIntersecting } = options;
|
|
132
|
+
let currentState = {
|
|
133
|
+
isIntersecting: fallbackIntersecting,
|
|
134
|
+
hasTriggered: false,
|
|
135
|
+
entry: null,
|
|
136
|
+
};
|
|
137
|
+
let listeners = new Set();
|
|
138
|
+
let intersectionObserver = null;
|
|
139
|
+
let currentElement = null;
|
|
140
|
+
let hasTriggered = false;
|
|
141
|
+
const handleIntersection = (entries) => {
|
|
142
|
+
const [entry] = entries;
|
|
143
|
+
if (!entry)
|
|
144
|
+
return;
|
|
145
|
+
const isIntersecting = entry.isIntersecting;
|
|
146
|
+
// Handle once option - disconnect after first intersection
|
|
147
|
+
if (once && isIntersecting && !hasTriggered) {
|
|
148
|
+
hasTriggered = true;
|
|
149
|
+
if (intersectionObserver) {
|
|
150
|
+
intersectionObserver.disconnect();
|
|
151
|
+
intersectionObserver = null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Update hasTriggered if intersecting for first time
|
|
155
|
+
if (isIntersecting && !hasTriggered) {
|
|
156
|
+
hasTriggered = true;
|
|
157
|
+
}
|
|
158
|
+
const newState = {
|
|
159
|
+
isIntersecting,
|
|
160
|
+
hasTriggered,
|
|
161
|
+
entry,
|
|
162
|
+
};
|
|
163
|
+
// Only update if state actually changed
|
|
164
|
+
if (newState.isIntersecting !== currentState.isIntersecting ||
|
|
165
|
+
newState.hasTriggered !== currentState.hasTriggered ||
|
|
166
|
+
newState.entry !== currentState.entry) {
|
|
167
|
+
currentState = newState;
|
|
168
|
+
listeners.forEach((listener) => listener());
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const getSnapshot = () => {
|
|
172
|
+
return currentState;
|
|
173
|
+
};
|
|
174
|
+
const subscribe = (callback) => {
|
|
175
|
+
listeners.add(callback);
|
|
176
|
+
// If element is already set and we're on client, start observing
|
|
177
|
+
if (currentElement && !intersectionObserver && typeof window !== 'undefined') {
|
|
178
|
+
intersectionObserver = new IntersectionObserver(handleIntersection, {
|
|
179
|
+
threshold,
|
|
180
|
+
root,
|
|
181
|
+
rootMargin,
|
|
182
|
+
});
|
|
183
|
+
intersectionObserver.observe(currentElement);
|
|
184
|
+
}
|
|
185
|
+
return () => {
|
|
186
|
+
listeners.delete(callback);
|
|
187
|
+
// Cleanup observer when last listener is removed
|
|
188
|
+
if (listeners.size === 0 && intersectionObserver) {
|
|
189
|
+
intersectionObserver.disconnect();
|
|
190
|
+
intersectionObserver = null;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
const setElement = (element) => {
|
|
195
|
+
// Disconnect existing observer
|
|
196
|
+
if (intersectionObserver) {
|
|
197
|
+
intersectionObserver.disconnect();
|
|
198
|
+
intersectionObserver = null;
|
|
199
|
+
}
|
|
200
|
+
currentElement = element;
|
|
201
|
+
// Observe new element if we have listeners and we're on client
|
|
202
|
+
if (element && listeners.size > 0 && typeof window !== 'undefined') {
|
|
203
|
+
intersectionObserver = new IntersectionObserver(handleIntersection, {
|
|
204
|
+
threshold,
|
|
205
|
+
root,
|
|
206
|
+
rootMargin,
|
|
207
|
+
});
|
|
208
|
+
intersectionObserver.observe(element);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
return {
|
|
212
|
+
subscribe,
|
|
213
|
+
getSnapshot,
|
|
214
|
+
getServerSnapshot: () => ({
|
|
215
|
+
isIntersecting: fallbackIntersecting,
|
|
216
|
+
hasTriggered: false,
|
|
217
|
+
entry: null,
|
|
218
|
+
}),
|
|
219
|
+
setElement,
|
|
77
220
|
};
|
|
78
221
|
}
|