@ringcentral/spring-base 1.11.2 → 1.13.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/hooks.js CHANGED
@@ -1,71 +1,98 @@
1
- import { unstable_useEventCallback, unstable_ownerDocument, unstable_Timeout, unstable_useForkRef } from '@mui/utils';
2
- export { unstable_useControlled as useControlled } from '@mui/utils';
3
- import * as React from 'react';
4
- import { useState, useRef, useEffect, useCallback, useLayoutEffect, useMemo } from 'react';
5
- import { useWindowContext, useResponsiveContext, getMatchDetail } from './contexts.js';
6
- import uniqueId from 'lodash/uniqueId';
7
- import { useButton as useButton$1 } from '@mui/base/useButton';
8
- import { c as combineProps, d as createAriaHideOutsideController, g as getRefElement, l as logInDev, o as ownerWindow } from './chunks/twMerge.js';
9
- import './config.js';
10
- import 'lodash/omit';
11
- import 'lodash/pick';
12
- import 'lodash/isPlainObject';
13
- import debounce from 'lodash/debounce';
14
- import noop from 'lodash/noop';
15
- import throttle from 'lodash/throttle';
16
- import 'tailwind-merge';
17
-
18
- const useEventCallback = (func) => {
1
+ import { unstable_Timeout, unstable_useControlled, unstable_useEventCallback, unstable_useForkRef } from "@mui/utils";
2
+ import react, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
+ import uniqueId from "lodash/uniqueId";
4
+ import { useButton } from "@mui/base";
5
+ import debounce from "lodash/debounce";
6
+ import noop from "lodash/noop";
7
+ import throttle from "lodash/throttle";
8
+ import { getMatchDetail, useWindowContext, useResponsiveContext } from "./contexts.js";
9
+ import { LIVE_ANNOUNCER_ATTR, announce, applyVisuallyHiddenStyles } from "./liveAnnouncer.js";
10
+ import { createAriaHideOutsideController, getRefElement, logInDev, ownerWindow, combineProps, ownerDocument as unstable_ownerDocument } from "./utils.js";
11
+
12
+ import * as __rspack_external_react from "react";
13
+
14
+ // this file template replace material-ui/core/utils method
15
+ // when upgrade mui that will not need assign type anymore
16
+
17
+ const useEventCallback = (func)=>{
19
18
  return unstable_useEventCallback(func);
20
19
  };
21
20
 
22
- const defaultCheckKeys = ['Enter', ' '];
23
- const useA11yKeyEvent = (fn, { onlyOnFocus, preventDefault = true, checkKeys = defaultCheckKeys, } = {}) => {
24
- return useEventCallback((event) => {
25
- if (!fn || (onlyOnFocus && event.target !== event.currentTarget))
26
- return;
21
+
22
+
23
+
24
+ const defaultCheckKeys = [
25
+ 'Enter',
26
+ ' '
27
+ ];
28
+ /**
29
+ * bind event with a11y keyboard, `space`, `enter`, only trigger when key includes in `checkKeys`
30
+ *
31
+ * that will auto preventDefault by default, if you don't want that, set `false` with `preventDefault`
32
+ */ const useA11yKeyEvent = (fn, { onlyOnFocus, preventDefault = true, checkKeys = defaultCheckKeys } = {})=>{
33
+ return useEventCallback((event)=>{
34
+ if (!fn || onlyOnFocus && event.target !== event.currentTarget) return;
27
35
  if (checkKeys.includes(event.key)) {
28
- if (preventDefault)
29
- event.preventDefault();
36
+ if (preventDefault) event.preventDefault();
30
37
  fn(event);
31
38
  }
32
39
  });
33
40
  };
34
41
 
35
- const isNativeButton = (el) => {
42
+
43
+
44
+
45
+ // base https://github.com/mui/material-ui/blob/master/packages/mui-base/src/useButton/useButton.ts
46
+
47
+
48
+ const isNativeButton = (el)=>{
36
49
  const tag = el.tagName;
37
- return (tag === 'BUTTON' ||
38
- (tag === 'INPUT' &&
39
- ['button', 'submit', 'reset'].includes(el?.type)) ||
40
- (tag === 'A' && el?.href));
50
+ return tag === 'BUTTON' || tag === 'INPUT' && [
51
+ 'button',
52
+ 'submit',
53
+ 'reset'
54
+ ].includes(el?.type) || tag === 'A' && el?.href;
41
55
  };
42
- const useActive = ({ onKeyDown, onKeyUp, onMouseDown, onClick, disabled = false, } = {}) => {
56
+ /**
57
+ * Bind event with a11y keyboard (`space`) to help non-native button elements implement `:active` state.
58
+ *
59
+ * Also handles mouse interactions with proper window context support.
60
+ */ const useActive = ({ onKeyDown, onKeyUp, onMouseDown, onClick, disabled = false } = {})=>{
43
61
  const [active, setActive] = useState(false);
44
62
  const activeTargetRef = useRef(null);
45
63
  const { document: targetDocument = document } = useWindowContext();
46
- useEffect(() => {
64
+ useEffect(()=>{
47
65
  if (disabled) {
48
66
  setActive(false);
49
67
  activeTargetRef.current = null;
50
68
  }
51
- }, [disabled]);
69
+ }, [
70
+ disabled
71
+ ]);
52
72
  if (disabled) {
53
73
  return {
54
- activeHandlers: { onKeyDown, onKeyUp, onMouseDown, onClick },
55
- active: false,
74
+ activeHandlers: {
75
+ onKeyDown,
76
+ onKeyUp,
77
+ onMouseDown,
78
+ onClick
79
+ },
80
+ active: false
56
81
  };
57
82
  }
58
- const handleMouseDown = (event) => {
83
+ const handleMouseDown = (event)=>{
59
84
  onMouseDown?.(event);
60
85
  if (event.defaultPrevented) {
61
86
  return;
62
87
  }
63
88
  setActive(true);
64
- targetDocument.addEventListener('mouseup', () => {
89
+ targetDocument.addEventListener('mouseup', ()=>{
65
90
  setActive(false);
66
- }, { once: true });
91
+ }, {
92
+ once: true
93
+ });
67
94
  };
68
- const handleKeyDown = (event) => {
95
+ const handleKeyDown = (event)=>{
69
96
  const isSpaceKey = event.key === ' ';
70
97
  onKeyDown?.(event);
71
98
  if (event.defaultPrevented) {
@@ -81,15 +108,16 @@ const useActive = ({ onKeyDown, onKeyUp, onMouseDown, onClick, disabled = false,
81
108
  }
82
109
  setActive(true);
83
110
  }
84
- if (event.target === event.currentTarget &&
85
- !isNativeButton(event.target) &&
86
- event.key === 'Enter') {
111
+ // Keyboard accessibility for non interactive elements
112
+ if (event.target === event.currentTarget && !isNativeButton(event.target) && event.key === 'Enter') {
87
113
  onClick?.(event);
88
114
  event.preventDefault();
89
115
  }
90
116
  };
91
- const handleKeyUp = (event) => {
117
+ const handleKeyUp = (event)=>{
92
118
  const isSpaceKey = event.key === ' ';
119
+ // calling preventDefault in keyUp on a <button> will not dispatch a click event if Space is pressed
120
+ // https://codesandbox.io/s/button-keyup-preventdefault-dn7f0
93
121
  if (event.target === event.currentTarget) {
94
122
  setActive(false);
95
123
  }
@@ -98,11 +126,8 @@ const useActive = ({ onKeyDown, onKeyUp, onMouseDown, onClick, disabled = false,
98
126
  if (isSpaceKey) {
99
127
  activeTargetRef.current = null;
100
128
  }
101
- if (hadMatchingSpaceKeyDown &&
102
- event.target === event.currentTarget &&
103
- !isNativeButton(event.target) &&
104
- isSpaceKey &&
105
- !event.defaultPrevented) {
129
+ // Keyboard accessibility for non interactive elements
130
+ if (hadMatchingSpaceKeyDown && event.target === event.currentTarget && !isNativeButton(event.target) && isSpaceKey && !event.defaultPrevented) {
106
131
  onClick?.(event);
107
132
  }
108
133
  };
@@ -111,13 +136,27 @@ const useActive = ({ onKeyDown, onKeyUp, onMouseDown, onClick, disabled = false,
111
136
  onKeyDown: handleKeyDown,
112
137
  onKeyUp: handleKeyUp,
113
138
  onMouseDown: handleMouseDown,
114
- onClick,
139
+ onClick
115
140
  },
116
- active,
141
+ active
117
142
  };
118
143
  };
119
144
 
120
- function useResultRef(cb) {
145
+
146
+
147
+
148
+
149
+
150
+
151
+
152
+ /**
153
+ * provide a ref for only calculate `cb` once
154
+ * prevent `useRef(getValue())`, that `getValue` calculate every re-render
155
+ *
156
+ * @param cb that get method of get this ref init value, only calculate once
157
+ * @returns same as `useRef` result
158
+ *
159
+ */ function useResultRef(cb) {
121
160
  const renderRef = useRef(0);
122
161
  const ref = useRef();
123
162
  if (renderRef.current === 0) {
@@ -127,9 +166,27 @@ function useResultRef(cb) {
127
166
  return ref;
128
167
  }
129
168
 
169
+
170
+
171
+
172
+
173
+
130
174
  const DEFAULT_PREFIX = 'spring-id-';
131
- function useId(id, prefixMode) {
132
- const { current: resultId } = useResultRef(() => {
175
+ /**
176
+ * Generates a unique ID.
177
+ *
178
+ * @param id - Default component id or prefix id.
179
+ * @param prefixMode Whether enable prefix the ID with.
180
+ * @returns unique ID
181
+ * @example
182
+ * useId() => 'spring-id-xxxx'
183
+ * useId('') => 'spring-id-xxxx'
184
+ * useId(null) => 'spring-id-xxxx'
185
+ * useId(undefined) => 'spring-id-xxxx'
186
+ * useId('nameId') => 'nameId'
187
+ * useId('nameId', true) => 'nameId-xxxx'
188
+ */ function useLegacyId(id, prefixMode) {
189
+ const { current: resultId } = useResultRef(()=>{
133
190
  if (id) {
134
191
  if (prefixMode) {
135
192
  return uniqueId(`${id}-`);
@@ -140,47 +197,93 @@ function useId(id, prefixMode) {
140
197
  });
141
198
  return resultId;
142
199
  }
200
+ const reactUseId = react.useId;
201
+ function useReactId(id, prefixMode) {
202
+ const reactId = reactUseId();
203
+ if (id) {
204
+ return prefixMode ? `${id}-${reactId}` : id;
205
+ }
206
+ return `${DEFAULT_PREFIX}${reactId}`;
207
+ }
208
+ const useId = reactUseId ? useReactId : useLegacyId;
209
+
143
210
 
144
- const visuallyHiddenStyles = `
145
- clip-path: inset(100%);
146
- clip: rect(1px, 1px, 1px, 1px);
147
- height: 1px;
148
- overflow: hidden;
149
- position: absolute;
150
- white-space: nowrap;
151
- width: 1px;
152
- `;
153
- const useAnnouncer = (id) => {
211
+
212
+
213
+
214
+
215
+
216
+ const isUseAnnouncerOptions = (value)=>{
217
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
218
+ };
219
+ /**
220
+ * Instance-owned live announcer. Each hook call manages its own body region.
221
+ *
222
+ * Prefer `announce` (or `useGlobalAnnouncer` in a pop-out window) so the page
223
+ * shares one polite/assertive live-region pair per document. Use this hook
224
+ * only when you need an isolated region that must not share that pair.
225
+ *
226
+ * `id` only labels the region's DOM `id` (`rc-announcer-${id}`). The same `id`
227
+ * does not share or reuse one node: each hook call still owns its own region.
228
+ * Pass distinct ids when you set one.
229
+ *
230
+ * @param idOrOptions - Optional DOM id label, or options when no id is needed
231
+ * @param maybeOptions - Options when the first argument is an id
232
+ * @returns announce function that accepts a message string
233
+ *
234
+ * @example
235
+ * ```ts
236
+ * const announceLocal = useAnnouncer('feature-a', { politeness: 'polite' });
237
+ * announceLocal('Saved');
238
+ *
239
+ * // Do not call useAnnouncer inside list items or other high-cardinality trees.
240
+ * // Lift one instance to the page/feature boundary and pass `announce` down.
241
+ * ```
242
+ */ function useAnnouncer(idOrOptions, maybeOptions) {
243
+ const id = typeof idOrOptions === 'string' ? idOrOptions : undefined;
244
+ const options = isUseAnnouncerOptions(idOrOptions) ? idOrOptions : maybeOptions;
245
+ const politeness = options?.politeness ?? 'assertive';
246
+ // TODO(breaking-change): rename the generated id prefix from `rc-announcer`
247
+ // to `sui-announcer` in the next major cleanup.
154
248
  const _id = useId(id ? `rc-announcer-${id}` : 'rc-announcer', !id);
155
- const { document: targetDocument, externalWindow, globalThis: contextGlobal, } = useWindowContext();
249
+ const { document: targetDocument, externalWindow, globalThis: contextGlobal } = useWindowContext();
156
250
  const win = externalWindow ?? contextGlobal;
157
251
  const ref = useRef(null);
158
252
  const lastMessageRef = useRef();
159
253
  const pendingTimerRef = useRef(null);
160
- const clearPendingTimer = useCallback(() => {
254
+ const pendingRemovalRef = useRef(null);
255
+ const clearPendingTimer = useCallback(()=>{
161
256
  pendingTimerRef.current?.();
162
257
  pendingTimerRef.current = null;
163
258
  }, []);
164
- useEffect(() => {
165
- const currentDocument = targetDocument ??
166
- (typeof contextGlobal.document !== 'undefined'
167
- ? contextGlobal.document
168
- : undefined);
259
+ const clearPendingRemoval = useCallback(()=>{
260
+ pendingRemovalRef.current?.();
261
+ pendingRemovalRef.current = null;
262
+ }, []);
263
+ useEffect(()=>{
264
+ const currentDocument = targetDocument ?? (typeof contextGlobal.document !== 'undefined' ? contextGlobal.document : undefined);
169
265
  if (!currentDocument?.body) {
170
266
  return undefined;
171
267
  }
172
- const el = currentDocument.createElement('div');
173
- ref.current = el;
174
- el.id = _id;
175
- el.setAttribute('aria-live', 'assertive');
176
- el.setAttribute('aria-atomic', 'true');
177
- el.setAttribute('aria-hidden', 'false');
178
- el.setAttribute('data-live-announcer', 'true');
179
- el.setAttribute('style', visuallyHiddenStyles);
180
- currentDocument.body.appendChild(el);
181
- return () => {
268
+ const currentEl = ref.current;
269
+ const canReuseCurrentEl = currentEl?.ownerDocument === currentDocument && currentEl.id === _id && currentDocument.body.contains(currentEl);
270
+ const el = canReuseCurrentEl ? currentEl : currentDocument.createElement('div');
271
+ if (canReuseCurrentEl) {
272
+ clearPendingRemoval();
273
+ el.setAttribute('aria-live', politeness);
274
+ } else {
275
+ ref.current = el;
276
+ el.id = _id;
277
+ el.setAttribute('aria-live', politeness);
278
+ el.setAttribute('aria-atomic', 'true');
279
+ el.setAttribute('aria-hidden', 'false');
280
+ el.setAttribute(LIVE_ANNOUNCER_ATTR, 'true');
281
+ applyVisuallyHiddenStyles(el);
282
+ currentDocument.body.appendChild(el);
283
+ }
284
+ return ()=>{
182
285
  clearPendingTimer();
183
- win.setTimeout(() => {
286
+ const timerId = win.setTimeout(()=>{
184
287
  const body = currentDocument.body;
185
288
  if (body.contains(el)) {
186
289
  body.removeChild(el);
@@ -188,13 +291,33 @@ const useAnnouncer = (id) => {
188
291
  if (el === ref.current) {
189
292
  ref.current = null;
190
293
  }
294
+ if (pendingRemovalRef.current === cancelRemoval) {
295
+ pendingRemovalRef.current = null;
296
+ }
191
297
  });
298
+ const cancelRemoval = ()=>{
299
+ win.clearTimeout(timerId);
300
+ };
301
+ pendingRemovalRef.current = cancelRemoval;
192
302
  };
193
- }, [_id, targetDocument, contextGlobal, win, clearPendingTimer]);
194
- return useCallback((message) => {
303
+ }, [
304
+ _id,
305
+ politeness,
306
+ targetDocument,
307
+ contextGlobal,
308
+ win,
309
+ clearPendingTimer,
310
+ clearPendingRemoval
311
+ ]);
312
+ /**
313
+ * Announce method that calls screen readers to speak the message.
314
+ * Uses zero-width space technique to ensure repeated messages are re-announced.
315
+ *
316
+ * The technique sets content to a zero-width space (invisible) then to the message,
317
+ * forcing a content change that screen readers detect as new content.
318
+ */ return useCallback((message)=>{
195
319
  const el = ref.current;
196
- if (!el)
197
- return;
320
+ if (!el) return;
198
321
  clearPendingTimer();
199
322
  if (!message) {
200
323
  el.textContent = '';
@@ -206,8 +329,8 @@ const useAnnouncer = (id) => {
206
329
  lastMessageRef.current = message;
207
330
  return;
208
331
  }
209
- el.textContent = '\u200B';
210
- const timerId = win.setTimeout(() => {
332
+ el.textContent = '\u200B'; // Zero-width space (invisible but different content)
333
+ const timerId = win.setTimeout(()=>{
211
334
  if (el && ref.current === el) {
212
335
  el.textContent = message;
213
336
  lastMessageRef.current = message;
@@ -216,54 +339,113 @@ const useAnnouncer = (id) => {
216
339
  pendingTimerRef.current = null;
217
340
  }
218
341
  }, 100);
219
- const cancelTimer = () => {
342
+ const cancelTimer = ()=>{
220
343
  win.clearTimeout(timerId);
221
344
  };
222
345
  pendingTimerRef.current = cancelTimer;
223
- }, [clearPendingTimer, win]);
224
- };
346
+ }, [
347
+ clearPendingTimer,
348
+ win
349
+ ]);
350
+ }
351
+
352
+
353
+
354
+
355
+
356
+
357
+
358
+ /**
359
+ * Window-scoped wrapper around `announce`. Fills in
360
+ * `useWindowContext().document` so pop-out callers do not need to pass `{ document }`.
361
+ * Shares one polite/assertive pair per document, not per app.
362
+ *
363
+ * Unlike `useAnnouncer`, this does not mount an isolated region per hook call.
364
+ *
365
+ * Do not `destroyAnnouncer` on unmount. The window owner should call
366
+ * `destroyAnnouncer(win.document)` when the pop-out window closes.
367
+ *
368
+ * @example
369
+ * ```ts
370
+ * const announce = useGlobalAnnouncer();
371
+ * announce('Saved', 'polite');
372
+ * ```
373
+ */ function useGlobalAnnouncer() {
374
+ const { document: targetDocument } = useWindowContext();
375
+ return useCallback((message, politeness = 'assertive', timeoutOrOptions)=>{
376
+ const timeout = typeof timeoutOrOptions === 'number' ? timeoutOrOptions : timeoutOrOptions?.timeout;
377
+ announce(message, politeness, {
378
+ timeout,
379
+ document: targetDocument
380
+ });
381
+ }, [
382
+ targetDocument
383
+ ]);
384
+ }
385
+
386
+
387
+
225
388
 
226
- function useButton(parameters) {
227
- const muiButtonResult = useButton$1(parameters);
389
+
390
+
391
+
392
+ /**
393
+ * Spring UI's useButton hook that wraps MUI Base's useButton with enhanced active state management.
394
+ *
395
+ * Uses the correct document reference from WindowContext to ensure event listeners
396
+ * are attached to the appropriate document.
397
+ *
398
+ * @param parameters - Same parameters as MUI Base's useButton
399
+ * @returns Button props with proper active state management
400
+ */ function useButton_useButton(parameters) {
401
+ const muiButtonResult = useButton(parameters);
228
402
  const { active: customActive, activeHandlers } = useActive({
229
- disabled: parameters.disabled,
403
+ disabled: parameters.disabled
230
404
  });
231
405
  const originalGetRootProps = muiButtonResult.getRootProps;
232
- const getRootProps = (externalProps) => {
406
+ const getRootProps = (externalProps)=>{
233
407
  const mergedProps = combineProps(externalProps, activeHandlers);
234
408
  return originalGetRootProps(mergedProps);
235
409
  };
236
410
  return {
237
411
  ...muiButtonResult,
238
412
  active: customActive,
239
- getRootProps,
413
+ getRootProps
240
414
  };
241
415
  }
242
416
 
243
- const useAriaHideOutside = (props = {}) => {
417
+
418
+
419
+
420
+
421
+
422
+
423
+ const useAriaHideOutside = (props = {})=>{
244
424
  const { enable = true, shouldUseInert = false } = props;
245
425
  const { document } = useWindowContext();
246
426
  const ariaHideOutsideRef = useRef(null);
247
427
  const branchNodesRef = useRef(new Set());
248
428
  const controllerRef = useRef();
249
- const setAriaHideOutsideTarget = useCallback((node, isActive) => {
429
+ const setAriaHideOutsideTarget = useCallback((node, isActive)=>{
250
430
  if (isActive) {
251
431
  branchNodesRef.current.add(node);
252
432
  controllerRef.current?.addTarget(node);
253
- }
254
- else {
433
+ } else {
255
434
  branchNodesRef.current.delete(node);
256
435
  controllerRef.current?.removeTarget(node);
257
436
  }
258
437
  }, []);
259
- useLayoutEffect(() => {
438
+ useLayoutEffect(()=>{
260
439
  if (enable && ariaHideOutsideRef.current) {
261
- const controller = createAriaHideOutsideController([ariaHideOutsideRef.current, ...branchNodesRef.current], {
440
+ const controller = createAriaHideOutsideController([
441
+ ariaHideOutsideRef.current,
442
+ ...branchNodesRef.current
443
+ ], {
262
444
  root: document?.body,
263
- shouldUseInert,
445
+ shouldUseInert
264
446
  });
265
447
  controllerRef.current = controller;
266
- return () => {
448
+ return ()=>{
267
449
  if (controllerRef.current === controller) {
268
450
  controllerRef.current = undefined;
269
451
  }
@@ -272,38 +454,87 @@ const useAriaHideOutside = (props = {}) => {
272
454
  }
273
455
  controllerRef.current = undefined;
274
456
  return undefined;
275
- }, [enable, shouldUseInert, document?.body]);
457
+ }, [
458
+ enable,
459
+ shouldUseInert,
460
+ document?.body
461
+ ]);
276
462
  return {
277
463
  ariaHideOutsideRef: ariaHideOutsideRef,
278
- ariaHideEnabled: enable,
279
- setAriaHideOutsideTarget,
464
+ /**
465
+ * Whether outside elements are made unavailable to assistive technology.
466
+ */ ariaHideEnabled: enable,
467
+ setAriaHideOutsideTarget
280
468
  };
281
469
  };
282
470
 
283
- const useAudio = (init) => {
284
- const audioRef = useResultRef(() => {
471
+
472
+
473
+
474
+
475
+
476
+ /**
477
+ * get Browser `Audio` instance, and auto destroy when component be destroyed.
478
+ *
479
+ *
480
+ * @param init a callback when you init that audio
481
+ *
482
+ * @example
483
+ *
484
+ * ```ts
485
+ * const audio = useAudio((instance) => {
486
+ * instance.src = 'https://your.audio'
487
+ * });
488
+ *
489
+ * const click = () => audio.play();
490
+ *
491
+ * click();
492
+ * ```
493
+ * @see stories {@link https://develop-rcui-int.fiji.gliprc.com/?path=/story/%F0%9F%94%A7-foundation-hooks-useaudio--simple-announcer 🔧-foundation-hooks-useaudio--simple-announcer}
494
+ * @see {@link https://chromium-review.googlesource.com/c/chromium/src/+/2816118}
495
+ */ const useAudio = (init)=>{
496
+ const audioRef = useResultRef(()=>{
285
497
  const audio = new Audio();
286
498
  init?.(audio);
287
499
  return audio;
288
500
  });
289
- useEffect(() => {
290
- return () => {
291
- audioRef.current.srcObject = null;
292
- audioRef.current = null;
501
+ useEffect(()=>{
502
+ const audio = audioRef.current;
503
+ return ()=>{
504
+ audio.srcObject = null;
293
505
  };
506
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
294
507
  }, []);
295
508
  return audioRef.current;
296
509
  };
297
510
 
298
- function usePrevious(cb, isDefault) {
511
+
512
+
513
+
514
+
515
+
516
+ /**
517
+ * get previous render value
518
+ * @param cb that get method of target value
519
+ * @param isDefault if set `true`, the first time will get value from `cb`
520
+ * @returns previous value get from `cb`
521
+ */ function usePrevious(cb, isDefault) {
299
522
  const ref = useRef(isDefault ? cb() : undefined);
300
- useEffect(() => {
523
+ useEffect(()=>{
301
524
  ref.current = cb();
302
525
  });
303
526
  return ref.current;
304
527
  }
305
528
 
306
- const useChange = (cb, listenCb) => {
529
+
530
+
531
+
532
+
533
+ /**
534
+ * get value when prev listen value change
535
+ * @param cb that method you want calculate to get value
536
+ * @param listenCb prev compare listener value
537
+ */ const useChange = (cb, listenCb)=>{
307
538
  const countRef = useRef(0);
308
539
  const valueRef = useRef();
309
540
  const prev = usePrevious(listenCb);
@@ -314,138 +545,234 @@ const useChange = (cb, listenCb) => {
314
545
  }
315
546
  return valueRef.current;
316
547
  };
317
- const useDepsChange = (cb, deps) => {
548
+ /**
549
+ * get value when prev deps one of array value change
550
+ * @param cb that method you want calculate to get value
551
+ * @param deps prev compare value array
552
+ */ const useDepsChange = (cb, deps)=>{
318
553
  const countRef = useRef(0);
319
554
  const valueRef = useRef();
320
- const prev = usePrevious(() => deps, true);
555
+ const prev = usePrevious(()=>deps, true);
321
556
  const curr = deps;
322
- if (countRef.current === 0 ||
323
- curr.some((dep, index) => dep !== prev[index])) {
557
+ if (countRef.current === 0 || curr.some((dep, index)=>dep !== prev[index])) {
324
558
  countRef.current = 1;
325
559
  valueRef.current = cb(prev, curr);
326
560
  }
327
561
  return valueRef.current;
328
562
  };
329
563
 
564
+
565
+
566
+
567
+ // base https://github.com/mui/material-ui/blob/master/packages/mui-base/src/ClickAwayListener/ClickAwayListener.tsx
568
+
569
+
570
+
330
571
  function clickedRootScrollbar(event, doc) {
331
- return (doc.documentElement.clientWidth < event.clientX ||
332
- doc.documentElement.clientHeight < event.clientY);
572
+ return doc.documentElement.clientWidth < event.clientX || doc.documentElement.clientHeight < event.clientY;
333
573
  }
334
- const useClickAway = (onClickAway, props = {}) => {
574
+ /**
575
+ * @deprecated Use `ClickAwayListener` from `@mui/base` instead.
576
+ *
577
+ * @param onClickAway Callback fired when a "click away" event is detected.
578
+ */ const useClickAway = (onClickAway, props = {})=>{
335
579
  const { disableReactTree = false, isEnabled = true } = props;
336
580
  const movedRef = useRef(false);
337
581
  const activatedRef = useRef(false);
338
582
  const syntheticEventRef = useRef(false);
339
583
  const [target, setTarget] = useState();
340
- const clickAwayRef = useEventCallback((e) => {
584
+ // This hook is usually triggered after target appears
585
+ // so we need to trigger re-render component in ref callback to make sure useEffect can get target correctly
586
+ const clickAwayRef = useEventCallback((e)=>{
341
587
  setTarget(e);
342
588
  });
343
589
  const shouldActive = Boolean(target);
344
- useEffect(() => {
345
- if (!isEnabled)
346
- return;
590
+ useEffect(()=>{
591
+ if (!isEnabled) return;
347
592
  if (shouldActive) {
348
- const timer = setTimeout(() => {
593
+ // Ensure that this component is not "activated" synchronously.
594
+ // https://github.com/facebook/react/issues/20074
595
+ const timer = setTimeout(()=>{
349
596
  activatedRef.current = true;
350
597
  }, 0);
351
- return () => {
598
+ return ()=>{
352
599
  clearTimeout(timer);
353
600
  activatedRef.current = false;
354
601
  };
355
602
  }
356
603
  return undefined;
357
- }, [isEnabled, shouldActive]);
358
- const handleClickAway = useEventCallback((event) => {
604
+ }, [
605
+ isEnabled,
606
+ shouldActive
607
+ ]);
608
+ // The handler doesn't take event.defaultPrevented into account:
609
+ //
610
+ // event.preventDefault() is meant to stop default behaviors like
611
+ // clicking a checkbox to check it, hitting a button to submit a form,
612
+ // and hitting left arrow to move the cursor in a text input etc.
613
+ // Only special HTML elements have these default behaviors.
614
+ const handleClickAway = useEventCallback((event)=>{
615
+ // Given developers can stop the propagation of the synthetic event,
616
+ // we can only be confident with a positive value.
359
617
  const insideReactTree = syntheticEventRef.current;
360
618
  syntheticEventRef.current = false;
361
619
  const doc = unstable_ownerDocument(target);
362
- if (!activatedRef.current ||
363
- !target ||
364
- ('clientX' in event && clickedRootScrollbar(event, doc))) {
620
+ // 1. IE11 support, which trigger the handleClickAway even after the unbind
621
+ // 2. The child might render null.
622
+ // 3. Behave like a blur listener.
623
+ if (!activatedRef.current || !target || 'clientX' in event && clickedRootScrollbar(event, doc)) {
365
624
  return;
366
625
  }
626
+ // Do not act if user performed touchmove
367
627
  if (movedRef.current) {
368
628
  movedRef.current = false;
369
629
  return;
370
630
  }
371
631
  let insideDOM;
632
+ // If not enough, can use https://github.com/DieterHolvoet/event-propagation-path/blob/master/propagationPath.js
372
633
  if (event.composedPath) {
373
634
  insideDOM = event.composedPath().indexOf(target) > -1;
374
- }
375
- else {
376
- insideDOM =
377
- !doc.documentElement.contains(event.target) ||
378
- target.contains(event.target);
635
+ } else {
636
+ insideDOM = !doc.documentElement.contains(// @ts-expect-error returns `false` as intended when not dispatched from a Node
637
+ event.target) || target.contains(// @ts-expect-error returns `false` as intended when not dispatched from a Node
638
+ event.target);
379
639
  }
380
640
  if (!insideDOM && (disableReactTree || !insideReactTree)) {
381
641
  onClickAway(event);
382
642
  }
383
643
  });
384
- const handleSynthetic = (e) => {
644
+ // Keep track of mouse/touch events that bubbled up through the portal.
645
+ const handleSynthetic = (e)=>{
385
646
  syntheticEventRef.current = true;
386
647
  };
387
- useEffect(() => {
388
- if (!isEnabled)
389
- return;
648
+ useEffect(()=>{
649
+ if (!isEnabled) return;
390
650
  if (target) {
391
651
  const doc = unstable_ownerDocument(target);
392
- const handleTouchMove = () => {
652
+ const handleTouchMove = ()=>{
393
653
  movedRef.current = true;
394
654
  };
395
655
  doc.addEventListener('touchend', handleClickAway);
396
656
  doc.addEventListener('touchmove', handleTouchMove);
397
657
  doc.addEventListener('click', handleClickAway);
398
- return () => {
658
+ return ()=>{
399
659
  doc.removeEventListener('touchend', handleClickAway);
400
660
  doc.removeEventListener('touchmove', handleTouchMove);
401
661
  doc.removeEventListener('click', handleClickAway);
402
662
  };
403
663
  }
404
664
  return undefined;
405
- }, [handleClickAway, isEnabled, target]);
665
+ }, [
666
+ handleClickAway,
667
+ isEnabled,
668
+ target
669
+ ]);
406
670
  return {
407
671
  clickAwayRef,
408
672
  clickAwayHandler: {
409
673
  onClick: handleSynthetic,
410
- onTouchEnd: handleSynthetic,
411
- },
674
+ onTouchEnd: handleSynthetic
675
+ }
412
676
  };
413
677
  };
414
678
 
415
- const useDebounce = (fn, debounceTime = 200, options) => {
679
+
680
+
681
+
682
+
683
+
684
+
685
+
686
+
687
+
688
+
689
+
690
+
691
+ /**
692
+ * provide a debounce method for debounce method
693
+ * @param fn method for debounce
694
+ * @param debounceTime debounce timeout value, default is `200`ms
695
+ * @param options debounce setting, default is `{trailing: true, leading: flase}`, detail see lodash
696
+ */ // oxlint-disable-next-line @typescript-eslint/no-explicit-any
697
+ const useDebounce = (fn, debounceTime = 200, options)=>{
416
698
  const memoFn = useEventCallback(fn);
699
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
417
700
  const debounceFn = useCallback(debounce(memoFn, debounceTime, options), []);
418
- useEffect(() => () => debounceFn.cancel(), []);
701
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
702
+ useEffect(()=>()=>debounceFn.cancel(), []);
419
703
  return debounceFn;
420
704
  };
421
705
 
422
- function useEventListener(target, key, callback, options) {
706
+
707
+
708
+
709
+
710
+
711
+
712
+
713
+
714
+ /**
715
+ * bind event when component mount and auto remove event when destroy.
716
+ * can remove listener by call returned function or pass empty 'target'.
717
+ *
718
+ * @param target
719
+ * listener will be removed when target is changed to null,
720
+ * or rebinding to new element when `ref.current` change
721
+ * @returns a method object
722
+ * - listen() listen listener again, if that have listener, that will not listen again
723
+ * - remove() remove listener
724
+ * @example
725
+ * ```ts
726
+ * useEventListener(window, 'keyup', () => console.log('window key up'))
727
+ * ```
728
+ */ function useEventListener(target, key, callback, options) {
423
729
  const { startImmediately = true, ...addEventListenerOptions } = options ?? {};
424
730
  const listener = useEventCallback(callback);
425
731
  const cancelRef = useRef(noop);
426
732
  const bindRef = useRef(noop);
427
- useEffect(() => {
428
- if (!target)
429
- return;
733
+ useEffect(()=>{
734
+ if (!target) return;
430
735
  const currentElm = getRefElement(target);
431
736
  if (!currentElm) {
432
737
  return cancelRef.current;
433
738
  }
434
- bindRef.current = () => currentElm.addEventListener(key, listener, addEventListenerOptions);
435
- cancelRef.current = () => currentElm.removeEventListener(key, listener, addEventListenerOptions);
739
+ bindRef.current = ()=>currentElm.addEventListener(key, listener, addEventListenerOptions);
740
+ cancelRef.current = ()=>currentElm.removeEventListener(key, listener, addEventListenerOptions);
436
741
  if (startImmediately) {
437
742
  bindRef.current();
438
743
  }
439
744
  return cancelRef.current;
440
- }, [target]);
745
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
746
+ }, [
747
+ target
748
+ ]);
441
749
  return {
442
- listen: () => bindRef.current(),
443
- remove: () => cancelRef.current(),
750
+ listen: ()=>bindRef.current(),
751
+ remove: ()=>cancelRef.current()
444
752
  };
445
753
  }
446
754
 
447
- const defaultEverTargetValue = (value) => !!value;
448
- const useEver = (value, targetValue = defaultEverTargetValue) => {
755
+
756
+
757
+
758
+
759
+ const defaultEverTargetValue = (value)=>!!value;
760
+ /**
761
+ * that value ever to be `you want value`
762
+ * Return true when the value ever to be `you want value` once.
763
+ *
764
+ * @example
765
+ * ```ts
766
+ * // once that isOpen is true, that result will always be `true`
767
+ * const isEverOpen = useEver(isOpen);
768
+ *
769
+ * const isEverToBe2 = useEver(count, (val) => val === 2);
770
+ * ```
771
+ */ const useEver = (/** to be checked value */ value, /**
772
+ * value ever to be callback, default when value to be exited.
773
+ *
774
+ * @default (value) => !!value
775
+ */ targetValue = defaultEverTargetValue)=>{
449
776
  const result = useRef(false);
450
777
  if (!result.current) {
451
778
  result.current = targetValue(value);
@@ -453,6 +780,15 @@ const useEver = (value, targetValue = defaultEverTargetValue) => {
453
780
  return result.current;
454
781
  };
455
782
 
783
+
784
+
785
+
786
+
787
+
788
+ /* oxlint-disable sonarjs/no-collapsible-if */ /* oxlint-disable sonarjs/prefer-single-boolean-return */
789
+ // based on https://github.com/WICG/focus-visible/blob/v4.1.5/src/focus-visible.js
790
+
791
+
456
792
  let hadKeyboardEvent = true;
457
793
  let hadFocusVisibleRecently = false;
458
794
  const hadFocusVisibleRecentlyTimeout = new unstable_Timeout();
@@ -469,13 +805,17 @@ const inputTypesWhitelist = {
469
805
  week: true,
470
806
  time: true,
471
807
  datetime: true,
472
- 'datetime-local': true,
808
+ 'datetime-local': true
473
809
  };
474
- function focusTriggersKeyboardModality(node) {
810
+ /**
811
+ * Computes whether the given element should automatically trigger the
812
+ * `focus-visible` class being added, i.e. whether it should always match
813
+ * `:focus-visible` when focused.
814
+ * @param {Element} node
815
+ * @returns {boolean}
816
+ */ function focusTriggersKeyboardModality(node) {
475
817
  const { type, tagName } = node;
476
- if (tagName === 'INPUT' &&
477
- inputTypesWhitelist[type] &&
478
- !node.readOnly) {
818
+ if (tagName === 'INPUT' && inputTypesWhitelist[type] && !node.readOnly) {
479
819
  return true;
480
820
  }
481
821
  if (tagName === 'TEXTAREA' && !node.readOnly) {
@@ -486,31 +826,47 @@ function focusTriggersKeyboardModality(node) {
486
826
  }
487
827
  return false;
488
828
  }
489
- function handleKeyDown(event) {
829
+ /**
830
+ * Keep track of our keyboard modality state with `hadKeyboardEvent`.
831
+ * If the most recent user interaction was via the keyboard;
832
+ * and the key press did not include a meta, alt/option, or control key;
833
+ * then the modality is keyboard. Otherwise, the modality is not keyboard.
834
+ * @param {KeyboardEvent} event
835
+ */ function useIsFocusVisible_handleKeyDown(event) {
490
836
  if (event.metaKey || event.altKey || event.ctrlKey) {
491
837
  return;
492
838
  }
493
839
  hadKeyboardEvent = true;
494
840
  }
495
- function handlePointerDown() {
841
+ /**
842
+ * If at any point a user clicks with a pointing device, ensure that we change
843
+ * the modality away from keyboard.
844
+ * This avoids the situation where a user presses a key on an already focused
845
+ * element, and then clicks on a different element, focusing it with a
846
+ * pointing device, while we still think we're in keyboard modality.
847
+ */ function handlePointerDown() {
496
848
  hadKeyboardEvent = false;
497
849
  }
498
850
  function handleVisibilityChange() {
499
851
  if (this.visibilityState === 'hidden') {
852
+ // If the tab becomes active again, the browser will handle calling focus
853
+ // on the element (Safari actually calls it twice).
854
+ // If this tab change caused a blur on an element with focus-visible,
855
+ // re-apply the class when the user switches back to the tab.
500
856
  if (hadFocusVisibleRecently) {
501
857
  hadKeyboardEvent = true;
502
858
  }
503
859
  }
504
860
  }
505
861
  function prepare(doc) {
506
- doc.addEventListener('keydown', handleKeyDown, true);
862
+ doc.addEventListener('keydown', useIsFocusVisible_handleKeyDown, true);
507
863
  doc.addEventListener('mousedown', handlePointerDown, true);
508
864
  doc.addEventListener('pointerdown', handlePointerDown, true);
509
865
  doc.addEventListener('touchstart', handlePointerDown, true);
510
866
  doc.addEventListener('visibilitychange', handleVisibilityChange, true);
511
867
  }
512
868
  function teardown(doc) {
513
- doc.removeEventListener('keydown', handleKeyDown, true);
869
+ doc.removeEventListener('keydown', useIsFocusVisible_handleKeyDown, true);
514
870
  doc.removeEventListener('mousedown', handlePointerDown, true);
515
871
  doc.removeEventListener('pointerdown', handlePointerDown, true);
516
872
  doc.removeEventListener('touchstart', handlePointerDown, true);
@@ -520,22 +876,38 @@ function isFocusVisible(event) {
520
876
  const { target } = event;
521
877
  try {
522
878
  return target.matches(':focus-visible');
879
+ } catch (error) {
880
+ // Browsers not implementing :focus-visible will throw a SyntaxError.
881
+ // We use our own heuristic for those browsers.
882
+ // Rethrow might be better if it's not the expected error but do we really
883
+ // want to crash if focus-visible malfunctioned?
523
884
  }
524
- catch (error) {
525
- }
885
+ // No need for validFocusTarget check. The user does that by attaching it to
886
+ // focusable events only.
526
887
  return hadKeyboardEvent || focusTriggersKeyboardModality(target);
527
888
  }
528
889
  function useIsFocusVisible() {
529
- const ref = React.useCallback((node) => {
890
+ const ref = __rspack_external_react.useCallback((node)=>{
530
891
  if (node != null) {
531
892
  prepare(node.ownerDocument);
532
893
  }
533
894
  }, []);
534
- const isFocusVisibleRef = React.useRef(false);
535
- function handleBlurVisible() {
895
+ const isFocusVisibleRef = __rspack_external_react.useRef(false);
896
+ /**
897
+ * Should be called if a blur event is fired
898
+ */ function handleBlurVisible() {
899
+ // checking against potential state variable does not suffice if we focus and blur synchronously.
900
+ // React wouldn't have time to trigger a re-render so `focusVisible` would be stale.
901
+ // Ideally we would adjust `isFocusVisible(event)` to look at `relatedTarget` for blur events.
902
+ // This doesn't work in IE11 due to https://github.com/facebook/react/issues/3751
903
+ // TODO: check again if React releases their internal changes to focus event handling (https://github.com/facebook/react/pull/19186).
536
904
  if (isFocusVisibleRef.current) {
905
+ // To detect a tab/window switch, we look for a blur event followed
906
+ // rapidly by a visibility change.
907
+ // If we don't see a visibility change within 100ms, it's probably a
908
+ // regular focus change.
537
909
  hadFocusVisibleRecently = true;
538
- hadFocusVisibleRecentlyTimeout.start(100, () => {
910
+ hadFocusVisibleRecentlyTimeout.start(100, ()=>{
539
911
  hadFocusVisibleRecently = false;
540
912
  });
541
913
  isFocusVisibleRef.current = false;
@@ -543,7 +915,9 @@ function useIsFocusVisible() {
543
915
  }
544
916
  return false;
545
917
  }
546
- function handleFocusVisible(event) {
918
+ /**
919
+ * Should be called if a blur event is fired
920
+ */ function handleFocusVisible(event) {
547
921
  if (isFocusVisible(event)) {
548
922
  isFocusVisibleRef.current = true;
549
923
  return true;
@@ -554,75 +928,105 @@ function useIsFocusVisible() {
554
928
  isFocusVisibleRef,
555
929
  onFocus: handleFocusVisible,
556
930
  onBlur: handleBlurVisible,
557
- ref,
931
+ ref
558
932
  };
559
933
  }
560
934
 
561
- const useFocusVisible = ({ onFocusVisible, onFocus, onBlur, within = false, } = {}) => {
562
- const { isFocusVisibleRef, onFocus: handleFocusVisible, onBlur: handleBlurVisible, ref: focusVisibleRef, } = useIsFocusVisible();
935
+ // NOTE: this hook has been removed from @mui/utils in https://github.com/mui/material-ui/pull/42467
936
+ // TODO: update related usage in components (remove useIsFocusVisible and use isFocusVisible instead, prefer to use native :focus-visible in new components):
937
+ // reference: https://github.com/mui/material-ui/commit/089c40ca82becca90a9e865f78b76b3816983c41#diff-cf7c814aad470aa459786651c67c4ea5df1004df21668cf3f7941a1fbd3a7ab5
938
+
939
+
940
+
941
+
942
+
943
+ /**
944
+ * bind event with a11y keyboard, `space`, `enter`, only trigger when key includes in `checkKeys`
945
+ *
946
+ * that will auto preventDefault by default, if you don't want that, set `false` with `preventDefault`
947
+ */ const useFocusVisible = ({ onFocusVisible, onFocus, onBlur, within = false } = {})=>{
948
+ const { isFocusVisibleRef, onFocus: handleFocusVisible, onBlur: handleBlurVisible, ref: focusVisibleRef } = useIsFocusVisible();
563
949
  const [focusVisible, setFocusVisible] = useState(false);
564
950
  const [focusVisibleWithin, setFocusVisibleWithin] = useState(false);
951
+ // Used only when `within: true` — tracks the global focus listener that
952
+ // detects focus leaving the container via DOM unmount (where the synthetic
953
+ // blur event is unreliable). Mirrors the pattern used by React Aria's
954
+ // useFocusWithin hook.
565
955
  const globalFocusListenerRef = useRef(null);
566
956
  const containerRef = useRef(null);
957
+ // Store ownerDocument from the container so the listener is registered on
958
+ // the correct document when rendering inside iframes or detached windows.
959
+ // Initialized to null to avoid referencing `document` during SSR.
567
960
  const ownerDocumentRef = useRef(null);
568
- const removeGlobalFocusListener = useCallback(() => {
961
+ const removeGlobalFocusListener = useCallback(()=>{
569
962
  if (globalFocusListenerRef.current) {
570
963
  ownerDocumentRef.current?.removeEventListener('focus', globalFocusListenerRef.current, true);
571
964
  globalFocusListenerRef.current = null;
572
965
  }
573
966
  }, []);
574
- useEffect(() => {
967
+ useEffect(()=>{
575
968
  return removeGlobalFocusListener;
576
- }, [removeGlobalFocusListener]);
577
- const handleBlur = (event) => {
969
+ }, [
970
+ removeGlobalFocusListener
971
+ ]);
972
+ const handleBlur = (event)=>{
578
973
  handleBlurVisible(event);
579
974
  if (isFocusVisibleRef.current === false) {
580
975
  setFocusVisible(false);
581
- if (within &&
582
- !event.currentTarget.contains(event.relatedTarget)) {
976
+ /**
977
+ * when focus element by press `tab`, will trigger focus event in next event loop
978
+ *
979
+ * if current target is containing related target and we don't check whether it is containing related target,
980
+ * `FocusVisibleWithin` will be false before `focus` trigger,
981
+ * then `FocusVisibleWithin` will be true after `focus` trigger
982
+ *
983
+ * And if current target is containing related target, `FocusVisibleWithin` will never be false
984
+ */ if (within && !event.currentTarget.contains(event.relatedTarget)) {
583
985
  setFocusVisibleWithin(false);
584
986
  }
585
987
  }
586
- if (within &&
587
- globalFocusListenerRef.current &&
588
- !event.currentTarget.contains(event.relatedTarget)) {
988
+ // When focus leaves the container via normal interaction (Tab / click),
989
+ // remove the global listener — it is only needed for the unmount path.
990
+ if (within && globalFocusListenerRef.current && !event.currentTarget.contains(event.relatedTarget)) {
589
991
  removeGlobalFocusListener();
590
992
  }
591
993
  onBlur?.(event);
592
994
  };
593
- const handleFocus = (event) => {
995
+ const handleFocus = (event)=>{
594
996
  let updateState;
595
997
  if (within) {
596
998
  if (event.target === event.currentTarget) {
597
- updateState = () => {
999
+ updateState = ()=>{
598
1000
  setFocusVisibleWithin(true);
599
1001
  setFocusVisible(true);
600
1002
  };
601
- }
602
- else {
603
- updateState = () => {
1003
+ } else {
1004
+ updateState = ()=>{
604
1005
  setFocusVisibleWithin(true);
605
1006
  };
606
1007
  }
607
- }
608
- else if (event.target === event.currentTarget) {
609
- updateState = () => {
1008
+ } else if (event.target === event.currentTarget) {
1009
+ updateState = ()=>{
610
1010
  setFocusVisible(true);
611
1011
  };
612
- }
613
- else {
1012
+ } else {
614
1013
  return;
615
1014
  }
616
1015
  handleFocusVisible(event);
617
1016
  if (isFocusVisibleRef.current === true) {
618
1017
  updateState();
619
1018
  onFocusVisible?.(event);
1019
+ // Register a global focus listener so that if the focused descendant is
1020
+ // removed from the DOM (making the synthetic blur unreliable), we still
1021
+ // detect when focus moves outside and reset focusVisibleWithin correctly.
1022
+ // Mirrors the approach in React Aria's useFocusWithin:
1023
+ // https://github.com/adobe/react-spectrum/blob/main/packages/react-aria/src/interactions/useFocusWithin.ts
620
1024
  if (within) {
621
1025
  removeGlobalFocusListener();
622
1026
  containerRef.current = event.currentTarget;
623
1027
  ownerDocumentRef.current = event.currentTarget.ownerDocument;
624
1028
  const ownerDoc = ownerDocumentRef.current;
625
- const listener = (e) => {
1029
+ const listener = (e)=>{
626
1030
  if (e.target && !containerRef.current?.contains(e.target)) {
627
1031
  setFocusVisibleWithin(false);
628
1032
  ownerDoc.removeEventListener('focus', listener, true);
@@ -632,8 +1036,7 @@ const useFocusVisible = ({ onFocusVisible, onFocus, onBlur, within = false, } =
632
1036
  globalFocusListenerRef.current = listener;
633
1037
  ownerDoc.addEventListener('focus', listener, true);
634
1038
  }
635
- }
636
- else if (within) {
1039
+ } else if (within) {
637
1040
  setFocusVisibleWithin(false);
638
1041
  removeGlobalFocusListener();
639
1042
  }
@@ -642,55 +1045,97 @@ const useFocusVisible = ({ onFocusVisible, onFocus, onBlur, within = false, } =
642
1045
  return {
643
1046
  focusVisibleHandlers: {
644
1047
  onBlur: handleBlur,
645
- onFocus: handleFocus,
1048
+ onFocus: handleFocus
646
1049
  },
1050
+ // oxlint-disable-next-line @typescript-eslint/no-explicit-any
647
1051
  focusVisibleRef: focusVisibleRef,
648
1052
  focusVisible,
649
- focusVisibleWithin,
1053
+ focusVisibleWithin
650
1054
  };
651
1055
  };
652
1056
 
653
- function useForceUpdate() {
1057
+
1058
+
1059
+
1060
+
1061
+
1062
+ /**
1063
+ * a method for trigger force update
1064
+ * @returns force update method
1065
+ */ function useForceUpdate() {
654
1066
  const [, dispatch] = useState(Object.create(null));
655
- return useEventCallback(() => {
1067
+ return useEventCallback(()=>{
656
1068
  dispatch(Object.create(null));
657
1069
  });
658
1070
  }
659
1071
 
660
- const useForkRef = unstable_useForkRef;
1072
+
1073
+
1074
+
1075
+
1076
+ /**
1077
+ * That method will give you ability fork same value `ref object` or `method`
1078
+ * Example:
1079
+ * ```ts
1080
+ * const refA = useRef();
1081
+ * const refB = useRef();
1082
+ *
1083
+ * const ref = useForkRef(refA, refB)
1084
+ *
1085
+ * useEffect(() => {
1086
+ * console.log(refA); // get that current div dom
1087
+ * console.log(refB); // also get that current div dom
1088
+ * },[])
1089
+ *
1090
+ * return <div ref={ref} />
1091
+ * ```
1092
+ */ const useForkRef = unstable_useForkRef;
1093
+
1094
+
1095
+
661
1096
 
662
1097
  const globalListenerEventMap = new Map();
663
- const createGlobalListener = (key, listener, options, { customKey = key, target: targetProp = window, } = {}) => {
1098
+ /**
1099
+ * create globalListener handler for share one listener event
1100
+ *
1101
+ * @example
1102
+ * ```ts
1103
+ * const globalListener = createGlobalListener('focus', () => { console.log('focus') });
1104
+ * const globalListener2 = createGlobalListener('focus', () => { console.log('focus') });
1105
+ * globalListener.listen();
1106
+ * globalListener2.listen();
1107
+ *
1108
+ * => // that will only create an event listener on window, but every callback will get emit value when event triggered
1109
+ * ```
1110
+ */ const createGlobalListener = (key, listener, options, { customKey = key, target: targetProp = window } = {})=>{
664
1111
  let listening = false;
665
- const getMapValue = () => globalListenerEventMap.get(customKey);
666
- const addListener = () => {
667
- if (listening)
668
- return;
1112
+ const getMapValue = ()=>globalListenerEventMap.get(customKey);
1113
+ const addListener = ()=>{
1114
+ if (listening) return;
669
1115
  const savedEvent = getMapValue();
670
1116
  if (!savedEvent?.exec) {
671
- const exec = (e) => {
1117
+ const exec = (e)=>{
672
1118
  const event = getMapValue();
673
- event?.listeners.forEach((c) => c(e));
1119
+ event?.listeners.forEach((c)=>c(e));
674
1120
  };
675
1121
  globalListenerEventMap.set(customKey, {
676
1122
  exec,
677
- listeners: new Set([listener]),
1123
+ listeners: new Set([
1124
+ listener
1125
+ ])
678
1126
  });
679
1127
  const target = getRefElement(targetProp);
680
1128
  target?.addEventListener(key, exec, options);
681
- }
682
- else {
1129
+ } else {
683
1130
  savedEvent.listeners.add(listener);
684
1131
  }
685
1132
  listening = true;
686
1133
  };
687
- const removeListener = () => {
688
- if (!listening)
689
- return;
1134
+ const removeListener = ()=>{
1135
+ if (!listening) return;
690
1136
  listening = false;
691
1137
  const _savedEvent = getMapValue();
692
- if (!_savedEvent)
693
- return;
1138
+ if (!_savedEvent) return;
694
1139
  const { listeners } = _savedEvent;
695
1140
  listeners.delete(listener);
696
1141
  if (listeners.size === 0) {
@@ -700,34 +1145,80 @@ const createGlobalListener = (key, listener, options, { customKey = key, target:
700
1145
  }
701
1146
  };
702
1147
  return {
703
- listen: () => addListener(),
704
- remove: () => removeListener(),
705
- get state() {
1148
+ listen: ()=>addListener(),
1149
+ /**
1150
+ * remove listener
1151
+ */ remove: ()=>removeListener(),
1152
+ /**
1153
+ * current listener state
1154
+ */ get state () {
706
1155
  return {
707
- get count() {
1156
+ /**
1157
+ * current listener state count
1158
+ */ get count () {
708
1159
  return getMapValue()?.listeners.size || 0;
709
1160
  },
710
- listening,
1161
+ /**
1162
+ * is that be listening
1163
+ */ listening
711
1164
  };
712
- },
1165
+ }
713
1166
  };
714
1167
  };
715
1168
 
716
- function useGlobalListener(key, listener, ...args) {
1169
+
1170
+
1171
+
1172
+
1173
+
1174
+ /**
1175
+ * bind global event, when you bind same key event,
1176
+ * that will reuse one event listener and trigger both callback once that listener be triggered.
1177
+ *
1178
+ * also you can control listener with method `listen` and `remove`
1179
+ * and get listener `state` for check listener count number and current listing state.
1180
+ *
1181
+ * config:
1182
+ * - `target`: custom binding event target, default is `window`
1183
+ * - `customKey`: custom key for determining different event group, default is same as `key`
1184
+ * - `startImmediately`: start listener immediately, default is `true`
1185
+ *
1186
+ * * `key`, `options` only work when set first time, never change after first render
1187
+ *
1188
+ * @example
1189
+ * ```ts
1190
+ * useGlobalListener('keyup', () => console.log('window key up1'))
1191
+ *
1192
+ * useGlobalListener('keyup', () => console.log('window key up2'))
1193
+ *
1194
+ * useGlobalListener('touchend', () => console.log('window key up3'), {
1195
+ * target: someWantHostElement,
1196
+ * customKey: 'hostTouchend',
1197
+ * })
1198
+ *
1199
+ * useGlobalListener('touchend', () => console.log('window key up3'), { passive: false }, {
1200
+ * target: someWantHostElement,
1201
+ * customKey: 'hostTouchend',
1202
+ * })
1203
+ *
1204
+ * => // that will only create an event listener on `window`, but every callback will get emit value when event triggered
1205
+ * ```
1206
+ */ function useGlobalListener(key, listener, ...args) {
717
1207
  const { options, config } = getListenerOverloadOption(args);
718
- const { customKey = key, target: targetProp = window, startImmediately = true, } = config || {};
1208
+ const { customKey = key, target: targetProp = window, startImmediately = true } = config || {};
719
1209
  const _listener = useEventCallback(listener);
720
1210
  const { current: globalListener } = useRef(createGlobalListener(key, _listener, options, {
721
1211
  customKey,
722
- target: targetProp,
1212
+ target: targetProp
723
1213
  }));
724
1214
  if (process.env.NODE_ENV !== 'production') {
725
- useEffect(() => {
1215
+ // oxlint-disable-next-line react-hooks/rules-of-hooks
1216
+ useEffect(()=>{
726
1217
  if (customKey === key) {
727
1218
  if (Object.keys(options).length > 0) {
728
1219
  logInDev({
729
1220
  scope: 'useGlobalListener',
730
- message: 'when have set options, suggest you also bind customKey, otherwise, it may use previous listener without same options',
1221
+ message: 'when have set options, suggest you also bind customKey, otherwise, it may use previous listener without same options'
731
1222
  });
732
1223
  }
733
1224
  const target = getRefElement(targetProp);
@@ -735,17 +1226,19 @@ function useGlobalListener(key, listener, ...args) {
735
1226
  logInDev({
736
1227
  scope: 'useGlobalListener',
737
1228
  message: 'When you custom binding event target, you must custom key for determining different event group',
738
- level: 'error',
1229
+ level: 'error'
739
1230
  });
740
1231
  }
741
1232
  }
1233
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
742
1234
  }, []);
743
1235
  }
744
- useEffect(() => {
1236
+ useEffect(()=>{
745
1237
  if (startImmediately) {
746
1238
  globalListener.listen();
747
1239
  }
748
1240
  return globalListener.remove;
1241
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
749
1242
  }, []);
750
1243
  return globalListener;
751
1244
  }
@@ -755,30 +1248,40 @@ function getListenerOverloadOption(args) {
755
1248
  if (typeof args[0] === 'boolean') {
756
1249
  options = args[0];
757
1250
  config = args[1];
758
- }
759
- else {
1251
+ } else {
760
1252
  const { startImmediately, customKey, target, ...restOptions } = {
761
1253
  ...args[0],
762
- ...args[1],
1254
+ ...args[1]
763
1255
  };
764
1256
  options = restOptions;
765
1257
  config = {
766
1258
  startImmediately,
767
1259
  customKey,
768
- target,
1260
+ target
769
1261
  };
770
1262
  }
771
- return { options, config };
1263
+ return {
1264
+ options,
1265
+ config
1266
+ };
772
1267
  }
773
1268
 
774
- const useHiddenTabindex = (target) => {
775
- const handleFocus = useEventCallback(() => {
1269
+
1270
+
1271
+
1272
+
1273
+
1274
+ /**
1275
+ * group hidden logic, when binding events element focus,
1276
+ * that hiddenRef will auto be `tabindex={-1}` *
1277
+ */ const useHiddenTabindex = (target)=>{
1278
+ const handleFocus = useEventCallback(()=>{
776
1279
  const hiddenElm = getRefElement(target);
777
1280
  if (hiddenElm && hiddenElm.tabIndex === 0) {
778
1281
  hiddenElm.tabIndex = -1;
779
1282
  }
780
1283
  });
781
- const handleBlur = useEventCallback(() => {
1284
+ const handleBlur = useEventCallback(()=>{
782
1285
  const hiddenElm = getRefElement(target);
783
1286
  if (hiddenElm && hiddenElm.tabIndex === -1) {
784
1287
  hiddenElm.tabIndex = 0;
@@ -786,86 +1289,111 @@ const useHiddenTabindex = (target) => {
786
1289
  });
787
1290
  return {
788
1291
  onFocus: handleFocus,
789
- onBlur: handleBlur,
1292
+ onBlur: handleBlur
790
1293
  };
791
1294
  };
792
1295
 
793
- const useHover = ({ onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, disabled = false, disableReactTree = true, } = {}) => {
1296
+
1297
+
1298
+
1299
+
1300
+ const useHover = ({ onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, disabled = false, disableReactTree = true } = {})=>{
794
1301
  const [hover, setHover] = useState(false);
795
1302
  const hoverRef = useRef(false);
796
- useEffect(() => {
1303
+ useEffect(()=>{
797
1304
  if (disabled) {
798
1305
  setHover(false);
799
- }
800
- else if (!disabled && hoverRef.current) {
1306
+ } else if (!disabled && hoverRef.current) {
801
1307
  setHover(true);
802
1308
  }
803
- }, [disabled]);
804
- const updateHoverState = (hover, event) => {
805
- if (disableReactTree &&
806
- !event.currentTarget.contains(event.target)) {
1309
+ }, [
1310
+ disabled
1311
+ ]);
1312
+ const updateHoverState = (hover, event)=>{
1313
+ if (disableReactTree && !event.currentTarget.contains(event.target)) {
807
1314
  return;
808
1315
  }
809
- if (!disabled)
810
- setHover(hover);
1316
+ if (!disabled) setHover(hover);
811
1317
  hoverRef.current = hover;
812
1318
  };
813
- const onMouseEnter = (event) => {
1319
+ const onMouseEnter = (event)=>{
814
1320
  handleMouseEnter?.(event);
815
1321
  updateHoverState(true, event);
816
1322
  };
817
- const onMouseLeave = (event) => {
1323
+ const onMouseLeave = (event)=>{
818
1324
  handleMouseLeave?.(event);
819
1325
  updateHoverState(false, event);
820
1326
  };
821
1327
  return {
822
- hoverHandlers: { onMouseEnter, onMouseLeave },
823
- hover,
1328
+ hoverHandlers: {
1329
+ onMouseEnter,
1330
+ onMouseLeave
1331
+ },
1332
+ hover
824
1333
  };
825
1334
  };
826
1335
 
827
- const useHoverDirty = ({ target, disabled = false, }) => {
1336
+
1337
+
1338
+
1339
+ const useHoverDirty = ({ target, disabled = false })=>{
828
1340
  const [hover, setHover] = useState(false);
829
- useEffect(() => {
830
- if (disabled || !target)
831
- setHover(false);
832
- }, [disabled, target]);
833
- useEventListener(target, 'mouseenter', () => {
834
- if (disabled)
835
- return;
1341
+ useEffect(()=>{
1342
+ if (disabled || !target) setHover(false);
1343
+ }, [
1344
+ disabled,
1345
+ target
1346
+ ]);
1347
+ useEventListener(target, 'mouseenter', ()=>{
1348
+ if (disabled) return;
836
1349
  setHover(true);
837
1350
  });
838
- useEventListener(target, 'mouseleave', () => {
839
- if (disabled)
840
- return;
1351
+ useEventListener(target, 'mouseleave', ()=>{
1352
+ if (disabled) return;
841
1353
  setHover(false);
842
1354
  });
843
1355
  return {
844
- hover,
1356
+ hover
845
1357
  };
846
1358
  };
847
1359
 
848
- const useImgLoaded = ({ crossOrigin, referrerPolicy, src, srcSet, }) => {
1360
+
1361
+
1362
+
1363
+
1364
+
1365
+ const useIsoLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
1366
+ /**
1367
+ * get load state of an image by passing src or srcSet
1368
+ * - false: not load yet
1369
+ * - 'loaded': successfully loaded
1370
+ * - 'error': fail to load
1371
+ *
1372
+ * @example
1373
+ * ```ts
1374
+ * const loaded = useImgLoaded({
1375
+ * src,
1376
+ * srcSet,
1377
+ * });
1378
+ * ```
1379
+ */ const useImgLoaded = ({ crossOrigin, referrerPolicy, src, srcSet })=>{
849
1380
  const [loaded, setLoaded] = useState(false);
850
- useEffect(() => {
1381
+ useIsoLayoutEffect(()=>{
851
1382
  if (!src && !srcSet) {
1383
+ setLoaded('error');
852
1384
  return undefined;
853
1385
  }
854
1386
  setLoaded(false);
855
1387
  let active = true;
856
- const image = new Image();
857
- image.onload = () => {
858
- if (!active) {
859
- return;
860
- }
861
- setLoaded('loaded');
862
- };
863
- image.onerror = () => {
864
- if (!active) {
865
- return;
866
- }
867
- setLoaded('error');
868
- };
1388
+ const image = new window.Image();
1389
+ const updateLoaded = (status)=>()=>{
1390
+ if (!active) {
1391
+ return;
1392
+ }
1393
+ setLoaded(status);
1394
+ };
1395
+ image.onload = updateLoaded('loaded');
1396
+ image.onerror = updateLoaded('error');
869
1397
  crossOrigin && (image.crossOrigin = crossOrigin);
870
1398
  referrerPolicy && (image.referrerPolicy = referrerPolicy);
871
1399
  if (src) {
@@ -874,71 +1402,133 @@ const useImgLoaded = ({ crossOrigin, referrerPolicy, src, srcSet, }) => {
874
1402
  if (srcSet) {
875
1403
  image.srcset = srcSet;
876
1404
  }
877
- return () => {
1405
+ if (image.complete) {
1406
+ setLoaded(image.naturalWidth > 0 ? 'loaded' : 'error');
1407
+ }
1408
+ return ()=>{
878
1409
  active = false;
879
1410
  };
880
- }, [crossOrigin, referrerPolicy, src, srcSet]);
1411
+ }, [
1412
+ crossOrigin,
1413
+ referrerPolicy,
1414
+ src,
1415
+ srcSet
1416
+ ]);
881
1417
  return loaded;
882
1418
  };
883
1419
 
884
- const useInterval = (callback, delay = 0, startImmediately = true) => {
1420
+
1421
+
1422
+
1423
+
1424
+
1425
+ /**
1426
+ * controllable interval, provide easy way to control interval
1427
+ *
1428
+ * - cancel: cancel current interval
1429
+ * - pause: pause current interval
1430
+ * - play: play current interval
1431
+ * - getRunning: return current interval running state
1432
+ *
1433
+ * @example
1434
+ * ```ts
1435
+ * const { cancel, play, pause } = useInterval(() => {
1436
+ * console.log('interval be triggered');
1437
+ * }, 1000)
1438
+ *
1439
+ * // get current run times
1440
+ * useInterval((times) => {
1441
+ * console.log('current times start from 1', times);
1442
+ * }, 2000)
1443
+ *
1444
+ * // not start interval immediately
1445
+ * useInterval((times) => {
1446
+ * console.log('current times start from 1', times);
1447
+ * }, 2000, false)
1448
+ * ```
1449
+ */ const useInterval = (/** callback with times, times start from 1 */ callback, /** what delay time of that interval */ delay = 0, /** is that start interval immediately */ startImmediately = true)=>{
885
1450
  const intervalRef = useRef();
886
1451
  const countRef = useRef(0);
887
- const getRunning = useEventCallback(() => !!intervalRef.current);
888
- const savedCallback = useEventCallback((times) => {
1452
+ const getRunning = useEventCallback(()=>!!intervalRef.current);
1453
+ const savedCallback = useEventCallback((times)=>{
889
1454
  callback(times);
890
1455
  });
891
- const pause = useCallback(() => {
1456
+ const pause = useCallback(()=>{
892
1457
  if (intervalRef.current) {
893
1458
  clearInterval(intervalRef.current);
894
1459
  intervalRef.current = undefined;
895
1460
  }
896
1461
  }, []);
897
- const cancel = useCallback(() => {
1462
+ const cancel = useCallback(()=>{
898
1463
  pause();
899
1464
  countRef.current = 0;
900
- }, [pause]);
901
- const play = useCallback(() => {
1465
+ }, [
1466
+ pause
1467
+ ]);
1468
+ const play = useCallback(()=>{
902
1469
  const isRunning = getRunning();
903
- if (isRunning)
904
- return;
905
- intervalRef.current = setInterval(() => {
1470
+ if (isRunning) return;
1471
+ intervalRef.current = setInterval(()=>{
906
1472
  countRef.current++;
907
1473
  savedCallback(countRef.current);
908
1474
  }, delay);
909
- }, [delay, getRunning, savedCallback]);
910
- useEffect(() => {
1475
+ }, [
1476
+ delay,
1477
+ getRunning,
1478
+ savedCallback
1479
+ ]);
1480
+ useEffect(()=>{
911
1481
  if (startImmediately) {
912
1482
  play();
913
1483
  }
914
- return () => cancel();
915
- }, [cancel, play, startImmediately]);
916
- return {
1484
+ return ()=>cancel();
1485
+ }, [
917
1486
  cancel,
918
- pause,
919
1487
  play,
920
- getRunning,
1488
+ startImmediately
1489
+ ]);
1490
+ return {
1491
+ /**
1492
+ * cancel current interval
1493
+ */ cancel,
1494
+ /**
1495
+ * pause current interval
1496
+ */ pause,
1497
+ /**
1498
+ * play current interval
1499
+ */ play,
1500
+ /**
1501
+ * return current interval running state
1502
+ */ getRunning
921
1503
  };
922
1504
  };
923
1505
 
924
- const useKeyDownOnce = (includeKeys, { onKeyDown, onKeyUp }) => {
1506
+
1507
+
1508
+
1509
+
1510
+
1511
+ /**
1512
+ * Provide user trigger `onKeyDown` only one time when hold press on key.
1513
+ * @param includeKeys that check include keys
1514
+ * @param methods trigger when first keydown or keyup
1515
+ * @returns
1516
+ */ const useKeyDownOnce = (includeKeys, { onKeyDown, onKeyUp })=>{
925
1517
  const isKeyDownMapRef = useRef({});
926
- const handleKeyDown = useEventCallback((e) => {
1518
+ const handleKeyDown = useEventCallback((e)=>{
927
1519
  if (includeKeys.includes(e.key)) {
928
1520
  if (!isKeyDownMapRef.current[e.key]) {
929
1521
  onKeyDown?.(e);
930
- }
931
- else {
1522
+ } else {
932
1523
  e.preventDefault();
933
1524
  e.stopPropagation();
934
1525
  }
935
1526
  isKeyDownMapRef.current[e.key] = true;
936
- }
937
- else {
1527
+ } else {
938
1528
  onKeyDown?.(e);
939
1529
  }
940
1530
  });
941
- const handleKeyUp = useEventCallback((e) => {
1531
+ const handleKeyUp = useEventCallback((e)=>{
942
1532
  if (includeKeys.includes(e.key)) {
943
1533
  isKeyDownMapRef.current[e.key] = false;
944
1534
  }
@@ -946,48 +1536,57 @@ const useKeyDownOnce = (includeKeys, { onKeyDown, onKeyUp }) => {
946
1536
  });
947
1537
  return {
948
1538
  onKeyDown: handleKeyDown,
949
- onKeyUp: handleKeyUp,
1539
+ onKeyUp: handleKeyUp
950
1540
  };
951
1541
  };
952
1542
 
953
- function moveIndexInTwoDimension(direction, options) {
1543
+
1544
+
1545
+ /**
1546
+ * that can move index in column array
1547
+ */ function moveIndexInTwoDimension(direction, options) {
954
1548
  const { currIndex, columns, total, infinite } = options;
1549
+ // start index never small than 0
1550
+ // const index = Math.max(currIndex, 0);
955
1551
  const lastIndex = total - 1;
956
1552
  const isInOrder = infinite === 'order';
957
1553
  const fillTotal = Math.ceil(total / columns) * columns;
958
1554
  let resultIndex;
959
- switch (direction) {
960
- case 'ArrowUp': {
961
- const toIndex = (Math.max(currIndex, 0) - columns + fillTotal) % fillTotal;
962
- resultIndex = infinite ? toIndex : Math.min(toIndex, currIndex);
963
- break;
964
- }
965
- case 'ArrowRight': {
966
- if (isInOrder) {
967
- const tmp = currIndex + 1;
968
- resultIndex = tmp > lastIndex ? 0 : tmp;
1555
+ switch(direction){
1556
+ case 'ArrowUp':
1557
+ {
1558
+ const toIndex = (Math.max(currIndex, 0) - columns + fillTotal) % fillTotal;
1559
+ resultIndex = infinite ? toIndex : Math.min(toIndex, currIndex);
969
1560
  break;
970
1561
  }
971
- const toIndex = Math.floor(currIndex / columns) * columns + ((currIndex + 1) % columns);
972
- resultIndex = infinite ? toIndex : Math.max(toIndex, currIndex);
973
- break;
974
- }
975
- case 'ArrowDown': {
976
- const toIndex = (currIndex + columns) % fillTotal;
977
- resultIndex = infinite ? toIndex : Math.max(toIndex, currIndex);
978
- break;
979
- }
980
- case 'ArrowLeft': {
981
- if (isInOrder) {
982
- const tmp = currIndex - 1;
983
- resultIndex = tmp < 0 ? lastIndex : tmp;
1562
+ case 'ArrowRight':
1563
+ {
1564
+ if (isInOrder) {
1565
+ const tmp = currIndex + 1;
1566
+ resultIndex = tmp > lastIndex ? 0 : tmp;
1567
+ break;
1568
+ }
1569
+ const toIndex = Math.floor(currIndex / columns) * columns + (currIndex + 1) % columns;
1570
+ resultIndex = infinite ? toIndex : Math.max(toIndex, currIndex);
1571
+ break;
1572
+ }
1573
+ case 'ArrowDown':
1574
+ {
1575
+ const toIndex = (currIndex + columns) % fillTotal;
1576
+ resultIndex = infinite ? toIndex : Math.max(toIndex, currIndex);
1577
+ break;
1578
+ }
1579
+ case 'ArrowLeft':
1580
+ {
1581
+ if (isInOrder) {
1582
+ const tmp = currIndex - 1;
1583
+ resultIndex = tmp < 0 ? lastIndex : tmp;
1584
+ break;
1585
+ }
1586
+ const toIndex = Math.floor(currIndex / columns) * columns + (currIndex - 1 + columns) % columns;
1587
+ resultIndex = infinite ? toIndex : Math.min(toIndex, currIndex);
984
1588
  break;
985
1589
  }
986
- const toIndex = Math.floor(currIndex / columns) * columns +
987
- ((currIndex - 1 + columns) % columns);
988
- resultIndex = infinite ? toIndex : Math.min(toIndex, currIndex);
989
- break;
990
- }
991
1590
  case 'Home':
992
1591
  resultIndex = 0;
993
1592
  break;
@@ -1000,55 +1599,56 @@ function moveIndexInTwoDimension(direction, options) {
1000
1599
  if (!infinite && resultIndex >= total) {
1001
1600
  return currIndex;
1002
1601
  }
1602
+ // * when that bigger then sourceTotal, mean that touch end line
1603
+ // * move to current full latest and move again.
1003
1604
  if (resultIndex >= total) {
1004
1605
  if (direction === 'ArrowRight') {
1005
1606
  resultIndex = fillTotal - 1;
1006
1607
  }
1007
1608
  return moveIndexInTwoDimension(direction, {
1008
1609
  ...options,
1009
- currIndex: resultIndex,
1610
+ currIndex: resultIndex
1010
1611
  });
1011
1612
  }
1012
1613
  return resultIndex;
1013
1614
  }
1014
1615
 
1015
- const useKeyboardMoveFocus = ({ infinite, options = [], focusedIndexRef, onFocusedIndexChange, getOptionDisabled, getOptionSearchText, columns = 1, endSearchDebounceTime = 500, }) => {
1616
+
1617
+
1618
+
1619
+
1620
+ /**
1621
+ * use keyboard move index in two dimensional
1622
+ */ const useKeyboardMoveFocus = ({ infinite, options = [], focusedIndexRef, onFocusedIndexChange, getOptionDisabled, getOptionSearchText, columns = 1, endSearchDebounceTime = 500 })=>{
1016
1623
  const total = options.length;
1017
1624
  const searchText = useRef('');
1018
- const debounceEndSearch = useDebounce(() => (searchText.current = ''), endSearchDebounceTime);
1019
- const searchTextAndFocus = (event) => {
1625
+ const debounceEndSearch = useDebounce(()=>searchText.current = '', endSearchDebounceTime);
1626
+ const searchTextAndFocus = (event)=>{
1020
1627
  const key = event.key;
1021
- if (!getOptionSearchText || key.length !== 1)
1022
- return;
1628
+ if (!getOptionSearchText || key.length !== 1) return;
1023
1629
  debounceEndSearch();
1024
1630
  searchText.current += key.toLowerCase();
1025
- const matchedIndex = options.findIndex((option) => {
1631
+ const matchedIndex = options.findIndex((option)=>{
1026
1632
  const text = getOptionSearchText(option);
1027
- return Boolean(!getOptionDisabled?.(option) &&
1028
- text &&
1029
- text.toLowerCase().startsWith(searchText.current));
1633
+ return Boolean(!getOptionDisabled?.(option) && text && text.toLowerCase().startsWith(searchText.current));
1030
1634
  });
1031
1635
  if (matchedIndex > -1) {
1032
1636
  onFocusedIndexChange(event, matchedIndex);
1033
1637
  }
1034
1638
  };
1035
- const onKeyFocusedIndexHandle = (event) => {
1639
+ const onKeyFocusedIndexHandle = (event)=>{
1036
1640
  let currIndex = focusedIndexRef.current;
1037
1641
  const key = event.key;
1038
1642
  let findDirection;
1039
- if (total === 0)
1040
- return;
1041
- while (true) {
1042
- const toIndex = findDirection
1043
- ? findDirection === 'next'
1044
- ? currIndex + 1
1045
- : currIndex - 1
1046
- : moveIndexInTwoDimension(key, {
1047
- currIndex,
1048
- columns,
1049
- total,
1050
- infinite,
1051
- });
1643
+ if (total === 0) return;
1644
+ // oxlint-disable-next-line no-constant-condition
1645
+ while(true){
1646
+ const toIndex = findDirection ? findDirection === 'next' ? currIndex + 1 : currIndex - 1 : moveIndexInTwoDimension(key, {
1647
+ currIndex,
1648
+ columns,
1649
+ total,
1650
+ infinite
1651
+ });
1052
1652
  if (toIndex === currIndex) {
1053
1653
  searchTextAndFocus(event);
1054
1654
  break;
@@ -1058,83 +1658,107 @@ const useKeyboardMoveFocus = ({ infinite, options = [], focusedIndexRef, onFocus
1058
1658
  onFocusedIndexChange(event, toIndex);
1059
1659
  }
1060
1660
  break;
1061
- }
1062
- else {
1063
- if (focusedIndexRef.current !== -1 &&
1064
- toIndex === Math.max(focusedIndexRef.current, 0)) {
1661
+ } else {
1662
+ // * find toIndex is same as current, mean that already find all
1663
+ if (focusedIndexRef.current !== -1 && toIndex === Math.max(focusedIndexRef.current, 0)) {
1065
1664
  break;
1066
1665
  }
1666
+ // * if no item is focused currently and toIndex is already the last item checked,
1667
+ // * it means no item in the list is focusable
1067
1668
  if (focusedIndexRef.current === -1) {
1068
- if ((Math.abs(currIndex - toIndex) !== 1 || currIndex > toIndex) &&
1069
- toIndex === 0) {
1669
+ // from bottom to top and the first item in the list is not focusable
1670
+ if ((Math.abs(currIndex - toIndex) !== 1 || currIndex > toIndex) && toIndex === 0) {
1070
1671
  break;
1071
1672
  }
1072
- if (Math.abs(currIndex - toIndex) === 1 &&
1073
- currIndex < toIndex &&
1074
- toIndex === total - 1) {
1673
+ // from top to bottom and the last item in the list is not focusable
1674
+ if (Math.abs(currIndex - toIndex) === 1 && currIndex < toIndex && toIndex === total - 1) {
1075
1675
  break;
1076
1676
  }
1077
1677
  }
1078
1678
  currIndex = toIndex;
1679
+ // * if that is Hom or End, find use increase or decrease
1079
1680
  if (key === 'Home') {
1080
1681
  findDirection = 'next';
1081
- }
1082
- else if (key === 'End') {
1682
+ } else if (key === 'End') {
1083
1683
  findDirection = 'prev';
1084
1684
  }
1085
1685
  }
1086
1686
  }
1087
1687
  };
1088
- const getPrevFocusableOption = useCallback((index = focusedIndexRef.current, isInfinite = true) => {
1089
- for (let i = index; i >= 0; i--) {
1688
+ const getPrevFocusableOption = useCallback((index = focusedIndexRef.current, /** is that infinite search */ isInfinite = true)=>{
1689
+ for(let i = index; i >= 0; i--){
1090
1690
  if (!getOptionDisabled?.(options[i])) {
1091
1691
  return i;
1092
1692
  }
1093
1693
  }
1094
1694
  if (isInfinite) {
1095
- for (let i = total - 1; i > index; i--) {
1695
+ // loop find
1696
+ for(let i = total - 1; i > index; i--){
1096
1697
  if (!getOptionDisabled?.(options[i])) {
1097
1698
  return i;
1098
1699
  }
1099
1700
  }
1100
1701
  }
1101
1702
  return -1;
1102
- }, [focusedIndexRef, getOptionDisabled, options, total]);
1103
- const getNextFocusableOption = useCallback((index = focusedIndexRef.current, isInfinite = true) => {
1104
- for (let i = index; i < total; i++) {
1703
+ }, [
1704
+ focusedIndexRef,
1705
+ getOptionDisabled,
1706
+ options,
1707
+ total
1708
+ ]);
1709
+ const getNextFocusableOption = useCallback((index = focusedIndexRef.current, /** is that infinite search */ isInfinite = true)=>{
1710
+ for(let i = index; i < total; i++){
1105
1711
  if (!getOptionDisabled?.(options[i])) {
1106
1712
  return i;
1107
1713
  }
1108
1714
  }
1109
1715
  if (isInfinite) {
1110
- for (let i = 0; i < index; i++) {
1716
+ // loop find
1717
+ for(let i = 0; i < index; i++){
1111
1718
  if (!getOptionDisabled?.(options[i])) {
1112
1719
  return i;
1113
1720
  }
1114
1721
  }
1115
1722
  }
1116
1723
  return -1;
1117
- }, [focusedIndexRef, getOptionDisabled, options, total]);
1724
+ }, [
1725
+ focusedIndexRef,
1726
+ getOptionDisabled,
1727
+ options,
1728
+ total
1729
+ ]);
1118
1730
  return {
1119
- getPrevFocusableOption,
1120
- getNextFocusableOption,
1121
- onKeyFocusedIndexHandle,
1731
+ /**
1732
+ * Get previous focusable option(include current index).
1733
+ */ getPrevFocusableOption,
1734
+ /**
1735
+ * Get next focusable option(include current index).
1736
+ */ getNextFocusableOption,
1737
+ /**
1738
+ * Use to handle keyboard event.
1739
+ */ onKeyFocusedIndexHandle
1122
1740
  };
1123
1741
  };
1124
1742
 
1743
+
1744
+
1125
1745
  const MOVE_ATTR_KEY = 'data-move-focused';
1126
- const useOnlyOneFocusable = ({ focusedIndexRef, containerRef, }) => {
1127
- const moveFocusedId = useId(MOVE_ATTR_KEY, true);
1128
- const getIndexElm = (toIndex) => containerRef.current?.querySelector(`[${moveFocusedId}="${toIndex}"]`);
1129
- const setIndexTabIndex = (toIndex) => {
1746
+ const MOVE_INDEX_ATTR_KEY = 'data-move-index';
1747
+ /**
1748
+ * when focus in container below element with binding `getItemProps`,
1749
+ * that will only have one element can be focusable.
1750
+ */ const useOnlyOneFocusable = ({ focusedIndexRef, containerRef })=>{
1751
+ const moveFocusedId = useId();
1752
+ const getIndexElm = (toIndex)=>containerRef.current?.querySelector(`[${MOVE_ATTR_KEY}="${moveFocusedId}"][${MOVE_INDEX_ATTR_KEY}="${toIndex}"]`);
1753
+ const setIndexTabIndex = (toIndex)=>{
1130
1754
  const toElm = getIndexElm(toIndex);
1131
1755
  if (toElm) {
1132
- const prevElm = containerRef.current?.querySelector(`[${moveFocusedId}][tabindex="0"]`);
1756
+ const prevElm = containerRef.current?.querySelector(`[${MOVE_ATTR_KEY}="${moveFocusedId}"][tabindex="0"]`);
1133
1757
  prevElm?.setAttribute('tabindex', '-1');
1134
1758
  toElm.setAttribute('tabindex', '0');
1135
1759
  }
1136
1760
  };
1137
- const focusIndex = (toIndex) => {
1761
+ const focusIndex = (toIndex)=>{
1138
1762
  const toElm = getIndexElm(toIndex);
1139
1763
  toElm?.focus();
1140
1764
  };
@@ -1142,22 +1766,53 @@ const useOnlyOneFocusable = ({ focusedIndexRef, containerRef, }) => {
1142
1766
  moveFocusedId,
1143
1767
  focusIndex,
1144
1768
  setIndexTabIndex,
1145
- getItemProps: (index, options) => {
1769
+ getItemProps: (index, options)=>{
1146
1770
  return combineProps({
1147
- [moveFocusedId]: index,
1771
+ [MOVE_ATTR_KEY]: moveFocusedId,
1772
+ [MOVE_INDEX_ATTR_KEY]: String(index),
1148
1773
  tabIndex: index === focusedIndexRef.current ? 0 : -1,
1149
- onFocus: () => {
1774
+ onFocus: ()=>{
1150
1775
  focusedIndexRef.current = index;
1151
1776
  setIndexTabIndex(index);
1152
- },
1153
- }, options);
1154
- },
1777
+ }
1778
+ }, options ?? {});
1779
+ }
1155
1780
  };
1156
1781
  };
1157
1782
 
1158
- const useTouchMouseEvent = ({ onMouseDown: onMouseDownArg, onMouseUp: onMouseUpArg, onTouchStart: onTouchStartArg, onTouchEnd: onTouchEndArg, }, { actionRef } = {}) => {
1783
+
1784
+
1785
+
1786
+
1787
+
1788
+
1789
+
1790
+
1791
+
1792
+ /**
1793
+ * Provide a utils for switch trigger touch event or mouse event between different device
1794
+ *
1795
+ * - Trigger `touch` event in `touch support` device
1796
+ * - onTouchStart
1797
+ * - onTouchEnd
1798
+ * - Trigger `mouse` event in `non touch support` device
1799
+ * - onMouseDown
1800
+ * - onMouseUp
1801
+ *
1802
+ * ### Once that is touch support device, that mouse event will never can be trigger
1803
+ * https://developer.mozilla.org/en-US/docs/Web/API/Touch_events/Supporting_both_TouchEvent_and_MouseEvent
1804
+ *
1805
+ * Event order:
1806
+ * - touchstart
1807
+ * - Zero or more touchmove events, depending on movement of the finger(s)
1808
+ * - touchend
1809
+ * - mousemove
1810
+ * - mousedown
1811
+ * - mouseup
1812
+ * - click
1813
+ */ const useTouchMouseEvent = ({ onMouseDown: onMouseDownArg, onMouseUp: onMouseUpArg, onTouchStart: onTouchStartArg, onTouchEnd: onTouchEndArg }, { actionRef } = {})=>{
1159
1814
  const stopMouseEventRef = useRef(false);
1160
- const onTouchStart = useEventCallback((e) => {
1815
+ const onTouchStart = useEventCallback((e)=>{
1161
1816
  stopMouseEventRef.current = true;
1162
1817
  onTouchStartArg?.(e);
1163
1818
  });
@@ -1168,7 +1823,7 @@ const useTouchMouseEvent = ({ onMouseDown: onMouseDownArg, onMouseUp: onMouseUpA
1168
1823
  }
1169
1824
  return false;
1170
1825
  }
1171
- const onTouchEnd = useEventCallback((e) => {
1826
+ const onTouchEnd = useEventCallback((e)=>{
1172
1827
  let isInside;
1173
1828
  if (actionRef && e.touches.length < 2 && e.changedTouches.length < 2) {
1174
1829
  const touch = e.touches[0] || e.changedTouches[0];
@@ -1177,16 +1832,15 @@ const useTouchMouseEvent = ({ onMouseDown: onMouseDownArg, onMouseUp: onMouseUpA
1177
1832
  }
1178
1833
  onTouchEndArg?.(e, isInside);
1179
1834
  });
1180
- const onMouseDown = useEventCallback((e) => {
1835
+ const onMouseDown = useEventCallback((e)=>{
1181
1836
  if (stopMouseEventRef.current) {
1182
1837
  return;
1183
1838
  }
1184
1839
  onMouseDownArg?.(e);
1185
1840
  });
1186
- const onMouseUp = useEventCallback((e) => {
1841
+ const onMouseUp = useEventCallback((e)=>{
1187
1842
  let isInside;
1188
- if (stopMouseEventRef.current)
1189
- return;
1843
+ if (stopMouseEventRef.current) return;
1190
1844
  if (actionRef) {
1191
1845
  const elm = e.target;
1192
1846
  isInside = !!elm && checkInside(elm);
@@ -1197,30 +1851,50 @@ const useTouchMouseEvent = ({ onMouseDown: onMouseDownArg, onMouseUp: onMouseUpA
1197
1851
  onTouchStart,
1198
1852
  onTouchEnd,
1199
1853
  onMouseDown,
1200
- onMouseUp,
1854
+ onMouseUp
1201
1855
  };
1202
1856
  };
1203
- const isElmEqualOrContain = (sourceTarget, containTarget) => {
1857
+ /**
1858
+ * check is element equal or contains target element
1859
+ */ const isElmEqualOrContain = (sourceTarget, containTarget)=>{
1204
1860
  const elm = getRefElement(sourceTarget);
1205
1861
  const target = getRefElement(containTarget);
1206
1862
  return elm && target && (target === elm || target.contains(elm));
1207
1863
  };
1208
1864
 
1209
- const isTouchEvent = (ev) => {
1865
+
1866
+
1867
+
1868
+
1869
+
1870
+
1871
+
1872
+
1873
+
1874
+ const isTouchEvent = (ev)=>{
1210
1875
  return 'touches' in ev;
1211
1876
  };
1212
- const useLongPress = ({ onTap, onPress }, { onTouchStart: onTouchStartArg, onTouchEnd: onTouchEndArg, onMouseDown, onKeyDown, onKeyUp, onMouseUp: onMouseUpArg, } = {}, { isPreventDefault = true, delay = 300, externalWindow = window, } = {}) => {
1877
+ /**
1878
+ * Provide longPress helper, both `click`/`tab`/`keydown` will trigger event
1879
+ *
1880
+ * - Trigger `onTap` when user action leave small than delay time.
1881
+ * - Trigger `onPress` when action time is long than delay time.
1882
+ *
1883
+ * @default UseLongPressOptions { delay = 300 }
1884
+ */ const useLongPress = ({ onTap, onPress }, { onTouchStart: onTouchStartArg, onTouchEnd: onTouchEndArg, onMouseDown, onKeyDown, onKeyUp, onMouseUp: onMouseUpArg } = {}, { isPreventDefault = true, delay = 300, externalWindow = window } = {})=>{
1213
1885
  const isEmittedRef = useRef(false);
1214
1886
  const isA11yDownRef = useRef(false);
1215
1887
  const actionRef = useRef(null);
1216
1888
  const reasonRef = useRef('click');
1217
- const emitLongPress = useDebounce((e) => {
1889
+ const emitLongPress = useDebounce((e)=>{
1890
+ // * only when have onPress set have emit
1218
1891
  if (onPress) {
1219
1892
  onPress(e, reasonRef.current);
1220
1893
  isEmittedRef.current = true;
1221
1894
  }
1222
1895
  }, delay);
1223
- const end = (e, isInside) => {
1896
+ const end = (e, isInside)=>{
1897
+ // ! mouse up only trigger when up on element, so we host that in document
1224
1898
  globalMouseUpListener.remove();
1225
1899
  if (!isEmittedRef.current && isInside) {
1226
1900
  onTap?.(e, reasonRef.current);
@@ -1229,7 +1903,8 @@ const useLongPress = ({ onTap, onPress }, { onTouchStart: onTouchStartArg, onTou
1229
1903
  isEmittedRef.current = false;
1230
1904
  emitLongPress.cancel();
1231
1905
  };
1232
- const start = (e) => {
1906
+ const start = (e)=>{
1907
+ // ! mouse up only trigger when up on element, so we host that in document
1233
1908
  globalMouseUpListener.listen();
1234
1909
  const isTouch = e.type === 'touchstart';
1235
1910
  if (isTouch) {
@@ -1238,111 +1913,173 @@ const useLongPress = ({ onTap, onPress }, { onTouchStart: onTouchStartArg, onTou
1238
1913
  emitLongPress(e);
1239
1914
  };
1240
1915
  const { onMouseUp, onTouchEnd, onTouchStart, ...events } = useTouchMouseEvent({
1241
- onTouchStart: (e) => {
1916
+ onTouchStart: (e)=>{
1917
+ // prevent long touch leave element focus
1242
1918
  if (isPreventDefault) {
1243
1919
  e.preventDefault();
1244
1920
  }
1245
1921
  onTouchStartArg?.(e);
1246
1922
  start(e);
1247
1923
  },
1248
- onTouchEnd: (e, isInside) => {
1924
+ onTouchEnd: (e, isInside)=>{
1925
+ // prevent leave element focus
1249
1926
  if (isPreventDefault) {
1250
1927
  e.preventDefault();
1251
1928
  }
1252
1929
  onTouchEndArg?.(e, isInside);
1253
1930
  end(e, isInside);
1254
1931
  },
1255
- onMouseDown: (e) => {
1932
+ onMouseDown: (e)=>{
1256
1933
  if (isPreventDefault) {
1257
1934
  e.preventDefault();
1258
1935
  }
1259
1936
  onMouseDown?.(e);
1260
1937
  start(e);
1261
1938
  },
1262
- onMouseUp: (e, isInside) => {
1939
+ onMouseUp: (e, isInside)=>{
1940
+ // prevent leave element focus
1263
1941
  if (isPreventDefault) {
1264
1942
  e.preventDefault();
1265
1943
  }
1266
1944
  onMouseUpArg?.(e);
1267
1945
  end(e, isInside);
1268
- },
1269
- }, { actionRef });
1946
+ }
1947
+ }, {
1948
+ actionRef
1949
+ });
1950
+ // use event listener for make event `passive` work in safari
1270
1951
  useEventListener(actionRef, 'touchstart', onTouchStart, {
1271
- passive: false,
1952
+ passive: false
1272
1953
  });
1954
+ // use event listener for make event `passive` work in safari
1273
1955
  useEventListener(actionRef, 'touchend', onTouchEnd, {
1274
- passive: false,
1956
+ passive: false
1275
1957
  });
1276
1958
  const globalMouseUpListener = useGlobalListener('mouseup', onMouseUp, {
1277
1959
  target: externalWindow,
1278
- startImmediately: false,
1960
+ startImmediately: false
1279
1961
  });
1280
- const handleDeleteKeyDown = useA11yKeyEvent((e) => {
1962
+ const handleDeleteKeyDown = useA11yKeyEvent((e)=>{
1281
1963
  reasonRef.current = 'keyboard';
1964
+ // * keydown when hold will trigger many time, show when current is a11y down, not trigger again
1282
1965
  if (!isA11yDownRef.current) {
1283
1966
  emitLongPress(e);
1284
1967
  }
1285
1968
  isA11yDownRef.current = true;
1286
- }, { preventDefault: false });
1287
- const handleKeyDown = useEventCallback((e) => {
1969
+ }, {
1970
+ preventDefault: false
1971
+ });
1972
+ const handleKeyDown = useEventCallback((e)=>{
1288
1973
  onKeyDown?.(e);
1289
1974
  handleDeleteKeyDown(e);
1290
1975
  });
1291
- const handleKeyUp = useEventCallback((e) => {
1976
+ const handleKeyUp = useEventCallback((e)=>{
1292
1977
  onKeyUp?.(e);
1293
- if (isA11yDownRef.current)
1294
- end(e, true);
1978
+ if (isA11yDownRef.current) end(e, true);
1295
1979
  isA11yDownRef.current = false;
1296
1980
  });
1297
1981
  return {
1298
1982
  ref: actionRef,
1299
1983
  onKeyDown: handleKeyDown,
1300
1984
  onKeyUp: handleKeyUp,
1301
- ...events,
1985
+ ...events
1302
1986
  };
1303
1987
  };
1304
1988
 
1305
- const useMountState = () => {
1989
+
1990
+
1991
+
1992
+
1993
+ /**
1994
+ * get that component mounted state `ref`
1995
+ *
1996
+ * @example
1997
+ *
1998
+ * ```ts
1999
+ const isMountedRef = useMountState();
2000
+
2001
+ if (isMountedRef.current) {
2002
+ console.log('component mounted');
2003
+ }
2004
+ *
2005
+ * ```
2006
+ */ const useMountState = ()=>{
1306
2007
  const isMountedRef = useRef(false);
1307
- useEffect(() => {
2008
+ useEffect(()=>{
1308
2009
  isMountedRef.current = true;
1309
- return () => {
2010
+ return ()=>{
1310
2011
  isMountedRef.current = false;
1311
2012
  };
1312
2013
  }, []);
1313
2014
  return isMountedRef;
1314
2015
  };
1315
2016
 
1316
- const useOnLayoutReRender = (cb, deps) => {
2017
+
2018
+
2019
+
2020
+
2021
+
2022
+ /**
2023
+ * only trigger when re-render (in useLayoutEffect), not trigger on component first render
2024
+ */ const useOnLayoutReRender = (cb, deps)=>{
1317
2025
  const count = useRef(0);
1318
2026
  const method = useEventCallback(cb);
1319
- useLayoutEffect(() => {
2027
+ useLayoutEffect(()=>{
1320
2028
  if (count.current === 0) {
1321
2029
  count.current = 1;
1322
- return () => {
2030
+ return ()=>{
2031
+ //
1323
2032
  };
1324
2033
  }
1325
2034
  return method();
2035
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
1326
2036
  }, deps);
1327
2037
  };
1328
2038
 
1329
- const useOnReRender = (cb, deps) => {
2039
+
2040
+
2041
+
2042
+
2043
+
2044
+ /**
2045
+ * only trigger when re-render, not trigger on component first render
2046
+ */ const useOnReRender = (cb, deps)=>{
1330
2047
  const count = useRef(0);
1331
2048
  const method = useEventCallback(cb);
1332
- useEffect(() => {
2049
+ useEffect(()=>{
1333
2050
  if (count.current === 0) {
1334
2051
  count.current = 1;
1335
- return () => {
2052
+ return ()=>{
2053
+ //
1336
2054
  };
1337
2055
  }
1338
2056
  return method();
2057
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
1339
2058
  }, deps);
1340
2059
  };
1341
2060
 
1342
- const useResizeObserver = (target, callback, performance = {}) => {
1343
- const { mode = 'debounce', time: performanceTime = 200, ignoreFireWhenObserve, options, } = performance;
2061
+
2062
+
2063
+
2064
+
2065
+
2066
+
2067
+
2068
+
2069
+
2070
+
2071
+
2072
+
2073
+ /**
2074
+ * get resize event with auto disconnect when layout destroy.
2075
+ * bring with debounce for event not too many
2076
+ * @param ref that ref for element
2077
+ * @param callback callback when element resize
2078
+ * @param performance options that improve performance
2079
+ */ const useResizeObserver = (/** target element ref */ target, /** trigger when `ResizeObserver` emit */ callback, /** performance options, `mode`, `time`, `ignoreFireWhenObserve` */ performance = {})=>{
2080
+ const { mode = 'debounce', time: performanceTime = 200, ignoreFireWhenObserve, options } = performance;
1344
2081
  const countRef = useRef(0);
1345
- const cb = useEventCallback((...args) => {
2082
+ const cb = useEventCallback((...args)=>{
1346
2083
  if (ignoreFireWhenObserve && countRef.current === 0) {
1347
2084
  countRef.current = 1;
1348
2085
  return;
@@ -1351,20 +2088,18 @@ const useResizeObserver = (target, callback, performance = {}) => {
1351
2088
  });
1352
2089
  const el = getRefElement(target);
1353
2090
  const currentWindow = el ? ownerWindow(el) : window;
1354
- const observerRef = useResultRef(() => {
2091
+ const observerRef = useResultRef(()=>{
1355
2092
  let resizeCallback;
1356
2093
  if (mode === 'debounce') {
1357
2094
  resizeCallback = debounce(cb, performanceTime, options);
1358
- }
1359
- else if (mode === 'throttle') {
2095
+ } else if (mode === 'throttle') {
1360
2096
  resizeCallback = throttle(cb, performanceTime, options);
1361
- }
1362
- else {
2097
+ } else {
1363
2098
  resizeCallback = cb;
1364
2099
  }
1365
2100
  return new currentWindow.ResizeObserver(resizeCallback);
1366
2101
  });
1367
- useLayoutEffect(() => {
2102
+ useLayoutEffect(()=>{
1368
2103
  if (!currentWindow?.ResizeObserver) {
1369
2104
  return;
1370
2105
  }
@@ -1374,110 +2109,193 @@ const useResizeObserver = (target, callback, performance = {}) => {
1374
2109
  throw new Error('please check element exist before bind resize observer');
1375
2110
  }
1376
2111
  observer.observe(el);
1377
- return () => {
2112
+ return ()=>{
1378
2113
  observer.disconnect();
1379
2114
  };
1380
- }, [el]);
2115
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
2116
+ }, [
2117
+ el
2118
+ ]);
1381
2119
  return observerRef.current;
1382
2120
  };
1383
2121
 
1384
- const useOverflow = (target, cb, performance = {
2122
+
2123
+
2124
+
2125
+
2126
+
2127
+ /**
2128
+ * trigger when overflow state change
2129
+ */ const useOverflow = (/** target element ref */ target, /** trigger when `ResizeObserver` emit */ cb, /**
2130
+ * performance options, `mode`, `time`, `ignoreFireWhenObserve`
2131
+ *
2132
+ * @default { mode: 'throttle' time: 200 }
2133
+ */ performance = {
1385
2134
  mode: 'throttle',
1386
- time: 200,
1387
- }) => {
2135
+ time: 200
2136
+ })=>{
2137
+ // * not have default value for that can trigger change first time.
1388
2138
  const showRef = useRef();
1389
- return useResizeObserver(target, () => {
2139
+ return useResizeObserver(target, ()=>{
1390
2140
  const element = getRefElement(target);
1391
- if (!element)
1392
- return;
2141
+ if (!element) return;
1393
2142
  const { scrollWidth, clientWidth, scrollHeight, clientHeight } = element;
1394
2143
  const show = scrollWidth > clientWidth || scrollHeight > clientHeight;
1395
- if (showRef.current === show)
1396
- return;
2144
+ if (showRef.current === show) return;
2145
+ // only emit when state change
1397
2146
  showRef.current = show;
1398
2147
  cb(show, {
1399
2148
  scrollWidth,
1400
2149
  clientWidth,
1401
2150
  scrollHeight,
1402
- clientHeight,
2151
+ clientHeight
1403
2152
  });
1404
2153
  }, performance);
1405
2154
  };
1406
2155
 
1407
- const useRefState = (value, customUpdate) => {
2156
+
2157
+
2158
+
2159
+
2160
+
2161
+
2162
+ /**
2163
+ * Returns a stateful like state with Ref, and a function to update it current value.
2164
+ * can trigger update value without re-render
2165
+ *
2166
+ * @param value init value
2167
+ * @param customUpdate you can custom you update method, or just set your `forceUpdate` to reuse same instance
2168
+ * @returns an array with [ref, update]
2169
+ *
2170
+ * @example
2171
+ * ```ts
2172
+ * const [inputRef, updateInput] = useRefState(123);
2173
+ *
2174
+ * if(needReRender){
2175
+ * updateInput(456); // trigger re-render
2176
+ * } else {
2177
+ * updateInput(456, false); // not trigger re-render
2178
+ * }
2179
+ * ```
2180
+ */ const useRefState = (value, customUpdate)=>{
2181
+ // oxlint-disable-next-line react-hooks/rules-of-hooks
1408
2182
  const forceUpdate = customUpdate ?? useForceUpdate();
1409
2183
  const innerValueRef = useRef(value);
1410
- const setInnerValue = useEventCallback((toValue, isUpdate = true) => {
2184
+ /**
2185
+ * update ref value, set isUpdate to `false` will not trigger re-render
2186
+ */ const setInnerValue = useEventCallback((toValue, /**
2187
+ * is re-render
2188
+ * @default true
2189
+ */ isUpdate = true)=>{
1411
2190
  innerValueRef.current = toValue;
1412
2191
  if (isUpdate) {
1413
2192
  forceUpdate();
1414
2193
  }
1415
2194
  });
1416
- return [innerValueRef, setInnerValue];
2195
+ return [
2196
+ innerValueRef,
2197
+ setInnerValue
2198
+ ];
1417
2199
  };
1418
2200
 
1419
- const useResponsiveMatch = (defaultBreakpoint) => {
2201
+
2202
+
2203
+
2204
+ /**
2205
+ * get all matched breakpoints
2206
+ * @param defaultBreakpoint use defaultBreakpoint when Responsive init (will pass undefined)
2207
+ */ const useResponsiveMatch = (defaultBreakpoint)=>{
1420
2208
  const _breakpoint = useResponsiveContext();
1421
2209
  const breakpoint = _breakpoint === undefined ? defaultBreakpoint : _breakpoint;
1422
- if (breakpoint === undefined)
1423
- return {};
2210
+ if (breakpoint === undefined) return {};
1424
2211
  const matchDetail = getMatchDetail(breakpoint);
1425
2212
  return matchDetail.matchMap;
1426
2213
  };
1427
2214
 
1428
- const useSleep = () => {
2215
+
2216
+
2217
+
2218
+
2219
+
2220
+
2221
+ /**
2222
+ * cancelable sleep method.
2223
+ *
2224
+ * - sleep: a promise sleep
2225
+ * - cancel: cancel that sleep event
2226
+ * - getSleeping: get current sleeping state
2227
+ */ const useSleep = ()=>{
1429
2228
  const timeoutIdRef = useRef();
1430
2229
  const rejectRef = useRef();
1431
2230
  const sleepingRef = useRef(false);
1432
- const clearCurrTimeout = () => {
2231
+ const clearCurrTimeout = ()=>{
1433
2232
  const id = timeoutIdRef.current;
1434
2233
  sleepingRef.current = false;
1435
- if (id !== undefined)
1436
- clearTimeout(id);
2234
+ if (id !== undefined) clearTimeout(id);
1437
2235
  };
1438
- const sleep = (time) => new Promise((resolve, reject) => {
1439
- clearCurrTimeout();
1440
- sleepingRef.current = true;
1441
- rejectRef.current = reject;
1442
- timeoutIdRef.current = setTimeout(() => {
1443
- sleepingRef.current = false;
1444
- resolve();
1445
- }, time);
1446
- });
1447
- const cancel = () => {
2236
+ const sleep = (time)=>new Promise((resolve, reject)=>{
2237
+ clearCurrTimeout();
2238
+ sleepingRef.current = true;
2239
+ rejectRef.current = reject;
2240
+ timeoutIdRef.current = setTimeout(()=>{
2241
+ sleepingRef.current = false;
2242
+ resolve();
2243
+ }, time);
2244
+ });
2245
+ const cancel = ()=>{
1448
2246
  clearCurrTimeout();
1449
2247
  rejectRef.current?.();
1450
2248
  };
1451
- useEffect(() => {
1452
- return () => {
2249
+ useEffect(()=>{
2250
+ return ()=>{
1453
2251
  clearCurrTimeout();
1454
2252
  };
1455
2253
  }, []);
1456
2254
  return {
1457
- sleep,
1458
- cancel,
1459
- getSleeping: () => sleepingRef.current,
2255
+ /**
2256
+ * sleep method, that will be cancelable with `cancel`,
2257
+ * and when trigger many time that will always clear previous sleep
2258
+ */ sleep,
2259
+ /**
2260
+ * cancel current sleep timer
2261
+ */ cancel,
2262
+ /**
2263
+ * return is current is sleeping
2264
+ */ getSleeping: ()=>sleepingRef.current
1460
2265
  };
1461
2266
  };
1462
2267
 
1463
- const useRetry = (cb, options) => {
2268
+
2269
+
2270
+ /* oxlint-disable no-await-in-loop */
2271
+
2272
+ /**
2273
+ * retry times when exec callback fail
2274
+ *
2275
+ * * `retry`: that will auto add `times` end of your call back
2276
+ * * `cancel`: cancel that retry event, that only cancel inner interval promise, **if you need cancel your `cb` promise, you should handle by yourself.**
2277
+ */ const useRetry = (/**
2278
+ * exec callback, when you want to end that retry, return `true` with your callback
2279
+ */ cb, /** retry options */ options)=>{
1464
2280
  const { sleep, cancel: cancelSleep } = useSleep();
1465
2281
  const isCancelRef = useRef(true);
1466
- const complete = () => {
2282
+ const complete = ()=>{
1467
2283
  isCancelRef.current = true;
1468
2284
  options?.onComplete?.();
1469
2285
  };
1470
- const cancel = () => {
2286
+ const cancel = ()=>{
1471
2287
  if (!isCancelRef.current) {
2288
+ // * when outside cancel
1472
2289
  options?.onCancel?.();
1473
2290
  complete();
1474
2291
  }
1475
2292
  cancelSleep();
1476
2293
  };
1477
- const retry = async (...args) => {
2294
+ const retry = async (...args)=>{
1478
2295
  cancel();
1479
2296
  isCancelRef.current = false;
1480
2297
  options?.onStart?.();
2298
+ // * when exec success, just return directly.
1481
2299
  if (await cb(...args)) {
1482
2300
  if (!isCancelRef.current) {
1483
2301
  options?.onSuccess?.();
@@ -1485,17 +2303,15 @@ const useRetry = (cb, options) => {
1485
2303
  }
1486
2304
  return;
1487
2305
  }
1488
- if (!options)
1489
- return;
2306
+ if (!options) return;
1490
2307
  const { retryTimes, intervalTime, onSuccess } = options;
1491
- for (let i = 0; i < retryTimes; i++) {
2308
+ for(let i = 0; i < retryTimes; i++){
1492
2309
  if (isCancelRef.current) {
1493
2310
  return;
1494
2311
  }
1495
2312
  try {
1496
2313
  await sleep(intervalTime);
1497
- }
1498
- catch (error) {
2314
+ } catch (error) {
1499
2315
  return;
1500
2316
  }
1501
2317
  if (await cb(...args, i)) {
@@ -1509,24 +2325,34 @@ const useRetry = (cb, options) => {
1509
2325
  complete();
1510
2326
  };
1511
2327
  return {
1512
- retry,
1513
- cancel,
2328
+ /**
2329
+ * retry method, that will be cancelable with `cancel`,
2330
+ * and when trigger many time that will always clear previous retry
2331
+ */ retry,
2332
+ /**
2333
+ * cancel current retry
2334
+ */ cancel
1514
2335
  };
1515
2336
  };
1516
2337
 
1517
- const useTabOut = (onTabOut) => {
2338
+
2339
+
2340
+
2341
+
2342
+ /**
2343
+ * @param onTabOut Callback fired when a "tab out" event is detected.
2344
+ */ const useTabOut = (onTabOut)=>{
1518
2345
  const lastKeyDownIsTabRef = useRef(false);
1519
- const handleKeyDown = (e) => {
2346
+ const handleKeyDown = (e)=>{
1520
2347
  if (e.key === 'Tab') {
1521
2348
  lastKeyDownIsTabRef.current = true;
1522
2349
  }
1523
2350
  };
1524
- const handleKeyUp = (e) => {
2351
+ const handleKeyUp = (e)=>{
1525
2352
  lastKeyDownIsTabRef.current = false;
1526
2353
  };
1527
- const handleBlur = (e) => {
1528
- if (!e.currentTarget.contains(e.relatedTarget) &&
1529
- lastKeyDownIsTabRef.current) {
2354
+ const handleBlur = (e)=>{
2355
+ if (!e.currentTarget.contains(e.relatedTarget) && lastKeyDownIsTabRef.current) {
1530
2356
  onTabOut(e);
1531
2357
  }
1532
2358
  };
@@ -1534,63 +2360,153 @@ const useTabOut = (onTabOut) => {
1534
2360
  tabOutHandler: {
1535
2361
  onKeyDown: handleKeyDown,
1536
2362
  onKeyUp: handleKeyUp,
1537
- onBlur: handleBlur,
1538
- },
2363
+ onBlur: handleBlur
2364
+ }
1539
2365
  };
1540
2366
  };
1541
2367
 
1542
- const useThrottle = (fn, throttleTime = 200, options) => {
2368
+
2369
+
2370
+
2371
+
2372
+
2373
+
2374
+
2375
+ /**
2376
+ * provide a throttle method for throttle method
2377
+ * @param fn method for throttle
2378
+ * @param throttleTime throttle timeout value, default is `200`ms
2379
+ * @param options throttle setting, default is `{trailing: true, leading: true}`, detail see lodash
2380
+ */ // oxlint-disable-next-line @typescript-eslint/no-explicit-any
2381
+ const useThrottle = (fn, throttleTime = 200, options)=>{
1543
2382
  const memoFn = useEventCallback(fn);
2383
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
1544
2384
  const throttleFn = useCallback(throttle(memoFn, throttleTime, options), []);
1545
- useEffect(() => () => throttleFn.cancel(), []);
2385
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
2386
+ useEffect(()=>()=>throttleFn.cancel(), []);
1546
2387
  return throttleFn;
1547
2388
  };
1548
2389
 
1549
- const useTestRunnerDetection = () => {
1550
- return useMemo(() => {
1551
- return (typeof window !== 'undefined' &&
1552
- (window.navigator?.webdriver || window.chrome?.webdriver));
2390
+
2391
+
2392
+
2393
+
2394
+ /**
2395
+ * Internal hook to detect if the component is running in a test runner environment
2396
+ * Used by Storybook stories to disable network requests and animations during testing
2397
+ * @returns {boolean} true if running in test runner (headless browser), false otherwise
2398
+ * @internal
2399
+ */ const useTestRunnerDetection = ()=>{
2400
+ return useMemo(()=>{
2401
+ return typeof window !== 'undefined' && (window.navigator?.webdriver || window.chrome?.webdriver);
1553
2402
  }, []);
1554
2403
  };
1555
2404
 
2405
+
2406
+
1556
2407
  function useTimeout(callback, delay) {
1557
2408
  const savedCallback = useRef(callback);
1558
2409
  savedCallback.current = callback;
1559
- useEffect(() => {
2410
+ // Set up the timeout.
2411
+ useEffect(()=>{
2412
+ // Don't schedule if no delay is specified.
2413
+ // Note: 0 is a valid value for delay.
1560
2414
  if (!delay && delay !== 0) {
1561
2415
  return;
1562
2416
  }
1563
- const id = setTimeout(() => savedCallback.current(), delay);
1564
- return () => clearTimeout(id);
1565
- }, [delay]);
2417
+ const id = setTimeout(()=>savedCallback.current(), delay);
2418
+ return ()=>clearTimeout(id);
2419
+ }, [
2420
+ delay
2421
+ ]);
1566
2422
  }
2423
+ /* export default */ var useTimeout_useTimeout = ((/* unused pure expression or super */ null && (useTimeout)));
2424
+
2425
+
2426
+
1567
2427
 
1568
- const isFontLoadingAPIAvailable = () => {
1569
- return (typeof document !== 'undefined' &&
1570
- document.fonts &&
1571
- typeof document.fonts.ready === 'object' &&
1572
- typeof document.fonts.ready.then === 'function');
2428
+
2429
+ /**
2430
+ * Check if Font Loading API is available in the current environment
2431
+ * @returns boolean indicating if document.fonts.ready is available
2432
+ */ const isFontLoadingAPIAvailable = ()=>{
2433
+ return typeof document !== 'undefined' && document.fonts && typeof document.fonts.ready === 'object' && typeof document.fonts.ready.then === 'function';
1573
2434
  };
1574
- const useFontLoaded = (enabled = true) => {
1575
- const [loaded, setLoaded] = useState(() => !enabled || !isFontLoadingAPIAvailable());
1576
- useEffect(() => {
2435
+ /**
2436
+ * Hook to detect when fonts are loaded
2437
+ *
2438
+ * This hook provides graceful degradation:
2439
+ * - When Font Loading API is not available (older browsers, SSR, test environments),
2440
+ * it assumes fonts are loaded to prevent blocking UI
2441
+ * - When disabled, it immediately returns true without listening to font events
2442
+ * - When enabled and API is available, it listens for font loading completion
2443
+ *
2444
+ * @param enabled - Whether to listen for font loading events (default: true)
2445
+ * @returns boolean indicating whether fonts are loaded
2446
+ */ const useFontLoaded = (enabled = true)=>{
2447
+ const [loaded, setLoaded] = useState(()=>!enabled || !isFontLoadingAPIAvailable());
2448
+ useEffect(()=>{
1577
2449
  if (!enabled || !isFontLoadingAPIAvailable()) {
1578
2450
  return;
1579
2451
  }
1580
2452
  let cancelled = false;
1581
- document.fonts.ready.then(() => {
2453
+ document.fonts.ready.then(()=>{
1582
2454
  if (!cancelled) {
1583
2455
  setLoaded(true);
1584
2456
  }
1585
2457
  });
1586
- return () => {
2458
+ return ()=>{
1587
2459
  cancelled = true;
1588
2460
  };
1589
- }, [enabled]);
2461
+ }, [
2462
+ enabled
2463
+ ]);
1590
2464
  return loaded;
1591
2465
  };
1592
2466
 
1593
- function createInitialState() {
2467
+
2468
+
2469
+
2470
+ /**
2471
+ * bodyScrollLock.ts
2472
+ *
2473
+ * Standalone scroll-lock utility with no external runtime dependencies.
2474
+ *
2475
+ * Features
2476
+ * --------
2477
+ * - Pure, caller-managed state: every function accepts an explicit `WindowState` and
2478
+ * `win`. There are no module-level globals — state lifetime and storage are the
2479
+ * caller's responsibility (see `useBodyScrollLock.ts` for the per-window storage).
2480
+ * - Ordered lock entries: every lock call registers an entry in insertion
2481
+ * order. Body lock stays enabled while any entry is active, while all
2482
+ * entries (active or inactive) may continue to contribute bounded inner
2483
+ * scrolling boundaries.
2484
+ * - iOS (touchmove-based locking):
2485
+ * • Active entries decide whether body scroll should currently be locked.
2486
+ * • All entries may contribute element boundaries or allowTouchMove-based
2487
+ * inner-scrolling candidates.
2488
+ * • Matching targets run bounded DOM-walk checks, allowing inner
2489
+ * scrollables to move until they hit their boundary.
2490
+ * • Pinch-zoom allowed — skips locking when touches.length > 1 OR when
2491
+ * ctrlKey is set (covers trackpad-pinch on hybrid devices where the OS
2492
+ * synthesises a ctrl+touchmove).
2493
+ * • cancelable guard — never calls preventDefault on non-cancelable events.
2494
+ * - Non-iOS: sets `body { overflow: hidden }` and optionally reserves the
2495
+ * scrollbar gap to prevent layout shift.
2496
+ *
2497
+ * API
2498
+ * ---
2499
+ * lock(state, win, targetElement?, options?) — acquire or reactivate a lock entry
2500
+ * unlock(state, win, targetElement?, options?) — remove a lock entry
2501
+ * pause(state, win, targetElement?, options?) — temporarily disable an entry in place
2502
+ * clearAllLocks(state, win) — internal recovery reset for one window
2503
+ *
2504
+ * `targetElement` should be the container element that defines the lock
2505
+ * boundary for an element-scoped lock. It scopes the iOS touch boundary:
2506
+ * scrollable descendants of that container may keep scrolling, while touches
2507
+ * outside it are prevented. Without `targetElement`, the lock is anonymous and
2508
+ * iOS treats it as a pure global body lock.
2509
+ */ function createInitialState() {
1594
2510
  return {
1595
2511
  entries: [],
1596
2512
  savedBodyOverflow: '',
@@ -1600,48 +2516,47 @@ function createInitialState() {
1600
2516
  touchStartY: 0,
1601
2517
  touchStartX: 0,
1602
2518
  nonIOSLocked: false,
1603
- nonIOSReservedScrollBarGap: false,
2519
+ nonIOSReservedScrollBarGap: false
1604
2520
  };
1605
2521
  }
1606
2522
  function isIOS(win) {
1607
2523
  const { platform, maxTouchPoints, userAgent } = win.navigator;
1608
- return (/iP(ad|hone|od)/.test(platform) ||
1609
- (platform === 'MacIntel' && maxTouchPoints > 1) ||
1610
- /iPad|iPhone|iPod/.test(userAgent));
2524
+ return /iP(ad|hone|od)/.test(platform) || platform === 'MacIntel' && maxTouchPoints > 1 || /iPad|iPhone|iPod/.test(userAgent);
1611
2525
  }
1612
2526
  function isScrollableElement(el, win) {
1613
2527
  const { overflow, overflowX, overflowY } = win.getComputedStyle(el);
1614
- if (!/(auto|scroll|overlay)/.test(overflow + overflowX + overflowY))
1615
- return false;
1616
- return (el.scrollHeight > el.clientHeight + 1 || el.scrollWidth > el.clientWidth + 1);
2528
+ if (!/(auto|scroll|overlay)/.test(overflow + overflowX + overflowY)) return false;
2529
+ return el.scrollHeight > el.clientHeight + 1 || el.scrollWidth > el.clientWidth + 1;
1617
2530
  }
1618
2531
  function isScrollBoundaryHit(scrollable, dx, dy) {
1619
2532
  if (Math.abs(dy) >= Math.abs(dx)) {
1620
- if (dy > 0 && scrollable.scrollTop <= 0)
1621
- return true;
1622
- return (dy < 0 &&
1623
- scrollable.scrollTop + scrollable.clientHeight >=
1624
- scrollable.scrollHeight - 1);
2533
+ if (dy > 0 && scrollable.scrollTop <= 0) return true;
2534
+ return dy < 0 && scrollable.scrollTop + scrollable.clientHeight >= scrollable.scrollHeight - 1;
1625
2535
  }
1626
- if (dx > 0 && scrollable.scrollLeft <= 0)
1627
- return true;
1628
- return (dx < 0 &&
1629
- scrollable.scrollLeft + scrollable.clientWidth >= scrollable.scrollWidth - 1);
2536
+ if (dx > 0 && scrollable.scrollLeft <= 0) return true;
2537
+ return dx < 0 && scrollable.scrollLeft + scrollable.clientWidth >= scrollable.scrollWidth - 1;
1630
2538
  }
1631
- function findNearestScrollableAncestorWithinBoundary(el, win, boundary) {
2539
+ /**
2540
+ * Walk up the DOM from `el` to find the nearest scrollable ancestor.
2541
+ *
2542
+ * Always stops before `<body>` and `<html>` so body scroll is never returned.
2543
+ * When `boundary` is provided (element lock), the walk also stops at that
2544
+ * container element, preventing it from finding scrollable ancestors outside
2545
+ * the active lock boundary. When `boundary` is null (anonymous lock), the walk
2546
+ * has no element boundary of its own and may find scrollable elements higher in
2547
+ * the tree.
2548
+ */ function findNearestScrollableAncestorWithinBoundary(el, win, boundary) {
1632
2549
  const { body, documentElement } = win.document;
1633
2550
  let cur = el;
1634
- while (cur && cur !== body && cur !== documentElement) {
1635
- if (isScrollableElement(cur, win))
1636
- return cur;
1637
- if (cur === boundary)
1638
- break;
2551
+ while(cur && cur !== body && cur !== documentElement){
2552
+ if (isScrollableElement(cur, win)) return cur;
2553
+ if (cur === boundary) break;
1639
2554
  cur = cur.parentElement;
1640
2555
  }
1641
2556
  return null;
1642
2557
  }
1643
2558
  function getActiveEntries(state) {
1644
- return state.entries.filter((entry) => entry.active);
2559
+ return state.entries.filter((entry)=>entry.active);
1645
2560
  }
1646
2561
  function getBoundaryForEntry(topEntry) {
1647
2562
  return topEntry.kind === 'element' ? topEntry.element : null;
@@ -1660,10 +2575,9 @@ function getEntryScopedScrollable(topEntry, target, win, { allowAnonymous = fals
1660
2575
  return getScrollableWithinBoundary(target, win, boundary);
1661
2576
  }
1662
2577
  function getAllowTouchMoveScrollable(entry, target, win) {
1663
- if (entry.allowTouchMove?.(target) !== true)
1664
- return null;
2578
+ if (entry.allowTouchMove?.(target) !== true) return null;
1665
2579
  return getEntryScopedScrollable(entry, target, win, {
1666
- allowAnonymous: true,
2580
+ allowAnonymous: true
1667
2581
  });
1668
2582
  }
1669
2583
  function canEntryAllowInnerScroll(entry, target, win, dx, dy) {
@@ -1679,10 +2593,10 @@ function canEntryAllowInnerScroll(entry, target, win, dx, dy) {
1679
2593
  }
1680
2594
  function findEntryIndex(state, targetElement, entryKey) {
1681
2595
  if (entryKey) {
1682
- return state.entries.findIndex((entry) => entry.key === entryKey);
2596
+ return state.entries.findIndex((entry)=>entry.key === entryKey);
1683
2597
  }
1684
2598
  if (targetElement) {
1685
- for (let i = state.entries.length - 1; i >= 0; i--) {
2599
+ for(let i = state.entries.length - 1; i >= 0; i--){
1686
2600
  const entry = state.entries[i];
1687
2601
  if (entry.kind === 'element' && entry.element === targetElement) {
1688
2602
  return i;
@@ -1690,7 +2604,7 @@ function findEntryIndex(state, targetElement, entryKey) {
1690
2604
  }
1691
2605
  return -1;
1692
2606
  }
1693
- for (let i = state.entries.length - 1; i >= 0; i--) {
2607
+ for(let i = state.entries.length - 1; i >= 0; i--){
1694
2608
  if (state.entries[i].kind === 'anonymous') {
1695
2609
  return i;
1696
2610
  }
@@ -1714,32 +2628,26 @@ function ensureEntry(state, targetElement, options = {}) {
1714
2628
  element: targetElement ?? null,
1715
2629
  active: true,
1716
2630
  reserveScrollBarGap: Boolean(options.reserveScrollBarGap),
1717
- allowTouchMove: options.allowTouchMove,
2631
+ allowTouchMove: options.allowTouchMove
1718
2632
  };
1719
2633
  state.entries.push(entry);
1720
2634
  return entry;
1721
2635
  }
1722
2636
  function shouldPreventTouchMove(e, state, win) {
1723
- if (e.touches.length > 1)
1724
- return false;
1725
- if (e.ctrlKey)
1726
- return false;
1727
- if (!e.cancelable)
1728
- return false;
2637
+ if (e.touches.length > 1) return false;
2638
+ if (e.ctrlKey) return false;
2639
+ if (!e.cancelable) return false;
1729
2640
  const touch = e.touches[0];
1730
- if (!touch)
1731
- return false;
2641
+ if (!touch) return false;
1732
2642
  const dy = touch.clientY - state.touchStartY;
1733
2643
  const dx = touch.clientX - state.touchStartX;
1734
- if (dy === 0 && dx === 0)
1735
- return false;
1736
- if (getActiveEntries(state).length === 0)
1737
- return false;
2644
+ if (dy === 0 && dx === 0) return false;
2645
+ if (getActiveEntries(state).length === 0) return false;
1738
2646
  const target = e.target;
1739
2647
  if (!(target instanceof win.Element)) {
1740
2648
  return true;
1741
2649
  }
1742
- for (let i = state.entries.length - 1; i >= 0; i--) {
2650
+ for(let i = state.entries.length - 1; i >= 0; i--){
1743
2651
  if (canEntryAllowInnerScroll(state.entries[i], target, win, dx, dy)) {
1744
2652
  return false;
1745
2653
  }
@@ -1748,22 +2656,28 @@ function shouldPreventTouchMove(e, state, win) {
1748
2656
  }
1749
2657
  function lockIOS(state, win) {
1750
2658
  const doc = win.document;
1751
- state.docTouchStartHandler = (e) => {
2659
+ // touchStartY/X are reset to 0 when listeners are first attached. If a touch
2660
+ // gesture is already in progress when the lock activates, the first touchmove
2661
+ // will compute dx/dy relative to (0,0) rather than the actual start point.
2662
+ // There is no Web API to read current touch coordinates without an event, so
2663
+ // this is an accepted limitation: one touchmove event may fire incorrect
2664
+ // boundary checks until the next touchstart resets the baseline.
2665
+ state.docTouchStartHandler = (e)=>{
1752
2666
  if (e.touches.length > 0) {
1753
2667
  state.touchStartY = e.touches[0].clientY;
1754
2668
  state.touchStartX = e.touches[0].clientX;
1755
2669
  }
1756
2670
  };
1757
- state.docTouchMoveHandler = (e) => {
2671
+ state.docTouchMoveHandler = (e)=>{
1758
2672
  if (shouldPreventTouchMove(e, state, win)) {
1759
2673
  e.preventDefault();
1760
2674
  }
1761
2675
  };
1762
2676
  doc.addEventListener('touchstart', state.docTouchStartHandler, {
1763
- passive: true,
2677
+ passive: true
1764
2678
  });
1765
2679
  doc.addEventListener('touchmove', state.docTouchMoveHandler, {
1766
- passive: false,
2680
+ passive: false
1767
2681
  });
1768
2682
  }
1769
2683
  function unlockIOS(state, win) {
@@ -1794,28 +2708,30 @@ function unlockNonIOS(state, win) {
1794
2708
  if (state.savedBodyPaddingRight !== null) {
1795
2709
  if (state.savedBodyPaddingRight === '') {
1796
2710
  body.style.removeProperty('padding-right');
1797
- }
1798
- else {
2711
+ } else {
1799
2712
  body.style.paddingRight = state.savedBodyPaddingRight;
1800
2713
  }
1801
2714
  state.savedBodyPaddingRight = null;
1802
2715
  }
1803
2716
  }
2717
+ // Non-iOS keeps body overflow locked while any entry remains active.
1804
2718
  function bodyLockDesired(state) {
1805
2719
  return getActiveEntries(state).length > 0;
1806
2720
  }
2721
+ // Scrollbar-gap compensation is aggregated across active entries so stacked
2722
+ // locks do not add/remove body padding while the window remains locked.
1807
2723
  function shouldReserveScrollBarGap(state) {
1808
- return state.entries.some((entry) => entry.active && entry.reserveScrollBarGap);
2724
+ return state.entries.some((entry)=>entry.active && entry.reserveScrollBarGap);
1809
2725
  }
1810
2726
  function syncIOS(state, win) {
2727
+ // Listeners stay attached while any entry exists so disabled entries can
2728
+ // continue contributing inner-scroll boundaries without repeated churn.
1811
2729
  const desired = state.entries.length > 0;
1812
2730
  const attached = state.docTouchMoveHandler !== null;
1813
- if (desired === attached)
1814
- return;
2731
+ if (desired === attached) return;
1815
2732
  if (desired) {
1816
2733
  lockIOS(state, win);
1817
- }
1818
- else {
2734
+ } else {
1819
2735
  unlockIOS(state, win);
1820
2736
  }
1821
2737
  }
@@ -1823,8 +2739,7 @@ function syncNonIOS(state, win) {
1823
2739
  const desired = bodyLockDesired(state);
1824
2740
  const reserveScrollBarGap = shouldReserveScrollBarGap(state);
1825
2741
  if (!desired) {
1826
- if (!state.nonIOSLocked)
1827
- return;
2742
+ if (!state.nonIOSLocked) return;
1828
2743
  unlockNonIOS(state, win);
1829
2744
  state.nonIOSLocked = false;
1830
2745
  state.nonIOSReservedScrollBarGap = false;
@@ -1832,23 +2747,23 @@ function syncNonIOS(state, win) {
1832
2747
  }
1833
2748
  if (!state.nonIOSLocked) {
1834
2749
  lockNonIOS(state, win, {
1835
- reserveScrollBarGap,
2750
+ reserveScrollBarGap
1836
2751
  });
1837
2752
  state.nonIOSLocked = true;
1838
2753
  state.nonIOSReservedScrollBarGap = reserveScrollBarGap;
1839
2754
  return;
1840
2755
  }
1841
- if (reserveScrollBarGap === state.nonIOSReservedScrollBarGap)
1842
- return;
2756
+ if (reserveScrollBarGap === state.nonIOSReservedScrollBarGap) return;
1843
2757
  unlockNonIOS(state, win);
1844
- lockNonIOS(state, win, { reserveScrollBarGap });
2758
+ lockNonIOS(state, win, {
2759
+ reserveScrollBarGap
2760
+ });
1845
2761
  state.nonIOSReservedScrollBarGap = reserveScrollBarGap;
1846
2762
  }
1847
2763
  function syncScrollLock(state, win) {
1848
2764
  if (isIOS(win)) {
1849
2765
  syncIOS(state, win);
1850
- }
1851
- else {
2766
+ } else {
1852
2767
  syncNonIOS(state, win);
1853
2768
  }
1854
2769
  }
@@ -1857,23 +2772,50 @@ function activateEntry(state, win, targetElement, options = {}) {
1857
2772
  entry.active = true;
1858
2773
  syncScrollLock(state, win);
1859
2774
  }
1860
- function lock(state, win, targetElement, options = {}) {
2775
+ /**
2776
+ * Acquire or re-enable a body-scroll lock entry. Calling on a paused entry
2777
+ * re-activates it without changing its position in the stack.
2778
+ */ function lock(state, win, targetElement, options = {}) {
1861
2779
  activateEntry(state, win, targetElement, options);
1862
2780
  }
1863
- function unlock(state, win, targetElement, options = {}) {
2781
+ /**
2782
+ * Release a body-scroll lock entry.
2783
+ *
2784
+ * When `options.entryKey` is provided, it takes precedence and removes that
2785
+ * exact entry.
2786
+ * `unlock()` without a target only removes the most recent anonymous entry.
2787
+ * `unlock(targetElement)` removes the most recent matching element entry.
2788
+ */ function unlock(state, win, targetElement, options = {}) {
1864
2789
  const index = findEntryIndex(state, targetElement, options.entryKey);
1865
- if (index < 0)
1866
- return;
2790
+ if (index < 0) return;
1867
2791
  state.entries.splice(index, 1);
1868
2792
  syncScrollLock(state, win);
1869
2793
  }
1870
- function pause(state, win, targetElement, options = {}) {
2794
+ /**
2795
+ * Temporarily disable a lock entry without changing its position in the stack.
2796
+ */ function bodyScrollLock_pause(state, win, targetElement, options = {}) {
1871
2797
  const index = findEntryIndex(state, targetElement, options.entryKey);
1872
- if (index < 0 || state.entries[index].active === false)
1873
- return;
2798
+ if (index < 0 || state.entries[index].active === false) return;
1874
2799
  state.entries[index].active = false;
1875
2800
  syncScrollLock(state, win);
1876
2801
  }
2802
+ /**
2803
+ * Internal recovery helper: forcibly clear all lock entries and restore the
2804
+ * window to an unlocked state.
2805
+ *
2806
+ * This bypasses normal per-entry lock/pause/unlock lifecycles and is intended
2807
+ * for testing or emergency recovery only.
2808
+ */ function clearAllLocks(state, win) {
2809
+ state.entries = [];
2810
+ if (state.docTouchStartHandler || state.docTouchMoveHandler) {
2811
+ unlockIOS(state, win);
2812
+ }
2813
+ if (state.nonIOSLocked || state.savedBodyOverflow !== '' || state.savedBodyPaddingRight !== null) {
2814
+ unlockNonIOS(state, win);
2815
+ }
2816
+ Object.assign(state, createInitialState());
2817
+ }
2818
+
1877
2819
 
1878
2820
  const SCROLL_LOCK_KEY = '__SUI_SCROLL_LOCK__';
1879
2821
  function getStoredScrollLockState(win) {
@@ -1884,66 +2826,120 @@ function setStoredScrollLockState(win, state) {
1884
2826
  global[SCROLL_LOCK_KEY] = state;
1885
2827
  return state;
1886
2828
  }
1887
- function getOrCreateScrollLockState(win) {
2829
+ /**
2830
+ * Get (or lazily create) the scroll-lock state for a given window.
2831
+ * State is stored on the window object under a unique key so it is
2832
+ * automatically isolated per window.
2833
+ */ function getOrCreateScrollLockState(win) {
1888
2834
  const state = getStoredScrollLockState(win);
1889
2835
  if (!state) {
1890
2836
  return setStoredScrollLockState(win, createInitialState());
1891
2837
  }
1892
2838
  return state;
1893
2839
  }
2840
+ function deleteStoredScrollLockState(win) {
2841
+ delete win[SCROLL_LOCK_KEY];
2842
+ }
2843
+
2844
+
2845
+
2846
+
2847
+
1894
2848
 
1895
2849
  function applyLockState(state, win, targetElement, options, disabled) {
1896
2850
  lock(state, win, targetElement, options);
1897
2851
  if (disabled) {
1898
- pause(state, win, targetElement, options);
2852
+ bodyScrollLock_pause(state, win, targetElement, options);
1899
2853
  }
1900
2854
  }
1901
2855
  function useResolvedElement(target) {
1902
- const [resolvedElement, setResolvedElement] = useState(() => (getRefElement(target) ?? null));
2856
+ const [resolvedElement, setResolvedElement] = useState(()=>getRefElement(target) ?? null);
1903
2857
  const targetRef = useRef(target);
1904
2858
  targetRef.current = target;
1905
- useEffect(() => {
1906
- const nextResolvedElement = (getRefElement(targetRef.current) ??
1907
- null);
1908
- setResolvedElement((current) => current === nextResolvedElement ? current : nextResolvedElement);
2859
+ // Re-evaluate after each render so a stable ref object can still refresh the
2860
+ // resolved element when ref.current appears or changes after commit.
2861
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
2862
+ useEffect(()=>{
2863
+ const nextResolvedElement = getRefElement(targetRef.current) ?? null;
2864
+ setResolvedElement((current)=>current === nextResolvedElement ? current : nextResolvedElement);
1909
2865
  });
1910
2866
  return resolvedElement;
1911
2867
  }
1912
- const useBodyScrollLock = (isLocked, targetElement, options = {}) => {
2868
+ /**
2869
+ * Lock body scroll while allowing bounded inner scrolling within an optional
2870
+ * target container.
2871
+ *
2872
+ * `targetElement` may be a concrete `HTMLElement` or a React `RefObject`.
2873
+ * When wiring this hook to a React overlay container, prefer passing a
2874
+ * concrete element from a callback ref / state bridge so the lock updates as
2875
+ * soon as the DOM node becomes available.
2876
+ *
2877
+ * @example
2878
+ * ```tsx
2879
+ * const [targetElement, setTargetElement] = useState<HTMLDivElement | null>(null);
2880
+ *
2881
+ * useBodyScrollLock(open, targetElement);
2882
+ *
2883
+ * return <div ref={setTargetElement}>...</div>;
2884
+ * ```
2885
+ *
2886
+ * @example
2887
+ * ```tsx
2888
+ * const targetElement = document.querySelector<HTMLElement>('#drawer-root');
2889
+ *
2890
+ * useBodyScrollLock(open, targetElement);
2891
+ * ```
2892
+ */ const useBodyScrollLock = (isLocked, targetElement, options = {})=>{
1913
2893
  const { disabled = false, reserveScrollBarGap = true } = options;
1914
2894
  const { allowTouchMove } = options;
1915
2895
  const entryKeyRef = useRef(Symbol('use-body-scroll-lock-entry'));
1916
2896
  const resolvedElement = useResolvedElement(targetElement);
2897
+ // globalThis is always defined including SSR; in a browser globalThis === window,
2898
+ // so the Win cast below is safe.
1917
2899
  const { globalThis: global } = useWindowContext();
1918
- useEffect(() => {
1919
- if (!isLocked)
1920
- return;
2900
+ // Lifetime: create one stable entry per hook instance while locked, even if
2901
+ // the resolved element is still null. The follow-up sync effect upgrades
2902
+ // anonymous → element in place once a concrete target becomes available.
2903
+ useEffect(()=>{
2904
+ if (!isLocked) return;
1921
2905
  const win = global;
1922
2906
  const state = getOrCreateScrollLockState(global);
1923
2907
  const entryKey = entryKeyRef.current;
1924
2908
  const syncOptions = {
1925
2909
  reserveScrollBarGap,
1926
2910
  allowTouchMove,
1927
- entryKey,
2911
+ entryKey
1928
2912
  };
1929
2913
  applyLockState(state, win, resolvedElement, syncOptions, disabled);
1930
- return () => {
2914
+ return ()=>{
2915
+ // Read the current live state rather than the captured one so that an
2916
+ // external clearAllLocks + delete cycle doesn't leave a dangling entry
2917
+ // in a freshly-created state object.
1931
2918
  const currentState = getStoredScrollLockState(global);
1932
2919
  if (currentState) {
1933
- unlock(currentState, win, resolvedElement, { entryKey });
2920
+ unlock(currentState, win, resolvedElement, {
2921
+ entryKey
2922
+ });
1934
2923
  }
1935
2924
  };
1936
- }, [isLocked, global]);
1937
- useEffect(() => {
1938
- if (!isLocked)
1939
- return;
2925
+ // targetElement and disabled are intentionally omitted so this entry keeps
2926
+ // its original stack position while the sync effect below mutates it.
2927
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
2928
+ }, [
2929
+ isLocked,
2930
+ global
2931
+ ]);
2932
+ // Sync current kind (anonymous vs element), disabled state, and gap preference
2933
+ // onto the stable entry without changing insertion order.
2934
+ useEffect(()=>{
2935
+ if (!isLocked) return;
1940
2936
  const win = global;
1941
2937
  const state = getOrCreateScrollLockState(global);
1942
2938
  const entryKey = entryKeyRef.current;
1943
2939
  const syncOptions = {
1944
2940
  reserveScrollBarGap,
1945
2941
  allowTouchMove,
1946
- entryKey,
2942
+ entryKey
1947
2943
  };
1948
2944
  applyLockState(state, win, resolvedElement, syncOptions, disabled);
1949
2945
  }, [
@@ -1952,18 +2948,95 @@ const useBodyScrollLock = (isLocked, targetElement, options = {}) => {
1952
2948
  isLocked,
1953
2949
  global,
1954
2950
  resolvedElement,
1955
- reserveScrollBarGap,
2951
+ reserveScrollBarGap
1956
2952
  ]);
1957
2953
  };
1958
2954
 
1959
- const useVirtuosoExternalWindow = () => {
2955
+
2956
+
2957
+
2958
+ /**
2959
+ * Hook to get external window props for Virtuoso component.
2960
+ *
2961
+ * The 'externalWindow' prop exists only in the forked version of Virtuoso.
2962
+ * Passing 'externalWindow' to the original Virtuoso component will result in a React error.
2963
+ *
2964
+ * If 'externalWindow' is present in the context, it indicates that the user is using the forked version.
2965
+ * Therefore, we only return 'externalWindow' when it exists and is not the same as the global window,
2966
+ * ensuring compatibility with detached window scenarios.
2967
+ *
2968
+ * @returns Props object with externalWindow if in detached window context, undefined otherwise
2969
+ *
2970
+ * @example
2971
+ * ```tsx
2972
+ * const externalWindowProps = useVirtuosoExternalWindow();
2973
+ *
2974
+ * return (
2975
+ * <Virtuoso
2976
+ * {...externalWindowProps}
2977
+ * // other props
2978
+ * />
2979
+ * );
2980
+ * ```
2981
+ */ const useVirtuosoExternalWindow = ()=>{
1960
2982
  const { externalWindow } = useWindowContext();
2983
+ // Only return externalWindow props when window is not the same as the global window
2984
+ // This is needed for detached window scenarios with the forked virtuoso
1961
2985
  if (externalWindow && externalWindow !== globalThis.window) {
1962
2986
  return {
1963
- externalWindow,
2987
+ externalWindow
1964
2988
  };
1965
2989
  }
1966
2990
  return undefined;
1967
2991
  };
1968
2992
 
1969
- export { createGlobalListener, globalListenerEventMap, isElmEqualOrContain, isTouchEvent, moveIndexInTwoDimension, teardown, useA11yKeyEvent, useActive, useAnnouncer, useAriaHideOutside, useAudio, useBodyScrollLock, useButton, useChange, useClickAway, useDebounce, useDepsChange, useEventCallback, useEventListener, useEver, useFocusVisible, useFontLoaded, useForceUpdate, useForkRef, useGlobalListener, useHiddenTabindex, useHover, useHoverDirty, useId, useImgLoaded, useInterval, useIsFocusVisible, useKeyDownOnce, useKeyboardMoveFocus, useLongPress, useMountState, useOnLayoutReRender, useOnReRender, useOnlyOneFocusable, useOverflow, usePrevious, useRefState, useResizeObserver, useResponsiveMatch, useResultRef, useRetry, useSleep, useTabOut, useTestRunnerDetection, useThrottle, useTimeout, useTouchMouseEvent, useVirtuosoExternalWindow, visuallyHiddenStyles };
2993
+
2994
+
2995
+
2996
+
2997
+
2998
+
2999
+
3000
+
3001
+
3002
+
3003
+
3004
+
3005
+
3006
+
3007
+
3008
+
3009
+
3010
+
3011
+
3012
+
3013
+
3014
+
3015
+
3016
+
3017
+
3018
+
3019
+
3020
+
3021
+
3022
+
3023
+
3024
+
3025
+
3026
+
3027
+
3028
+
3029
+
3030
+
3031
+
3032
+
3033
+
3034
+
3035
+
3036
+
3037
+
3038
+
3039
+
3040
+
3041
+
3042
+ export { createGlobalListener, globalListenerEventMap, isElmEqualOrContain, isTouchEvent, moveIndexInTwoDimension, teardown, unstable_useControlled as useControlled, useA11yKeyEvent, useActive, useAnnouncer, useAriaHideOutside, useAudio, useBodyScrollLock, useButton_useButton as useButton, useChange, useClickAway, useDebounce, useDepsChange, useEventCallback, useEventListener, useEver, useFocusVisible, useFontLoaded, useForceUpdate, useForkRef, useGlobalAnnouncer, useGlobalListener, useHiddenTabindex, useHover, useHoverDirty, useId, useImgLoaded, useInterval, useIsFocusVisible, useKeyDownOnce, useKeyboardMoveFocus, useLongPress, useMountState, useOnLayoutReRender, useOnReRender, useOnlyOneFocusable, useOverflow, usePrevious, useRefState, useResizeObserver, useResponsiveMatch, useResultRef, useRetry, useSleep, useTabOut, useTestRunnerDetection, useThrottle, useTimeout, useTouchMouseEvent, useVirtuosoExternalWindow };