@ringcentral/spring-base 1.7.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.
Files changed (146) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +61 -0
  3. package/chunks/twMerge.js +409 -0
  4. package/config.js +78 -0
  5. package/contexts.js +71 -0
  6. package/hooks.js +2101 -0
  7. package/index.d.ts +1 -0
  8. package/index.js +16 -0
  9. package/package.json +30 -0
  10. package/src/config.d.ts +40 -0
  11. package/src/contexts/ResponsiveContext/ResponsiveContext.d.ts +7 -0
  12. package/src/contexts/ResponsiveContext/breakpointList.d.ts +3 -0
  13. package/src/contexts/ResponsiveContext/getMatchDetail.d.ts +2 -0
  14. package/src/contexts/ResponsiveContext/index.d.ts +4 -0
  15. package/src/contexts/ResponsiveContext/types.d.ts +24 -0
  16. package/src/contexts/WindowContext.d.ts +9 -0
  17. package/src/contexts/index.d.ts +2 -0
  18. package/src/hooks/index.d.ts +43 -0
  19. package/src/hooks/useA11yKeyEvent/index.d.ts +1 -0
  20. package/src/hooks/useA11yKeyEvent/useA11yKeyEvent.d.ts +23 -0
  21. package/src/hooks/useActive/index.d.ts +1 -0
  22. package/src/hooks/useActive/useActive.d.ts +23 -0
  23. package/src/hooks/useAnnouncer/index.d.ts +1 -0
  24. package/src/hooks/useAnnouncer/useAnnouncer.d.ts +12 -0
  25. package/src/hooks/useAriaHideOutside/index.d.ts +1 -0
  26. package/src/hooks/useAriaHideOutside/useAriaHideOutside.d.ts +12 -0
  27. package/src/hooks/useAudio/index.d.ts +1 -0
  28. package/src/hooks/useAudio/useAudio.d.ts +21 -0
  29. package/src/hooks/useBodyScrollLock/index.d.ts +1 -0
  30. package/src/hooks/useBodyScrollLock/useBodyScrollLock.d.ts +25 -0
  31. package/src/hooks/useChange/index.d.ts +1 -0
  32. package/src/hooks/useChange/useChange.d.ts +12 -0
  33. package/src/hooks/useClickAway/index.d.ts +1 -0
  34. package/src/hooks/useClickAway/useClickAway.d.ts +22 -0
  35. package/src/hooks/useControlled/index.d.ts +1 -0
  36. package/src/hooks/useControlled/useControlled.d.ts +1 -0
  37. package/src/hooks/useDebounce/index.d.ts +1 -0
  38. package/src/hooks/useDebounce/useDebounce.d.ts +8 -0
  39. package/src/hooks/useEventCallback/index.d.ts +1 -0
  40. package/src/hooks/useEventCallback/useEventCallback.d.ts +1 -0
  41. package/src/hooks/useEventListener/index.d.ts +1 -0
  42. package/src/hooks/useEventListener/useEventListener.d.ts +32 -0
  43. package/src/hooks/useEver/index.d.ts +1 -0
  44. package/src/hooks/useEver/useEver.d.ts +13 -0
  45. package/src/hooks/useFocusVisible/index.d.ts +1 -0
  46. package/src/hooks/useFocusVisible/useFocusVisible.d.ts +22 -0
  47. package/src/hooks/useFontLoaded/index.d.ts +1 -0
  48. package/src/hooks/useFontLoaded/useFontLoaded.d.ts +13 -0
  49. package/src/hooks/useForceUpdate/index.d.ts +1 -0
  50. package/src/hooks/useForceUpdate/useForceUpdate.d.ts +5 -0
  51. package/src/hooks/useForkRef/index.d.ts +1 -0
  52. package/src/hooks/useForkRef/useForkRef.d.ts +19 -0
  53. package/src/hooks/useGlobalListener/createGlobalListener.d.ts +55 -0
  54. package/src/hooks/useGlobalListener/index.d.ts +2 -0
  55. package/src/hooks/useGlobalListener/useGlobalListener.d.ts +14 -0
  56. package/src/hooks/useHiddenTabindex/index.d.ts +1 -0
  57. package/src/hooks/useHiddenTabindex/useHiddenTabindex.d.ts +9 -0
  58. package/src/hooks/useHover/index.d.ts +2 -0
  59. package/src/hooks/useHover/useHover.d.ts +17 -0
  60. package/src/hooks/useHover/useHoverDirty.d.ts +8 -0
  61. package/src/hooks/useId/index.d.ts +1 -0
  62. package/src/hooks/useId/useId.d.ts +15 -0
  63. package/src/hooks/useImgLoaded/index.d.ts +1 -0
  64. package/src/hooks/useImgLoaded/useImgLoaded.d.ts +24 -0
  65. package/src/hooks/useInterval/index.d.ts +1 -0
  66. package/src/hooks/useInterval/useInterval.d.ts +43 -0
  67. package/src/hooks/useIsFocusVisible/index.d.ts +2 -0
  68. package/src/hooks/useIsFocusVisible/useIsFocusVisible.d.ts +9 -0
  69. package/src/hooks/useKeyDownOnce/index.d.ts +1 -0
  70. package/src/hooks/useKeyDownOnce/useKeyDownOnce.d.ts +15 -0
  71. package/src/hooks/useKeyboardMoveFocus/index.d.ts +3 -0
  72. package/src/hooks/useKeyboardMoveFocus/moveIndexInTwoDimension.d.ts +18 -0
  73. package/src/hooks/useKeyboardMoveFocus/useKeyboardMoveFocus.d.ts +49 -0
  74. package/src/hooks/useKeyboardMoveFocus/useOnlyOneFocusable.d.ts +17 -0
  75. package/src/hooks/useLongPress/index.d.ts +1 -0
  76. package/src/hooks/useLongPress/useLongPress.d.ts +39 -0
  77. package/src/hooks/useMountState/index.d.ts +1 -0
  78. package/src/hooks/useMountState/useMountState.d.ts +16 -0
  79. package/src/hooks/useOnLayoutReRender/index.d.ts +1 -0
  80. package/src/hooks/useOnLayoutReRender/useOnLayoutReRender.d.ts +5 -0
  81. package/src/hooks/useOnReRender/index.d.ts +1 -0
  82. package/src/hooks/useOnReRender/useOnReRender.d.ts +5 -0
  83. package/src/hooks/useOverflow/index.d.ts +1 -0
  84. package/src/hooks/useOverflow/useOverflow.d.ts +11 -0
  85. package/src/hooks/usePrevious/index.d.ts +1 -0
  86. package/src/hooks/usePrevious/usePrevious.d.ts +7 -0
  87. package/src/hooks/useRefState/index.d.ts +1 -0
  88. package/src/hooks/useRefState/useRefState.d.ts +21 -0
  89. package/src/hooks/useResizeObserver/index.d.ts +1 -0
  90. package/src/hooks/useResizeObserver/useResizeObserver.d.ts +24 -0
  91. package/src/hooks/useResponsiveMatch/index.d.ts +1 -0
  92. package/src/hooks/useResponsiveMatch/useResponsiveMatch.d.ts +7 -0
  93. package/src/hooks/useResultRef/index.d.ts +1 -0
  94. package/src/hooks/useResultRef/useResultRef.d.ts +10 -0
  95. package/src/hooks/useRetry/index.d.ts +1 -0
  96. package/src/hooks/useRetry/useRetry.d.ts +35 -0
  97. package/src/hooks/useSleep/index.d.ts +1 -0
  98. package/src/hooks/useSleep/useSleep.d.ts +22 -0
  99. package/src/hooks/useTabOut/index.d.ts +1 -0
  100. package/src/hooks/useTabOut/useTabOut.d.ts +10 -0
  101. package/src/hooks/useTestRunnerDetection.d.ts +7 -0
  102. package/src/hooks/useThrottle/index.d.ts +1 -0
  103. package/src/hooks/useThrottle/useThrottle.d.ts +8 -0
  104. package/src/hooks/useTimeout/index.d.ts +1 -0
  105. package/src/hooks/useTimeout/useTimeout.d.ts +2 -0
  106. package/src/hooks/useTouchMouseEvent/index.d.ts +1 -0
  107. package/src/hooks/useTouchMouseEvent/useTouchMouseEvent.d.ts +56 -0
  108. package/src/index.d.ts +5 -0
  109. package/src/typings/BaseColor.d.ts +14 -0
  110. package/src/typings/BaseSize.d.ts +15 -0
  111. package/src/typings/ComponentTypes.d.ts +50 -0
  112. package/src/typings/ExtractValue.d.ts +2 -0
  113. package/src/typings/OverridableTypes.d.ts +45 -0
  114. package/src/typings/UnionOmit.d.ts +3 -0
  115. package/src/typings/UnitMap.d.ts +28 -0
  116. package/src/typings/index.d.ts +7 -0
  117. package/src/utils/a11yKeyboardCode.d.ts +8 -0
  118. package/src/utils/ariaHideOutside.d.ts +9 -0
  119. package/src/utils/capitalize.d.ts +1 -0
  120. package/src/utils/checkDefaultPrevented.d.ts +3 -0
  121. package/src/utils/checkElementName.d.ts +2 -0
  122. package/src/utils/checkKeyboardEvent.d.ts +2 -0
  123. package/src/utils/classes/globalStateClasses.d.ts +14 -0
  124. package/src/utils/classes/index.d.ts +2 -0
  125. package/src/utils/classes/useClasses.d.ts +41 -0
  126. package/src/utils/combineProps.d.ts +90 -0
  127. package/src/utils/devlog.d.ts +7 -0
  128. package/src/utils/getRefElement.d.ts +6 -0
  129. package/src/utils/getTitle.d.ts +1 -0
  130. package/src/utils/index.d.ts +25 -0
  131. package/src/utils/isRef.d.ts +2 -0
  132. package/src/utils/isSuiElement.d.ts +1 -0
  133. package/src/utils/isUrl.d.ts +1 -0
  134. package/src/utils/isWebKit154.d.ts +9 -0
  135. package/src/utils/omit.d.ts +2 -0
  136. package/src/utils/outerSize.d.ts +8 -0
  137. package/src/utils/ownerDocument.d.ts +1 -0
  138. package/src/utils/ownerWindow.d.ts +1 -0
  139. package/src/utils/pick.d.ts +2 -0
  140. package/src/utils/preloadImg.d.ts +2 -0
  141. package/src/utils/removeClassName.d.ts +35 -0
  142. package/src/utils/runKeyHandler.d.ts +12 -0
  143. package/src/utils/selectionHandler.d.ts +23 -0
  144. package/src/utils/twMerge.d.ts +4 -0
  145. package/typings.js +1 -0
  146. package/utils.js +333 -0
package/hooks.js ADDED
@@ -0,0 +1,2101 @@
1
+ import { unstable_useEventCallback, unstable_ownerDocument, unstable_Timeout, unstable_useForkRef } from '@mui/utils';
2
+ export { unstable_useControlled as useControlled } from '@mui/utils';
3
+ import * as React from 'react';
4
+ import { useState, useEffect, useRef, useCallback, useLayoutEffect, useMemo } from 'react';
5
+ import uniqueId from 'lodash/uniqueId';
6
+ import { useWindowContext, useResponsiveContext, getMatchDetail } from './contexts.js';
7
+ import { a as ariaHideOutside, g as getRefElement, l as logInDev, c as combineProps, o as ownerWindow } from './chunks/twMerge.js';
8
+ import 'lodash/isPlainObject';
9
+ import './config.js';
10
+ import 'lodash/omit';
11
+ import 'lodash/pick';
12
+ import debounce from 'lodash/debounce';
13
+ import noop from 'lodash/noop';
14
+ import throttle from 'lodash/throttle';
15
+ import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock';
16
+ import 'tailwind-merge';
17
+
18
+ // this file template replace material-ui/core/utils method
19
+ // when upgrade mui that will not need assign type anymore
20
+ const useEventCallback = (func) => {
21
+ return unstable_useEventCallback(func);
22
+ };
23
+
24
+ const defaultCheckKeys = ['Enter', ' '];
25
+ /**
26
+ * bind event with a11y keyboard, `space`, `enter`, only trigger when key includes in `checkKeys`
27
+ *
28
+ * that will auto preventDefault by default, if you don't want that, set `false` with `preventDefault`
29
+ */
30
+ const useA11yKeyEvent = (fn, { onlyOnFocus, preventDefault = true, checkKeys = defaultCheckKeys, } = {}) => {
31
+ return useEventCallback((event) => {
32
+ if (!fn || (onlyOnFocus && event.target !== event.currentTarget))
33
+ return;
34
+ if (checkKeys.includes(event.key)) {
35
+ if (preventDefault)
36
+ event.preventDefault();
37
+ fn(event);
38
+ }
39
+ });
40
+ };
41
+
42
+ // base https://github.com/mui/material-ui/blob/master/packages/mui-base/src/useButton/useButton.ts
43
+ const isNativeButton = (el) => {
44
+ const tag = el.tagName;
45
+ return (tag === 'BUTTON' ||
46
+ (tag === 'INPUT' &&
47
+ ['button', 'submit', 'reset'].includes(el?.type)) ||
48
+ (tag === 'A' && el?.href));
49
+ };
50
+ /**
51
+ * bind event with a11y keyboard, `space`. help not native button to implement `:active`
52
+ *
53
+ * that will auto preventDefault by default, if you don't want that, set `false` with `preventDefault`
54
+ */
55
+ const useActive = ({ onKeyDown, onKeyUp, onClick, disabled = false, } = {}) => {
56
+ const [active, setActive] = useState(false);
57
+ useEffect(() => {
58
+ if (disabled)
59
+ setActive(false);
60
+ }, [disabled]);
61
+ if (disabled) {
62
+ return {
63
+ activeHandlers: { onKeyDown, onKeyUp, onClick },
64
+ active: false,
65
+ };
66
+ }
67
+ const handleKeyDown = (event) => {
68
+ onKeyDown?.(event);
69
+ if (event.defaultPrevented) {
70
+ return;
71
+ }
72
+ if (event.target === event.currentTarget &&
73
+ !isNativeButton(event.target) &&
74
+ event.key === ' ') {
75
+ event.preventDefault();
76
+ }
77
+ if (event.target === event.currentTarget && event.key === ' ') {
78
+ setActive(true);
79
+ }
80
+ // Keyboard accessibility for non interactive elements
81
+ if (event.target === event.currentTarget &&
82
+ !isNativeButton(event.target) &&
83
+ event.key === 'Enter') {
84
+ onClick?.(event);
85
+ event.preventDefault();
86
+ }
87
+ };
88
+ const handleKeyUp = (event) => {
89
+ // calling preventDefault in keyUp on a <button> will not dispatch a click event if Space is pressed
90
+ // https://codesandbox.io/s/button-keyup-preventdefault-dn7f0
91
+ if (event.target === event.currentTarget) {
92
+ setActive(false);
93
+ }
94
+ onKeyUp?.(event);
95
+ // Keyboard accessibility for non interactive elements
96
+ if (event.target === event.currentTarget &&
97
+ !isNativeButton(event.target) &&
98
+ event.key === ' ' &&
99
+ !event.defaultPrevented) {
100
+ onClick?.(event);
101
+ }
102
+ };
103
+ return {
104
+ activeHandlers: { onKeyDown: handleKeyDown, onKeyUp: handleKeyUp, onClick },
105
+ active,
106
+ };
107
+ };
108
+
109
+ /**
110
+ * provide a ref for only calculate `cb` once
111
+ * prevent `useRef(getValue())`, that `getValue` calculate every re-render
112
+ *
113
+ * @param cb that get method of get this ref init value, only calculate once
114
+ * @returns same as `useRef` result
115
+ *
116
+ */
117
+ function useResultRef(cb) {
118
+ const renderRef = useRef(0);
119
+ const ref = useRef();
120
+ if (renderRef.current === 0) {
121
+ ref.current = cb();
122
+ renderRef.current = 1;
123
+ }
124
+ return ref;
125
+ }
126
+
127
+ const DEFAULT_PREFIX = 'spring-id-';
128
+ /**
129
+ * Generates a unique ID.
130
+ *
131
+ * @param id - Default component id or prefix id.
132
+ * @param prefixMode Whether enable prefix the ID with.
133
+ * @returns unique ID
134
+ * @example
135
+ * useId() => 'spring-id-xxxx'
136
+ * useId('') => 'spring-id-xxxx'
137
+ * useId(null) => 'spring-id-xxxx'
138
+ * useId(undefined) => 'spring-id-xxxx'
139
+ * useId('nameId') => 'nameId'
140
+ * useId('nameId', true) => 'nameId-xxxx'
141
+ */
142
+ function useId(id, prefixMode) {
143
+ const { current: resultId } = useResultRef(() => {
144
+ if (id) {
145
+ if (prefixMode) {
146
+ return uniqueId(`${id}-`);
147
+ }
148
+ return id;
149
+ }
150
+ return uniqueId(DEFAULT_PREFIX);
151
+ });
152
+ return resultId;
153
+ }
154
+
155
+ const visuallyHiddenStyles = `
156
+ clip-path: inset(100%);
157
+ clip: rect(1px, 1px, 1px, 1px);
158
+ height: 1px;
159
+ overflow: hidden;
160
+ position: absolute;
161
+ white-space: nowrap;
162
+ width: 1px;
163
+ `;
164
+ /**
165
+ * read announce when need read message.
166
+ *
167
+ * @example
168
+ * ```ts
169
+ * const announce = useAnnouncer('simpleId');
170
+ *
171
+ * announce(`You click "Announce" button`);
172
+ * ```
173
+ */
174
+ const useAnnouncer = (id) => {
175
+ const _id = useId(id ? `rc-announcer-${id}` : 'rc-announcer', !id);
176
+ const ref = useRef(null);
177
+ useEffect(() => {
178
+ const el = document.createElement('div');
179
+ ref.current = el;
180
+ el.id = _id;
181
+ el.setAttribute('aria-live', 'assertive');
182
+ el.setAttribute('aria-atomic', 'true');
183
+ el.setAttribute('aria-hidden', 'false');
184
+ el.setAttribute('style', visuallyHiddenStyles);
185
+ document.body.appendChild(el);
186
+ return () => {
187
+ setTimeout(() => {
188
+ const body = document.body;
189
+ if (body.contains(el)) {
190
+ body.removeChild(el);
191
+ }
192
+ if (el === ref.current) {
193
+ ref.current = null;
194
+ }
195
+ });
196
+ };
197
+ }, [_id]);
198
+ /** announce method, call with message that want let reader speak */
199
+ return useCallback((message) => {
200
+ const el = ref.current;
201
+ if (el && message) {
202
+ el.setAttribute('aria-hidden', 'false');
203
+ el.textContent = message;
204
+ }
205
+ }, []);
206
+ };
207
+
208
+ const useAriaHideOutside = (props = {}) => {
209
+ const { enable = true } = props;
210
+ const { externalWindow, document } = useWindowContext();
211
+ const ariaHideOutsideRef = useRef(null);
212
+ useEffect(() => {
213
+ if (enable && ariaHideOutsideRef.current) {
214
+ return ariaHideOutside([ariaHideOutsideRef.current], document?.body);
215
+ }
216
+ return undefined;
217
+ }, [enable, externalWindow, document?.body]);
218
+ return {
219
+ ariaHideOutsideRef: ariaHideOutsideRef,
220
+ /**
221
+ * Whether the `aria-hidden` attribute is being applied to outside elements.
222
+ * When true, elements outside the ref are hidden from screen readers.
223
+ */
224
+ ariaHideEnabled: enable,
225
+ };
226
+ };
227
+
228
+ /**
229
+ * get Browser `Audio` instance, and auto destroy when component be destroyed.
230
+ *
231
+ *
232
+ * @param init a callback when you init that audio
233
+ *
234
+ * @example
235
+ *
236
+ * ```ts
237
+ * const audio = useAudio((instance) => {
238
+ * instance.src = 'https://your.audio'
239
+ * });
240
+ *
241
+ * const click = () => audio.play();
242
+ *
243
+ * click();
244
+ * ```
245
+ * @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}
246
+ * @see {@link https://chromium-review.googlesource.com/c/chromium/src/+/2816118}
247
+ */
248
+ const useAudio = (init) => {
249
+ const audioRef = useResultRef(() => {
250
+ const audio = new Audio();
251
+ init?.(audio);
252
+ return audio;
253
+ });
254
+ useEffect(() => {
255
+ return () => {
256
+ audioRef.current.srcObject = null;
257
+ // just for remove ref, confirm browser GC work correctly
258
+ audioRef.current = null;
259
+ };
260
+ // eslint-disable-next-line react-hooks/exhaustive-deps
261
+ }, []);
262
+ return audioRef.current;
263
+ };
264
+
265
+ /**
266
+ * get previous render value
267
+ * @param cb that get method of target value
268
+ * @param isDefault if set `true`, the first time will get value from `cb`
269
+ * @returns previous value get from `cb`
270
+ */
271
+ function usePrevious(cb, isDefault) {
272
+ const ref = useRef(isDefault ? cb() : undefined);
273
+ useEffect(() => {
274
+ ref.current = cb();
275
+ });
276
+ return ref.current;
277
+ }
278
+
279
+ /**
280
+ * get value when prev listen value change
281
+ * @param cb that method you want calculate to get value
282
+ * @param listenCb prev compare listener value
283
+ */
284
+ const useChange = (cb, listenCb) => {
285
+ const countRef = useRef(0);
286
+ const valueRef = useRef();
287
+ const prev = usePrevious(listenCb);
288
+ const curr = listenCb();
289
+ if (countRef.current === 0 || prev !== curr) {
290
+ countRef.current = 1;
291
+ valueRef.current = cb(prev, curr);
292
+ }
293
+ return valueRef.current;
294
+ };
295
+ /**
296
+ * get value when prev deps one of array value change
297
+ * @param cb that method you want calculate to get value
298
+ * @param deps prev compare value array
299
+ */
300
+ const useDepsChange = (cb, deps) => {
301
+ const countRef = useRef(0);
302
+ const valueRef = useRef();
303
+ const prev = usePrevious(() => deps, true);
304
+ const curr = deps;
305
+ if (countRef.current === 0 ||
306
+ curr.some((dep, index) => dep !== prev[index])) {
307
+ countRef.current = 1;
308
+ valueRef.current = cb(prev, curr);
309
+ }
310
+ return valueRef.current;
311
+ };
312
+
313
+ // base https://github.com/mui/material-ui/blob/master/packages/mui-base/src/ClickAwayListener/ClickAwayListener.tsx
314
+ function clickedRootScrollbar(event, doc) {
315
+ return (doc.documentElement.clientWidth < event.clientX ||
316
+ doc.documentElement.clientHeight < event.clientY);
317
+ }
318
+ /**
319
+ * @deprecated Use `ClickAwayListener` from `@mui/base` instead.
320
+ *
321
+ * @param onClickAway Callback fired when a "click away" event is detected.
322
+ */
323
+ const useClickAway = (onClickAway, props = {}) => {
324
+ const { disableReactTree = false, isEnabled = true } = props;
325
+ const movedRef = useRef(false);
326
+ const activatedRef = useRef(false);
327
+ const syntheticEventRef = useRef(false);
328
+ const [target, setTarget] = useState();
329
+ // This hook is usually triggered after target appears
330
+ // so we need to trigger re-render component in ref callback to make sure useEffect can get target correctly
331
+ const clickAwayRef = useEventCallback((e) => {
332
+ setTarget(e);
333
+ });
334
+ const shouldActive = Boolean(target);
335
+ useEffect(() => {
336
+ if (!isEnabled)
337
+ return;
338
+ if (shouldActive) {
339
+ // Ensure that this component is not "activated" synchronously.
340
+ // https://github.com/facebook/react/issues/20074
341
+ const timer = setTimeout(() => {
342
+ activatedRef.current = true;
343
+ }, 0);
344
+ return () => {
345
+ clearTimeout(timer);
346
+ activatedRef.current = false;
347
+ };
348
+ }
349
+ return undefined;
350
+ }, [isEnabled, shouldActive]);
351
+ // The handler doesn't take event.defaultPrevented into account:
352
+ //
353
+ // event.preventDefault() is meant to stop default behaviors like
354
+ // clicking a checkbox to check it, hitting a button to submit a form,
355
+ // and hitting left arrow to move the cursor in a text input etc.
356
+ // Only special HTML elements have these default behaviors.
357
+ const handleClickAway = useEventCallback((event) => {
358
+ // Given developers can stop the propagation of the synthetic event,
359
+ // we can only be confident with a positive value.
360
+ const insideReactTree = syntheticEventRef.current;
361
+ syntheticEventRef.current = false;
362
+ const doc = unstable_ownerDocument(target);
363
+ // 1. IE11 support, which trigger the handleClickAway even after the unbind
364
+ // 2. The child might render null.
365
+ // 3. Behave like a blur listener.
366
+ if (!activatedRef.current ||
367
+ !target ||
368
+ ('clientX' in event && clickedRootScrollbar(event, doc))) {
369
+ return;
370
+ }
371
+ // Do not act if user performed touchmove
372
+ if (movedRef.current) {
373
+ movedRef.current = false;
374
+ return;
375
+ }
376
+ let insideDOM;
377
+ // If not enough, can use https://github.com/DieterHolvoet/event-propagation-path/blob/master/propagationPath.js
378
+ if (event.composedPath) {
379
+ insideDOM = event.composedPath().indexOf(target) > -1;
380
+ }
381
+ else {
382
+ insideDOM =
383
+ !doc.documentElement.contains(
384
+ // @ts-expect-error returns `false` as intended when not dispatched from a Node
385
+ event.target) ||
386
+ target.contains(
387
+ // @ts-expect-error returns `false` as intended when not dispatched from a Node
388
+ event.target);
389
+ }
390
+ if (!insideDOM && (disableReactTree || !insideReactTree)) {
391
+ onClickAway(event);
392
+ }
393
+ });
394
+ // Keep track of mouse/touch events that bubbled up through the portal.
395
+ const handleSynthetic = (e) => {
396
+ syntheticEventRef.current = true;
397
+ };
398
+ useEffect(() => {
399
+ if (!isEnabled)
400
+ return;
401
+ if (target) {
402
+ const doc = unstable_ownerDocument(target);
403
+ const handleTouchMove = () => {
404
+ movedRef.current = true;
405
+ };
406
+ doc.addEventListener('touchend', handleClickAway);
407
+ doc.addEventListener('touchmove', handleTouchMove);
408
+ doc.addEventListener('click', handleClickAway);
409
+ return () => {
410
+ doc.removeEventListener('touchend', handleClickAway);
411
+ doc.removeEventListener('touchmove', handleTouchMove);
412
+ doc.removeEventListener('click', handleClickAway);
413
+ };
414
+ }
415
+ return undefined;
416
+ }, [handleClickAway, isEnabled, target]);
417
+ return {
418
+ clickAwayRef,
419
+ clickAwayHandler: {
420
+ onClick: handleSynthetic,
421
+ onTouchEnd: handleSynthetic,
422
+ },
423
+ };
424
+ };
425
+
426
+ /**
427
+ * provide a debounce method for debounce method
428
+ * @param fn method for debounce
429
+ * @param debounceTime debounce timeout value, default is `200`ms
430
+ * @param options debounce setting, default is `{trailing: true, leading: flase}`, detail see lodash
431
+ */
432
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
433
+ const useDebounce = (fn, debounceTime = 200, options) => {
434
+ const memoFn = useEventCallback(fn);
435
+ // eslint-disable-next-line react-hooks/exhaustive-deps
436
+ const debounceFn = useCallback(debounce(memoFn, debounceTime, options), []);
437
+ // eslint-disable-next-line react-hooks/exhaustive-deps
438
+ useEffect(() => () => debounceFn.cancel(), []);
439
+ return debounceFn;
440
+ };
441
+
442
+ /**
443
+ * bind event when component mount and auto remove event when destroy.
444
+ * can remove listener by call returned function or pass empty 'target'.
445
+ *
446
+ * @param target
447
+ * listener will be removed when target is changed to null,
448
+ * or rebinding to new element when `ref.current` change
449
+ * @returns a method object
450
+ * - listen() listen listener again, if that have listener, that will not listen again
451
+ * - remove() remove listener
452
+ * @example
453
+ * ```ts
454
+ * useEventListener(window, 'keyup', () => console.log('window key up'))
455
+ * ```
456
+ */
457
+ function useEventListener(target, key, callback, options) {
458
+ const { startImmediately = true, ...addEventListenerOptions } = options ?? {};
459
+ const listener = useEventCallback(callback);
460
+ const cancelRef = useRef(noop);
461
+ const bindRef = useRef(noop);
462
+ useEffect(() => {
463
+ if (!target)
464
+ return;
465
+ const currentElm = getRefElement(target);
466
+ if (!currentElm) {
467
+ return cancelRef.current;
468
+ }
469
+ bindRef.current = () => currentElm.addEventListener(key, listener, addEventListenerOptions);
470
+ cancelRef.current = () => currentElm.removeEventListener(key, listener, addEventListenerOptions);
471
+ if (startImmediately) {
472
+ bindRef.current();
473
+ }
474
+ return cancelRef.current;
475
+ // eslint-disable-next-line react-hooks/exhaustive-deps
476
+ }, [target]);
477
+ return {
478
+ listen: () => bindRef.current(),
479
+ remove: () => cancelRef.current(),
480
+ };
481
+ }
482
+
483
+ const defaultEverTargetValue = (value) => !!value;
484
+ /**
485
+ * that value ever to be `you want value`
486
+ * Return true when the value ever to be `you want value` once.
487
+ *
488
+ * @example
489
+ * ```ts
490
+ * // once that isOpen is true, that result will always be `true`
491
+ * const isEverOpen = useEver(isOpen);
492
+ *
493
+ * const isEverToBe2 = useEver(count, (val) => val === 2);
494
+ * ```
495
+ */
496
+ const useEver = (
497
+ /** to be checked value */
498
+ value,
499
+ /**
500
+ * value ever to be callback, default when value to be exited.
501
+ *
502
+ * @default (value) => !!value
503
+ */
504
+ targetValue = defaultEverTargetValue) => {
505
+ const result = useRef(false);
506
+ if (!result.current) {
507
+ result.current = targetValue(value);
508
+ }
509
+ return result.current;
510
+ };
511
+
512
+ /* eslint-disable sonarjs/no-collapsible-if */
513
+ /* eslint-disable sonarjs/prefer-single-boolean-return */
514
+ let hadKeyboardEvent = true;
515
+ let hadFocusVisibleRecently = false;
516
+ const hadFocusVisibleRecentlyTimeout = new unstable_Timeout();
517
+ const inputTypesWhitelist = {
518
+ text: true,
519
+ search: true,
520
+ url: true,
521
+ tel: true,
522
+ email: true,
523
+ password: true,
524
+ number: true,
525
+ date: true,
526
+ month: true,
527
+ week: true,
528
+ time: true,
529
+ datetime: true,
530
+ 'datetime-local': true,
531
+ };
532
+ /**
533
+ * Computes whether the given element should automatically trigger the
534
+ * `focus-visible` class being added, i.e. whether it should always match
535
+ * `:focus-visible` when focused.
536
+ * @param {Element} node
537
+ * @returns {boolean}
538
+ */
539
+ function focusTriggersKeyboardModality(node) {
540
+ const { type, tagName } = node;
541
+ if (tagName === 'INPUT' &&
542
+ inputTypesWhitelist[type] &&
543
+ !node.readOnly) {
544
+ return true;
545
+ }
546
+ if (tagName === 'TEXTAREA' && !node.readOnly) {
547
+ return true;
548
+ }
549
+ if (node.isContentEditable) {
550
+ return true;
551
+ }
552
+ return false;
553
+ }
554
+ /**
555
+ * Keep track of our keyboard modality state with `hadKeyboardEvent`.
556
+ * If the most recent user interaction was via the keyboard;
557
+ * and the key press did not include a meta, alt/option, or control key;
558
+ * then the modality is keyboard. Otherwise, the modality is not keyboard.
559
+ * @param {KeyboardEvent} event
560
+ */
561
+ function handleKeyDown(event) {
562
+ if (event.metaKey || event.altKey || event.ctrlKey) {
563
+ return;
564
+ }
565
+ hadKeyboardEvent = true;
566
+ }
567
+ /**
568
+ * If at any point a user clicks with a pointing device, ensure that we change
569
+ * the modality away from keyboard.
570
+ * This avoids the situation where a user presses a key on an already focused
571
+ * element, and then clicks on a different element, focusing it with a
572
+ * pointing device, while we still think we're in keyboard modality.
573
+ */
574
+ function handlePointerDown() {
575
+ hadKeyboardEvent = false;
576
+ }
577
+ function handleVisibilityChange() {
578
+ if (this.visibilityState === 'hidden') {
579
+ // If the tab becomes active again, the browser will handle calling focus
580
+ // on the element (Safari actually calls it twice).
581
+ // If this tab change caused a blur on an element with focus-visible,
582
+ // re-apply the class when the user switches back to the tab.
583
+ if (hadFocusVisibleRecently) {
584
+ hadKeyboardEvent = true;
585
+ }
586
+ }
587
+ }
588
+ function prepare(doc) {
589
+ doc.addEventListener('keydown', handleKeyDown, true);
590
+ doc.addEventListener('mousedown', handlePointerDown, true);
591
+ doc.addEventListener('pointerdown', handlePointerDown, true);
592
+ doc.addEventListener('touchstart', handlePointerDown, true);
593
+ doc.addEventListener('visibilitychange', handleVisibilityChange, true);
594
+ }
595
+ function teardown(doc) {
596
+ doc.removeEventListener('keydown', handleKeyDown, true);
597
+ doc.removeEventListener('mousedown', handlePointerDown, true);
598
+ doc.removeEventListener('pointerdown', handlePointerDown, true);
599
+ doc.removeEventListener('touchstart', handlePointerDown, true);
600
+ doc.removeEventListener('visibilitychange', handleVisibilityChange, true);
601
+ }
602
+ function isFocusVisible(event) {
603
+ const { target } = event;
604
+ try {
605
+ return target.matches(':focus-visible');
606
+ }
607
+ catch (error) {
608
+ // Browsers not implementing :focus-visible will throw a SyntaxError.
609
+ // We use our own heuristic for those browsers.
610
+ // Rethrow might be better if it's not the expected error but do we really
611
+ // want to crash if focus-visible malfunctioned?
612
+ }
613
+ // No need for validFocusTarget check. The user does that by attaching it to
614
+ // focusable events only.
615
+ return hadKeyboardEvent || focusTriggersKeyboardModality(target);
616
+ }
617
+ function useIsFocusVisible() {
618
+ const ref = React.useCallback((node) => {
619
+ if (node != null) {
620
+ prepare(node.ownerDocument);
621
+ }
622
+ }, []);
623
+ const isFocusVisibleRef = React.useRef(false);
624
+ /**
625
+ * Should be called if a blur event is fired
626
+ */
627
+ function handleBlurVisible() {
628
+ // checking against potential state variable does not suffice if we focus and blur synchronously.
629
+ // React wouldn't have time to trigger a re-render so `focusVisible` would be stale.
630
+ // Ideally we would adjust `isFocusVisible(event)` to look at `relatedTarget` for blur events.
631
+ // This doesn't work in IE11 due to https://github.com/facebook/react/issues/3751
632
+ // TODO: check again if React releases their internal changes to focus event handling (https://github.com/facebook/react/pull/19186).
633
+ if (isFocusVisibleRef.current) {
634
+ // To detect a tab/window switch, we look for a blur event followed
635
+ // rapidly by a visibility change.
636
+ // If we don't see a visibility change within 100ms, it's probably a
637
+ // regular focus change.
638
+ hadFocusVisibleRecently = true;
639
+ hadFocusVisibleRecentlyTimeout.start(100, () => {
640
+ hadFocusVisibleRecently = false;
641
+ });
642
+ isFocusVisibleRef.current = false;
643
+ return true;
644
+ }
645
+ return false;
646
+ }
647
+ /**
648
+ * Should be called if a blur event is fired
649
+ */
650
+ function handleFocusVisible(event) {
651
+ if (isFocusVisible(event)) {
652
+ isFocusVisibleRef.current = true;
653
+ return true;
654
+ }
655
+ return false;
656
+ }
657
+ return {
658
+ isFocusVisibleRef,
659
+ onFocus: handleFocusVisible,
660
+ onBlur: handleBlurVisible,
661
+ ref,
662
+ };
663
+ }
664
+
665
+ /**
666
+ * bind event with a11y keyboard, `space`, `enter`, only trigger when key includes in `checkKeys`
667
+ *
668
+ * that will auto preventDefault by default, if you don't want that, set `false` with `preventDefault`
669
+ */
670
+ const useFocusVisible = ({ onFocusVisible, onFocus, onBlur, within = false, } = {}) => {
671
+ const { isFocusVisibleRef, onFocus: handleFocusVisible, onBlur: handleBlurVisible, ref: focusVisibleRef, } = useIsFocusVisible();
672
+ const [focusVisible, setFocusVisible] = useState(false);
673
+ const [focusVisibleWithin, setFocusVisibleWithin] = useState(false);
674
+ const handleBlur = (event) => {
675
+ handleBlurVisible(event);
676
+ if (isFocusVisibleRef.current === false) {
677
+ setFocusVisible(false);
678
+ /**
679
+ * when focus element by press `tab`, will trigger focus event in next event loop
680
+ *
681
+ * if current target is containing related target and we don't check whether it is containing related target,
682
+ * `FocusVisibleWithin` will be false before `focus` trigger,
683
+ * then `FocusVisibleWithin` will be true after `focus` trigger
684
+ *
685
+ * And if current target is containing related target, `FocusVisibleWithin` will never be false
686
+ */
687
+ if (within &&
688
+ !event.currentTarget.contains(event.relatedTarget)) {
689
+ setFocusVisibleWithin(false);
690
+ }
691
+ }
692
+ onBlur?.(event);
693
+ };
694
+ const handleFocus = (event) => {
695
+ let updateState;
696
+ if (within) {
697
+ if (event.target === event.currentTarget) {
698
+ updateState = () => {
699
+ setFocusVisibleWithin(true);
700
+ setFocusVisible(true);
701
+ };
702
+ }
703
+ else {
704
+ updateState = () => {
705
+ setFocusVisibleWithin(true);
706
+ };
707
+ }
708
+ }
709
+ else if (event.target === event.currentTarget) {
710
+ updateState = () => {
711
+ setFocusVisible(true);
712
+ };
713
+ }
714
+ else {
715
+ return;
716
+ }
717
+ handleFocusVisible(event);
718
+ if (isFocusVisibleRef.current === true) {
719
+ updateState();
720
+ onFocusVisible?.(event);
721
+ }
722
+ onFocus?.(event);
723
+ };
724
+ return {
725
+ focusVisibleHandlers: {
726
+ onBlur: handleBlur,
727
+ onFocus: handleFocus,
728
+ },
729
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
730
+ focusVisibleRef: focusVisibleRef,
731
+ focusVisible,
732
+ focusVisibleWithin,
733
+ };
734
+ };
735
+
736
+ /**
737
+ * a method for trigger force update
738
+ * @returns force update method
739
+ */
740
+ function useForceUpdate() {
741
+ const [, dispatch] = useState(Object.create(null));
742
+ return useEventCallback(() => {
743
+ dispatch(Object.create(null));
744
+ });
745
+ }
746
+
747
+ /**
748
+ * That method will give you ability fork same value `ref object` or `method`
749
+ * Example:
750
+ * ```ts
751
+ * const refA = useRef();
752
+ * const refB = useRef();
753
+ *
754
+ * const ref = useForkRef(refA, refB)
755
+ *
756
+ * useEffect(() => {
757
+ * console.log(refA); // get that current div dom
758
+ * console.log(refB); // also get that current div dom
759
+ * },[])
760
+ *
761
+ * return <div ref={ref} />
762
+ * ```
763
+ */
764
+ const useForkRef = unstable_useForkRef;
765
+
766
+ const globalListenerEventMap = new Map();
767
+ /**
768
+ * create globalListener handler for share one listener event
769
+ *
770
+ * @example
771
+ * ```ts
772
+ * const globalListener = createGlobalListener('focus', () => { console.log('focus') });
773
+ * const globalListener2 = createGlobalListener('focus', () => { console.log('focus') });
774
+ * globalListener.listen();
775
+ * globalListener2.listen();
776
+ *
777
+ * => // that will only create an event listener on window, but every callback will get emit value when event triggered
778
+ * ```
779
+ */
780
+ const createGlobalListener = (key, listener, options, { customKey = key, target: targetProp = window, } = {}) => {
781
+ let listening = false;
782
+ const getMapValue = () => globalListenerEventMap.get(customKey);
783
+ const addListener = () => {
784
+ if (listening)
785
+ return;
786
+ const savedEvent = getMapValue();
787
+ if (!savedEvent?.exec) {
788
+ const exec = (e) => {
789
+ const event = getMapValue();
790
+ event?.listeners.forEach((c) => c(e));
791
+ };
792
+ globalListenerEventMap.set(customKey, {
793
+ exec,
794
+ listeners: new Set([listener]),
795
+ });
796
+ const target = getRefElement(targetProp);
797
+ target?.addEventListener(key, exec, options);
798
+ }
799
+ else {
800
+ savedEvent.listeners.add(listener);
801
+ }
802
+ listening = true;
803
+ };
804
+ const removeListener = () => {
805
+ if (!listening)
806
+ return;
807
+ listening = false;
808
+ const _savedEvent = getMapValue();
809
+ if (!_savedEvent)
810
+ return;
811
+ const { listeners } = _savedEvent;
812
+ listeners.delete(listener);
813
+ if (listeners.size === 0) {
814
+ const target = getRefElement(targetProp);
815
+ target?.removeEventListener(key, _savedEvent.exec, options);
816
+ globalListenerEventMap.delete(customKey);
817
+ }
818
+ };
819
+ return {
820
+ listen: () => addListener(),
821
+ /**
822
+ * remove listener
823
+ */
824
+ remove: () => removeListener(),
825
+ /**
826
+ * current listener state
827
+ */
828
+ get state() {
829
+ return {
830
+ /**
831
+ * current listener state count
832
+ */
833
+ get count() {
834
+ return getMapValue()?.listeners.size || 0;
835
+ },
836
+ /**
837
+ * is that be listening
838
+ */
839
+ listening,
840
+ };
841
+ },
842
+ };
843
+ };
844
+
845
+ /**
846
+ * bind global event, when you bind same key event,
847
+ * that will reuse one event listener and trigger both callback once that listener be triggered.
848
+ *
849
+ * also you can control listener with method `listen` and `remove`
850
+ * and get listener `state` for check listener count number and current listing state.
851
+ *
852
+ * config:
853
+ * - `target`: custom binding event target, default is `window`
854
+ * - `customKey`: custom key for determining different event group, default is same as `key`
855
+ * - `startImmediately`: start listener immediately, default is `true`
856
+ *
857
+ * * `key`, `options` only work when set first time, never change after first render
858
+ *
859
+ * @example
860
+ * ```ts
861
+ * useGlobalListener('keyup', () => console.log('window key up1'))
862
+ *
863
+ * useGlobalListener('keyup', () => console.log('window key up2'))
864
+ *
865
+ * useGlobalListener('touchend', () => console.log('window key up3'), {
866
+ * target: someWantHostElement,
867
+ * customKey: 'hostTouchend',
868
+ * })
869
+ *
870
+ * useGlobalListener('touchend', () => console.log('window key up3'), { passive: false }, {
871
+ * target: someWantHostElement,
872
+ * customKey: 'hostTouchend',
873
+ * })
874
+ *
875
+ * => // that will only create an event listener on `window`, but every callback will get emit value when event triggered
876
+ * ```
877
+ */
878
+ function useGlobalListener(key, listener, ...args) {
879
+ const { options, config } = getListenerOverloadOption(args);
880
+ const { customKey = key, target: targetProp = window, startImmediately = true, } = config || {};
881
+ const _listener = useEventCallback(listener);
882
+ const { current: globalListener } = useRef(createGlobalListener(key, _listener, options, {
883
+ customKey,
884
+ target: targetProp,
885
+ }));
886
+ if (process.env.NODE_ENV !== 'production') {
887
+ // eslint-disable-next-line react-hooks/rules-of-hooks
888
+ useEffect(() => {
889
+ if (customKey === key) {
890
+ if (Object.keys(options).length > 0) {
891
+ logInDev({
892
+ scope: 'useGlobalListener',
893
+ message: 'when have set options, suggest you also bind customKey, otherwise, it may use previous listener without same options',
894
+ });
895
+ }
896
+ const target = getRefElement(targetProp);
897
+ if (!('document' in target && 'screen' in target)) {
898
+ logInDev({
899
+ scope: 'useGlobalListener',
900
+ message: 'When you custom binding event target, you must custom key for determining different event group',
901
+ level: 'error',
902
+ });
903
+ }
904
+ }
905
+ // eslint-disable-next-line react-hooks/exhaustive-deps
906
+ }, []);
907
+ }
908
+ useEffect(() => {
909
+ if (startImmediately) {
910
+ globalListener.listen();
911
+ }
912
+ return globalListener.remove;
913
+ // eslint-disable-next-line react-hooks/exhaustive-deps
914
+ }, []);
915
+ return globalListener;
916
+ }
917
+ function getListenerOverloadOption(args) {
918
+ let options;
919
+ let config;
920
+ if (typeof args[0] === 'boolean') {
921
+ options = args[0];
922
+ config = args[1];
923
+ }
924
+ else {
925
+ const { startImmediately, customKey, target, ...restOptions } = {
926
+ ...(args[0] || {}),
927
+ ...(args[1] || {}),
928
+ };
929
+ options = restOptions;
930
+ config = {
931
+ startImmediately,
932
+ customKey,
933
+ target,
934
+ };
935
+ }
936
+ return { options, config };
937
+ }
938
+
939
+ /**
940
+ * group hidden logic, when binding events element focus,
941
+ * that hiddenRef will auto be `tabindex={-1}` *
942
+ */
943
+ const useHiddenTabindex = (target) => {
944
+ const handleFocus = useEventCallback(() => {
945
+ const hiddenElm = getRefElement(target);
946
+ if (hiddenElm && hiddenElm.tabIndex === 0) {
947
+ hiddenElm.tabIndex = -1;
948
+ }
949
+ });
950
+ const handleBlur = useEventCallback(() => {
951
+ const hiddenElm = getRefElement(target);
952
+ if (hiddenElm && hiddenElm.tabIndex === -1) {
953
+ hiddenElm.tabIndex = 0;
954
+ }
955
+ });
956
+ return {
957
+ onFocus: handleFocus,
958
+ onBlur: handleBlur,
959
+ };
960
+ };
961
+
962
+ const useHover = ({ onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, disabled = false, disableReactTree = true, } = {}) => {
963
+ const [hover, setHover] = useState(false);
964
+ const hoverRef = useRef(false);
965
+ useEffect(() => {
966
+ if (disabled) {
967
+ setHover(false);
968
+ }
969
+ else if (!disabled && hoverRef.current) {
970
+ setHover(true);
971
+ }
972
+ }, [disabled]);
973
+ const updateHoverState = (hover, event) => {
974
+ if (disableReactTree &&
975
+ !event.currentTarget.contains(event.target)) {
976
+ return;
977
+ }
978
+ if (!disabled)
979
+ setHover(hover);
980
+ hoverRef.current = hover;
981
+ };
982
+ const onMouseEnter = (event) => {
983
+ handleMouseEnter?.(event);
984
+ updateHoverState(true, event);
985
+ };
986
+ const onMouseLeave = (event) => {
987
+ handleMouseLeave?.(event);
988
+ updateHoverState(false, event);
989
+ };
990
+ return {
991
+ hoverHandlers: { onMouseEnter, onMouseLeave },
992
+ hover,
993
+ };
994
+ };
995
+
996
+ const useHoverDirty = ({ target, disabled = false, }) => {
997
+ const [hover, setHover] = useState(false);
998
+ useEffect(() => {
999
+ if (disabled || !target)
1000
+ setHover(false);
1001
+ }, [disabled, target]);
1002
+ useEventListener(target, 'mouseenter', () => {
1003
+ if (disabled)
1004
+ return;
1005
+ setHover(true);
1006
+ });
1007
+ useEventListener(target, 'mouseleave', () => {
1008
+ if (disabled)
1009
+ return;
1010
+ setHover(false);
1011
+ });
1012
+ return {
1013
+ hover,
1014
+ };
1015
+ };
1016
+
1017
+ /**
1018
+ * get load state of an image by passing src or srcSet
1019
+ * - false: not load yet
1020
+ * - 'loaded': successfully loaded
1021
+ * - 'error': fail to load
1022
+ *
1023
+ * @example
1024
+ * ```ts
1025
+ * const loaded = useImgLoaded({
1026
+ * src,
1027
+ * srcSet,
1028
+ * });
1029
+ * ```
1030
+ */
1031
+ const useImgLoaded = ({ crossOrigin, referrerPolicy, src, srcSet, }) => {
1032
+ const [loaded, setLoaded] = useState(false);
1033
+ useEffect(() => {
1034
+ if (!src && !srcSet) {
1035
+ return undefined;
1036
+ }
1037
+ setLoaded(false);
1038
+ let active = true;
1039
+ const image = new Image();
1040
+ image.onload = () => {
1041
+ if (!active) {
1042
+ return;
1043
+ }
1044
+ setLoaded('loaded');
1045
+ };
1046
+ image.onerror = () => {
1047
+ if (!active) {
1048
+ return;
1049
+ }
1050
+ setLoaded('error');
1051
+ };
1052
+ crossOrigin && (image.crossOrigin = crossOrigin);
1053
+ referrerPolicy && (image.referrerPolicy = referrerPolicy);
1054
+ if (src) {
1055
+ image.src = src;
1056
+ }
1057
+ if (srcSet) {
1058
+ image.srcset = srcSet;
1059
+ }
1060
+ return () => {
1061
+ active = false;
1062
+ };
1063
+ }, [crossOrigin, referrerPolicy, src, srcSet]);
1064
+ return loaded;
1065
+ };
1066
+
1067
+ /**
1068
+ * controllable interval, provide easy way to control interval
1069
+ *
1070
+ * - cancel: cancel current interval
1071
+ * - pause: pause current interval
1072
+ * - play: play current interval
1073
+ * - getRunning: return current interval running state
1074
+ *
1075
+ * @example
1076
+ * ```ts
1077
+ * const { cancel, play, pause } = useInterval(() => {
1078
+ * console.log('interval be triggered');
1079
+ * }, 1000)
1080
+ *
1081
+ * // get current run times
1082
+ * useInterval((times) => {
1083
+ * console.log('current times start from 1', times);
1084
+ * }, 2000)
1085
+ *
1086
+ * // not start interval immediately
1087
+ * useInterval((times) => {
1088
+ * console.log('current times start from 1', times);
1089
+ * }, 2000, false)
1090
+ * ```
1091
+ */
1092
+ const useInterval = (
1093
+ /** callback with times, times start from 1 */
1094
+ callback,
1095
+ /** what delay time of that interval */
1096
+ delay = 0,
1097
+ /** is that start interval immediately */
1098
+ startImmediately = true) => {
1099
+ const intervalRef = useRef();
1100
+ const countRef = useRef(0);
1101
+ const getRunning = useEventCallback(() => !!intervalRef.current);
1102
+ const savedCallback = useEventCallback((times) => {
1103
+ callback(times);
1104
+ });
1105
+ const pause = useCallback(() => {
1106
+ if (intervalRef.current) {
1107
+ clearInterval(intervalRef.current);
1108
+ intervalRef.current = undefined;
1109
+ }
1110
+ }, []);
1111
+ const cancel = useCallback(() => {
1112
+ pause();
1113
+ countRef.current = 0;
1114
+ }, [pause]);
1115
+ const play = useCallback(() => {
1116
+ const isRunning = getRunning();
1117
+ if (isRunning)
1118
+ return;
1119
+ intervalRef.current = setInterval(() => {
1120
+ countRef.current++;
1121
+ savedCallback(countRef.current);
1122
+ }, delay);
1123
+ }, [delay, getRunning, savedCallback]);
1124
+ useEffect(() => {
1125
+ if (startImmediately) {
1126
+ play();
1127
+ }
1128
+ return () => cancel();
1129
+ }, [cancel, play, startImmediately]);
1130
+ return {
1131
+ /**
1132
+ * cancel current interval
1133
+ */
1134
+ cancel,
1135
+ /**
1136
+ * pause current interval
1137
+ */
1138
+ pause,
1139
+ /**
1140
+ * play current interval
1141
+ */
1142
+ play,
1143
+ /**
1144
+ * return current interval running state
1145
+ */
1146
+ getRunning,
1147
+ };
1148
+ };
1149
+
1150
+ /**
1151
+ * Provide user trigger `onKeyDown` only one time when hold press on key.
1152
+ * @param includeKeys that check include keys
1153
+ * @param methods trigger when first keydown or keyup
1154
+ * @returns
1155
+ */
1156
+ const useKeyDownOnce = (includeKeys, { onKeyDown, onKeyUp }) => {
1157
+ const isKeyDownMapRef = useRef({});
1158
+ const handleKeyDown = useEventCallback((e) => {
1159
+ if (includeKeys.includes(e.key)) {
1160
+ if (!isKeyDownMapRef.current[e.key]) {
1161
+ onKeyDown?.(e);
1162
+ }
1163
+ else {
1164
+ e.preventDefault();
1165
+ e.stopPropagation();
1166
+ }
1167
+ isKeyDownMapRef.current[e.key] = true;
1168
+ }
1169
+ else {
1170
+ onKeyDown?.(e);
1171
+ }
1172
+ });
1173
+ const handleKeyUp = useEventCallback((e) => {
1174
+ if (includeKeys.includes(e.key)) {
1175
+ isKeyDownMapRef.current[e.key] = false;
1176
+ }
1177
+ onKeyUp?.(e);
1178
+ });
1179
+ return {
1180
+ onKeyDown: handleKeyDown,
1181
+ onKeyUp: handleKeyUp,
1182
+ };
1183
+ };
1184
+
1185
+ /**
1186
+ * that can move index in column array
1187
+ */
1188
+ function moveIndexInTwoDimension(direction, options) {
1189
+ const { currIndex, columns, total, infinite } = options;
1190
+ // start index never small than 0
1191
+ // const index = Math.max(currIndex, 0);
1192
+ const lastIndex = total - 1;
1193
+ const isInOrder = infinite === 'order';
1194
+ const fillTotal = Math.ceil(total / columns) * columns;
1195
+ let resultIndex;
1196
+ switch (direction) {
1197
+ case 'ArrowUp': {
1198
+ const toIndex = (Math.max(currIndex, 0) - columns + fillTotal) % fillTotal;
1199
+ resultIndex = infinite ? toIndex : Math.min(toIndex, currIndex);
1200
+ break;
1201
+ }
1202
+ case 'ArrowRight': {
1203
+ if (isInOrder) {
1204
+ const tmp = currIndex + 1;
1205
+ resultIndex = tmp > lastIndex ? 0 : tmp;
1206
+ break;
1207
+ }
1208
+ const toIndex = Math.floor(currIndex / columns) * columns + ((currIndex + 1) % columns);
1209
+ resultIndex = infinite ? toIndex : Math.max(toIndex, currIndex);
1210
+ break;
1211
+ }
1212
+ case 'ArrowDown': {
1213
+ const toIndex = (currIndex + columns) % fillTotal;
1214
+ resultIndex = infinite ? toIndex : Math.max(toIndex, currIndex);
1215
+ break;
1216
+ }
1217
+ case 'ArrowLeft': {
1218
+ if (isInOrder) {
1219
+ const tmp = currIndex - 1;
1220
+ resultIndex = tmp < 0 ? lastIndex : tmp;
1221
+ break;
1222
+ }
1223
+ const toIndex = Math.floor(currIndex / columns) * columns +
1224
+ ((currIndex - 1 + columns) % columns);
1225
+ resultIndex = infinite ? toIndex : Math.min(toIndex, currIndex);
1226
+ break;
1227
+ }
1228
+ case 'Home':
1229
+ resultIndex = 0;
1230
+ break;
1231
+ case 'End':
1232
+ resultIndex = lastIndex;
1233
+ break;
1234
+ default:
1235
+ resultIndex = currIndex;
1236
+ }
1237
+ if (!infinite && resultIndex >= total) {
1238
+ return currIndex;
1239
+ }
1240
+ // * when that bigger then sourceTotal, mean that touch end line
1241
+ // * move to current full latest and move again.
1242
+ if (resultIndex >= total) {
1243
+ if (direction === 'ArrowRight') {
1244
+ resultIndex = fillTotal - 1;
1245
+ }
1246
+ return moveIndexInTwoDimension(direction, {
1247
+ ...options,
1248
+ currIndex: resultIndex,
1249
+ });
1250
+ }
1251
+ return resultIndex;
1252
+ }
1253
+
1254
+ /**
1255
+ * use keyboard move index in two dimensional
1256
+ */
1257
+ const useKeyboardMoveFocus = ({ infinite, options = [], focusedIndexRef, onFocusedIndexChange, getOptionDisabled, getOptionSearchText, columns = 1, endSearchDebounceTime = 500, }) => {
1258
+ const total = options.length;
1259
+ const searchText = useRef('');
1260
+ const debounceEndSearch = useDebounce(() => (searchText.current = ''), endSearchDebounceTime);
1261
+ const searchTextAndFocus = (event) => {
1262
+ const key = event.key;
1263
+ if (!getOptionSearchText || key.length !== 1)
1264
+ return;
1265
+ debounceEndSearch();
1266
+ searchText.current += key.toLowerCase();
1267
+ const matchedIndex = options.findIndex((option) => {
1268
+ const text = getOptionSearchText(option);
1269
+ return Boolean(!getOptionDisabled?.(option) &&
1270
+ text &&
1271
+ text.toLowerCase().startsWith(searchText.current));
1272
+ });
1273
+ if (matchedIndex > -1) {
1274
+ onFocusedIndexChange(event, matchedIndex);
1275
+ }
1276
+ };
1277
+ const onKeyFocusedIndexHandle = (event) => {
1278
+ let currIndex = focusedIndexRef.current;
1279
+ const key = event.key;
1280
+ let findDirection;
1281
+ if (total === 0)
1282
+ return;
1283
+ // eslint-disable-next-line no-constant-condition
1284
+ while (true) {
1285
+ const toIndex = findDirection
1286
+ ? findDirection === 'next'
1287
+ ? currIndex + 1
1288
+ : currIndex - 1
1289
+ : moveIndexInTwoDimension(key, {
1290
+ currIndex,
1291
+ columns,
1292
+ total,
1293
+ infinite,
1294
+ });
1295
+ if (toIndex === currIndex) {
1296
+ searchTextAndFocus(event);
1297
+ break;
1298
+ }
1299
+ if (!getOptionDisabled?.(options[toIndex])) {
1300
+ if (toIndex !== focusedIndexRef.current) {
1301
+ onFocusedIndexChange(event, toIndex);
1302
+ }
1303
+ break;
1304
+ }
1305
+ // * mean that item is disabled, use same direction find next
1306
+ else {
1307
+ // * find toIndex is same as current, mean that already find all
1308
+ if (focusedIndexRef.current !== -1 &&
1309
+ toIndex === Math.max(focusedIndexRef.current, 0)) {
1310
+ break;
1311
+ }
1312
+ // * if no item is focused currently and toIndex is already the last item checked,
1313
+ // * it means no item in the list is focusable
1314
+ if (focusedIndexRef.current === -1) {
1315
+ // from bottom to top and the first item in the list is not focusable
1316
+ if ((Math.abs(currIndex - toIndex) !== 1 || currIndex > toIndex) &&
1317
+ toIndex === 0) {
1318
+ break;
1319
+ }
1320
+ // from top to bottom and the last item in the list is not focusable
1321
+ if (Math.abs(currIndex - toIndex) === 1 &&
1322
+ currIndex < toIndex &&
1323
+ toIndex === total - 1) {
1324
+ break;
1325
+ }
1326
+ }
1327
+ currIndex = toIndex;
1328
+ // * if that is Hom or End, find use increase or decrease
1329
+ if (key === 'Home') {
1330
+ findDirection = 'next';
1331
+ }
1332
+ else if (key === 'End') {
1333
+ findDirection = 'prev';
1334
+ }
1335
+ }
1336
+ }
1337
+ };
1338
+ const getPrevFocusableOption = useCallback((index = focusedIndexRef.current,
1339
+ /** is that infinite search */
1340
+ isInfinite = true) => {
1341
+ for (let i = index; i >= 0; i--) {
1342
+ if (!getOptionDisabled?.(options[i])) {
1343
+ return i;
1344
+ }
1345
+ }
1346
+ if (isInfinite) {
1347
+ // loop find
1348
+ for (let i = total - 1; i > index; i--) {
1349
+ if (!getOptionDisabled?.(options[i])) {
1350
+ return i;
1351
+ }
1352
+ }
1353
+ }
1354
+ return -1;
1355
+ }, [focusedIndexRef, getOptionDisabled, options, total]);
1356
+ const getNextFocusableOption = useCallback((index = focusedIndexRef.current,
1357
+ /** is that infinite search */
1358
+ isInfinite = true) => {
1359
+ for (let i = index; i < total; i++) {
1360
+ if (!getOptionDisabled?.(options[i])) {
1361
+ return i;
1362
+ }
1363
+ }
1364
+ if (isInfinite) {
1365
+ // loop find
1366
+ for (let i = 0; i < index; i++) {
1367
+ if (!getOptionDisabled?.(options[i])) {
1368
+ return i;
1369
+ }
1370
+ }
1371
+ }
1372
+ return -1;
1373
+ }, [focusedIndexRef, getOptionDisabled, options, total]);
1374
+ return {
1375
+ /**
1376
+ * Get previous focusable option(include current index).
1377
+ */
1378
+ getPrevFocusableOption,
1379
+ /**
1380
+ * Get next focusable option(include current index).
1381
+ */
1382
+ getNextFocusableOption,
1383
+ /**
1384
+ * Use to handle keyboard event.
1385
+ */
1386
+ onKeyFocusedIndexHandle,
1387
+ };
1388
+ };
1389
+
1390
+ const MOVE_ATTR_KEY = 'data-move-focused';
1391
+ /**
1392
+ * when focus in container below element with binding `getItemProps`,
1393
+ * that will only have one element can be focusable.
1394
+ */
1395
+ const useOnlyOneFocusable = ({ focusedIndexRef, containerRef, }) => {
1396
+ const moveFocusedId = useId(MOVE_ATTR_KEY, true);
1397
+ const getIndexElm = (toIndex) => containerRef.current?.querySelector(`[${moveFocusedId}="${toIndex}"]`);
1398
+ const setIndexTabIndex = (toIndex) => {
1399
+ const toElm = getIndexElm(toIndex);
1400
+ if (toElm) {
1401
+ const prevElm = containerRef.current?.querySelector(`[${moveFocusedId}][tabindex="0"]`);
1402
+ prevElm?.setAttribute('tabindex', '-1');
1403
+ toElm.setAttribute('tabindex', '0');
1404
+ }
1405
+ };
1406
+ const focusIndex = (toIndex) => {
1407
+ const toElm = getIndexElm(toIndex);
1408
+ toElm?.focus();
1409
+ };
1410
+ return {
1411
+ moveFocusedId,
1412
+ focusIndex,
1413
+ setIndexTabIndex,
1414
+ getItemProps: (index, options) => {
1415
+ return combineProps({
1416
+ [moveFocusedId]: index,
1417
+ tabIndex: index === focusedIndexRef.current ? 0 : -1,
1418
+ onFocus: () => {
1419
+ focusedIndexRef.current = index;
1420
+ setIndexTabIndex(index);
1421
+ },
1422
+ }, options);
1423
+ },
1424
+ };
1425
+ };
1426
+
1427
+ /**
1428
+ * Provide a utils for switch trigger touch event or mouse event between different device
1429
+ *
1430
+ * - Trigger `touch` event in `touch support` device
1431
+ * - onTouchStart
1432
+ * - onTouchEnd
1433
+ * - Trigger `mouse` event in `non touch support` device
1434
+ * - onMouseDown
1435
+ * - onMouseUp
1436
+ *
1437
+ * ### Once that is touch support device, that mouse event will never can be trigger
1438
+ * https://developer.mozilla.org/en-US/docs/Web/API/Touch_events/Supporting_both_TouchEvent_and_MouseEvent
1439
+ *
1440
+ * Event order:
1441
+ * - touchstart
1442
+ * - Zero or more touchmove events, depending on movement of the finger(s)
1443
+ * - touchend
1444
+ * - mousemove
1445
+ * - mousedown
1446
+ * - mouseup
1447
+ * - click
1448
+ */
1449
+ const useTouchMouseEvent = ({ onMouseDown: onMouseDownArg, onMouseUp: onMouseUpArg, onTouchStart: onTouchStartArg, onTouchEnd: onTouchEndArg, }, { actionRef } = {}) => {
1450
+ const stopMouseEventRef = useRef(false);
1451
+ const onTouchStart = useEventCallback((e) => {
1452
+ stopMouseEventRef.current = true;
1453
+ onTouchStartArg?.(e);
1454
+ });
1455
+ function checkInside(elm) {
1456
+ const hostElement = getRefElement(actionRef);
1457
+ if (hostElement) {
1458
+ return !!(elm && isElmEqualOrContain(elm, hostElement));
1459
+ }
1460
+ return false;
1461
+ }
1462
+ const onTouchEnd = useEventCallback((e) => {
1463
+ let isInside;
1464
+ if (actionRef && e.touches.length < 2 && e.changedTouches.length < 2) {
1465
+ const touch = e.touches[0] || e.changedTouches[0];
1466
+ const elm = unstable_ownerDocument(e.currentTarget).elementFromPoint(touch.pageX, touch.pageY);
1467
+ isInside = !!elm && checkInside(elm);
1468
+ }
1469
+ onTouchEndArg?.(e, isInside);
1470
+ });
1471
+ const onMouseDown = useEventCallback((e) => {
1472
+ if (stopMouseEventRef.current) {
1473
+ return;
1474
+ }
1475
+ onMouseDownArg?.(e);
1476
+ });
1477
+ const onMouseUp = useEventCallback((e) => {
1478
+ let isInside;
1479
+ if (stopMouseEventRef.current)
1480
+ return;
1481
+ if (actionRef) {
1482
+ const elm = e.target;
1483
+ isInside = !!elm && checkInside(elm);
1484
+ }
1485
+ onMouseUpArg?.(e, isInside);
1486
+ });
1487
+ return {
1488
+ onTouchStart,
1489
+ onTouchEnd,
1490
+ onMouseDown,
1491
+ onMouseUp,
1492
+ };
1493
+ };
1494
+ /**
1495
+ * check is element equal or contains target element
1496
+ */
1497
+ const isElmEqualOrContain = (sourceTarget, containTarget) => {
1498
+ const elm = getRefElement(sourceTarget);
1499
+ const target = getRefElement(containTarget);
1500
+ return elm && target && (target === elm || target.contains(elm));
1501
+ };
1502
+
1503
+ const isTouchEvent = (ev) => {
1504
+ return 'touches' in ev;
1505
+ };
1506
+ /**
1507
+ * Provide longPress helper, both `click`/`tab`/`keydown` will trigger event
1508
+ *
1509
+ * - Trigger `onTap` when user action leave small than delay time.
1510
+ * - Trigger `onPress` when action time is long than delay time.
1511
+ *
1512
+ * @default UseLongPressOptions { delay = 300 }
1513
+ */
1514
+ const useLongPress = ({ onTap, onPress }, { onTouchStart: onTouchStartArg, onTouchEnd: onTouchEndArg, onMouseDown, onKeyDown, onKeyUp, onMouseUp: onMouseUpArg, } = {}, { isPreventDefault = true, delay = 300, externalWindow = window, } = {}) => {
1515
+ const isEmittedRef = useRef(false);
1516
+ const isA11yDownRef = useRef(false);
1517
+ const actionRef = useRef(null);
1518
+ const reasonRef = useRef('click');
1519
+ const emitLongPress = useDebounce((e) => {
1520
+ // * only when have onPress set have emit
1521
+ if (onPress) {
1522
+ onPress(e, reasonRef.current);
1523
+ isEmittedRef.current = true;
1524
+ }
1525
+ }, delay);
1526
+ const end = (e, isInside) => {
1527
+ // ! mouse up only trigger when up on element, so we host that in document
1528
+ globalMouseUpListener.remove();
1529
+ if (!isEmittedRef.current && isInside) {
1530
+ onTap?.(e, reasonRef.current);
1531
+ }
1532
+ reasonRef.current = 'click';
1533
+ isEmittedRef.current = false;
1534
+ emitLongPress.cancel();
1535
+ };
1536
+ const start = (e) => {
1537
+ // ! mouse up only trigger when up on element, so we host that in document
1538
+ globalMouseUpListener.listen();
1539
+ const isTouch = e.type === 'touchstart';
1540
+ if (isTouch) {
1541
+ reasonRef.current = 'tap';
1542
+ }
1543
+ emitLongPress(e);
1544
+ };
1545
+ const { onMouseUp, onTouchEnd, onTouchStart, ...events } = useTouchMouseEvent({
1546
+ onTouchStart: (e) => {
1547
+ // prevent long touch leave element focus
1548
+ if (isPreventDefault) {
1549
+ e.preventDefault();
1550
+ }
1551
+ onTouchStartArg?.(e);
1552
+ start(e);
1553
+ },
1554
+ onTouchEnd: (e, isInside) => {
1555
+ // prevent leave element focus
1556
+ if (isPreventDefault) {
1557
+ e.preventDefault();
1558
+ }
1559
+ onTouchEndArg?.(e, isInside);
1560
+ end(e, isInside);
1561
+ },
1562
+ onMouseDown: (e) => {
1563
+ if (isPreventDefault) {
1564
+ e.preventDefault();
1565
+ }
1566
+ onMouseDown?.(e);
1567
+ start(e);
1568
+ },
1569
+ onMouseUp: (e, isInside) => {
1570
+ // prevent leave element focus
1571
+ if (isPreventDefault) {
1572
+ e.preventDefault();
1573
+ }
1574
+ onMouseUpArg?.(e);
1575
+ end(e, isInside);
1576
+ },
1577
+ }, { actionRef });
1578
+ // use event listener for make event `passive` work in safari
1579
+ useEventListener(actionRef, 'touchstart', onTouchStart, {
1580
+ passive: false,
1581
+ });
1582
+ // use event listener for make event `passive` work in safari
1583
+ useEventListener(actionRef, 'touchend', onTouchEnd, {
1584
+ passive: false,
1585
+ });
1586
+ const globalMouseUpListener = useGlobalListener('mouseup', onMouseUp, {
1587
+ target: externalWindow,
1588
+ startImmediately: false,
1589
+ });
1590
+ const handleDeleteKeyDown = useA11yKeyEvent((e) => {
1591
+ reasonRef.current = 'keyboard';
1592
+ // * keydown when hold will trigger many time, show when current is a11y down, not trigger again
1593
+ if (!isA11yDownRef.current) {
1594
+ emitLongPress(e);
1595
+ }
1596
+ isA11yDownRef.current = true;
1597
+ }, { preventDefault: false });
1598
+ const handleKeyDown = useEventCallback((e) => {
1599
+ onKeyDown?.(e);
1600
+ handleDeleteKeyDown(e);
1601
+ });
1602
+ const handleKeyUp = useEventCallback((e) => {
1603
+ onKeyUp?.(e);
1604
+ if (isA11yDownRef.current)
1605
+ end(e, true);
1606
+ isA11yDownRef.current = false;
1607
+ });
1608
+ return {
1609
+ ref: actionRef,
1610
+ onKeyDown: handleKeyDown,
1611
+ onKeyUp: handleKeyUp,
1612
+ ...events,
1613
+ };
1614
+ };
1615
+
1616
+ /**
1617
+ * get that component mounted state `ref`
1618
+ *
1619
+ * @example
1620
+ *
1621
+ * ```ts
1622
+ const isMountedRef = useMountState();
1623
+
1624
+ if (isMountedRef.current) {
1625
+ console.log('component mounted');
1626
+ }
1627
+ *
1628
+ * ```
1629
+ */
1630
+ const useMountState = () => {
1631
+ const isMountedRef = useRef(false);
1632
+ useEffect(() => {
1633
+ isMountedRef.current = true;
1634
+ return () => {
1635
+ isMountedRef.current = false;
1636
+ };
1637
+ }, []);
1638
+ return isMountedRef;
1639
+ };
1640
+
1641
+ /**
1642
+ * only trigger when re-render (in useLayoutEffect), not trigger on component first render
1643
+ */
1644
+ const useOnLayoutReRender = (cb, deps) => {
1645
+ const count = useRef(0);
1646
+ const method = useEventCallback(cb);
1647
+ useLayoutEffect(() => {
1648
+ if (count.current === 0) {
1649
+ count.current = 1;
1650
+ return () => {
1651
+ //
1652
+ };
1653
+ }
1654
+ return method();
1655
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1656
+ }, deps);
1657
+ };
1658
+
1659
+ /**
1660
+ * only trigger when re-render, not trigger on component first render
1661
+ */
1662
+ const useOnReRender = (cb, deps) => {
1663
+ const count = useRef(0);
1664
+ const method = useEventCallback(cb);
1665
+ useEffect(() => {
1666
+ if (count.current === 0) {
1667
+ count.current = 1;
1668
+ return () => {
1669
+ //
1670
+ };
1671
+ }
1672
+ return method();
1673
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1674
+ }, deps);
1675
+ };
1676
+
1677
+ /**
1678
+ * get resize event with auto disconnect when layout destroy.
1679
+ * bring with debounce for event not too many
1680
+ * @param ref that ref for element
1681
+ * @param callback callback when element resize
1682
+ * @param performance options that improve performance
1683
+ */
1684
+ const useResizeObserver = (
1685
+ /** target element ref */
1686
+ target,
1687
+ /** trigger when `ResizeObserver` emit */
1688
+ callback,
1689
+ /** performance options, `mode`, `time`, `ignoreFireWhenObserve` */
1690
+ performance = {}) => {
1691
+ const { mode = 'debounce', time: performanceTime = 200, ignoreFireWhenObserve, options, } = performance;
1692
+ const countRef = useRef(0);
1693
+ const cb = useEventCallback((...args) => {
1694
+ if (ignoreFireWhenObserve && countRef.current === 0) {
1695
+ countRef.current = 1;
1696
+ return;
1697
+ }
1698
+ return callback(...args);
1699
+ });
1700
+ const el = getRefElement(target);
1701
+ const currentWindow = el ? ownerWindow(el) : window;
1702
+ const observerRef = useResultRef(() => {
1703
+ let resizeCallback;
1704
+ if (mode === 'debounce') {
1705
+ resizeCallback = debounce(cb, performanceTime, options);
1706
+ }
1707
+ else if (mode === 'throttle') {
1708
+ resizeCallback = throttle(cb, performanceTime, options);
1709
+ }
1710
+ else {
1711
+ resizeCallback = cb;
1712
+ }
1713
+ return new currentWindow.ResizeObserver(resizeCallback);
1714
+ });
1715
+ useLayoutEffect(() => {
1716
+ if (!currentWindow?.ResizeObserver) {
1717
+ return;
1718
+ }
1719
+ const el = getRefElement(target);
1720
+ const observer = observerRef.current;
1721
+ if (!el) {
1722
+ throw new Error('please check element exist before bind resize observer');
1723
+ }
1724
+ observer.observe(el);
1725
+ return () => {
1726
+ observer.disconnect();
1727
+ };
1728
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1729
+ }, [el]);
1730
+ return observerRef.current;
1731
+ };
1732
+
1733
+ /**
1734
+ * trigger when overflow state change
1735
+ */
1736
+ const useOverflow = (
1737
+ /** target element ref */
1738
+ target,
1739
+ /** trigger when `ResizeObserver` emit */
1740
+ cb,
1741
+ /**
1742
+ * performance options, `mode`, `time`, `ignoreFireWhenObserve`
1743
+ *
1744
+ * @default { mode: 'throttle' time: 200 }
1745
+ */
1746
+ performance = {
1747
+ mode: 'throttle',
1748
+ time: 200,
1749
+ }) => {
1750
+ // * not have default value for that can trigger change first time.
1751
+ const showRef = useRef();
1752
+ return useResizeObserver(target, () => {
1753
+ const element = getRefElement(target);
1754
+ if (!element)
1755
+ return;
1756
+ const { scrollWidth, clientWidth, scrollHeight, clientHeight } = element;
1757
+ const show = scrollWidth > clientWidth || scrollHeight > clientHeight;
1758
+ if (showRef.current === show)
1759
+ return;
1760
+ // only emit when state change
1761
+ showRef.current = show;
1762
+ cb(show, {
1763
+ scrollWidth,
1764
+ clientWidth,
1765
+ scrollHeight,
1766
+ clientHeight,
1767
+ });
1768
+ }, performance);
1769
+ };
1770
+
1771
+ /**
1772
+ * Returns a stateful like state with Ref, and a function to update it current value.
1773
+ * can trigger update value without re-render
1774
+ *
1775
+ * @param value init value
1776
+ * @param customUpdate you can custom you update method, or just set your `forceUpdate` to reuse same instance
1777
+ * @returns an array with [ref, update]
1778
+ *
1779
+ * @example
1780
+ * ```ts
1781
+ * const [inputRef, updateInput] = useRefState(123);
1782
+ *
1783
+ * if(needReRender){
1784
+ * updateInput(456); // trigger re-render
1785
+ * } else {
1786
+ * updateInput(456, false); // not trigger re-render
1787
+ * }
1788
+ * ```
1789
+ */
1790
+ const useRefState = (value, customUpdate) => {
1791
+ // eslint-disable-next-line react-hooks/rules-of-hooks
1792
+ const forceUpdate = customUpdate ?? useForceUpdate();
1793
+ const innerValueRef = useRef(value);
1794
+ /**
1795
+ * update ref value, set isUpdate to `false` will not trigger re-render
1796
+ */
1797
+ const setInnerValue = useEventCallback((toValue,
1798
+ /**
1799
+ * is re-render
1800
+ * @default true
1801
+ */
1802
+ isUpdate = true) => {
1803
+ innerValueRef.current = toValue;
1804
+ if (isUpdate) {
1805
+ forceUpdate();
1806
+ }
1807
+ });
1808
+ return [innerValueRef, setInnerValue];
1809
+ };
1810
+
1811
+ /**
1812
+ * get all matched breakpoints
1813
+ * @param defaultBreakpoint use defaultBreakpoint when Responsive init (will pass undefined)
1814
+ */
1815
+ const useResponsiveMatch = (defaultBreakpoint) => {
1816
+ const _breakpoint = useResponsiveContext();
1817
+ const breakpoint = _breakpoint === undefined ? defaultBreakpoint : _breakpoint;
1818
+ if (breakpoint === undefined)
1819
+ return {};
1820
+ const matchDetail = getMatchDetail(breakpoint);
1821
+ return matchDetail.matchMap;
1822
+ };
1823
+
1824
+ /**
1825
+ * cancelable sleep method.
1826
+ *
1827
+ * - sleep: a promise sleep
1828
+ * - cancel: cancel that sleep event
1829
+ * - getSleeping: get current sleeping state
1830
+ */
1831
+ const useSleep = () => {
1832
+ const timeoutIdRef = useRef();
1833
+ const rejectRef = useRef();
1834
+ const sleepingRef = useRef(false);
1835
+ const clearCurrTimeout = () => {
1836
+ const id = timeoutIdRef.current;
1837
+ sleepingRef.current = false;
1838
+ if (id !== undefined)
1839
+ clearTimeout(id);
1840
+ };
1841
+ const sleep = (time) => new Promise((resolve, reject) => {
1842
+ clearCurrTimeout();
1843
+ sleepingRef.current = true;
1844
+ rejectRef.current = reject;
1845
+ timeoutIdRef.current = setTimeout(() => {
1846
+ sleepingRef.current = false;
1847
+ resolve();
1848
+ }, time);
1849
+ });
1850
+ const cancel = () => {
1851
+ clearCurrTimeout();
1852
+ rejectRef.current?.();
1853
+ };
1854
+ useEffect(() => {
1855
+ return () => {
1856
+ clearCurrTimeout();
1857
+ };
1858
+ }, []);
1859
+ return {
1860
+ /**
1861
+ * sleep method, that will be cancelable with `cancel`,
1862
+ * and when trigger many time that will always clear previous sleep
1863
+ */
1864
+ sleep,
1865
+ /**
1866
+ * cancel current sleep timer
1867
+ */
1868
+ cancel,
1869
+ /**
1870
+ * return is current is sleeping
1871
+ */
1872
+ getSleeping: () => sleepingRef.current,
1873
+ };
1874
+ };
1875
+
1876
+ /* eslint-disable no-await-in-loop */
1877
+ /**
1878
+ * retry times when exec callback fail
1879
+ *
1880
+ * * `retry`: that will auto add `times` end of your call back
1881
+ * * `cancel`: cancel that retry event, that only cancel inner interval promise, **if you need cancel your `cb` promise, you should handle by yourself.**
1882
+ */
1883
+ const useRetry = (
1884
+ /**
1885
+ * exec callback, when you want to end that retry, return `true` with your callback
1886
+ */
1887
+ cb,
1888
+ /** retry options */
1889
+ options) => {
1890
+ const { sleep, cancel: cancelSleep } = useSleep();
1891
+ const isCancelRef = useRef(true);
1892
+ const complete = () => {
1893
+ isCancelRef.current = true;
1894
+ options?.onComplete?.();
1895
+ };
1896
+ const cancel = () => {
1897
+ if (!isCancelRef.current) {
1898
+ // * when outside cancel
1899
+ options?.onCancel?.();
1900
+ complete();
1901
+ }
1902
+ cancelSleep();
1903
+ };
1904
+ const retry = async (...args) => {
1905
+ cancel();
1906
+ isCancelRef.current = false;
1907
+ options?.onStart?.();
1908
+ // * when exec success, just return directly.
1909
+ if (await cb(...args)) {
1910
+ if (!isCancelRef.current) {
1911
+ options?.onSuccess?.();
1912
+ complete();
1913
+ }
1914
+ return;
1915
+ }
1916
+ if (!options)
1917
+ return;
1918
+ const { retryTimes, intervalTime, onSuccess } = options;
1919
+ for (let i = 0; i < retryTimes; i++) {
1920
+ if (isCancelRef.current) {
1921
+ return;
1922
+ }
1923
+ try {
1924
+ await sleep(intervalTime);
1925
+ }
1926
+ catch (error) {
1927
+ return;
1928
+ }
1929
+ if (await cb(...args, i)) {
1930
+ if (!isCancelRef.current) {
1931
+ onSuccess?.();
1932
+ complete();
1933
+ }
1934
+ return;
1935
+ }
1936
+ }
1937
+ complete();
1938
+ };
1939
+ return {
1940
+ /**
1941
+ * retry method, that will be cancelable with `cancel`,
1942
+ * and when trigger many time that will always clear previous retry
1943
+ */
1944
+ retry,
1945
+ /**
1946
+ * cancel current retry
1947
+ */
1948
+ cancel,
1949
+ };
1950
+ };
1951
+
1952
+ /**
1953
+ * @param onTabOut Callback fired when a "tab out" event is detected.
1954
+ */
1955
+ const useTabOut = (onTabOut) => {
1956
+ const lastKeyDownIsTabRef = useRef(false);
1957
+ const handleKeyDown = (e) => {
1958
+ if (e.key === 'Tab') {
1959
+ lastKeyDownIsTabRef.current = true;
1960
+ }
1961
+ };
1962
+ const handleKeyUp = (e) => {
1963
+ lastKeyDownIsTabRef.current = false;
1964
+ };
1965
+ const handleBlur = (e) => {
1966
+ if (!e.currentTarget.contains(e.relatedTarget) &&
1967
+ lastKeyDownIsTabRef.current) {
1968
+ onTabOut(e);
1969
+ }
1970
+ };
1971
+ return {
1972
+ tabOutHandler: {
1973
+ onKeyDown: handleKeyDown,
1974
+ onKeyUp: handleKeyUp,
1975
+ onBlur: handleBlur,
1976
+ },
1977
+ };
1978
+ };
1979
+
1980
+ /**
1981
+ * provide a throttle method for throttle method
1982
+ * @param fn method for throttle
1983
+ * @param throttleTime throttle timeout value, default is `200`ms
1984
+ * @param options throttle setting, default is `{trailing: true, leading: true}`, detail see lodash
1985
+ */
1986
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1987
+ const useThrottle = (fn, throttleTime = 200, options) => {
1988
+ const memoFn = useEventCallback(fn);
1989
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1990
+ const throttleFn = useCallback(throttle(memoFn, throttleTime, options), []);
1991
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1992
+ useEffect(() => () => throttleFn.cancel(), []);
1993
+ return throttleFn;
1994
+ };
1995
+
1996
+ /**
1997
+ * Internal hook to detect if the component is running in a test runner environment
1998
+ * Used by Storybook stories to disable network requests and animations during testing
1999
+ * @returns {boolean} true if running in test runner (headless browser), false otherwise
2000
+ * @internal
2001
+ */
2002
+ const useTestRunnerDetection = () => {
2003
+ return useMemo(() => {
2004
+ return (typeof window !== 'undefined' &&
2005
+ (window.navigator?.webdriver || window.chrome?.webdriver));
2006
+ }, []);
2007
+ };
2008
+
2009
+ function useTimeout(callback, delay) {
2010
+ const savedCallback = useRef(callback);
2011
+ savedCallback.current = callback;
2012
+ // Set up the timeout.
2013
+ useEffect(() => {
2014
+ // Don't schedule if no delay is specified.
2015
+ // Note: 0 is a valid value for delay.
2016
+ if (!delay && delay !== 0) {
2017
+ return;
2018
+ }
2019
+ const id = setTimeout(() => savedCallback.current(), delay);
2020
+ return () => clearTimeout(id);
2021
+ }, [delay]);
2022
+ }
2023
+
2024
+ /**
2025
+ * Check if Font Loading API is available in the current environment
2026
+ * @returns boolean indicating if document.fonts.ready is available
2027
+ */
2028
+ const isFontLoadingAPIAvailable = () => {
2029
+ return (typeof document !== 'undefined' &&
2030
+ document.fonts &&
2031
+ typeof document.fonts.ready === 'object' &&
2032
+ typeof document.fonts.ready.then === 'function');
2033
+ };
2034
+ /**
2035
+ * Hook to detect when fonts are loaded
2036
+ *
2037
+ * This hook provides graceful degradation:
2038
+ * - When Font Loading API is not available (older browsers, SSR, test environments),
2039
+ * it assumes fonts are loaded to prevent blocking UI
2040
+ * - When disabled, it immediately returns true without listening to font events
2041
+ * - When enabled and API is available, it listens for font loading completion
2042
+ *
2043
+ * @param enabled - Whether to listen for font loading events (default: true)
2044
+ * @returns boolean indicating whether fonts are loaded
2045
+ */
2046
+ const useFontLoaded = (enabled = true) => {
2047
+ const [loaded, setLoaded] = useState(() => !enabled || !isFontLoadingAPIAvailable());
2048
+ useEffect(() => {
2049
+ if (!enabled || !isFontLoadingAPIAvailable()) {
2050
+ return;
2051
+ }
2052
+ let cancelled = false;
2053
+ document.fonts.ready.then(() => {
2054
+ if (!cancelled) {
2055
+ setLoaded(true);
2056
+ }
2057
+ });
2058
+ return () => {
2059
+ cancelled = true;
2060
+ };
2061
+ }, [enabled]);
2062
+ return loaded;
2063
+ };
2064
+
2065
+ /**
2066
+ * Hook to lock body scroll while allowing scroll on target element
2067
+ * Uses body-scroll-lock library for cross-platform compatibility
2068
+ *
2069
+ * @param isLocked - Whether scroll should be locked
2070
+ * @param targetElement - Element that should remain scrollable (optional)
2071
+ * @param options - Configuration options
2072
+ */
2073
+ const useBodyScrollLock = (isLocked, targetElement, options = {}) => {
2074
+ const { disabled = false, reserveScrollBarGap = true, allowTouchMove, } = options;
2075
+ const { document } = useWindowContext();
2076
+ useEffect(() => {
2077
+ if (disabled || !document?.body)
2078
+ return;
2079
+ const element = targetElement || document.body;
2080
+ if (isLocked) {
2081
+ disableBodyScroll(element, {
2082
+ reserveScrollBarGap,
2083
+ allowTouchMove,
2084
+ });
2085
+ }
2086
+ return () => {
2087
+ if (isLocked) {
2088
+ enableBodyScroll(element);
2089
+ }
2090
+ };
2091
+ }, [
2092
+ isLocked,
2093
+ targetElement,
2094
+ disabled,
2095
+ reserveScrollBarGap,
2096
+ allowTouchMove,
2097
+ document,
2098
+ ]);
2099
+ };
2100
+
2101
+ export { createGlobalListener, globalListenerEventMap, isElmEqualOrContain, isTouchEvent, moveIndexInTwoDimension, teardown, useA11yKeyEvent, useActive, useAnnouncer, useAriaHideOutside, useAudio, useBodyScrollLock, useChange, useClickAway, useDebounce, useDepsChange, useEventCallback, useEventListener, useEver, useFocusVisible, useFontLoaded, useForceUpdate, useForkRef, useGlobalListener, useHiddenTabindex, useHover, useHoverDirty, useId, useImgLoaded, useInterval, useIsFocusVisible, useKeyDownOnce, useKeyboardMoveFocus, useLongPress, useMountState, useOnLayoutReRender, useOnReRender, useOnlyOneFocusable, useOverflow, usePrevious, useRefState, useResizeObserver, useResponsiveMatch, useResultRef, useRetry, useSleep, useTabOut, useTestRunnerDetection, useThrottle, useTimeout, useTouchMouseEvent, visuallyHiddenStyles };