@ringcentral/spring-base 1.12.0 → 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,72 +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 React__default, { useState, useRef, useEffect, useCallback, useLayoutEffect, useMemo } from 'react';
5
- import { useWindowContext, useResponsiveContext, getMatchDetail } from './contexts.js';
6
- import { b as LIVE_ANNOUNCER_ATTR, e as applyVisuallyHiddenStyles, a as announce } from './chunks/index.js';
7
- import uniqueId from 'lodash/uniqueId';
8
- import { useButton as useButton$1 } from '@mui/base/useButton';
9
- import { c as combineProps, d as createAriaHideOutsideController, g as getRefElement, l as logInDev, o as ownerWindow } from './chunks/twMerge.js';
10
- import './config.js';
11
- import 'lodash/omit';
12
- import 'lodash/pick';
13
- import 'lodash/isPlainObject';
14
- import debounce from 'lodash/debounce';
15
- import noop from 'lodash/noop';
16
- import throttle from 'lodash/throttle';
17
- import 'tailwind-merge';
18
-
19
- 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)=>{
20
18
  return unstable_useEventCallback(func);
21
19
  };
22
20
 
23
- const defaultCheckKeys = ['Enter', ' '];
24
- const useA11yKeyEvent = (fn, { onlyOnFocus, preventDefault = true, checkKeys = defaultCheckKeys, } = {}) => {
25
- return useEventCallback((event) => {
26
- if (!fn || (onlyOnFocus && event.target !== event.currentTarget))
27
- 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;
28
35
  if (checkKeys.includes(event.key)) {
29
- if (preventDefault)
30
- event.preventDefault();
36
+ if (preventDefault) event.preventDefault();
31
37
  fn(event);
32
38
  }
33
39
  });
34
40
  };
35
41
 
36
- 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)=>{
37
49
  const tag = el.tagName;
38
- return (tag === 'BUTTON' ||
39
- (tag === 'INPUT' &&
40
- ['button', 'submit', 'reset'].includes(el?.type)) ||
41
- (tag === 'A' && el?.href));
50
+ return tag === 'BUTTON' || tag === 'INPUT' && [
51
+ 'button',
52
+ 'submit',
53
+ 'reset'
54
+ ].includes(el?.type) || tag === 'A' && el?.href;
42
55
  };
43
- 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 } = {})=>{
44
61
  const [active, setActive] = useState(false);
45
62
  const activeTargetRef = useRef(null);
46
63
  const { document: targetDocument = document } = useWindowContext();
47
- useEffect(() => {
64
+ useEffect(()=>{
48
65
  if (disabled) {
49
66
  setActive(false);
50
67
  activeTargetRef.current = null;
51
68
  }
52
- }, [disabled]);
69
+ }, [
70
+ disabled
71
+ ]);
53
72
  if (disabled) {
54
73
  return {
55
- activeHandlers: { onKeyDown, onKeyUp, onMouseDown, onClick },
56
- active: false,
74
+ activeHandlers: {
75
+ onKeyDown,
76
+ onKeyUp,
77
+ onMouseDown,
78
+ onClick
79
+ },
80
+ active: false
57
81
  };
58
82
  }
59
- const handleMouseDown = (event) => {
83
+ const handleMouseDown = (event)=>{
60
84
  onMouseDown?.(event);
61
85
  if (event.defaultPrevented) {
62
86
  return;
63
87
  }
64
88
  setActive(true);
65
- targetDocument.addEventListener('mouseup', () => {
89
+ targetDocument.addEventListener('mouseup', ()=>{
66
90
  setActive(false);
67
- }, { once: true });
91
+ }, {
92
+ once: true
93
+ });
68
94
  };
69
- const handleKeyDown = (event) => {
95
+ const handleKeyDown = (event)=>{
70
96
  const isSpaceKey = event.key === ' ';
71
97
  onKeyDown?.(event);
72
98
  if (event.defaultPrevented) {
@@ -82,15 +108,16 @@ const useActive = ({ onKeyDown, onKeyUp, onMouseDown, onClick, disabled = false,
82
108
  }
83
109
  setActive(true);
84
110
  }
85
- if (event.target === event.currentTarget &&
86
- !isNativeButton(event.target) &&
87
- event.key === 'Enter') {
111
+ // Keyboard accessibility for non interactive elements
112
+ if (event.target === event.currentTarget && !isNativeButton(event.target) && event.key === 'Enter') {
88
113
  onClick?.(event);
89
114
  event.preventDefault();
90
115
  }
91
116
  };
92
- const handleKeyUp = (event) => {
117
+ const handleKeyUp = (event)=>{
93
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
94
121
  if (event.target === event.currentTarget) {
95
122
  setActive(false);
96
123
  }
@@ -99,11 +126,8 @@ const useActive = ({ onKeyDown, onKeyUp, onMouseDown, onClick, disabled = false,
99
126
  if (isSpaceKey) {
100
127
  activeTargetRef.current = null;
101
128
  }
102
- if (hadMatchingSpaceKeyDown &&
103
- event.target === event.currentTarget &&
104
- !isNativeButton(event.target) &&
105
- isSpaceKey &&
106
- !event.defaultPrevented) {
129
+ // Keyboard accessibility for non interactive elements
130
+ if (hadMatchingSpaceKeyDown && event.target === event.currentTarget && !isNativeButton(event.target) && isSpaceKey && !event.defaultPrevented) {
107
131
  onClick?.(event);
108
132
  }
109
133
  };
@@ -112,13 +136,27 @@ const useActive = ({ onKeyDown, onKeyUp, onMouseDown, onClick, disabled = false,
112
136
  onKeyDown: handleKeyDown,
113
137
  onKeyUp: handleKeyUp,
114
138
  onMouseDown: handleMouseDown,
115
- onClick,
139
+ onClick
116
140
  },
117
- active,
141
+ active
118
142
  };
119
143
  };
120
144
 
121
- 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) {
122
160
  const renderRef = useRef(0);
123
161
  const ref = useRef();
124
162
  if (renderRef.current === 0) {
@@ -128,9 +166,27 @@ function useResultRef(cb) {
128
166
  return ref;
129
167
  }
130
168
 
169
+
170
+
171
+
172
+
173
+
131
174
  const DEFAULT_PREFIX = 'spring-id-';
132
- function useLegacyId(id, prefixMode) {
133
- 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(()=>{
134
190
  if (id) {
135
191
  if (prefixMode) {
136
192
  return uniqueId(`${id}-`);
@@ -141,7 +197,7 @@ function useLegacyId(id, prefixMode) {
141
197
  });
142
198
  return resultId;
143
199
  }
144
- const reactUseId = React__default.useId;
200
+ const reactUseId = react.useId;
145
201
  function useReactId(id, prefixMode) {
146
202
  const reactId = reactUseId();
147
203
  if (id) {
@@ -151,50 +207,71 @@ function useReactId(id, prefixMode) {
151
207
  }
152
208
  const useId = reactUseId ? useReactId : useLegacyId;
153
209
 
154
- const isUseAnnouncerOptions = (value) => {
210
+
211
+
212
+
213
+
214
+
215
+
216
+ const isUseAnnouncerOptions = (value)=>{
155
217
  return typeof value === 'object' && value !== null && !Array.isArray(value);
156
218
  };
157
- function useAnnouncer(idOrOptions, maybeOptions) {
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) {
158
243
  const id = typeof idOrOptions === 'string' ? idOrOptions : undefined;
159
- const options = isUseAnnouncerOptions(idOrOptions)
160
- ? idOrOptions
161
- : maybeOptions;
244
+ const options = isUseAnnouncerOptions(idOrOptions) ? idOrOptions : maybeOptions;
162
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.
163
248
  const _id = useId(id ? `rc-announcer-${id}` : 'rc-announcer', !id);
164
- const { document: targetDocument, externalWindow, globalThis: contextGlobal, } = useWindowContext();
249
+ const { document: targetDocument, externalWindow, globalThis: contextGlobal } = useWindowContext();
165
250
  const win = externalWindow ?? contextGlobal;
166
251
  const ref = useRef(null);
167
252
  const lastMessageRef = useRef();
168
253
  const pendingTimerRef = useRef(null);
169
254
  const pendingRemovalRef = useRef(null);
170
- const clearPendingTimer = useCallback(() => {
255
+ const clearPendingTimer = useCallback(()=>{
171
256
  pendingTimerRef.current?.();
172
257
  pendingTimerRef.current = null;
173
258
  }, []);
174
- const clearPendingRemoval = useCallback(() => {
259
+ const clearPendingRemoval = useCallback(()=>{
175
260
  pendingRemovalRef.current?.();
176
261
  pendingRemovalRef.current = null;
177
262
  }, []);
178
- useEffect(() => {
179
- const currentDocument = targetDocument ??
180
- (typeof contextGlobal.document !== 'undefined'
181
- ? contextGlobal.document
182
- : undefined);
263
+ useEffect(()=>{
264
+ const currentDocument = targetDocument ?? (typeof contextGlobal.document !== 'undefined' ? contextGlobal.document : undefined);
183
265
  if (!currentDocument?.body) {
184
266
  return undefined;
185
267
  }
186
268
  const currentEl = ref.current;
187
- const canReuseCurrentEl = currentEl?.ownerDocument === currentDocument &&
188
- currentEl.id === _id &&
189
- currentDocument.body.contains(currentEl);
190
- const el = canReuseCurrentEl
191
- ? currentEl
192
- : currentDocument.createElement('div');
269
+ const canReuseCurrentEl = currentEl?.ownerDocument === currentDocument && currentEl.id === _id && currentDocument.body.contains(currentEl);
270
+ const el = canReuseCurrentEl ? currentEl : currentDocument.createElement('div');
193
271
  if (canReuseCurrentEl) {
194
272
  clearPendingRemoval();
195
273
  el.setAttribute('aria-live', politeness);
196
- }
197
- else {
274
+ } else {
198
275
  ref.current = el;
199
276
  el.id = _id;
200
277
  el.setAttribute('aria-live', politeness);
@@ -204,9 +281,9 @@ function useAnnouncer(idOrOptions, maybeOptions) {
204
281
  applyVisuallyHiddenStyles(el);
205
282
  currentDocument.body.appendChild(el);
206
283
  }
207
- return () => {
284
+ return ()=>{
208
285
  clearPendingTimer();
209
- const timerId = win.setTimeout(() => {
286
+ const timerId = win.setTimeout(()=>{
210
287
  const body = currentDocument.body;
211
288
  if (body.contains(el)) {
212
289
  body.removeChild(el);
@@ -218,7 +295,7 @@ function useAnnouncer(idOrOptions, maybeOptions) {
218
295
  pendingRemovalRef.current = null;
219
296
  }
220
297
  });
221
- const cancelRemoval = () => {
298
+ const cancelRemoval = ()=>{
222
299
  win.clearTimeout(timerId);
223
300
  };
224
301
  pendingRemovalRef.current = cancelRemoval;
@@ -230,12 +307,17 @@ function useAnnouncer(idOrOptions, maybeOptions) {
230
307
  contextGlobal,
231
308
  win,
232
309
  clearPendingTimer,
233
- clearPendingRemoval,
310
+ clearPendingRemoval
234
311
  ]);
235
- return useCallback((message) => {
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)=>{
236
319
  const el = ref.current;
237
- if (!el)
238
- return;
320
+ if (!el) return;
239
321
  clearPendingTimer();
240
322
  if (!message) {
241
323
  el.textContent = '';
@@ -247,8 +329,8 @@ function useAnnouncer(idOrOptions, maybeOptions) {
247
329
  lastMessageRef.current = message;
248
330
  return;
249
331
  }
250
- el.textContent = '\u200B';
251
- const timerId = win.setTimeout(() => {
332
+ el.textContent = '\u200B'; // Zero-width space (invisible but different content)
333
+ const timerId = win.setTimeout(()=>{
252
334
  if (el && ref.current === el) {
253
335
  el.textContent = message;
254
336
  lastMessageRef.current = message;
@@ -257,67 +339,113 @@ function useAnnouncer(idOrOptions, maybeOptions) {
257
339
  pendingTimerRef.current = null;
258
340
  }
259
341
  }, 100);
260
- const cancelTimer = () => {
342
+ const cancelTimer = ()=>{
261
343
  win.clearTimeout(timerId);
262
344
  };
263
345
  pendingTimerRef.current = cancelTimer;
264
- }, [clearPendingTimer, win]);
346
+ }, [
347
+ clearPendingTimer,
348
+ win
349
+ ]);
265
350
  }
266
351
 
267
- function useGlobalAnnouncer() {
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() {
268
374
  const { document: targetDocument } = useWindowContext();
269
- return useCallback((message, politeness = 'assertive', timeoutOrOptions) => {
270
- const timeout = typeof timeoutOrOptions === 'number'
271
- ? timeoutOrOptions
272
- : timeoutOrOptions?.timeout;
375
+ return useCallback((message, politeness = 'assertive', timeoutOrOptions)=>{
376
+ const timeout = typeof timeoutOrOptions === 'number' ? timeoutOrOptions : timeoutOrOptions?.timeout;
273
377
  announce(message, politeness, {
274
378
  timeout,
275
- document: targetDocument,
379
+ document: targetDocument
276
380
  });
277
- }, [targetDocument]);
381
+ }, [
382
+ targetDocument
383
+ ]);
278
384
  }
279
385
 
280
- function useButton(parameters) {
281
- const muiButtonResult = useButton$1(parameters);
386
+
387
+
388
+
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);
282
402
  const { active: customActive, activeHandlers } = useActive({
283
- disabled: parameters.disabled,
403
+ disabled: parameters.disabled
284
404
  });
285
405
  const originalGetRootProps = muiButtonResult.getRootProps;
286
- const getRootProps = (externalProps) => {
406
+ const getRootProps = (externalProps)=>{
287
407
  const mergedProps = combineProps(externalProps, activeHandlers);
288
408
  return originalGetRootProps(mergedProps);
289
409
  };
290
410
  return {
291
411
  ...muiButtonResult,
292
412
  active: customActive,
293
- getRootProps,
413
+ getRootProps
294
414
  };
295
415
  }
296
416
 
297
- const useAriaHideOutside = (props = {}) => {
417
+
418
+
419
+
420
+
421
+
422
+
423
+ const useAriaHideOutside = (props = {})=>{
298
424
  const { enable = true, shouldUseInert = false } = props;
299
425
  const { document } = useWindowContext();
300
426
  const ariaHideOutsideRef = useRef(null);
301
427
  const branchNodesRef = useRef(new Set());
302
428
  const controllerRef = useRef();
303
- const setAriaHideOutsideTarget = useCallback((node, isActive) => {
429
+ const setAriaHideOutsideTarget = useCallback((node, isActive)=>{
304
430
  if (isActive) {
305
431
  branchNodesRef.current.add(node);
306
432
  controllerRef.current?.addTarget(node);
307
- }
308
- else {
433
+ } else {
309
434
  branchNodesRef.current.delete(node);
310
435
  controllerRef.current?.removeTarget(node);
311
436
  }
312
437
  }, []);
313
- useLayoutEffect(() => {
438
+ useLayoutEffect(()=>{
314
439
  if (enable && ariaHideOutsideRef.current) {
315
- const controller = createAriaHideOutsideController([ariaHideOutsideRef.current, ...branchNodesRef.current], {
440
+ const controller = createAriaHideOutsideController([
441
+ ariaHideOutsideRef.current,
442
+ ...branchNodesRef.current
443
+ ], {
316
444
  root: document?.body,
317
- shouldUseInert,
445
+ shouldUseInert
318
446
  });
319
447
  controllerRef.current = controller;
320
- return () => {
448
+ return ()=>{
321
449
  if (controllerRef.current === controller) {
322
450
  controllerRef.current = undefined;
323
451
  }
@@ -326,38 +454,87 @@ const useAriaHideOutside = (props = {}) => {
326
454
  }
327
455
  controllerRef.current = undefined;
328
456
  return undefined;
329
- }, [enable, shouldUseInert, document?.body]);
457
+ }, [
458
+ enable,
459
+ shouldUseInert,
460
+ document?.body
461
+ ]);
330
462
  return {
331
463
  ariaHideOutsideRef: ariaHideOutsideRef,
332
- ariaHideEnabled: enable,
333
- setAriaHideOutsideTarget,
464
+ /**
465
+ * Whether outside elements are made unavailable to assistive technology.
466
+ */ ariaHideEnabled: enable,
467
+ setAriaHideOutsideTarget
334
468
  };
335
469
  };
336
470
 
337
- const useAudio = (init) => {
338
- 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(()=>{
339
497
  const audio = new Audio();
340
498
  init?.(audio);
341
499
  return audio;
342
500
  });
343
- useEffect(() => {
501
+ useEffect(()=>{
344
502
  const audio = audioRef.current;
345
- return () => {
503
+ return ()=>{
346
504
  audio.srcObject = null;
347
505
  };
506
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
348
507
  }, []);
349
508
  return audioRef.current;
350
509
  };
351
510
 
352
- 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) {
353
522
  const ref = useRef(isDefault ? cb() : undefined);
354
- useEffect(() => {
523
+ useEffect(()=>{
355
524
  ref.current = cb();
356
525
  });
357
526
  return ref.current;
358
527
  }
359
528
 
360
- 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)=>{
361
538
  const countRef = useRef(0);
362
539
  const valueRef = useRef();
363
540
  const prev = usePrevious(listenCb);
@@ -368,138 +545,234 @@ const useChange = (cb, listenCb) => {
368
545
  }
369
546
  return valueRef.current;
370
547
  };
371
- 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)=>{
372
553
  const countRef = useRef(0);
373
554
  const valueRef = useRef();
374
- const prev = usePrevious(() => deps, true);
555
+ const prev = usePrevious(()=>deps, true);
375
556
  const curr = deps;
376
- if (countRef.current === 0 ||
377
- curr.some((dep, index) => dep !== prev[index])) {
557
+ if (countRef.current === 0 || curr.some((dep, index)=>dep !== prev[index])) {
378
558
  countRef.current = 1;
379
559
  valueRef.current = cb(prev, curr);
380
560
  }
381
561
  return valueRef.current;
382
562
  };
383
563
 
564
+
565
+
566
+
567
+ // base https://github.com/mui/material-ui/blob/master/packages/mui-base/src/ClickAwayListener/ClickAwayListener.tsx
568
+
569
+
570
+
384
571
  function clickedRootScrollbar(event, doc) {
385
- return (doc.documentElement.clientWidth < event.clientX ||
386
- doc.documentElement.clientHeight < event.clientY);
572
+ return doc.documentElement.clientWidth < event.clientX || doc.documentElement.clientHeight < event.clientY;
387
573
  }
388
- 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 = {})=>{
389
579
  const { disableReactTree = false, isEnabled = true } = props;
390
580
  const movedRef = useRef(false);
391
581
  const activatedRef = useRef(false);
392
582
  const syntheticEventRef = useRef(false);
393
583
  const [target, setTarget] = useState();
394
- 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)=>{
395
587
  setTarget(e);
396
588
  });
397
589
  const shouldActive = Boolean(target);
398
- useEffect(() => {
399
- if (!isEnabled)
400
- return;
590
+ useEffect(()=>{
591
+ if (!isEnabled) return;
401
592
  if (shouldActive) {
402
- const timer = setTimeout(() => {
593
+ // Ensure that this component is not "activated" synchronously.
594
+ // https://github.com/facebook/react/issues/20074
595
+ const timer = setTimeout(()=>{
403
596
  activatedRef.current = true;
404
597
  }, 0);
405
- return () => {
598
+ return ()=>{
406
599
  clearTimeout(timer);
407
600
  activatedRef.current = false;
408
601
  };
409
602
  }
410
603
  return undefined;
411
- }, [isEnabled, shouldActive]);
412
- 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.
413
617
  const insideReactTree = syntheticEventRef.current;
414
618
  syntheticEventRef.current = false;
415
619
  const doc = unstable_ownerDocument(target);
416
- if (!activatedRef.current ||
417
- !target ||
418
- ('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)) {
419
624
  return;
420
625
  }
626
+ // Do not act if user performed touchmove
421
627
  if (movedRef.current) {
422
628
  movedRef.current = false;
423
629
  return;
424
630
  }
425
631
  let insideDOM;
632
+ // If not enough, can use https://github.com/DieterHolvoet/event-propagation-path/blob/master/propagationPath.js
426
633
  if (event.composedPath) {
427
634
  insideDOM = event.composedPath().indexOf(target) > -1;
428
- }
429
- else {
430
- insideDOM =
431
- !doc.documentElement.contains(event.target) ||
432
- 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);
433
639
  }
434
640
  if (!insideDOM && (disableReactTree || !insideReactTree)) {
435
641
  onClickAway(event);
436
642
  }
437
643
  });
438
- const handleSynthetic = (e) => {
644
+ // Keep track of mouse/touch events that bubbled up through the portal.
645
+ const handleSynthetic = (e)=>{
439
646
  syntheticEventRef.current = true;
440
647
  };
441
- useEffect(() => {
442
- if (!isEnabled)
443
- return;
648
+ useEffect(()=>{
649
+ if (!isEnabled) return;
444
650
  if (target) {
445
651
  const doc = unstable_ownerDocument(target);
446
- const handleTouchMove = () => {
652
+ const handleTouchMove = ()=>{
447
653
  movedRef.current = true;
448
654
  };
449
655
  doc.addEventListener('touchend', handleClickAway);
450
656
  doc.addEventListener('touchmove', handleTouchMove);
451
657
  doc.addEventListener('click', handleClickAway);
452
- return () => {
658
+ return ()=>{
453
659
  doc.removeEventListener('touchend', handleClickAway);
454
660
  doc.removeEventListener('touchmove', handleTouchMove);
455
661
  doc.removeEventListener('click', handleClickAway);
456
662
  };
457
663
  }
458
664
  return undefined;
459
- }, [handleClickAway, isEnabled, target]);
665
+ }, [
666
+ handleClickAway,
667
+ isEnabled,
668
+ target
669
+ ]);
460
670
  return {
461
671
  clickAwayRef,
462
672
  clickAwayHandler: {
463
673
  onClick: handleSynthetic,
464
- onTouchEnd: handleSynthetic,
465
- },
674
+ onTouchEnd: handleSynthetic
675
+ }
466
676
  };
467
677
  };
468
678
 
469
- 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)=>{
470
698
  const memoFn = useEventCallback(fn);
699
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
471
700
  const debounceFn = useCallback(debounce(memoFn, debounceTime, options), []);
472
- useEffect(() => () => debounceFn.cancel(), []);
701
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
702
+ useEffect(()=>()=>debounceFn.cancel(), []);
473
703
  return debounceFn;
474
704
  };
475
705
 
476
- 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) {
477
729
  const { startImmediately = true, ...addEventListenerOptions } = options ?? {};
478
730
  const listener = useEventCallback(callback);
479
731
  const cancelRef = useRef(noop);
480
732
  const bindRef = useRef(noop);
481
- useEffect(() => {
482
- if (!target)
483
- return;
733
+ useEffect(()=>{
734
+ if (!target) return;
484
735
  const currentElm = getRefElement(target);
485
736
  if (!currentElm) {
486
737
  return cancelRef.current;
487
738
  }
488
- bindRef.current = () => currentElm.addEventListener(key, listener, addEventListenerOptions);
489
- cancelRef.current = () => currentElm.removeEventListener(key, listener, addEventListenerOptions);
739
+ bindRef.current = ()=>currentElm.addEventListener(key, listener, addEventListenerOptions);
740
+ cancelRef.current = ()=>currentElm.removeEventListener(key, listener, addEventListenerOptions);
490
741
  if (startImmediately) {
491
742
  bindRef.current();
492
743
  }
493
744
  return cancelRef.current;
494
- }, [target]);
745
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
746
+ }, [
747
+ target
748
+ ]);
495
749
  return {
496
- listen: () => bindRef.current(),
497
- remove: () => cancelRef.current(),
750
+ listen: ()=>bindRef.current(),
751
+ remove: ()=>cancelRef.current()
498
752
  };
499
753
  }
500
754
 
501
- const defaultEverTargetValue = (value) => !!value;
502
- 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)=>{
503
776
  const result = useRef(false);
504
777
  if (!result.current) {
505
778
  result.current = targetValue(value);
@@ -507,6 +780,15 @@ const useEver = (value, targetValue = defaultEverTargetValue) => {
507
780
  return result.current;
508
781
  };
509
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
+
510
792
  let hadKeyboardEvent = true;
511
793
  let hadFocusVisibleRecently = false;
512
794
  const hadFocusVisibleRecentlyTimeout = new unstable_Timeout();
@@ -523,13 +805,17 @@ const inputTypesWhitelist = {
523
805
  week: true,
524
806
  time: true,
525
807
  datetime: true,
526
- 'datetime-local': true,
808
+ 'datetime-local': true
527
809
  };
528
- 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) {
529
817
  const { type, tagName } = node;
530
- if (tagName === 'INPUT' &&
531
- inputTypesWhitelist[type] &&
532
- !node.readOnly) {
818
+ if (tagName === 'INPUT' && inputTypesWhitelist[type] && !node.readOnly) {
533
819
  return true;
534
820
  }
535
821
  if (tagName === 'TEXTAREA' && !node.readOnly) {
@@ -540,31 +826,47 @@ function focusTriggersKeyboardModality(node) {
540
826
  }
541
827
  return false;
542
828
  }
543
- 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) {
544
836
  if (event.metaKey || event.altKey || event.ctrlKey) {
545
837
  return;
546
838
  }
547
839
  hadKeyboardEvent = true;
548
840
  }
549
- 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() {
550
848
  hadKeyboardEvent = false;
551
849
  }
552
850
  function handleVisibilityChange() {
553
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.
554
856
  if (hadFocusVisibleRecently) {
555
857
  hadKeyboardEvent = true;
556
858
  }
557
859
  }
558
860
  }
559
861
  function prepare(doc) {
560
- doc.addEventListener('keydown', handleKeyDown, true);
862
+ doc.addEventListener('keydown', useIsFocusVisible_handleKeyDown, true);
561
863
  doc.addEventListener('mousedown', handlePointerDown, true);
562
864
  doc.addEventListener('pointerdown', handlePointerDown, true);
563
865
  doc.addEventListener('touchstart', handlePointerDown, true);
564
866
  doc.addEventListener('visibilitychange', handleVisibilityChange, true);
565
867
  }
566
868
  function teardown(doc) {
567
- doc.removeEventListener('keydown', handleKeyDown, true);
869
+ doc.removeEventListener('keydown', useIsFocusVisible_handleKeyDown, true);
568
870
  doc.removeEventListener('mousedown', handlePointerDown, true);
569
871
  doc.removeEventListener('pointerdown', handlePointerDown, true);
570
872
  doc.removeEventListener('touchstart', handlePointerDown, true);
@@ -574,22 +876,38 @@ function isFocusVisible(event) {
574
876
  const { target } = event;
575
877
  try {
576
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?
577
884
  }
578
- catch (error) {
579
- }
885
+ // No need for validFocusTarget check. The user does that by attaching it to
886
+ // focusable events only.
580
887
  return hadKeyboardEvent || focusTriggersKeyboardModality(target);
581
888
  }
582
889
  function useIsFocusVisible() {
583
- const ref = React.useCallback((node) => {
890
+ const ref = __rspack_external_react.useCallback((node)=>{
584
891
  if (node != null) {
585
892
  prepare(node.ownerDocument);
586
893
  }
587
894
  }, []);
588
- const isFocusVisibleRef = React.useRef(false);
589
- 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).
590
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.
591
909
  hadFocusVisibleRecently = true;
592
- hadFocusVisibleRecentlyTimeout.start(100, () => {
910
+ hadFocusVisibleRecentlyTimeout.start(100, ()=>{
593
911
  hadFocusVisibleRecently = false;
594
912
  });
595
913
  isFocusVisibleRef.current = false;
@@ -597,7 +915,9 @@ function useIsFocusVisible() {
597
915
  }
598
916
  return false;
599
917
  }
600
- function handleFocusVisible(event) {
918
+ /**
919
+ * Should be called if a blur event is fired
920
+ */ function handleFocusVisible(event) {
601
921
  if (isFocusVisible(event)) {
602
922
  isFocusVisibleRef.current = true;
603
923
  return true;
@@ -608,75 +928,105 @@ function useIsFocusVisible() {
608
928
  isFocusVisibleRef,
609
929
  onFocus: handleFocusVisible,
610
930
  onBlur: handleBlurVisible,
611
- ref,
931
+ ref
612
932
  };
613
933
  }
614
934
 
615
- const useFocusVisible = ({ onFocusVisible, onFocus, onBlur, within = false, } = {}) => {
616
- 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();
617
949
  const [focusVisible, setFocusVisible] = useState(false);
618
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.
619
955
  const globalFocusListenerRef = useRef(null);
620
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.
621
960
  const ownerDocumentRef = useRef(null);
622
- const removeGlobalFocusListener = useCallback(() => {
961
+ const removeGlobalFocusListener = useCallback(()=>{
623
962
  if (globalFocusListenerRef.current) {
624
963
  ownerDocumentRef.current?.removeEventListener('focus', globalFocusListenerRef.current, true);
625
964
  globalFocusListenerRef.current = null;
626
965
  }
627
966
  }, []);
628
- useEffect(() => {
967
+ useEffect(()=>{
629
968
  return removeGlobalFocusListener;
630
- }, [removeGlobalFocusListener]);
631
- const handleBlur = (event) => {
969
+ }, [
970
+ removeGlobalFocusListener
971
+ ]);
972
+ const handleBlur = (event)=>{
632
973
  handleBlurVisible(event);
633
974
  if (isFocusVisibleRef.current === false) {
634
975
  setFocusVisible(false);
635
- if (within &&
636
- !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)) {
637
985
  setFocusVisibleWithin(false);
638
986
  }
639
987
  }
640
- if (within &&
641
- globalFocusListenerRef.current &&
642
- !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)) {
643
991
  removeGlobalFocusListener();
644
992
  }
645
993
  onBlur?.(event);
646
994
  };
647
- const handleFocus = (event) => {
995
+ const handleFocus = (event)=>{
648
996
  let updateState;
649
997
  if (within) {
650
998
  if (event.target === event.currentTarget) {
651
- updateState = () => {
999
+ updateState = ()=>{
652
1000
  setFocusVisibleWithin(true);
653
1001
  setFocusVisible(true);
654
1002
  };
655
- }
656
- else {
657
- updateState = () => {
1003
+ } else {
1004
+ updateState = ()=>{
658
1005
  setFocusVisibleWithin(true);
659
1006
  };
660
1007
  }
661
- }
662
- else if (event.target === event.currentTarget) {
663
- updateState = () => {
1008
+ } else if (event.target === event.currentTarget) {
1009
+ updateState = ()=>{
664
1010
  setFocusVisible(true);
665
1011
  };
666
- }
667
- else {
1012
+ } else {
668
1013
  return;
669
1014
  }
670
1015
  handleFocusVisible(event);
671
1016
  if (isFocusVisibleRef.current === true) {
672
1017
  updateState();
673
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
674
1024
  if (within) {
675
1025
  removeGlobalFocusListener();
676
1026
  containerRef.current = event.currentTarget;
677
1027
  ownerDocumentRef.current = event.currentTarget.ownerDocument;
678
1028
  const ownerDoc = ownerDocumentRef.current;
679
- const listener = (e) => {
1029
+ const listener = (e)=>{
680
1030
  if (e.target && !containerRef.current?.contains(e.target)) {
681
1031
  setFocusVisibleWithin(false);
682
1032
  ownerDoc.removeEventListener('focus', listener, true);
@@ -686,8 +1036,7 @@ const useFocusVisible = ({ onFocusVisible, onFocus, onBlur, within = false, } =
686
1036
  globalFocusListenerRef.current = listener;
687
1037
  ownerDoc.addEventListener('focus', listener, true);
688
1038
  }
689
- }
690
- else if (within) {
1039
+ } else if (within) {
691
1040
  setFocusVisibleWithin(false);
692
1041
  removeGlobalFocusListener();
693
1042
  }
@@ -696,55 +1045,97 @@ const useFocusVisible = ({ onFocusVisible, onFocus, onBlur, within = false, } =
696
1045
  return {
697
1046
  focusVisibleHandlers: {
698
1047
  onBlur: handleBlur,
699
- onFocus: handleFocus,
1048
+ onFocus: handleFocus
700
1049
  },
1050
+ // oxlint-disable-next-line @typescript-eslint/no-explicit-any
701
1051
  focusVisibleRef: focusVisibleRef,
702
1052
  focusVisible,
703
- focusVisibleWithin,
1053
+ focusVisibleWithin
704
1054
  };
705
1055
  };
706
1056
 
707
- function useForceUpdate() {
1057
+
1058
+
1059
+
1060
+
1061
+
1062
+ /**
1063
+ * a method for trigger force update
1064
+ * @returns force update method
1065
+ */ function useForceUpdate() {
708
1066
  const [, dispatch] = useState(Object.create(null));
709
- return useEventCallback(() => {
1067
+ return useEventCallback(()=>{
710
1068
  dispatch(Object.create(null));
711
1069
  });
712
1070
  }
713
1071
 
714
- 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
+
715
1096
 
716
1097
  const globalListenerEventMap = new Map();
717
- 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 } = {})=>{
718
1111
  let listening = false;
719
- const getMapValue = () => globalListenerEventMap.get(customKey);
720
- const addListener = () => {
721
- if (listening)
722
- return;
1112
+ const getMapValue = ()=>globalListenerEventMap.get(customKey);
1113
+ const addListener = ()=>{
1114
+ if (listening) return;
723
1115
  const savedEvent = getMapValue();
724
1116
  if (!savedEvent?.exec) {
725
- const exec = (e) => {
1117
+ const exec = (e)=>{
726
1118
  const event = getMapValue();
727
- event?.listeners.forEach((c) => c(e));
1119
+ event?.listeners.forEach((c)=>c(e));
728
1120
  };
729
1121
  globalListenerEventMap.set(customKey, {
730
1122
  exec,
731
- listeners: new Set([listener]),
1123
+ listeners: new Set([
1124
+ listener
1125
+ ])
732
1126
  });
733
1127
  const target = getRefElement(targetProp);
734
1128
  target?.addEventListener(key, exec, options);
735
- }
736
- else {
1129
+ } else {
737
1130
  savedEvent.listeners.add(listener);
738
1131
  }
739
1132
  listening = true;
740
1133
  };
741
- const removeListener = () => {
742
- if (!listening)
743
- return;
1134
+ const removeListener = ()=>{
1135
+ if (!listening) return;
744
1136
  listening = false;
745
1137
  const _savedEvent = getMapValue();
746
- if (!_savedEvent)
747
- return;
1138
+ if (!_savedEvent) return;
748
1139
  const { listeners } = _savedEvent;
749
1140
  listeners.delete(listener);
750
1141
  if (listeners.size === 0) {
@@ -754,34 +1145,80 @@ const createGlobalListener = (key, listener, options, { customKey = key, target:
754
1145
  }
755
1146
  };
756
1147
  return {
757
- listen: () => addListener(),
758
- remove: () => removeListener(),
759
- get state() {
1148
+ listen: ()=>addListener(),
1149
+ /**
1150
+ * remove listener
1151
+ */ remove: ()=>removeListener(),
1152
+ /**
1153
+ * current listener state
1154
+ */ get state () {
760
1155
  return {
761
- get count() {
1156
+ /**
1157
+ * current listener state count
1158
+ */ get count () {
762
1159
  return getMapValue()?.listeners.size || 0;
763
1160
  },
764
- listening,
1161
+ /**
1162
+ * is that be listening
1163
+ */ listening
765
1164
  };
766
- },
1165
+ }
767
1166
  };
768
1167
  };
769
1168
 
770
- 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) {
771
1207
  const { options, config } = getListenerOverloadOption(args);
772
- const { customKey = key, target: targetProp = window, startImmediately = true, } = config || {};
1208
+ const { customKey = key, target: targetProp = window, startImmediately = true } = config || {};
773
1209
  const _listener = useEventCallback(listener);
774
1210
  const { current: globalListener } = useRef(createGlobalListener(key, _listener, options, {
775
1211
  customKey,
776
- target: targetProp,
1212
+ target: targetProp
777
1213
  }));
778
1214
  if (process.env.NODE_ENV !== 'production') {
779
- useEffect(() => {
1215
+ // oxlint-disable-next-line react-hooks/rules-of-hooks
1216
+ useEffect(()=>{
780
1217
  if (customKey === key) {
781
1218
  if (Object.keys(options).length > 0) {
782
1219
  logInDev({
783
1220
  scope: 'useGlobalListener',
784
- 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'
785
1222
  });
786
1223
  }
787
1224
  const target = getRefElement(targetProp);
@@ -789,17 +1226,19 @@ function useGlobalListener(key, listener, ...args) {
789
1226
  logInDev({
790
1227
  scope: 'useGlobalListener',
791
1228
  message: 'When you custom binding event target, you must custom key for determining different event group',
792
- level: 'error',
1229
+ level: 'error'
793
1230
  });
794
1231
  }
795
1232
  }
1233
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
796
1234
  }, []);
797
1235
  }
798
- useEffect(() => {
1236
+ useEffect(()=>{
799
1237
  if (startImmediately) {
800
1238
  globalListener.listen();
801
1239
  }
802
1240
  return globalListener.remove;
1241
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
803
1242
  }, []);
804
1243
  return globalListener;
805
1244
  }
@@ -809,30 +1248,40 @@ function getListenerOverloadOption(args) {
809
1248
  if (typeof args[0] === 'boolean') {
810
1249
  options = args[0];
811
1250
  config = args[1];
812
- }
813
- else {
1251
+ } else {
814
1252
  const { startImmediately, customKey, target, ...restOptions } = {
815
1253
  ...args[0],
816
- ...args[1],
1254
+ ...args[1]
817
1255
  };
818
1256
  options = restOptions;
819
1257
  config = {
820
1258
  startImmediately,
821
1259
  customKey,
822
- target,
1260
+ target
823
1261
  };
824
1262
  }
825
- return { options, config };
1263
+ return {
1264
+ options,
1265
+ config
1266
+ };
826
1267
  }
827
1268
 
828
- const useHiddenTabindex = (target) => {
829
- 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(()=>{
830
1279
  const hiddenElm = getRefElement(target);
831
1280
  if (hiddenElm && hiddenElm.tabIndex === 0) {
832
1281
  hiddenElm.tabIndex = -1;
833
1282
  }
834
1283
  });
835
- const handleBlur = useEventCallback(() => {
1284
+ const handleBlur = useEventCallback(()=>{
836
1285
  const hiddenElm = getRefElement(target);
837
1286
  if (hiddenElm && hiddenElm.tabIndex === -1) {
838
1287
  hiddenElm.tabIndex = 0;
@@ -840,86 +1289,111 @@ const useHiddenTabindex = (target) => {
840
1289
  });
841
1290
  return {
842
1291
  onFocus: handleFocus,
843
- onBlur: handleBlur,
1292
+ onBlur: handleBlur
844
1293
  };
845
1294
  };
846
1295
 
847
- 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 } = {})=>{
848
1301
  const [hover, setHover] = useState(false);
849
1302
  const hoverRef = useRef(false);
850
- useEffect(() => {
1303
+ useEffect(()=>{
851
1304
  if (disabled) {
852
1305
  setHover(false);
853
- }
854
- else if (!disabled && hoverRef.current) {
1306
+ } else if (!disabled && hoverRef.current) {
855
1307
  setHover(true);
856
1308
  }
857
- }, [disabled]);
858
- const updateHoverState = (hover, event) => {
859
- if (disableReactTree &&
860
- !event.currentTarget.contains(event.target)) {
1309
+ }, [
1310
+ disabled
1311
+ ]);
1312
+ const updateHoverState = (hover, event)=>{
1313
+ if (disableReactTree && !event.currentTarget.contains(event.target)) {
861
1314
  return;
862
1315
  }
863
- if (!disabled)
864
- setHover(hover);
1316
+ if (!disabled) setHover(hover);
865
1317
  hoverRef.current = hover;
866
1318
  };
867
- const onMouseEnter = (event) => {
1319
+ const onMouseEnter = (event)=>{
868
1320
  handleMouseEnter?.(event);
869
1321
  updateHoverState(true, event);
870
1322
  };
871
- const onMouseLeave = (event) => {
1323
+ const onMouseLeave = (event)=>{
872
1324
  handleMouseLeave?.(event);
873
1325
  updateHoverState(false, event);
874
1326
  };
875
1327
  return {
876
- hoverHandlers: { onMouseEnter, onMouseLeave },
877
- hover,
1328
+ hoverHandlers: {
1329
+ onMouseEnter,
1330
+ onMouseLeave
1331
+ },
1332
+ hover
878
1333
  };
879
1334
  };
880
1335
 
881
- const useHoverDirty = ({ target, disabled = false, }) => {
1336
+
1337
+
1338
+
1339
+ const useHoverDirty = ({ target, disabled = false })=>{
882
1340
  const [hover, setHover] = useState(false);
883
- useEffect(() => {
884
- if (disabled || !target)
885
- setHover(false);
886
- }, [disabled, target]);
887
- useEventListener(target, 'mouseenter', () => {
888
- if (disabled)
889
- return;
1341
+ useEffect(()=>{
1342
+ if (disabled || !target) setHover(false);
1343
+ }, [
1344
+ disabled,
1345
+ target
1346
+ ]);
1347
+ useEventListener(target, 'mouseenter', ()=>{
1348
+ if (disabled) return;
890
1349
  setHover(true);
891
1350
  });
892
- useEventListener(target, 'mouseleave', () => {
893
- if (disabled)
894
- return;
1351
+ useEventListener(target, 'mouseleave', ()=>{
1352
+ if (disabled) return;
895
1353
  setHover(false);
896
1354
  });
897
1355
  return {
898
- hover,
1356
+ hover
899
1357
  };
900
1358
  };
901
1359
 
902
- 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 })=>{
903
1380
  const [loaded, setLoaded] = useState(false);
904
- useEffect(() => {
1381
+ useIsoLayoutEffect(()=>{
905
1382
  if (!src && !srcSet) {
1383
+ setLoaded('error');
906
1384
  return undefined;
907
1385
  }
908
1386
  setLoaded(false);
909
1387
  let active = true;
910
- const image = new Image();
911
- image.onload = () => {
912
- if (!active) {
913
- return;
914
- }
915
- setLoaded('loaded');
916
- };
917
- image.onerror = () => {
918
- if (!active) {
919
- return;
920
- }
921
- setLoaded('error');
922
- };
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');
923
1397
  crossOrigin && (image.crossOrigin = crossOrigin);
924
1398
  referrerPolicy && (image.referrerPolicy = referrerPolicy);
925
1399
  if (src) {
@@ -928,71 +1402,133 @@ const useImgLoaded = ({ crossOrigin, referrerPolicy, src, srcSet, }) => {
928
1402
  if (srcSet) {
929
1403
  image.srcset = srcSet;
930
1404
  }
931
- return () => {
1405
+ if (image.complete) {
1406
+ setLoaded(image.naturalWidth > 0 ? 'loaded' : 'error');
1407
+ }
1408
+ return ()=>{
932
1409
  active = false;
933
1410
  };
934
- }, [crossOrigin, referrerPolicy, src, srcSet]);
1411
+ }, [
1412
+ crossOrigin,
1413
+ referrerPolicy,
1414
+ src,
1415
+ srcSet
1416
+ ]);
935
1417
  return loaded;
936
1418
  };
937
1419
 
938
- 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)=>{
939
1450
  const intervalRef = useRef();
940
1451
  const countRef = useRef(0);
941
- const getRunning = useEventCallback(() => !!intervalRef.current);
942
- const savedCallback = useEventCallback((times) => {
1452
+ const getRunning = useEventCallback(()=>!!intervalRef.current);
1453
+ const savedCallback = useEventCallback((times)=>{
943
1454
  callback(times);
944
1455
  });
945
- const pause = useCallback(() => {
1456
+ const pause = useCallback(()=>{
946
1457
  if (intervalRef.current) {
947
1458
  clearInterval(intervalRef.current);
948
1459
  intervalRef.current = undefined;
949
1460
  }
950
1461
  }, []);
951
- const cancel = useCallback(() => {
1462
+ const cancel = useCallback(()=>{
952
1463
  pause();
953
1464
  countRef.current = 0;
954
- }, [pause]);
955
- const play = useCallback(() => {
1465
+ }, [
1466
+ pause
1467
+ ]);
1468
+ const play = useCallback(()=>{
956
1469
  const isRunning = getRunning();
957
- if (isRunning)
958
- return;
959
- intervalRef.current = setInterval(() => {
1470
+ if (isRunning) return;
1471
+ intervalRef.current = setInterval(()=>{
960
1472
  countRef.current++;
961
1473
  savedCallback(countRef.current);
962
1474
  }, delay);
963
- }, [delay, getRunning, savedCallback]);
964
- useEffect(() => {
1475
+ }, [
1476
+ delay,
1477
+ getRunning,
1478
+ savedCallback
1479
+ ]);
1480
+ useEffect(()=>{
965
1481
  if (startImmediately) {
966
1482
  play();
967
1483
  }
968
- return () => cancel();
969
- }, [cancel, play, startImmediately]);
970
- return {
1484
+ return ()=>cancel();
1485
+ }, [
971
1486
  cancel,
972
- pause,
973
1487
  play,
974
- 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
975
1503
  };
976
1504
  };
977
1505
 
978
- 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 })=>{
979
1517
  const isKeyDownMapRef = useRef({});
980
- const handleKeyDown = useEventCallback((e) => {
1518
+ const handleKeyDown = useEventCallback((e)=>{
981
1519
  if (includeKeys.includes(e.key)) {
982
1520
  if (!isKeyDownMapRef.current[e.key]) {
983
1521
  onKeyDown?.(e);
984
- }
985
- else {
1522
+ } else {
986
1523
  e.preventDefault();
987
1524
  e.stopPropagation();
988
1525
  }
989
1526
  isKeyDownMapRef.current[e.key] = true;
990
- }
991
- else {
1527
+ } else {
992
1528
  onKeyDown?.(e);
993
1529
  }
994
1530
  });
995
- const handleKeyUp = useEventCallback((e) => {
1531
+ const handleKeyUp = useEventCallback((e)=>{
996
1532
  if (includeKeys.includes(e.key)) {
997
1533
  isKeyDownMapRef.current[e.key] = false;
998
1534
  }
@@ -1000,48 +1536,57 @@ const useKeyDownOnce = (includeKeys, { onKeyDown, onKeyUp }) => {
1000
1536
  });
1001
1537
  return {
1002
1538
  onKeyDown: handleKeyDown,
1003
- onKeyUp: handleKeyUp,
1539
+ onKeyUp: handleKeyUp
1004
1540
  };
1005
1541
  };
1006
1542
 
1007
- function moveIndexInTwoDimension(direction, options) {
1543
+
1544
+
1545
+ /**
1546
+ * that can move index in column array
1547
+ */ function moveIndexInTwoDimension(direction, options) {
1008
1548
  const { currIndex, columns, total, infinite } = options;
1549
+ // start index never small than 0
1550
+ // const index = Math.max(currIndex, 0);
1009
1551
  const lastIndex = total - 1;
1010
1552
  const isInOrder = infinite === 'order';
1011
1553
  const fillTotal = Math.ceil(total / columns) * columns;
1012
1554
  let resultIndex;
1013
- switch (direction) {
1014
- case 'ArrowUp': {
1015
- const toIndex = (Math.max(currIndex, 0) - columns + fillTotal) % fillTotal;
1016
- resultIndex = infinite ? toIndex : Math.min(toIndex, currIndex);
1017
- break;
1018
- }
1019
- case 'ArrowRight': {
1020
- if (isInOrder) {
1021
- const tmp = currIndex + 1;
1022
- 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);
1023
1560
  break;
1024
1561
  }
1025
- const toIndex = Math.floor(currIndex / columns) * columns + ((currIndex + 1) % columns);
1026
- resultIndex = infinite ? toIndex : Math.max(toIndex, currIndex);
1027
- break;
1028
- }
1029
- case 'ArrowDown': {
1030
- const toIndex = (currIndex + columns) % fillTotal;
1031
- resultIndex = infinite ? toIndex : Math.max(toIndex, currIndex);
1032
- break;
1033
- }
1034
- case 'ArrowLeft': {
1035
- if (isInOrder) {
1036
- const tmp = currIndex - 1;
1037
- 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);
1038
1588
  break;
1039
1589
  }
1040
- const toIndex = Math.floor(currIndex / columns) * columns +
1041
- ((currIndex - 1 + columns) % columns);
1042
- resultIndex = infinite ? toIndex : Math.min(toIndex, currIndex);
1043
- break;
1044
- }
1045
1590
  case 'Home':
1046
1591
  resultIndex = 0;
1047
1592
  break;
@@ -1054,55 +1599,56 @@ function moveIndexInTwoDimension(direction, options) {
1054
1599
  if (!infinite && resultIndex >= total) {
1055
1600
  return currIndex;
1056
1601
  }
1602
+ // * when that bigger then sourceTotal, mean that touch end line
1603
+ // * move to current full latest and move again.
1057
1604
  if (resultIndex >= total) {
1058
1605
  if (direction === 'ArrowRight') {
1059
1606
  resultIndex = fillTotal - 1;
1060
1607
  }
1061
1608
  return moveIndexInTwoDimension(direction, {
1062
1609
  ...options,
1063
- currIndex: resultIndex,
1610
+ currIndex: resultIndex
1064
1611
  });
1065
1612
  }
1066
1613
  return resultIndex;
1067
1614
  }
1068
1615
 
1069
- 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 })=>{
1070
1623
  const total = options.length;
1071
1624
  const searchText = useRef('');
1072
- const debounceEndSearch = useDebounce(() => (searchText.current = ''), endSearchDebounceTime);
1073
- const searchTextAndFocus = (event) => {
1625
+ const debounceEndSearch = useDebounce(()=>searchText.current = '', endSearchDebounceTime);
1626
+ const searchTextAndFocus = (event)=>{
1074
1627
  const key = event.key;
1075
- if (!getOptionSearchText || key.length !== 1)
1076
- return;
1628
+ if (!getOptionSearchText || key.length !== 1) return;
1077
1629
  debounceEndSearch();
1078
1630
  searchText.current += key.toLowerCase();
1079
- const matchedIndex = options.findIndex((option) => {
1631
+ const matchedIndex = options.findIndex((option)=>{
1080
1632
  const text = getOptionSearchText(option);
1081
- return Boolean(!getOptionDisabled?.(option) &&
1082
- text &&
1083
- text.toLowerCase().startsWith(searchText.current));
1633
+ return Boolean(!getOptionDisabled?.(option) && text && text.toLowerCase().startsWith(searchText.current));
1084
1634
  });
1085
1635
  if (matchedIndex > -1) {
1086
1636
  onFocusedIndexChange(event, matchedIndex);
1087
1637
  }
1088
1638
  };
1089
- const onKeyFocusedIndexHandle = (event) => {
1639
+ const onKeyFocusedIndexHandle = (event)=>{
1090
1640
  let currIndex = focusedIndexRef.current;
1091
1641
  const key = event.key;
1092
1642
  let findDirection;
1093
- if (total === 0)
1094
- return;
1095
- while (true) {
1096
- const toIndex = findDirection
1097
- ? findDirection === 'next'
1098
- ? currIndex + 1
1099
- : currIndex - 1
1100
- : moveIndexInTwoDimension(key, {
1101
- currIndex,
1102
- columns,
1103
- total,
1104
- infinite,
1105
- });
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
+ });
1106
1652
  if (toIndex === currIndex) {
1107
1653
  searchTextAndFocus(event);
1108
1654
  break;
@@ -1112,76 +1658,99 @@ const useKeyboardMoveFocus = ({ infinite, options = [], focusedIndexRef, onFocus
1112
1658
  onFocusedIndexChange(event, toIndex);
1113
1659
  }
1114
1660
  break;
1115
- }
1116
- else {
1117
- if (focusedIndexRef.current !== -1 &&
1118
- 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)) {
1119
1664
  break;
1120
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
1121
1668
  if (focusedIndexRef.current === -1) {
1122
- if ((Math.abs(currIndex - toIndex) !== 1 || currIndex > toIndex) &&
1123
- 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) {
1124
1671
  break;
1125
1672
  }
1126
- if (Math.abs(currIndex - toIndex) === 1 &&
1127
- currIndex < toIndex &&
1128
- 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) {
1129
1675
  break;
1130
1676
  }
1131
1677
  }
1132
1678
  currIndex = toIndex;
1679
+ // * if that is Hom or End, find use increase or decrease
1133
1680
  if (key === 'Home') {
1134
1681
  findDirection = 'next';
1135
- }
1136
- else if (key === 'End') {
1682
+ } else if (key === 'End') {
1137
1683
  findDirection = 'prev';
1138
1684
  }
1139
1685
  }
1140
1686
  }
1141
1687
  };
1142
- const getPrevFocusableOption = useCallback((index = focusedIndexRef.current, isInfinite = true) => {
1143
- 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--){
1144
1690
  if (!getOptionDisabled?.(options[i])) {
1145
1691
  return i;
1146
1692
  }
1147
1693
  }
1148
1694
  if (isInfinite) {
1149
- for (let i = total - 1; i > index; i--) {
1695
+ // loop find
1696
+ for(let i = total - 1; i > index; i--){
1150
1697
  if (!getOptionDisabled?.(options[i])) {
1151
1698
  return i;
1152
1699
  }
1153
1700
  }
1154
1701
  }
1155
1702
  return -1;
1156
- }, [focusedIndexRef, getOptionDisabled, options, total]);
1157
- const getNextFocusableOption = useCallback((index = focusedIndexRef.current, isInfinite = true) => {
1158
- 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++){
1159
1711
  if (!getOptionDisabled?.(options[i])) {
1160
1712
  return i;
1161
1713
  }
1162
1714
  }
1163
1715
  if (isInfinite) {
1164
- for (let i = 0; i < index; i++) {
1716
+ // loop find
1717
+ for(let i = 0; i < index; i++){
1165
1718
  if (!getOptionDisabled?.(options[i])) {
1166
1719
  return i;
1167
1720
  }
1168
1721
  }
1169
1722
  }
1170
1723
  return -1;
1171
- }, [focusedIndexRef, getOptionDisabled, options, total]);
1724
+ }, [
1725
+ focusedIndexRef,
1726
+ getOptionDisabled,
1727
+ options,
1728
+ total
1729
+ ]);
1172
1730
  return {
1173
- getPrevFocusableOption,
1174
- getNextFocusableOption,
1175
- 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
1176
1740
  };
1177
1741
  };
1178
1742
 
1743
+
1744
+
1179
1745
  const MOVE_ATTR_KEY = 'data-move-focused';
1180
1746
  const MOVE_INDEX_ATTR_KEY = 'data-move-index';
1181
- const useOnlyOneFocusable = ({ focusedIndexRef, containerRef, }) => {
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 })=>{
1182
1751
  const moveFocusedId = useId();
1183
- const getIndexElm = (toIndex) => containerRef.current?.querySelector(`[${MOVE_ATTR_KEY}="${moveFocusedId}"][${MOVE_INDEX_ATTR_KEY}="${toIndex}"]`);
1184
- const setIndexTabIndex = (toIndex) => {
1752
+ const getIndexElm = (toIndex)=>containerRef.current?.querySelector(`[${MOVE_ATTR_KEY}="${moveFocusedId}"][${MOVE_INDEX_ATTR_KEY}="${toIndex}"]`);
1753
+ const setIndexTabIndex = (toIndex)=>{
1185
1754
  const toElm = getIndexElm(toIndex);
1186
1755
  if (toElm) {
1187
1756
  const prevElm = containerRef.current?.querySelector(`[${MOVE_ATTR_KEY}="${moveFocusedId}"][tabindex="0"]`);
@@ -1189,7 +1758,7 @@ const useOnlyOneFocusable = ({ focusedIndexRef, containerRef, }) => {
1189
1758
  toElm.setAttribute('tabindex', '0');
1190
1759
  }
1191
1760
  };
1192
- const focusIndex = (toIndex) => {
1761
+ const focusIndex = (toIndex)=>{
1193
1762
  const toElm = getIndexElm(toIndex);
1194
1763
  toElm?.focus();
1195
1764
  };
@@ -1197,23 +1766,53 @@ const useOnlyOneFocusable = ({ focusedIndexRef, containerRef, }) => {
1197
1766
  moveFocusedId,
1198
1767
  focusIndex,
1199
1768
  setIndexTabIndex,
1200
- getItemProps: (index, options) => {
1769
+ getItemProps: (index, options)=>{
1201
1770
  return combineProps({
1202
1771
  [MOVE_ATTR_KEY]: moveFocusedId,
1203
1772
  [MOVE_INDEX_ATTR_KEY]: String(index),
1204
1773
  tabIndex: index === focusedIndexRef.current ? 0 : -1,
1205
- onFocus: () => {
1774
+ onFocus: ()=>{
1206
1775
  focusedIndexRef.current = index;
1207
1776
  setIndexTabIndex(index);
1208
- },
1777
+ }
1209
1778
  }, options ?? {});
1210
- },
1779
+ }
1211
1780
  };
1212
1781
  };
1213
1782
 
1214
- 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 } = {})=>{
1215
1814
  const stopMouseEventRef = useRef(false);
1216
- const onTouchStart = useEventCallback((e) => {
1815
+ const onTouchStart = useEventCallback((e)=>{
1217
1816
  stopMouseEventRef.current = true;
1218
1817
  onTouchStartArg?.(e);
1219
1818
  });
@@ -1224,7 +1823,7 @@ const useTouchMouseEvent = ({ onMouseDown: onMouseDownArg, onMouseUp: onMouseUpA
1224
1823
  }
1225
1824
  return false;
1226
1825
  }
1227
- const onTouchEnd = useEventCallback((e) => {
1826
+ const onTouchEnd = useEventCallback((e)=>{
1228
1827
  let isInside;
1229
1828
  if (actionRef && e.touches.length < 2 && e.changedTouches.length < 2) {
1230
1829
  const touch = e.touches[0] || e.changedTouches[0];
@@ -1233,16 +1832,15 @@ const useTouchMouseEvent = ({ onMouseDown: onMouseDownArg, onMouseUp: onMouseUpA
1233
1832
  }
1234
1833
  onTouchEndArg?.(e, isInside);
1235
1834
  });
1236
- const onMouseDown = useEventCallback((e) => {
1835
+ const onMouseDown = useEventCallback((e)=>{
1237
1836
  if (stopMouseEventRef.current) {
1238
1837
  return;
1239
1838
  }
1240
1839
  onMouseDownArg?.(e);
1241
1840
  });
1242
- const onMouseUp = useEventCallback((e) => {
1841
+ const onMouseUp = useEventCallback((e)=>{
1243
1842
  let isInside;
1244
- if (stopMouseEventRef.current)
1245
- return;
1843
+ if (stopMouseEventRef.current) return;
1246
1844
  if (actionRef) {
1247
1845
  const elm = e.target;
1248
1846
  isInside = !!elm && checkInside(elm);
@@ -1253,30 +1851,50 @@ const useTouchMouseEvent = ({ onMouseDown: onMouseDownArg, onMouseUp: onMouseUpA
1253
1851
  onTouchStart,
1254
1852
  onTouchEnd,
1255
1853
  onMouseDown,
1256
- onMouseUp,
1854
+ onMouseUp
1257
1855
  };
1258
1856
  };
1259
- const isElmEqualOrContain = (sourceTarget, containTarget) => {
1857
+ /**
1858
+ * check is element equal or contains target element
1859
+ */ const isElmEqualOrContain = (sourceTarget, containTarget)=>{
1260
1860
  const elm = getRefElement(sourceTarget);
1261
1861
  const target = getRefElement(containTarget);
1262
1862
  return elm && target && (target === elm || target.contains(elm));
1263
1863
  };
1264
1864
 
1265
- const isTouchEvent = (ev) => {
1865
+
1866
+
1867
+
1868
+
1869
+
1870
+
1871
+
1872
+
1873
+
1874
+ const isTouchEvent = (ev)=>{
1266
1875
  return 'touches' in ev;
1267
1876
  };
1268
- 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 } = {})=>{
1269
1885
  const isEmittedRef = useRef(false);
1270
1886
  const isA11yDownRef = useRef(false);
1271
1887
  const actionRef = useRef(null);
1272
1888
  const reasonRef = useRef('click');
1273
- const emitLongPress = useDebounce((e) => {
1889
+ const emitLongPress = useDebounce((e)=>{
1890
+ // * only when have onPress set have emit
1274
1891
  if (onPress) {
1275
1892
  onPress(e, reasonRef.current);
1276
1893
  isEmittedRef.current = true;
1277
1894
  }
1278
1895
  }, delay);
1279
- const end = (e, isInside) => {
1896
+ const end = (e, isInside)=>{
1897
+ // ! mouse up only trigger when up on element, so we host that in document
1280
1898
  globalMouseUpListener.remove();
1281
1899
  if (!isEmittedRef.current && isInside) {
1282
1900
  onTap?.(e, reasonRef.current);
@@ -1285,7 +1903,8 @@ const useLongPress = ({ onTap, onPress }, { onTouchStart: onTouchStartArg, onTou
1285
1903
  isEmittedRef.current = false;
1286
1904
  emitLongPress.cancel();
1287
1905
  };
1288
- const start = (e) => {
1906
+ const start = (e)=>{
1907
+ // ! mouse up only trigger when up on element, so we host that in document
1289
1908
  globalMouseUpListener.listen();
1290
1909
  const isTouch = e.type === 'touchstart';
1291
1910
  if (isTouch) {
@@ -1294,111 +1913,173 @@ const useLongPress = ({ onTap, onPress }, { onTouchStart: onTouchStartArg, onTou
1294
1913
  emitLongPress(e);
1295
1914
  };
1296
1915
  const { onMouseUp, onTouchEnd, onTouchStart, ...events } = useTouchMouseEvent({
1297
- onTouchStart: (e) => {
1916
+ onTouchStart: (e)=>{
1917
+ // prevent long touch leave element focus
1298
1918
  if (isPreventDefault) {
1299
1919
  e.preventDefault();
1300
1920
  }
1301
1921
  onTouchStartArg?.(e);
1302
1922
  start(e);
1303
1923
  },
1304
- onTouchEnd: (e, isInside) => {
1924
+ onTouchEnd: (e, isInside)=>{
1925
+ // prevent leave element focus
1305
1926
  if (isPreventDefault) {
1306
1927
  e.preventDefault();
1307
1928
  }
1308
1929
  onTouchEndArg?.(e, isInside);
1309
1930
  end(e, isInside);
1310
1931
  },
1311
- onMouseDown: (e) => {
1932
+ onMouseDown: (e)=>{
1312
1933
  if (isPreventDefault) {
1313
1934
  e.preventDefault();
1314
1935
  }
1315
1936
  onMouseDown?.(e);
1316
1937
  start(e);
1317
1938
  },
1318
- onMouseUp: (e, isInside) => {
1939
+ onMouseUp: (e, isInside)=>{
1940
+ // prevent leave element focus
1319
1941
  if (isPreventDefault) {
1320
1942
  e.preventDefault();
1321
1943
  }
1322
1944
  onMouseUpArg?.(e);
1323
1945
  end(e, isInside);
1324
- },
1325
- }, { actionRef });
1946
+ }
1947
+ }, {
1948
+ actionRef
1949
+ });
1950
+ // use event listener for make event `passive` work in safari
1326
1951
  useEventListener(actionRef, 'touchstart', onTouchStart, {
1327
- passive: false,
1952
+ passive: false
1328
1953
  });
1954
+ // use event listener for make event `passive` work in safari
1329
1955
  useEventListener(actionRef, 'touchend', onTouchEnd, {
1330
- passive: false,
1956
+ passive: false
1331
1957
  });
1332
1958
  const globalMouseUpListener = useGlobalListener('mouseup', onMouseUp, {
1333
1959
  target: externalWindow,
1334
- startImmediately: false,
1960
+ startImmediately: false
1335
1961
  });
1336
- const handleDeleteKeyDown = useA11yKeyEvent((e) => {
1962
+ const handleDeleteKeyDown = useA11yKeyEvent((e)=>{
1337
1963
  reasonRef.current = 'keyboard';
1964
+ // * keydown when hold will trigger many time, show when current is a11y down, not trigger again
1338
1965
  if (!isA11yDownRef.current) {
1339
1966
  emitLongPress(e);
1340
1967
  }
1341
1968
  isA11yDownRef.current = true;
1342
- }, { preventDefault: false });
1343
- const handleKeyDown = useEventCallback((e) => {
1969
+ }, {
1970
+ preventDefault: false
1971
+ });
1972
+ const handleKeyDown = useEventCallback((e)=>{
1344
1973
  onKeyDown?.(e);
1345
1974
  handleDeleteKeyDown(e);
1346
1975
  });
1347
- const handleKeyUp = useEventCallback((e) => {
1976
+ const handleKeyUp = useEventCallback((e)=>{
1348
1977
  onKeyUp?.(e);
1349
- if (isA11yDownRef.current)
1350
- end(e, true);
1978
+ if (isA11yDownRef.current) end(e, true);
1351
1979
  isA11yDownRef.current = false;
1352
1980
  });
1353
1981
  return {
1354
1982
  ref: actionRef,
1355
1983
  onKeyDown: handleKeyDown,
1356
1984
  onKeyUp: handleKeyUp,
1357
- ...events,
1985
+ ...events
1358
1986
  };
1359
1987
  };
1360
1988
 
1361
- 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 = ()=>{
1362
2007
  const isMountedRef = useRef(false);
1363
- useEffect(() => {
2008
+ useEffect(()=>{
1364
2009
  isMountedRef.current = true;
1365
- return () => {
2010
+ return ()=>{
1366
2011
  isMountedRef.current = false;
1367
2012
  };
1368
2013
  }, []);
1369
2014
  return isMountedRef;
1370
2015
  };
1371
2016
 
1372
- 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)=>{
1373
2025
  const count = useRef(0);
1374
2026
  const method = useEventCallback(cb);
1375
- useLayoutEffect(() => {
2027
+ useLayoutEffect(()=>{
1376
2028
  if (count.current === 0) {
1377
2029
  count.current = 1;
1378
- return () => {
2030
+ return ()=>{
2031
+ //
1379
2032
  };
1380
2033
  }
1381
2034
  return method();
2035
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
1382
2036
  }, deps);
1383
2037
  };
1384
2038
 
1385
- 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)=>{
1386
2047
  const count = useRef(0);
1387
2048
  const method = useEventCallback(cb);
1388
- useEffect(() => {
2049
+ useEffect(()=>{
1389
2050
  if (count.current === 0) {
1390
2051
  count.current = 1;
1391
- return () => {
2052
+ return ()=>{
2053
+ //
1392
2054
  };
1393
2055
  }
1394
2056
  return method();
2057
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
1395
2058
  }, deps);
1396
2059
  };
1397
2060
 
1398
- const useResizeObserver = (target, callback, performance = {}) => {
1399
- 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;
1400
2081
  const countRef = useRef(0);
1401
- const cb = useEventCallback((...args) => {
2082
+ const cb = useEventCallback((...args)=>{
1402
2083
  if (ignoreFireWhenObserve && countRef.current === 0) {
1403
2084
  countRef.current = 1;
1404
2085
  return;
@@ -1407,20 +2088,18 @@ const useResizeObserver = (target, callback, performance = {}) => {
1407
2088
  });
1408
2089
  const el = getRefElement(target);
1409
2090
  const currentWindow = el ? ownerWindow(el) : window;
1410
- const observerRef = useResultRef(() => {
2091
+ const observerRef = useResultRef(()=>{
1411
2092
  let resizeCallback;
1412
2093
  if (mode === 'debounce') {
1413
2094
  resizeCallback = debounce(cb, performanceTime, options);
1414
- }
1415
- else if (mode === 'throttle') {
2095
+ } else if (mode === 'throttle') {
1416
2096
  resizeCallback = throttle(cb, performanceTime, options);
1417
- }
1418
- else {
2097
+ } else {
1419
2098
  resizeCallback = cb;
1420
2099
  }
1421
2100
  return new currentWindow.ResizeObserver(resizeCallback);
1422
2101
  });
1423
- useLayoutEffect(() => {
2102
+ useLayoutEffect(()=>{
1424
2103
  if (!currentWindow?.ResizeObserver) {
1425
2104
  return;
1426
2105
  }
@@ -1430,110 +2109,193 @@ const useResizeObserver = (target, callback, performance = {}) => {
1430
2109
  throw new Error('please check element exist before bind resize observer');
1431
2110
  }
1432
2111
  observer.observe(el);
1433
- return () => {
2112
+ return ()=>{
1434
2113
  observer.disconnect();
1435
2114
  };
1436
- }, [el]);
2115
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
2116
+ }, [
2117
+ el
2118
+ ]);
1437
2119
  return observerRef.current;
1438
2120
  };
1439
2121
 
1440
- 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 = {
1441
2134
  mode: 'throttle',
1442
- time: 200,
1443
- }) => {
2135
+ time: 200
2136
+ })=>{
2137
+ // * not have default value for that can trigger change first time.
1444
2138
  const showRef = useRef();
1445
- return useResizeObserver(target, () => {
2139
+ return useResizeObserver(target, ()=>{
1446
2140
  const element = getRefElement(target);
1447
- if (!element)
1448
- return;
2141
+ if (!element) return;
1449
2142
  const { scrollWidth, clientWidth, scrollHeight, clientHeight } = element;
1450
2143
  const show = scrollWidth > clientWidth || scrollHeight > clientHeight;
1451
- if (showRef.current === show)
1452
- return;
2144
+ if (showRef.current === show) return;
2145
+ // only emit when state change
1453
2146
  showRef.current = show;
1454
2147
  cb(show, {
1455
2148
  scrollWidth,
1456
2149
  clientWidth,
1457
2150
  scrollHeight,
1458
- clientHeight,
2151
+ clientHeight
1459
2152
  });
1460
2153
  }, performance);
1461
2154
  };
1462
2155
 
1463
- 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
1464
2182
  const forceUpdate = customUpdate ?? useForceUpdate();
1465
2183
  const innerValueRef = useRef(value);
1466
- 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)=>{
1467
2190
  innerValueRef.current = toValue;
1468
2191
  if (isUpdate) {
1469
2192
  forceUpdate();
1470
2193
  }
1471
2194
  });
1472
- return [innerValueRef, setInnerValue];
2195
+ return [
2196
+ innerValueRef,
2197
+ setInnerValue
2198
+ ];
1473
2199
  };
1474
2200
 
1475
- 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)=>{
1476
2208
  const _breakpoint = useResponsiveContext();
1477
2209
  const breakpoint = _breakpoint === undefined ? defaultBreakpoint : _breakpoint;
1478
- if (breakpoint === undefined)
1479
- return {};
2210
+ if (breakpoint === undefined) return {};
1480
2211
  const matchDetail = getMatchDetail(breakpoint);
1481
2212
  return matchDetail.matchMap;
1482
2213
  };
1483
2214
 
1484
- 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 = ()=>{
1485
2228
  const timeoutIdRef = useRef();
1486
2229
  const rejectRef = useRef();
1487
2230
  const sleepingRef = useRef(false);
1488
- const clearCurrTimeout = () => {
2231
+ const clearCurrTimeout = ()=>{
1489
2232
  const id = timeoutIdRef.current;
1490
2233
  sleepingRef.current = false;
1491
- if (id !== undefined)
1492
- clearTimeout(id);
2234
+ if (id !== undefined) clearTimeout(id);
1493
2235
  };
1494
- const sleep = (time) => new Promise((resolve, reject) => {
1495
- clearCurrTimeout();
1496
- sleepingRef.current = true;
1497
- rejectRef.current = reject;
1498
- timeoutIdRef.current = setTimeout(() => {
1499
- sleepingRef.current = false;
1500
- resolve();
1501
- }, time);
1502
- });
1503
- 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 = ()=>{
1504
2246
  clearCurrTimeout();
1505
2247
  rejectRef.current?.();
1506
2248
  };
1507
- useEffect(() => {
1508
- return () => {
2249
+ useEffect(()=>{
2250
+ return ()=>{
1509
2251
  clearCurrTimeout();
1510
2252
  };
1511
2253
  }, []);
1512
2254
  return {
1513
- sleep,
1514
- cancel,
1515
- 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
1516
2265
  };
1517
2266
  };
1518
2267
 
1519
- 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)=>{
1520
2280
  const { sleep, cancel: cancelSleep } = useSleep();
1521
2281
  const isCancelRef = useRef(true);
1522
- const complete = () => {
2282
+ const complete = ()=>{
1523
2283
  isCancelRef.current = true;
1524
2284
  options?.onComplete?.();
1525
2285
  };
1526
- const cancel = () => {
2286
+ const cancel = ()=>{
1527
2287
  if (!isCancelRef.current) {
2288
+ // * when outside cancel
1528
2289
  options?.onCancel?.();
1529
2290
  complete();
1530
2291
  }
1531
2292
  cancelSleep();
1532
2293
  };
1533
- const retry = async (...args) => {
2294
+ const retry = async (...args)=>{
1534
2295
  cancel();
1535
2296
  isCancelRef.current = false;
1536
2297
  options?.onStart?.();
2298
+ // * when exec success, just return directly.
1537
2299
  if (await cb(...args)) {
1538
2300
  if (!isCancelRef.current) {
1539
2301
  options?.onSuccess?.();
@@ -1541,17 +2303,15 @@ const useRetry = (cb, options) => {
1541
2303
  }
1542
2304
  return;
1543
2305
  }
1544
- if (!options)
1545
- return;
2306
+ if (!options) return;
1546
2307
  const { retryTimes, intervalTime, onSuccess } = options;
1547
- for (let i = 0; i < retryTimes; i++) {
2308
+ for(let i = 0; i < retryTimes; i++){
1548
2309
  if (isCancelRef.current) {
1549
2310
  return;
1550
2311
  }
1551
2312
  try {
1552
2313
  await sleep(intervalTime);
1553
- }
1554
- catch (error) {
2314
+ } catch (error) {
1555
2315
  return;
1556
2316
  }
1557
2317
  if (await cb(...args, i)) {
@@ -1565,24 +2325,34 @@ const useRetry = (cb, options) => {
1565
2325
  complete();
1566
2326
  };
1567
2327
  return {
1568
- retry,
1569
- 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
1570
2335
  };
1571
2336
  };
1572
2337
 
1573
- const useTabOut = (onTabOut) => {
2338
+
2339
+
2340
+
2341
+
2342
+ /**
2343
+ * @param onTabOut Callback fired when a "tab out" event is detected.
2344
+ */ const useTabOut = (onTabOut)=>{
1574
2345
  const lastKeyDownIsTabRef = useRef(false);
1575
- const handleKeyDown = (e) => {
2346
+ const handleKeyDown = (e)=>{
1576
2347
  if (e.key === 'Tab') {
1577
2348
  lastKeyDownIsTabRef.current = true;
1578
2349
  }
1579
2350
  };
1580
- const handleKeyUp = (e) => {
2351
+ const handleKeyUp = (e)=>{
1581
2352
  lastKeyDownIsTabRef.current = false;
1582
2353
  };
1583
- const handleBlur = (e) => {
1584
- if (!e.currentTarget.contains(e.relatedTarget) &&
1585
- lastKeyDownIsTabRef.current) {
2354
+ const handleBlur = (e)=>{
2355
+ if (!e.currentTarget.contains(e.relatedTarget) && lastKeyDownIsTabRef.current) {
1586
2356
  onTabOut(e);
1587
2357
  }
1588
2358
  };
@@ -1590,63 +2360,153 @@ const useTabOut = (onTabOut) => {
1590
2360
  tabOutHandler: {
1591
2361
  onKeyDown: handleKeyDown,
1592
2362
  onKeyUp: handleKeyUp,
1593
- onBlur: handleBlur,
1594
- },
2363
+ onBlur: handleBlur
2364
+ }
1595
2365
  };
1596
2366
  };
1597
2367
 
1598
- 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)=>{
1599
2382
  const memoFn = useEventCallback(fn);
2383
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
1600
2384
  const throttleFn = useCallback(throttle(memoFn, throttleTime, options), []);
1601
- useEffect(() => () => throttleFn.cancel(), []);
2385
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
2386
+ useEffect(()=>()=>throttleFn.cancel(), []);
1602
2387
  return throttleFn;
1603
2388
  };
1604
2389
 
1605
- const useTestRunnerDetection = () => {
1606
- return useMemo(() => {
1607
- return (typeof window !== 'undefined' &&
1608
- (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);
1609
2402
  }, []);
1610
2403
  };
1611
2404
 
2405
+
2406
+
1612
2407
  function useTimeout(callback, delay) {
1613
2408
  const savedCallback = useRef(callback);
1614
2409
  savedCallback.current = callback;
1615
- 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.
1616
2414
  if (!delay && delay !== 0) {
1617
2415
  return;
1618
2416
  }
1619
- const id = setTimeout(() => savedCallback.current(), delay);
1620
- return () => clearTimeout(id);
1621
- }, [delay]);
2417
+ const id = setTimeout(()=>savedCallback.current(), delay);
2418
+ return ()=>clearTimeout(id);
2419
+ }, [
2420
+ delay
2421
+ ]);
1622
2422
  }
2423
+ /* export default */ var useTimeout_useTimeout = ((/* unused pure expression or super */ null && (useTimeout)));
2424
+
1623
2425
 
1624
- const isFontLoadingAPIAvailable = () => {
1625
- return (typeof document !== 'undefined' &&
1626
- document.fonts &&
1627
- typeof document.fonts.ready === 'object' &&
1628
- typeof document.fonts.ready.then === 'function');
2426
+
2427
+
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';
1629
2434
  };
1630
- const useFontLoaded = (enabled = true) => {
1631
- const [loaded, setLoaded] = useState(() => !enabled || !isFontLoadingAPIAvailable());
1632
- 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(()=>{
1633
2449
  if (!enabled || !isFontLoadingAPIAvailable()) {
1634
2450
  return;
1635
2451
  }
1636
2452
  let cancelled = false;
1637
- document.fonts.ready.then(() => {
2453
+ document.fonts.ready.then(()=>{
1638
2454
  if (!cancelled) {
1639
2455
  setLoaded(true);
1640
2456
  }
1641
2457
  });
1642
- return () => {
2458
+ return ()=>{
1643
2459
  cancelled = true;
1644
2460
  };
1645
- }, [enabled]);
2461
+ }, [
2462
+ enabled
2463
+ ]);
1646
2464
  return loaded;
1647
2465
  };
1648
2466
 
1649
- 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() {
1650
2510
  return {
1651
2511
  entries: [],
1652
2512
  savedBodyOverflow: '',
@@ -1656,48 +2516,47 @@ function createInitialState() {
1656
2516
  touchStartY: 0,
1657
2517
  touchStartX: 0,
1658
2518
  nonIOSLocked: false,
1659
- nonIOSReservedScrollBarGap: false,
2519
+ nonIOSReservedScrollBarGap: false
1660
2520
  };
1661
2521
  }
1662
2522
  function isIOS(win) {
1663
2523
  const { platform, maxTouchPoints, userAgent } = win.navigator;
1664
- return (/iP(ad|hone|od)/.test(platform) ||
1665
- (platform === 'MacIntel' && maxTouchPoints > 1) ||
1666
- /iPad|iPhone|iPod/.test(userAgent));
2524
+ return /iP(ad|hone|od)/.test(platform) || platform === 'MacIntel' && maxTouchPoints > 1 || /iPad|iPhone|iPod/.test(userAgent);
1667
2525
  }
1668
2526
  function isScrollableElement(el, win) {
1669
2527
  const { overflow, overflowX, overflowY } = win.getComputedStyle(el);
1670
- if (!/(auto|scroll|overlay)/.test(overflow + overflowX + overflowY))
1671
- return false;
1672
- 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;
1673
2530
  }
1674
2531
  function isScrollBoundaryHit(scrollable, dx, dy) {
1675
2532
  if (Math.abs(dy) >= Math.abs(dx)) {
1676
- if (dy > 0 && scrollable.scrollTop <= 0)
1677
- return true;
1678
- return (dy < 0 &&
1679
- scrollable.scrollTop + scrollable.clientHeight >=
1680
- scrollable.scrollHeight - 1);
2533
+ if (dy > 0 && scrollable.scrollTop <= 0) return true;
2534
+ return dy < 0 && scrollable.scrollTop + scrollable.clientHeight >= scrollable.scrollHeight - 1;
1681
2535
  }
1682
- if (dx > 0 && scrollable.scrollLeft <= 0)
1683
- return true;
1684
- return (dx < 0 &&
1685
- 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;
1686
2538
  }
1687
- 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) {
1688
2549
  const { body, documentElement } = win.document;
1689
2550
  let cur = el;
1690
- while (cur && cur !== body && cur !== documentElement) {
1691
- if (isScrollableElement(cur, win))
1692
- return cur;
1693
- if (cur === boundary)
1694
- break;
2551
+ while(cur && cur !== body && cur !== documentElement){
2552
+ if (isScrollableElement(cur, win)) return cur;
2553
+ if (cur === boundary) break;
1695
2554
  cur = cur.parentElement;
1696
2555
  }
1697
2556
  return null;
1698
2557
  }
1699
2558
  function getActiveEntries(state) {
1700
- return state.entries.filter((entry) => entry.active);
2559
+ return state.entries.filter((entry)=>entry.active);
1701
2560
  }
1702
2561
  function getBoundaryForEntry(topEntry) {
1703
2562
  return topEntry.kind === 'element' ? topEntry.element : null;
@@ -1716,10 +2575,9 @@ function getEntryScopedScrollable(topEntry, target, win, { allowAnonymous = fals
1716
2575
  return getScrollableWithinBoundary(target, win, boundary);
1717
2576
  }
1718
2577
  function getAllowTouchMoveScrollable(entry, target, win) {
1719
- if (entry.allowTouchMove?.(target) !== true)
1720
- return null;
2578
+ if (entry.allowTouchMove?.(target) !== true) return null;
1721
2579
  return getEntryScopedScrollable(entry, target, win, {
1722
- allowAnonymous: true,
2580
+ allowAnonymous: true
1723
2581
  });
1724
2582
  }
1725
2583
  function canEntryAllowInnerScroll(entry, target, win, dx, dy) {
@@ -1735,10 +2593,10 @@ function canEntryAllowInnerScroll(entry, target, win, dx, dy) {
1735
2593
  }
1736
2594
  function findEntryIndex(state, targetElement, entryKey) {
1737
2595
  if (entryKey) {
1738
- return state.entries.findIndex((entry) => entry.key === entryKey);
2596
+ return state.entries.findIndex((entry)=>entry.key === entryKey);
1739
2597
  }
1740
2598
  if (targetElement) {
1741
- for (let i = state.entries.length - 1; i >= 0; i--) {
2599
+ for(let i = state.entries.length - 1; i >= 0; i--){
1742
2600
  const entry = state.entries[i];
1743
2601
  if (entry.kind === 'element' && entry.element === targetElement) {
1744
2602
  return i;
@@ -1746,7 +2604,7 @@ function findEntryIndex(state, targetElement, entryKey) {
1746
2604
  }
1747
2605
  return -1;
1748
2606
  }
1749
- for (let i = state.entries.length - 1; i >= 0; i--) {
2607
+ for(let i = state.entries.length - 1; i >= 0; i--){
1750
2608
  if (state.entries[i].kind === 'anonymous') {
1751
2609
  return i;
1752
2610
  }
@@ -1770,32 +2628,26 @@ function ensureEntry(state, targetElement, options = {}) {
1770
2628
  element: targetElement ?? null,
1771
2629
  active: true,
1772
2630
  reserveScrollBarGap: Boolean(options.reserveScrollBarGap),
1773
- allowTouchMove: options.allowTouchMove,
2631
+ allowTouchMove: options.allowTouchMove
1774
2632
  };
1775
2633
  state.entries.push(entry);
1776
2634
  return entry;
1777
2635
  }
1778
2636
  function shouldPreventTouchMove(e, state, win) {
1779
- if (e.touches.length > 1)
1780
- return false;
1781
- if (e.ctrlKey)
1782
- return false;
1783
- if (!e.cancelable)
1784
- return false;
2637
+ if (e.touches.length > 1) return false;
2638
+ if (e.ctrlKey) return false;
2639
+ if (!e.cancelable) return false;
1785
2640
  const touch = e.touches[0];
1786
- if (!touch)
1787
- return false;
2641
+ if (!touch) return false;
1788
2642
  const dy = touch.clientY - state.touchStartY;
1789
2643
  const dx = touch.clientX - state.touchStartX;
1790
- if (dy === 0 && dx === 0)
1791
- return false;
1792
- if (getActiveEntries(state).length === 0)
1793
- return false;
2644
+ if (dy === 0 && dx === 0) return false;
2645
+ if (getActiveEntries(state).length === 0) return false;
1794
2646
  const target = e.target;
1795
2647
  if (!(target instanceof win.Element)) {
1796
2648
  return true;
1797
2649
  }
1798
- for (let i = state.entries.length - 1; i >= 0; i--) {
2650
+ for(let i = state.entries.length - 1; i >= 0; i--){
1799
2651
  if (canEntryAllowInnerScroll(state.entries[i], target, win, dx, dy)) {
1800
2652
  return false;
1801
2653
  }
@@ -1804,22 +2656,28 @@ function shouldPreventTouchMove(e, state, win) {
1804
2656
  }
1805
2657
  function lockIOS(state, win) {
1806
2658
  const doc = win.document;
1807
- 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)=>{
1808
2666
  if (e.touches.length > 0) {
1809
2667
  state.touchStartY = e.touches[0].clientY;
1810
2668
  state.touchStartX = e.touches[0].clientX;
1811
2669
  }
1812
2670
  };
1813
- state.docTouchMoveHandler = (e) => {
2671
+ state.docTouchMoveHandler = (e)=>{
1814
2672
  if (shouldPreventTouchMove(e, state, win)) {
1815
2673
  e.preventDefault();
1816
2674
  }
1817
2675
  };
1818
2676
  doc.addEventListener('touchstart', state.docTouchStartHandler, {
1819
- passive: true,
2677
+ passive: true
1820
2678
  });
1821
2679
  doc.addEventListener('touchmove', state.docTouchMoveHandler, {
1822
- passive: false,
2680
+ passive: false
1823
2681
  });
1824
2682
  }
1825
2683
  function unlockIOS(state, win) {
@@ -1850,28 +2708,30 @@ function unlockNonIOS(state, win) {
1850
2708
  if (state.savedBodyPaddingRight !== null) {
1851
2709
  if (state.savedBodyPaddingRight === '') {
1852
2710
  body.style.removeProperty('padding-right');
1853
- }
1854
- else {
2711
+ } else {
1855
2712
  body.style.paddingRight = state.savedBodyPaddingRight;
1856
2713
  }
1857
2714
  state.savedBodyPaddingRight = null;
1858
2715
  }
1859
2716
  }
2717
+ // Non-iOS keeps body overflow locked while any entry remains active.
1860
2718
  function bodyLockDesired(state) {
1861
2719
  return getActiveEntries(state).length > 0;
1862
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.
1863
2723
  function shouldReserveScrollBarGap(state) {
1864
- return state.entries.some((entry) => entry.active && entry.reserveScrollBarGap);
2724
+ return state.entries.some((entry)=>entry.active && entry.reserveScrollBarGap);
1865
2725
  }
1866
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.
1867
2729
  const desired = state.entries.length > 0;
1868
2730
  const attached = state.docTouchMoveHandler !== null;
1869
- if (desired === attached)
1870
- return;
2731
+ if (desired === attached) return;
1871
2732
  if (desired) {
1872
2733
  lockIOS(state, win);
1873
- }
1874
- else {
2734
+ } else {
1875
2735
  unlockIOS(state, win);
1876
2736
  }
1877
2737
  }
@@ -1879,8 +2739,7 @@ function syncNonIOS(state, win) {
1879
2739
  const desired = bodyLockDesired(state);
1880
2740
  const reserveScrollBarGap = shouldReserveScrollBarGap(state);
1881
2741
  if (!desired) {
1882
- if (!state.nonIOSLocked)
1883
- return;
2742
+ if (!state.nonIOSLocked) return;
1884
2743
  unlockNonIOS(state, win);
1885
2744
  state.nonIOSLocked = false;
1886
2745
  state.nonIOSReservedScrollBarGap = false;
@@ -1888,23 +2747,23 @@ function syncNonIOS(state, win) {
1888
2747
  }
1889
2748
  if (!state.nonIOSLocked) {
1890
2749
  lockNonIOS(state, win, {
1891
- reserveScrollBarGap,
2750
+ reserveScrollBarGap
1892
2751
  });
1893
2752
  state.nonIOSLocked = true;
1894
2753
  state.nonIOSReservedScrollBarGap = reserveScrollBarGap;
1895
2754
  return;
1896
2755
  }
1897
- if (reserveScrollBarGap === state.nonIOSReservedScrollBarGap)
1898
- return;
2756
+ if (reserveScrollBarGap === state.nonIOSReservedScrollBarGap) return;
1899
2757
  unlockNonIOS(state, win);
1900
- lockNonIOS(state, win, { reserveScrollBarGap });
2758
+ lockNonIOS(state, win, {
2759
+ reserveScrollBarGap
2760
+ });
1901
2761
  state.nonIOSReservedScrollBarGap = reserveScrollBarGap;
1902
2762
  }
1903
2763
  function syncScrollLock(state, win) {
1904
2764
  if (isIOS(win)) {
1905
2765
  syncIOS(state, win);
1906
- }
1907
- else {
2766
+ } else {
1908
2767
  syncNonIOS(state, win);
1909
2768
  }
1910
2769
  }
@@ -1913,23 +2772,50 @@ function activateEntry(state, win, targetElement, options = {}) {
1913
2772
  entry.active = true;
1914
2773
  syncScrollLock(state, win);
1915
2774
  }
1916
- 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 = {}) {
1917
2779
  activateEntry(state, win, targetElement, options);
1918
2780
  }
1919
- 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 = {}) {
1920
2789
  const index = findEntryIndex(state, targetElement, options.entryKey);
1921
- if (index < 0)
1922
- return;
2790
+ if (index < 0) return;
1923
2791
  state.entries.splice(index, 1);
1924
2792
  syncScrollLock(state, win);
1925
2793
  }
1926
- 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 = {}) {
1927
2797
  const index = findEntryIndex(state, targetElement, options.entryKey);
1928
- if (index < 0 || state.entries[index].active === false)
1929
- return;
2798
+ if (index < 0 || state.entries[index].active === false) return;
1930
2799
  state.entries[index].active = false;
1931
2800
  syncScrollLock(state, win);
1932
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
+
1933
2819
 
1934
2820
  const SCROLL_LOCK_KEY = '__SUI_SCROLL_LOCK__';
1935
2821
  function getStoredScrollLockState(win) {
@@ -1940,66 +2826,120 @@ function setStoredScrollLockState(win, state) {
1940
2826
  global[SCROLL_LOCK_KEY] = state;
1941
2827
  return state;
1942
2828
  }
1943
- 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) {
1944
2834
  const state = getStoredScrollLockState(win);
1945
2835
  if (!state) {
1946
2836
  return setStoredScrollLockState(win, createInitialState());
1947
2837
  }
1948
2838
  return state;
1949
2839
  }
2840
+ function deleteStoredScrollLockState(win) {
2841
+ delete win[SCROLL_LOCK_KEY];
2842
+ }
2843
+
2844
+
2845
+
2846
+
2847
+
1950
2848
 
1951
2849
  function applyLockState(state, win, targetElement, options, disabled) {
1952
2850
  lock(state, win, targetElement, options);
1953
2851
  if (disabled) {
1954
- pause(state, win, targetElement, options);
2852
+ bodyScrollLock_pause(state, win, targetElement, options);
1955
2853
  }
1956
2854
  }
1957
2855
  function useResolvedElement(target) {
1958
- const [resolvedElement, setResolvedElement] = useState(() => (getRefElement(target) ?? null));
2856
+ const [resolvedElement, setResolvedElement] = useState(()=>getRefElement(target) ?? null);
1959
2857
  const targetRef = useRef(target);
1960
2858
  targetRef.current = target;
1961
- useEffect(() => {
1962
- const nextResolvedElement = (getRefElement(targetRef.current) ??
1963
- null);
1964
- 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);
1965
2865
  });
1966
2866
  return resolvedElement;
1967
2867
  }
1968
- 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 = {})=>{
1969
2893
  const { disabled = false, reserveScrollBarGap = true } = options;
1970
2894
  const { allowTouchMove } = options;
1971
2895
  const entryKeyRef = useRef(Symbol('use-body-scroll-lock-entry'));
1972
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.
1973
2899
  const { globalThis: global } = useWindowContext();
1974
- useEffect(() => {
1975
- if (!isLocked)
1976
- 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;
1977
2905
  const win = global;
1978
2906
  const state = getOrCreateScrollLockState(global);
1979
2907
  const entryKey = entryKeyRef.current;
1980
2908
  const syncOptions = {
1981
2909
  reserveScrollBarGap,
1982
2910
  allowTouchMove,
1983
- entryKey,
2911
+ entryKey
1984
2912
  };
1985
2913
  applyLockState(state, win, resolvedElement, syncOptions, disabled);
1986
- 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.
1987
2918
  const currentState = getStoredScrollLockState(global);
1988
2919
  if (currentState) {
1989
- unlock(currentState, win, resolvedElement, { entryKey });
2920
+ unlock(currentState, win, resolvedElement, {
2921
+ entryKey
2922
+ });
1990
2923
  }
1991
2924
  };
1992
- }, [isLocked, global]);
1993
- useEffect(() => {
1994
- if (!isLocked)
1995
- 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;
1996
2936
  const win = global;
1997
2937
  const state = getOrCreateScrollLockState(global);
1998
2938
  const entryKey = entryKeyRef.current;
1999
2939
  const syncOptions = {
2000
2940
  reserveScrollBarGap,
2001
2941
  allowTouchMove,
2002
- entryKey,
2942
+ entryKey
2003
2943
  };
2004
2944
  applyLockState(state, win, resolvedElement, syncOptions, disabled);
2005
2945
  }, [
@@ -2008,18 +2948,95 @@ const useBodyScrollLock = (isLocked, targetElement, options = {}) => {
2008
2948
  isLocked,
2009
2949
  global,
2010
2950
  resolvedElement,
2011
- reserveScrollBarGap,
2951
+ reserveScrollBarGap
2012
2952
  ]);
2013
2953
  };
2014
2954
 
2015
- 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 = ()=>{
2016
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
2017
2985
  if (externalWindow && externalWindow !== globalThis.window) {
2018
2986
  return {
2019
- externalWindow,
2987
+ externalWindow
2020
2988
  };
2021
2989
  }
2022
2990
  return undefined;
2023
2991
  };
2024
2992
 
2025
- 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, 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 };
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 };