@versini/ui-hooks 5.3.2 → 6.0.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/README.md +35 -9
- package/dist/__tests__/__mocks__/ResizeObserver.d.ts +21 -0
- package/dist/__tests__/useClickOutside.test.d.ts +1 -0
- package/dist/__tests__/useHaptic.test.d.ts +1 -0
- package/dist/__tests__/useHotkeys.test.d.ts +1 -0
- package/dist/__tests__/useInterval.test.d.ts +1 -0
- package/dist/__tests__/useIsMounted.test.d.ts +1 -0
- package/dist/__tests__/useLocalStorage.test.d.ts +1 -0
- package/dist/__tests__/useMergeRefs.test.d.ts +1 -0
- package/dist/__tests__/useResizeObserver.test.d.ts +1 -0
- package/dist/__tests__/useUncontrolled.test.d.ts +1 -0
- package/dist/__tests__/useUniqueId.test.d.ts +1 -0
- package/dist/__tests__/useViewportSize.test.d.ts +1 -0
- package/dist/__tests__/utilities.test.d.ts +1 -0
- package/dist/useClickOutside/useClickOutside.d.ts +17 -0
- package/dist/useClickOutside/useClickOutside.js +68 -0
- package/dist/useHaptic/useHaptic.d.ts +24 -0
- package/dist/useHaptic/useHaptic.js +200 -0
- package/dist/useHotkeys/useHotkeys.d.ts +10 -0
- package/dist/useHotkeys/useHotkeys.js +65 -0
- package/dist/useHotkeys/utilities.d.ts +19 -0
- package/dist/useHotkeys/utilities.js +87 -0
- package/dist/useInViewport/useInViewport.d.ts +10 -0
- package/dist/useInViewport/useInViewport.js +52 -0
- package/dist/useInterval/useInterval.d.ts +21 -0
- package/dist/useInterval/useInterval.js +75 -0
- package/dist/useIsMounted/useIsMounted.d.ts +13 -0
- package/dist/useIsMounted/useIsMounted.js +46 -0
- package/dist/useLocalStorage/useLocalStorage.d.ts +82 -0
- package/dist/useLocalStorage/useLocalStorage.js +236 -0
- package/dist/useMergeRefs/useMergeRefs.d.ts +20 -0
- package/dist/useMergeRefs/useMergeRefs.js +62 -0
- package/dist/useResizeObserver/useResizeObserver.d.ts +17 -0
- package/dist/useResizeObserver/useResizeObserver.js +94 -0
- package/dist/useUncontrolled/useUncontrolled.d.ts +14 -0
- package/dist/useUncontrolled/useUncontrolled.js +76 -0
- package/dist/useUniqueId/useUniqueId.d.ts +36 -0
- package/dist/useUniqueId/useUniqueId.js +41 -0
- package/dist/useViewportSize/useViewportSize.d.ts +13 -0
- package/dist/useViewportSize/useViewportSize.js +60 -0
- package/dist/useVisualViewportSize/useVisualViewportSize.d.ts +14 -0
- package/dist/useVisualViewportSize/useVisualViewportSize.js +70 -0
- package/package.json +56 -4
- package/dist/index.d.ts +0 -316
- package/dist/index.js +0 -958
package/dist/index.js
DELETED
|
@@ -1,958 +0,0 @@
|
|
|
1
|
-
/*!
|
|
2
|
-
@versini/ui-hooks v5.3.2
|
|
3
|
-
© 2025 gizmette.com
|
|
4
|
-
*/
|
|
5
|
-
try {
|
|
6
|
-
if (!window.__VERSINI_UI_HOOKS__) {
|
|
7
|
-
window.__VERSINI_UI_HOOKS__ = {
|
|
8
|
-
version: "5.3.2",
|
|
9
|
-
buildTime: "12/16/2025 01:48 PM EST",
|
|
10
|
-
homepage: "https://www.npmjs.com/package/@versini/ui-hooks",
|
|
11
|
-
license: "MIT",
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
} catch (error) {
|
|
15
|
-
// nothing to declare officer
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
import { useCallback, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
19
|
-
|
|
20
|
-
;// CONCATENATED MODULE: external "react"
|
|
21
|
-
|
|
22
|
-
;// CONCATENATED MODULE: ./src/hooks/useClickOutside.tsx
|
|
23
|
-
|
|
24
|
-
const DEFAULT_EVENTS = [
|
|
25
|
-
"mousedown",
|
|
26
|
-
"touchstart"
|
|
27
|
-
];
|
|
28
|
-
/**
|
|
29
|
-
* Custom hooks that triggers a callback when a click is detected
|
|
30
|
-
* outside the target element.
|
|
31
|
-
*
|
|
32
|
-
* @param handler - Function to be called when clicked outside
|
|
33
|
-
* @param events - Array of events to listen to
|
|
34
|
-
* @param nodes - Array of nodes to check against
|
|
35
|
-
* @returns Ref to be attached to the target element
|
|
36
|
-
*
|
|
37
|
-
* @example
|
|
38
|
-
* const ref = useClickOutside(() => {
|
|
39
|
-
* console.log('Clicked outside!');
|
|
40
|
-
* });
|
|
41
|
-
* <div ref={ref}>Click me!</div>
|
|
42
|
-
*
|
|
43
|
-
*/ function useClickOutside(handler, events, nodes) {
|
|
44
|
-
const ref = useRef(null);
|
|
45
|
-
useEffect(()=>{
|
|
46
|
-
const listener = (event)=>{
|
|
47
|
-
/* v8 ignore next 1 */ const target = event ? event.target : undefined;
|
|
48
|
-
if (Array.isArray(nodes)) {
|
|
49
|
-
/* v8 ignore next 2 */ const shouldIgnore = !document.body.contains(target) && target.tagName !== "HTML";
|
|
50
|
-
const shouldTrigger = nodes.every((node)=>!!node && !event.composedPath().includes(node));
|
|
51
|
-
shouldTrigger && !shouldIgnore && handler();
|
|
52
|
-
} else if (ref.current && !ref.current.contains(target)) {
|
|
53
|
-
handler();
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
(events || DEFAULT_EVENTS).forEach((fn)=>document.addEventListener(fn, listener));
|
|
57
|
-
return ()=>{
|
|
58
|
-
(events || DEFAULT_EVENTS).forEach((fn)=>document.removeEventListener(fn, listener));
|
|
59
|
-
};
|
|
60
|
-
}, [
|
|
61
|
-
handler,
|
|
62
|
-
nodes,
|
|
63
|
-
events
|
|
64
|
-
]);
|
|
65
|
-
return ref;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
;// CONCATENATED MODULE: ./src/hooks/useHaptic.ts
|
|
69
|
-
|
|
70
|
-
const HAPTIC_DURATION_MS = 50;
|
|
71
|
-
const HAPTIC_INTERVAL_MS = 120;
|
|
72
|
-
/**
|
|
73
|
-
* Singleton state for the haptic element shared across all hook instances. This
|
|
74
|
-
* ensures only one DOM element is created regardless of how many components use
|
|
75
|
-
* the hook.
|
|
76
|
-
*/ let sharedLabelElement = null;
|
|
77
|
-
let refCount = 0;
|
|
78
|
-
/**
|
|
79
|
-
* Creates the shared haptic element if it doesn't exist. This function is
|
|
80
|
-
* idempotent - calling it multiple times is safe. Checks DOM directly to handle
|
|
81
|
-
* React Strict Mode double-mounting.
|
|
82
|
-
*/ const ensureHapticElement = ()=>{
|
|
83
|
-
/* c8 ignore next 3 */ if (typeof window === "undefined") {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
// First check: do we have valid references that exist in the DOM?
|
|
87
|
-
if (sharedLabelElement && document.body.contains(sharedLabelElement)) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Second check: does an element already exist in the DOM from a previous
|
|
92
|
-
* mount?
|
|
93
|
-
*/ const existingLabel = document.querySelector('label[data-haptic-singleton="true"]');
|
|
94
|
-
if (existingLabel) {
|
|
95
|
-
sharedLabelElement = existingLabel;
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
// Clear stale references.
|
|
99
|
-
sharedLabelElement = null;
|
|
100
|
-
// Create new elements.
|
|
101
|
-
const input = document.createElement("input");
|
|
102
|
-
input.type = "checkbox";
|
|
103
|
-
input.setAttribute("switch", "");
|
|
104
|
-
input.style.display = "none";
|
|
105
|
-
input.setAttribute("aria-hidden", "true");
|
|
106
|
-
input.dataset.hapticSingleton = "true";
|
|
107
|
-
const label = document.createElement("label");
|
|
108
|
-
label.style.display = "none";
|
|
109
|
-
label.setAttribute("aria-hidden", "true");
|
|
110
|
-
label.dataset.hapticSingleton = "true";
|
|
111
|
-
label.appendChild(input);
|
|
112
|
-
document.body.appendChild(label);
|
|
113
|
-
sharedLabelElement = label;
|
|
114
|
-
};
|
|
115
|
-
/**
|
|
116
|
-
* Removes the shared haptic element from the DOM and clears references. Only
|
|
117
|
-
* called when the last component using the hook unmounts.
|
|
118
|
-
*/ const cleanupHapticElement = ()=>{
|
|
119
|
-
if (sharedLabelElement && document.body && document.body.contains(sharedLabelElement)) {
|
|
120
|
-
document.body.removeChild(sharedLabelElement);
|
|
121
|
-
}
|
|
122
|
-
sharedLabelElement = null;
|
|
123
|
-
};
|
|
124
|
-
/**
|
|
125
|
-
* Custom hook providing imperative haptic feedback for mobile devices. Uses
|
|
126
|
-
* navigator.vibrate when available, falls back to iOS switch element trick for
|
|
127
|
-
* Safari on iOS devices that don't support the Vibration API.
|
|
128
|
-
*
|
|
129
|
-
* This hook uses a singleton pattern - only one haptic element is created in
|
|
130
|
-
* the DOM regardless of how many components use this hook. The element is
|
|
131
|
-
* automatically cleaned up when the last component unmounts.
|
|
132
|
-
*
|
|
133
|
-
* @example
|
|
134
|
-
* ```tsx
|
|
135
|
-
* const { haptic } = useHaptic();
|
|
136
|
-
*
|
|
137
|
-
* // Trigger a single haptic pulse
|
|
138
|
-
* haptic(1);
|
|
139
|
-
*
|
|
140
|
-
* // Trigger two rapid haptic pulses
|
|
141
|
-
* haptic(2);
|
|
142
|
-
* ```
|
|
143
|
-
*
|
|
144
|
-
*/ const useHaptic = ()=>{
|
|
145
|
-
const timeoutsRef = useRef(new Set());
|
|
146
|
-
useEffect(()=>{
|
|
147
|
-
// Increment reference count and create element if needed.
|
|
148
|
-
refCount++;
|
|
149
|
-
try {
|
|
150
|
-
ensureHapticElement();
|
|
151
|
-
} catch (error) {
|
|
152
|
-
/**
|
|
153
|
-
* If element creation fails, we need to decrement refCount immediately since
|
|
154
|
-
* the cleanup function won't be registered. This prevents refCount from
|
|
155
|
-
* getting out of sync.
|
|
156
|
-
*/ refCount--;
|
|
157
|
-
throw error;
|
|
158
|
-
}
|
|
159
|
-
return ()=>{
|
|
160
|
-
/**
|
|
161
|
-
* Cleanup function: clear all pending timeouts to prevent haptics from
|
|
162
|
-
* firing after unmount, and decrement the reference count. Only remove the
|
|
163
|
-
* DOM element when the last component unmounts.
|
|
164
|
-
*/ for (const timeoutId of timeoutsRef.current){
|
|
165
|
-
clearTimeout(timeoutId);
|
|
166
|
-
}
|
|
167
|
-
timeoutsRef.current.clear();
|
|
168
|
-
refCount--;
|
|
169
|
-
if (refCount === 0) {
|
|
170
|
-
cleanupHapticElement();
|
|
171
|
-
}
|
|
172
|
-
};
|
|
173
|
-
}, []);
|
|
174
|
-
/**
|
|
175
|
-
* Triggers a single haptic pulse using either the Vibration API or the switch
|
|
176
|
-
* element trick.
|
|
177
|
-
*/ const triggerSingleHaptic = useCallback(()=>{
|
|
178
|
-
try {
|
|
179
|
-
if (navigator?.vibrate) {
|
|
180
|
-
navigator.vibrate(HAPTIC_DURATION_MS);
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
sharedLabelElement?.click();
|
|
184
|
-
} catch {
|
|
185
|
-
// Silently fail if haptics are not supported.
|
|
186
|
-
}
|
|
187
|
-
}, []);
|
|
188
|
-
/**
|
|
189
|
-
* Triggers haptic feedback with the specified number of pulses.
|
|
190
|
-
*
|
|
191
|
-
* @param count - Number of haptic pulses to trigger (default: 1). For count > 1,
|
|
192
|
-
* pulses are triggered in rapid succession with a 120ms interval between each
|
|
193
|
-
* pulse.
|
|
194
|
-
*
|
|
195
|
-
* @example
|
|
196
|
-
* ```tsx
|
|
197
|
-
* const { haptic } = useHaptic();
|
|
198
|
-
*
|
|
199
|
-
* // Single haptic
|
|
200
|
-
* haptic(1);
|
|
201
|
-
*
|
|
202
|
-
* // Two rapid haptics
|
|
203
|
-
* haptic(2);
|
|
204
|
-
*
|
|
205
|
-
* // Three rapid haptics
|
|
206
|
-
* haptic(3);
|
|
207
|
-
* ```
|
|
208
|
-
*
|
|
209
|
-
*/ const haptic = useCallback((count = 1)=>{
|
|
210
|
-
/* c8 ignore next 3 */ if (typeof window === "undefined") {
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
if (count < 1) {
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
if (navigator?.vibrate && count > 1) {
|
|
217
|
-
/**
|
|
218
|
-
* For multi-haptic patterns with navigator.vibrate, create an array pattern
|
|
219
|
-
* like [50, 120, 50, 120, 50] for better timing control.
|
|
220
|
-
*/ const pattern = [];
|
|
221
|
-
for(let i = 0; i < count; i++){
|
|
222
|
-
pattern.push(HAPTIC_DURATION_MS);
|
|
223
|
-
if (i < count - 1) {
|
|
224
|
-
pattern.push(HAPTIC_INTERVAL_MS - HAPTIC_DURATION_MS);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
navigator.vibrate(pattern);
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
// For switch element or single vibration, trigger sequentially.
|
|
231
|
-
for(let i = 0; i < count; i++){
|
|
232
|
-
const timeoutId = setTimeout(()=>{
|
|
233
|
-
triggerSingleHaptic();
|
|
234
|
-
timeoutsRef.current.delete(timeoutId);
|
|
235
|
-
}, i * HAPTIC_INTERVAL_MS);
|
|
236
|
-
timeoutsRef.current.add(timeoutId);
|
|
237
|
-
}
|
|
238
|
-
}, [
|
|
239
|
-
triggerSingleHaptic
|
|
240
|
-
]);
|
|
241
|
-
return {
|
|
242
|
-
haptic
|
|
243
|
-
};
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
;// CONCATENATED MODULE: ./src/hooks/utilities.ts
|
|
247
|
-
function parseHotkey(hotkey) {
|
|
248
|
-
const keys = hotkey.toLowerCase().split("+").map((part)=>part.trim());
|
|
249
|
-
const modifiers = {
|
|
250
|
-
alt: keys.includes("alt"),
|
|
251
|
-
ctrl: keys.includes("ctrl"),
|
|
252
|
-
meta: keys.includes("meta"),
|
|
253
|
-
mod: keys.includes("mod"),
|
|
254
|
-
shift: keys.includes("shift")
|
|
255
|
-
};
|
|
256
|
-
const reservedKeys = [
|
|
257
|
-
"alt",
|
|
258
|
-
"ctrl",
|
|
259
|
-
"meta",
|
|
260
|
-
"shift",
|
|
261
|
-
"mod"
|
|
262
|
-
];
|
|
263
|
-
const freeKey = keys.find((key)=>!reservedKeys.includes(key));
|
|
264
|
-
return {
|
|
265
|
-
...modifiers,
|
|
266
|
-
key: freeKey
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
function isExactHotkey(hotkey, event) {
|
|
270
|
-
const { alt, ctrl, meta, mod, shift, key } = hotkey;
|
|
271
|
-
const { altKey, ctrlKey, metaKey, shiftKey, key: pressedKey } = event;
|
|
272
|
-
if (alt !== altKey) {
|
|
273
|
-
return false;
|
|
274
|
-
}
|
|
275
|
-
if (mod) {
|
|
276
|
-
if (!ctrlKey && !metaKey) {
|
|
277
|
-
return false;
|
|
278
|
-
}
|
|
279
|
-
} else {
|
|
280
|
-
if (ctrl !== ctrlKey) {
|
|
281
|
-
return false;
|
|
282
|
-
}
|
|
283
|
-
if (meta !== metaKey) {
|
|
284
|
-
return false;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
if (shift !== shiftKey) {
|
|
288
|
-
return false;
|
|
289
|
-
}
|
|
290
|
-
if (key && (pressedKey.toLowerCase() === key.toLowerCase() || event.code.replace("Key", "").toLowerCase() === key.toLowerCase())) {
|
|
291
|
-
return true;
|
|
292
|
-
}
|
|
293
|
-
return false;
|
|
294
|
-
}
|
|
295
|
-
function getHotkeyMatcher(hotkey) {
|
|
296
|
-
return (event)=>isExactHotkey(parseHotkey(hotkey), event);
|
|
297
|
-
}
|
|
298
|
-
function getHotkeyHandler(hotkeys) {
|
|
299
|
-
return (event)=>{
|
|
300
|
-
const _event = "nativeEvent" in event ? event.nativeEvent : event;
|
|
301
|
-
hotkeys.forEach(([hotkey, handler, options = {
|
|
302
|
-
preventDefault: true
|
|
303
|
-
}])=>{
|
|
304
|
-
if (getHotkeyMatcher(hotkey)(_event)) {
|
|
305
|
-
if (options.preventDefault) {
|
|
306
|
-
event.preventDefault();
|
|
307
|
-
}
|
|
308
|
-
handler(_event);
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
;// CONCATENATED MODULE: ./src/hooks/useHotkeys.ts
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
function shouldFireEvent(event, tagsToIgnore, triggerOnContentEditable = false) {
|
|
319
|
-
if (event.target instanceof HTMLElement) {
|
|
320
|
-
if (triggerOnContentEditable) {
|
|
321
|
-
return !tagsToIgnore.includes(event.target.tagName);
|
|
322
|
-
}
|
|
323
|
-
return !event.target.isContentEditable && !tagsToIgnore.includes(event.target.tagName);
|
|
324
|
-
}
|
|
325
|
-
return true;
|
|
326
|
-
}
|
|
327
|
-
function useHotkeys(hotkeys, tagsToIgnore = [
|
|
328
|
-
"INPUT",
|
|
329
|
-
"TEXTAREA",
|
|
330
|
-
"SELECT"
|
|
331
|
-
], triggerOnContentEditable = false) {
|
|
332
|
-
useEffect(()=>{
|
|
333
|
-
const keydownListener = (event)=>{
|
|
334
|
-
hotkeys.forEach(([hotkey, handler, options = {
|
|
335
|
-
preventDefault: true
|
|
336
|
-
}])=>{
|
|
337
|
-
if (getHotkeyMatcher(hotkey)(event) && shouldFireEvent(event, tagsToIgnore, triggerOnContentEditable)) {
|
|
338
|
-
if (options.preventDefault) {
|
|
339
|
-
event.preventDefault();
|
|
340
|
-
}
|
|
341
|
-
handler(event);
|
|
342
|
-
}
|
|
343
|
-
});
|
|
344
|
-
};
|
|
345
|
-
document.documentElement.addEventListener("keydown", keydownListener);
|
|
346
|
-
return ()=>document.documentElement.removeEventListener("keydown", keydownListener);
|
|
347
|
-
}, [
|
|
348
|
-
hotkeys,
|
|
349
|
-
tagsToIgnore,
|
|
350
|
-
triggerOnContentEditable
|
|
351
|
-
]);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
;// CONCATENATED MODULE: ./src/hooks/useInterval.ts
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Custom hook to call a function within a given interval.
|
|
358
|
-
*
|
|
359
|
-
* @param fn Callback function to be executed at each interval
|
|
360
|
-
* @param interval Interval time in milliseconds
|
|
361
|
-
* @returns An object containing start, stop, and active state
|
|
362
|
-
*
|
|
363
|
-
* @example
|
|
364
|
-
* const { start, stop, active } = useInterval(() => {
|
|
365
|
-
* console.log("Interval executed");
|
|
366
|
-
* }, 1000);
|
|
367
|
-
* start(); // To start the interval
|
|
368
|
-
* stop(); // To stop the interval
|
|
369
|
-
* console.log(active); // To check if the interval is active
|
|
370
|
-
*
|
|
371
|
-
*/ function useInterval(fn, interval) {
|
|
372
|
-
const [active, setActive] = useState(false);
|
|
373
|
-
const intervalRef = useRef(null);
|
|
374
|
-
const fnRef = useRef(null);
|
|
375
|
-
const start = useCallback(()=>{
|
|
376
|
-
setActive((old)=>{
|
|
377
|
-
if (!old && (!intervalRef.current || intervalRef.current === -1)) {
|
|
378
|
-
intervalRef.current = window.setInterval(fnRef.current, interval);
|
|
379
|
-
}
|
|
380
|
-
return true;
|
|
381
|
-
});
|
|
382
|
-
}, [
|
|
383
|
-
interval
|
|
384
|
-
]);
|
|
385
|
-
const stop = useCallback(()=>{
|
|
386
|
-
setActive(false);
|
|
387
|
-
window.clearInterval(intervalRef.current || -1);
|
|
388
|
-
intervalRef.current = -1;
|
|
389
|
-
}, []);
|
|
390
|
-
useEffect(()=>{
|
|
391
|
-
fnRef.current = fn;
|
|
392
|
-
active && start();
|
|
393
|
-
return stop;
|
|
394
|
-
}, [
|
|
395
|
-
fn,
|
|
396
|
-
active,
|
|
397
|
-
start,
|
|
398
|
-
stop
|
|
399
|
-
]);
|
|
400
|
-
return {
|
|
401
|
-
start,
|
|
402
|
-
stop,
|
|
403
|
-
active
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
;// CONCATENATED MODULE: ./src/hooks/useInViewport.ts
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Hook that checks if an element is visible in the viewport.
|
|
411
|
-
* @returns
|
|
412
|
-
* ref: React ref object to attach to the element you want to monitor.
|
|
413
|
-
* inViewport: Boolean indicating if the element is in the viewport.
|
|
414
|
-
*/ /* v8 ignore next 24 */ function useInViewport() {
|
|
415
|
-
const observer = useRef(null);
|
|
416
|
-
const [inViewport, setInViewport] = useState(false);
|
|
417
|
-
const ref = useCallback((node)=>{
|
|
418
|
-
if (typeof IntersectionObserver !== "undefined") {
|
|
419
|
-
if (node && !observer.current) {
|
|
420
|
-
observer.current = new IntersectionObserver((entries)=>setInViewport(entries.some((entry)=>entry.isIntersecting)));
|
|
421
|
-
} else {
|
|
422
|
-
observer.current?.disconnect();
|
|
423
|
-
}
|
|
424
|
-
if (node) {
|
|
425
|
-
observer.current?.observe(node);
|
|
426
|
-
} else {
|
|
427
|
-
setInViewport(false);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
}, []);
|
|
431
|
-
return {
|
|
432
|
-
ref,
|
|
433
|
-
inViewport
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
;// CONCATENATED MODULE: ./src/hooks/useIsMounted.ts
|
|
438
|
-
|
|
439
|
-
/**
|
|
440
|
-
* Custom hook that returns a function indicating whether the component
|
|
441
|
-
* is mounted or not.
|
|
442
|
-
*
|
|
443
|
-
* @returns A function that returns a boolean value indicating whether
|
|
444
|
-
* the component is mounted.
|
|
445
|
-
*
|
|
446
|
-
* @example
|
|
447
|
-
* const isMounted = useIsMounted();
|
|
448
|
-
* console.log(isMounted()); // true
|
|
449
|
-
*
|
|
450
|
-
*/ function useIsMounted() {
|
|
451
|
-
const isMounted = useRef(false);
|
|
452
|
-
useEffect(()=>{
|
|
453
|
-
isMounted.current = true;
|
|
454
|
-
return ()=>{
|
|
455
|
-
isMounted.current = false;
|
|
456
|
-
};
|
|
457
|
-
}, []);
|
|
458
|
-
return useCallback(()=>isMounted.current, []);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
;// CONCATENATED MODULE: ./src/hooks/useLocalStorage.ts
|
|
462
|
-
|
|
463
|
-
/**
|
|
464
|
-
* Dispatches a custom storage event to notify other parts of the application
|
|
465
|
-
* about localStorage changes. This is necessary because the native storage
|
|
466
|
-
* event only fires in other tabs/windows, not in the current one. By manually
|
|
467
|
-
* dispatching the event, we ensure that all components using the same key stay
|
|
468
|
-
* synchronized.
|
|
469
|
-
*
|
|
470
|
-
* @param key - The localStorage key that was modified
|
|
471
|
-
* @param newValue - The new value (stringified) or null if the item was removed
|
|
472
|
-
*
|
|
473
|
-
*/ function dispatchStorageEvent(key, newValue) {
|
|
474
|
-
window.dispatchEvent(new StorageEvent("storage", {
|
|
475
|
-
key,
|
|
476
|
-
newValue
|
|
477
|
-
}));
|
|
478
|
-
}
|
|
479
|
-
/**
|
|
480
|
-
* Sets an item in localStorage and dispatches a storage event. Handles both
|
|
481
|
-
* direct values and function updaters (similar to React's setState). Values are
|
|
482
|
-
* automatically serialized to JSON before storage.
|
|
483
|
-
*
|
|
484
|
-
* @param key - The localStorage key to set
|
|
485
|
-
* @param value - The value to store, or a function that returns the value to store
|
|
486
|
-
*
|
|
487
|
-
*/ const setLocalStorageItem = (key, value)=>{
|
|
488
|
-
/**
|
|
489
|
-
* If value is a function, call it to get the actual value (supports functional
|
|
490
|
-
* updates).
|
|
491
|
-
*/ const stringifiedValue = JSON.stringify(typeof value === "function" ? value() : value);
|
|
492
|
-
window.localStorage.setItem(key, stringifiedValue);
|
|
493
|
-
// Dispatch event to notify other components in the same window/tab.
|
|
494
|
-
dispatchStorageEvent(key, stringifiedValue);
|
|
495
|
-
};
|
|
496
|
-
/**
|
|
497
|
-
* Removes an item from localStorage and dispatches a storage event with null
|
|
498
|
-
* value. This ensures all components using this key are notified of the
|
|
499
|
-
* removal.
|
|
500
|
-
*
|
|
501
|
-
* @param key - The localStorage key to remove
|
|
502
|
-
*
|
|
503
|
-
*/ const removeLocalStorageItem = (key)=>{
|
|
504
|
-
window.localStorage.removeItem(key);
|
|
505
|
-
// Dispatch event with null to signal removal.
|
|
506
|
-
dispatchStorageEvent(key, null);
|
|
507
|
-
};
|
|
508
|
-
/**
|
|
509
|
-
* Retrieves an item from localStorage. Returns the raw stringified value or
|
|
510
|
-
* null if the key doesn't exist.
|
|
511
|
-
*
|
|
512
|
-
* @param key - The localStorage key to retrieve
|
|
513
|
-
* @returns The stored string value or null if not found
|
|
514
|
-
*
|
|
515
|
-
*/ const getLocalStorageItem = (key)=>{
|
|
516
|
-
return window.localStorage.getItem(key);
|
|
517
|
-
};
|
|
518
|
-
/**
|
|
519
|
-
* Creates a subscription to localStorage changes for use with React's
|
|
520
|
-
* useSyncExternalStore. This function is called by React to set up and tear
|
|
521
|
-
* down event listeners. It listens to both native storage events (from other
|
|
522
|
-
* tabs) and custom dispatched events (from this tab).
|
|
523
|
-
*
|
|
524
|
-
* @param callback - The callback function to invoke when storage changes occur
|
|
525
|
-
* @returns A cleanup function that removes the event listener
|
|
526
|
-
*
|
|
527
|
-
*/ const useLocalStorageSubscribe = (callback)=>{
|
|
528
|
-
window.addEventListener("storage", callback);
|
|
529
|
-
// Return cleanup function for React to call on unmount.
|
|
530
|
-
return ()=>window.removeEventListener("storage", callback);
|
|
531
|
-
};
|
|
532
|
-
/**
|
|
533
|
-
* A React hook for managing state synchronized with localStorage. Uses React
|
|
534
|
-
* 19's useSyncExternalStore for optimal concurrent rendering support and
|
|
535
|
-
* automatic synchronization across components. Changes are automatically
|
|
536
|
-
* persisted to localStorage and synchronized across all components using the
|
|
537
|
-
* same key, including across browser tabs.
|
|
538
|
-
*
|
|
539
|
-
* Features:
|
|
540
|
-
* - Automatic serialization/deserialization with JSON
|
|
541
|
-
* - Type-safe with TypeScript generics
|
|
542
|
-
* - Supports functional updates (similar to setState)
|
|
543
|
-
* - Synchronized across components and browser tabs
|
|
544
|
-
* - Handles edge cases (null, undefined, errors)
|
|
545
|
-
* - Compatible with React 19+ concurrent features
|
|
546
|
-
*
|
|
547
|
-
* @template T - The type of the value stored in localStorage
|
|
548
|
-
* @param {StorageProperties<T>} config - Configuration object with key and optional initialValue
|
|
549
|
-
* @returns {[T | null, (value: T | ((current: T) => T)) => void, () => void, () => void]} A tuple containing:
|
|
550
|
-
* - [0] current value from localStorage (or null if not set)
|
|
551
|
-
* - [1] setValue function to update the stored value (supports direct value or function updater)
|
|
552
|
-
* - [2] resetValue function to restore the initialValue
|
|
553
|
-
* - [3] removeValue function to remove the value from localStorage
|
|
554
|
-
*
|
|
555
|
-
* @example
|
|
556
|
-
* ```js
|
|
557
|
-
* // Basic usage with a string value
|
|
558
|
-
* import { useLocalStorage } from '@versini/ui-hooks';
|
|
559
|
-
* const [model, setModel, resetModel, removeModel] = useLocalStorage({
|
|
560
|
-
* key: 'gpt-model',
|
|
561
|
-
* initialValue: 'gpt-3',
|
|
562
|
-
* });
|
|
563
|
-
*
|
|
564
|
-
* // Direct update
|
|
565
|
-
* setModel('gpt-4'); // Stores "gpt-4"
|
|
566
|
-
*
|
|
567
|
-
* // Functional update (receives current value)
|
|
568
|
-
* setModel((current) => (current === 'gpt-3' ? 'gpt-4' : 'gpt-3'));
|
|
569
|
-
*
|
|
570
|
-
* // Reset to initial value
|
|
571
|
-
* resetModel(); // Restores "gpt-3"
|
|
572
|
-
*
|
|
573
|
-
* // Remove from localStorage
|
|
574
|
-
* removeModel(); // Sets value to null and removes from storage
|
|
575
|
-
* ```
|
|
576
|
-
*
|
|
577
|
-
* @example
|
|
578
|
-
* ```js
|
|
579
|
-
* // Usage with complex objects
|
|
580
|
-
* interface UserPreferences {
|
|
581
|
-
* theme: 'light' | 'dark';
|
|
582
|
-
* fontSize: number;
|
|
583
|
-
* }
|
|
584
|
-
*
|
|
585
|
-
* const [prefs, setPrefs] = useLocalStorage<UserPreferences>({
|
|
586
|
-
* key: 'user-preferences',
|
|
587
|
-
* initialValue: { theme: 'light', fontSize: 14 }
|
|
588
|
-
* });
|
|
589
|
-
*
|
|
590
|
-
* // Update specific property
|
|
591
|
-
* setPrefs(current => ({ ...current, theme: 'dark' }));
|
|
592
|
-
* ```
|
|
593
|
-
*
|
|
594
|
-
*/ function useLocalStorage({ key, initialValue }) {
|
|
595
|
-
/**
|
|
596
|
-
* Snapshot function for useSyncExternalStore - returns current localStorage
|
|
597
|
-
* value.
|
|
598
|
-
*/ const getSnapshot = ()=>getLocalStorageItem(key);
|
|
599
|
-
/**
|
|
600
|
-
* Use React's useSyncExternalStore to subscribe to localStorage changes. This
|
|
601
|
-
* ensures proper integration with React 19+ concurrent rendering and provides
|
|
602
|
-
* automatic re-rendering when the stored value changes (either from this
|
|
603
|
-
* component, other components, or other browser tabs).
|
|
604
|
-
*/ const store = useSyncExternalStore(useLocalStorageSubscribe, getSnapshot);
|
|
605
|
-
/**
|
|
606
|
-
* Updates the stored value in localStorage. Accepts either a direct value or a
|
|
607
|
-
* function that receives the current value. Setting to null or undefined will
|
|
608
|
-
* remove the item from localStorage.
|
|
609
|
-
*/ const setValue = useCallback((v)=>{
|
|
610
|
-
try {
|
|
611
|
-
// Support both direct values and functional updates.
|
|
612
|
-
const nextState = typeof v === "function" ? v(JSON.parse(store)) : v;
|
|
613
|
-
// Remove from storage if value is null or undefined.
|
|
614
|
-
if (nextState === undefined || nextState === null) {
|
|
615
|
-
removeLocalStorageItem(key);
|
|
616
|
-
} else {
|
|
617
|
-
setLocalStorageItem(key, nextState);
|
|
618
|
-
}
|
|
619
|
-
/* v8 ignore next 4 */ } catch (e) {
|
|
620
|
-
// Log parsing or storage errors without breaking the application.
|
|
621
|
-
console.warn(e);
|
|
622
|
-
}
|
|
623
|
-
}, [
|
|
624
|
-
key,
|
|
625
|
-
store
|
|
626
|
-
]);
|
|
627
|
-
/**
|
|
628
|
-
* Resets the stored value back to the initialValue provided in the
|
|
629
|
-
* configuration. If no initialValue was provided, this will remove the item
|
|
630
|
-
* from localStorage.
|
|
631
|
-
*/ const resetValue = useCallback(()=>{
|
|
632
|
-
setValue(initialValue);
|
|
633
|
-
}, [
|
|
634
|
-
initialValue,
|
|
635
|
-
setValue
|
|
636
|
-
]);
|
|
637
|
-
/**
|
|
638
|
-
* Removes the value from localStorage entirely. After calling this, the hook
|
|
639
|
-
* will return null until a new value is set.
|
|
640
|
-
*/ const removeValue = useCallback(()=>{
|
|
641
|
-
setValue(null);
|
|
642
|
-
}, [
|
|
643
|
-
setValue
|
|
644
|
-
]);
|
|
645
|
-
/**
|
|
646
|
-
* Initialize localStorage with the initialValue on first mount if the key
|
|
647
|
-
* doesn't exist. This effect only runs once when the component mounts and
|
|
648
|
-
* ensures that the initialValue is persisted to localStorage if no value is
|
|
649
|
-
* currently stored.
|
|
650
|
-
*/ useEffect(()=>{
|
|
651
|
-
try {
|
|
652
|
-
// Only set initialValue if key doesn't exist and initialValue is defined.
|
|
653
|
-
if (getLocalStorageItem(key) === null && typeof initialValue !== "undefined") {
|
|
654
|
-
setLocalStorageItem(key, initialValue);
|
|
655
|
-
}
|
|
656
|
-
/* v8 ignore next 4 */ } catch (e) {
|
|
657
|
-
// Log initialization errors without breaking the application.
|
|
658
|
-
console.warn(e);
|
|
659
|
-
}
|
|
660
|
-
}, [
|
|
661
|
-
key,
|
|
662
|
-
initialValue
|
|
663
|
-
]);
|
|
664
|
-
/**
|
|
665
|
-
* Return tuple: [currentValue, setValue, resetValue, removeValue] Parse the
|
|
666
|
-
* stored JSON string back to its original type, or return null if empty.
|
|
667
|
-
*/ return [
|
|
668
|
-
store ? JSON.parse(store) : null,
|
|
669
|
-
setValue,
|
|
670
|
-
resetValue,
|
|
671
|
-
removeValue
|
|
672
|
-
];
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
;// CONCATENATED MODULE: ./src/hooks/useMergeRefs.ts
|
|
676
|
-
/**
|
|
677
|
-
* React utility to merge refs.
|
|
678
|
-
*
|
|
679
|
-
* When developing low level UI components, it is common to have to use a local
|
|
680
|
-
* ref but also support an external one using React.forwardRef. Natively, React
|
|
681
|
-
* does not offer a way to set two refs inside the ref property.
|
|
682
|
-
*
|
|
683
|
-
* @param Array of refs (object, function, etc.)
|
|
684
|
-
*
|
|
685
|
-
* @example
|
|
686
|
-
*
|
|
687
|
-
* const Example = React.forwardRef(function Example(props, ref) {
|
|
688
|
-
* const localRef = React.useRef();
|
|
689
|
-
* const mergedRefs = useMergeRefs([localRef, ref]);
|
|
690
|
-
*
|
|
691
|
-
* return <div ref={mergedRefs} />;
|
|
692
|
-
* });
|
|
693
|
-
*
|
|
694
|
-
*/
|
|
695
|
-
function useMergeRefs(refs) {
|
|
696
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: refs array is used as dependency but spread for proper comparison
|
|
697
|
-
return useMemo(()=>{
|
|
698
|
-
if (refs.every((ref)=>ref == null)) {
|
|
699
|
-
return ()=>{};
|
|
700
|
-
}
|
|
701
|
-
return (value)=>{
|
|
702
|
-
refs.forEach((ref)=>{
|
|
703
|
-
if (typeof ref === "function") {
|
|
704
|
-
ref(value);
|
|
705
|
-
} else if (ref != null) {
|
|
706
|
-
ref.current = value;
|
|
707
|
-
}
|
|
708
|
-
});
|
|
709
|
-
};
|
|
710
|
-
}, [
|
|
711
|
-
...refs
|
|
712
|
-
]);
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
;// CONCATENATED MODULE: ./src/hooks/useResizeObserver.ts
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
const defaultState = {
|
|
719
|
-
x: 0,
|
|
720
|
-
y: 0,
|
|
721
|
-
width: 0,
|
|
722
|
-
height: 0,
|
|
723
|
-
top: 0,
|
|
724
|
-
left: 0,
|
|
725
|
-
bottom: 0,
|
|
726
|
-
right: 0
|
|
727
|
-
};
|
|
728
|
-
/**
|
|
729
|
-
* A custom hook that uses the ResizeObserver API to track the size changes of a DOM element.
|
|
730
|
-
*
|
|
731
|
-
* @template T - The type of the DOM element being observed.
|
|
732
|
-
* @param {ResizeObserverOptions} [options] - The options to configure the ResizeObserver.
|
|
733
|
-
* @returns {[React.RefObject<T>, ObserverRect]} - A tuple containing the ref object and
|
|
734
|
-
* the observed rectangle.
|
|
735
|
-
* @example
|
|
736
|
-
*
|
|
737
|
-
* const [rightElementRef, rect] = useResizeObserver<HTMLDivElement>();
|
|
738
|
-
* <div ref={componentRef}>
|
|
739
|
-
* Observed: <code>{JSON.stringify(rect)}</code>
|
|
740
|
-
* </div>
|
|
741
|
-
*/ function useResizeObserver(options) {
|
|
742
|
-
const isMounted = useIsMounted();
|
|
743
|
-
const frameID = useRef(0);
|
|
744
|
-
const ref = useRef(null);
|
|
745
|
-
const [rect, setRect] = useState(defaultState);
|
|
746
|
-
const observer = useMemo(()=>{
|
|
747
|
-
/* c8 ignore next 3 */ if (typeof ResizeObserver === "undefined") {
|
|
748
|
-
return null;
|
|
749
|
-
}
|
|
750
|
-
return new ResizeObserver((entries)=>{
|
|
751
|
-
const entry = entries[0];
|
|
752
|
-
if (entry) {
|
|
753
|
-
cancelAnimationFrame(frameID.current);
|
|
754
|
-
frameID.current = requestAnimationFrame(()=>{
|
|
755
|
-
if (ref.current && isMounted()) {
|
|
756
|
-
setRect(entry.contentRect);
|
|
757
|
-
}
|
|
758
|
-
});
|
|
759
|
-
}
|
|
760
|
-
});
|
|
761
|
-
}, [
|
|
762
|
-
isMounted
|
|
763
|
-
]);
|
|
764
|
-
useEffect(()=>{
|
|
765
|
-
/* c8 ignore next 3 */ if (ref.current) {
|
|
766
|
-
observer?.observe(ref.current, options);
|
|
767
|
-
}
|
|
768
|
-
return ()=>{
|
|
769
|
-
observer?.disconnect();
|
|
770
|
-
/* c8 ignore next 3 */ if (frameID.current) {
|
|
771
|
-
cancelAnimationFrame(frameID.current);
|
|
772
|
-
}
|
|
773
|
-
};
|
|
774
|
-
}, [
|
|
775
|
-
observer,
|
|
776
|
-
options
|
|
777
|
-
]);
|
|
778
|
-
return [
|
|
779
|
-
ref,
|
|
780
|
-
rect
|
|
781
|
-
];
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
;// CONCATENATED MODULE: ./src/hooks/useUncontrolled.ts
|
|
785
|
-
|
|
786
|
-
function useUncontrolled({ value, defaultValue, finalValue, onChange = ()=>{}, initialControlledDelay = 0 }) {
|
|
787
|
-
const [initialDelayDone, setInitialDelayDone] = useState(false);
|
|
788
|
-
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue !== undefined ? defaultValue : finalValue);
|
|
789
|
-
const handleUncontrolledChange = (val)=>{
|
|
790
|
-
setUncontrolledValue(val);
|
|
791
|
-
onChange?.(val);
|
|
792
|
-
};
|
|
793
|
-
useEffect(()=>{
|
|
794
|
-
(async ()=>{
|
|
795
|
-
/**
|
|
796
|
-
* If initialControlledDelay is provided, wait for the delay.
|
|
797
|
-
*/ if (value !== undefined) {
|
|
798
|
-
/* c8 ignore start */ if (!initialDelayDone && initialControlledDelay > 0) {
|
|
799
|
-
await new Promise((resolve)=>setTimeout(resolve, initialControlledDelay));
|
|
800
|
-
setInitialDelayDone(true);
|
|
801
|
-
}
|
|
802
|
-
/* c8 ignore end */ }
|
|
803
|
-
})();
|
|
804
|
-
}, [
|
|
805
|
-
value,
|
|
806
|
-
initialControlledDelay,
|
|
807
|
-
initialDelayDone
|
|
808
|
-
]);
|
|
809
|
-
/**
|
|
810
|
-
* If value is provided, return the controlled value.
|
|
811
|
-
* If there is a delay, we need to wait for the delay: we need to first send
|
|
812
|
-
* back a value of an empty string, then after the delay
|
|
813
|
-
* we can send the actual value.
|
|
814
|
-
*/ if (value !== undefined) {
|
|
815
|
-
if (!initialDelayDone && initialControlledDelay > 0) {
|
|
816
|
-
return [
|
|
817
|
-
"",
|
|
818
|
-
onChange,
|
|
819
|
-
true
|
|
820
|
-
];
|
|
821
|
-
} else {
|
|
822
|
-
return [
|
|
823
|
-
value,
|
|
824
|
-
onChange,
|
|
825
|
-
true
|
|
826
|
-
];
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
/**
|
|
830
|
-
* If value is not provided, return the uncontrolled value.
|
|
831
|
-
*/ return [
|
|
832
|
-
uncontrolledValue,
|
|
833
|
-
handleUncontrolledChange,
|
|
834
|
-
false
|
|
835
|
-
];
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
;// CONCATENATED MODULE: ./src/hooks/useUniqueId.ts
|
|
839
|
-
|
|
840
|
-
function useUniqueId(options) {
|
|
841
|
-
const generatedId = useId();
|
|
842
|
-
if (!options) {
|
|
843
|
-
return generatedId;
|
|
844
|
-
}
|
|
845
|
-
if (typeof options === "number" || typeof options === "string") {
|
|
846
|
-
return `${options}${generatedId}`;
|
|
847
|
-
}
|
|
848
|
-
if (typeof options === "object") {
|
|
849
|
-
const { id, prefix = "" } = options;
|
|
850
|
-
if (typeof id === "number" || typeof id === "string") {
|
|
851
|
-
return `${prefix}${id}`;
|
|
852
|
-
}
|
|
853
|
-
return `${prefix}${generatedId}`;
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
;// CONCATENATED MODULE: ./src/hooks/useViewportSize.tsx
|
|
858
|
-
|
|
859
|
-
function useWindowEvent(type, listener) {
|
|
860
|
-
useEffect(()=>{
|
|
861
|
-
window.addEventListener(type, listener, {
|
|
862
|
-
passive: true
|
|
863
|
-
});
|
|
864
|
-
return ()=>window.removeEventListener(type, listener);
|
|
865
|
-
}, [
|
|
866
|
-
type,
|
|
867
|
-
listener
|
|
868
|
-
]);
|
|
869
|
-
}
|
|
870
|
-
/**
|
|
871
|
-
* Custom hook that returns the current viewport size. It will update
|
|
872
|
-
* when the window is resized or the orientation changes.
|
|
873
|
-
*
|
|
874
|
-
* @returns The current viewport size
|
|
875
|
-
*
|
|
876
|
-
* @example
|
|
877
|
-
* const { width, height } = useViewportSize();
|
|
878
|
-
*/ function useViewportSize() {
|
|
879
|
-
const [windowSize, setWindowSize] = useState({
|
|
880
|
-
width: 0,
|
|
881
|
-
height: 0
|
|
882
|
-
});
|
|
883
|
-
const setSize = useCallback(()=>{
|
|
884
|
-
setWindowSize({
|
|
885
|
-
width: window.innerWidth || 0,
|
|
886
|
-
height: window.innerHeight || 0
|
|
887
|
-
});
|
|
888
|
-
}, []);
|
|
889
|
-
useWindowEvent("resize", setSize);
|
|
890
|
-
useWindowEvent("orientationchange", setSize);
|
|
891
|
-
useEffect(setSize, []);
|
|
892
|
-
return windowSize;
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
;// CONCATENATED MODULE: ./src/hooks/useVisualViewportSize.tsx
|
|
896
|
-
/* v8 ignore start */
|
|
897
|
-
/**
|
|
898
|
-
* Custom hook that returns the current visual viewport size. It will update
|
|
899
|
-
* when the window is resized (zoom, virtual keyboard displayed, etc.) or
|
|
900
|
-
* the orientation changes.
|
|
901
|
-
*
|
|
902
|
-
* @returns The current visual viewport size
|
|
903
|
-
*
|
|
904
|
-
* @example
|
|
905
|
-
* const { width, height } = useVisualViewportSize();
|
|
906
|
-
*/ function useVisualViewportSize() {
|
|
907
|
-
const [windowSize, setWindowSize] = useState({
|
|
908
|
-
width: 0,
|
|
909
|
-
height: 0
|
|
910
|
-
});
|
|
911
|
-
// Define setSize function once and never recreate it
|
|
912
|
-
const setSize = useCallback(()=>{
|
|
913
|
-
setWindowSize({
|
|
914
|
-
width: window?.visualViewport?.width || window.innerWidth || 0,
|
|
915
|
-
height: window?.visualViewport?.height || window.innerHeight || 0
|
|
916
|
-
});
|
|
917
|
-
}, []); // Empty dependency array is correct here
|
|
918
|
-
useEffect(()=>{
|
|
919
|
-
// Initial size setup
|
|
920
|
-
setSize();
|
|
921
|
-
// Set up event listeners
|
|
922
|
-
if (window.visualViewport) {
|
|
923
|
-
window.visualViewport.addEventListener("resize", setSize, {
|
|
924
|
-
passive: true
|
|
925
|
-
});
|
|
926
|
-
window.visualViewport.addEventListener("orientationchange", setSize, {
|
|
927
|
-
passive: true
|
|
928
|
-
});
|
|
929
|
-
}
|
|
930
|
-
// Cleanup
|
|
931
|
-
return ()=>{
|
|
932
|
-
if (window.visualViewport) {
|
|
933
|
-
window.visualViewport.removeEventListener("resize", setSize);
|
|
934
|
-
window.visualViewport.removeEventListener("orientationchange", setSize);
|
|
935
|
-
}
|
|
936
|
-
};
|
|
937
|
-
}, [
|
|
938
|
-
setSize
|
|
939
|
-
]);
|
|
940
|
-
return windowSize;
|
|
941
|
-
} /* v8 ignore end */
|
|
942
|
-
|
|
943
|
-
;// CONCATENATED MODULE: ./src/hooks/index.ts
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
export { getHotkeyHandler, shouldFireEvent, useClickOutside, useHaptic, useHotkeys, useInViewport, useInterval, useIsMounted, useLocalStorage, useMergeRefs, useResizeObserver, useUncontrolled, useUniqueId, useViewportSize, useVisualViewportSize };
|