@cloud-ru/ds-utils 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE +201 -0
  3. package/README.md +222 -0
  4. package/dist/cjs/hooks/index.d.ts +13 -0
  5. package/dist/cjs/hooks/index.js +29 -0
  6. package/dist/cjs/hooks/useCopyToClipboard.d.ts +9 -0
  7. package/dist/cjs/hooks/useCopyToClipboard.js +27 -0
  8. package/dist/cjs/hooks/useDataPersist/index.d.ts +2 -0
  9. package/dist/cjs/hooks/useDataPersist/index.js +18 -0
  10. package/dist/cjs/hooks/useDataPersist/sources/baseSource.d.ts +11 -0
  11. package/dist/cjs/hooks/useDataPersist/sources/baseSource.js +35 -0
  12. package/dist/cjs/hooks/useDataPersist/sources/index.d.ts +3 -0
  13. package/dist/cjs/hooks/useDataPersist/sources/index.js +19 -0
  14. package/dist/cjs/hooks/useDataPersist/sources/localStorageSource.d.ts +5 -0
  15. package/dist/cjs/hooks/useDataPersist/sources/localStorageSource.js +19 -0
  16. package/dist/cjs/hooks/useDataPersist/sources/queryParamSource.d.ts +5 -0
  17. package/dist/cjs/hooks/useDataPersist/sources/queryParamSource.js +21 -0
  18. package/dist/cjs/hooks/useDataPersist/useDataPersist.d.ts +16 -0
  19. package/dist/cjs/hooks/useDataPersist/useDataPersist.js +26 -0
  20. package/dist/cjs/hooks/useDataPersist/useSource.d.ts +8 -0
  21. package/dist/cjs/hooks/useDataPersist/useSource.js +15 -0
  22. package/dist/cjs/hooks/useDebounce.d.ts +6 -0
  23. package/dist/cjs/hooks/useDebounce.js +25 -0
  24. package/dist/cjs/hooks/useDynamicList.d.ts +18 -0
  25. package/dist/cjs/hooks/useDynamicList.js +99 -0
  26. package/dist/cjs/hooks/useEventHandler.d.ts +6 -0
  27. package/dist/cjs/hooks/useEventHandler.js +23 -0
  28. package/dist/cjs/hooks/useHierarchicalSelection.d.ts +12 -0
  29. package/dist/cjs/hooks/useHierarchicalSelection.js +11 -0
  30. package/dist/cjs/hooks/useIsomorphicLayoutEffect.d.ts +7 -0
  31. package/dist/cjs/hooks/useIsomorphicLayoutEffect.js +12 -0
  32. package/dist/cjs/hooks/useLocalStorage.d.ts +1 -0
  33. package/dist/cjs/hooks/useLocalStorage.js +15 -0
  34. package/dist/cjs/hooks/useModalOpenState.d.ts +18 -0
  35. package/dist/cjs/hooks/useModalOpenState.js +27 -0
  36. package/dist/cjs/hooks/usePopstateSubscription.d.ts +1 -0
  37. package/dist/cjs/hooks/usePopstateSubscription.js +14 -0
  38. package/dist/cjs/hooks/useSwipeable.d.ts +21 -0
  39. package/dist/cjs/hooks/useSwipeable.js +50 -0
  40. package/dist/cjs/hooks/useUncontrolledProp.d.ts +21 -0
  41. package/dist/cjs/hooks/useUncontrolledProp.js +23 -0
  42. package/dist/cjs/hooks/useValueControl.d.ts +20 -0
  43. package/dist/cjs/hooks/useValueControl.js +20 -0
  44. package/dist/cjs/index.d.ts +3 -0
  45. package/dist/cjs/index.js +19 -0
  46. package/dist/cjs/package.json +3 -0
  47. package/dist/cjs/types/index.d.ts +2 -0
  48. package/dist/cjs/types/index.js +18 -0
  49. package/dist/cjs/types/polymorphic.d.ts +3 -0
  50. package/dist/cjs/types/polymorphic.js +2 -0
  51. package/dist/cjs/types/valueOf.d.ts +5 -0
  52. package/dist/cjs/types/valueOf.js +2 -0
  53. package/dist/cjs/utils/componentPropsProcessors.d.ts +19 -0
  54. package/dist/cjs/utils/componentPropsProcessors.js +43 -0
  55. package/dist/cjs/utils/copyToClipboard.d.ts +12 -0
  56. package/dist/cjs/utils/copyToClipboard.js +47 -0
  57. package/dist/cjs/utils/debounce.d.ts +11 -0
  58. package/dist/cjs/utils/debounce.js +24 -0
  59. package/dist/cjs/utils/hierarchicalSelection/applySelectionDiff.d.ts +4 -0
  60. package/dist/cjs/utils/hierarchicalSelection/applySelectionDiff.js +16 -0
  61. package/dist/cjs/utils/hierarchicalSelection/checkGroupSelection.d.ts +5 -0
  62. package/dist/cjs/utils/hierarchicalSelection/checkGroupSelection.js +18 -0
  63. package/dist/cjs/utils/hierarchicalSelection/createHierarchicalSelectionHandlers.d.ts +19 -0
  64. package/dist/cjs/utils/hierarchicalSelection/createHierarchicalSelectionHandlers.js +20 -0
  65. package/dist/cjs/utils/hierarchicalSelection/getNodeSelectionState.d.ts +8 -0
  66. package/dist/cjs/utils/hierarchicalSelection/getNodeSelectionState.js +22 -0
  67. package/dist/cjs/utils/hierarchicalSelection/index.d.ts +6 -0
  68. package/dist/cjs/utils/hierarchicalSelection/index.js +22 -0
  69. package/dist/cjs/utils/hierarchicalSelection/toggleHierarchicalSelection.d.ts +5 -0
  70. package/dist/cjs/utils/hierarchicalSelection/toggleHierarchicalSelection.js +69 -0
  71. package/dist/cjs/utils/hierarchicalSelection/types.d.ts +23 -0
  72. package/dist/cjs/utils/hierarchicalSelection/types.js +2 -0
  73. package/dist/cjs/utils/index.d.ts +10 -0
  74. package/dist/cjs/utils/index.js +26 -0
  75. package/dist/cjs/utils/innerRef.d.ts +26 -0
  76. package/dist/cjs/utils/innerRef.js +37 -0
  77. package/dist/cjs/utils/isBrowser.d.ts +6 -0
  78. package/dist/cjs/utils/isBrowser.js +12 -0
  79. package/dist/cjs/utils/keyboard.d.ts +41 -0
  80. package/dist/cjs/utils/keyboard.js +66 -0
  81. package/dist/cjs/utils/preventEvent.d.ts +4 -0
  82. package/dist/cjs/utils/preventEvent.js +15 -0
  83. package/dist/cjs/utils/stopEventPropagation.d.ts +2 -0
  84. package/dist/cjs/utils/stopEventPropagation.js +6 -0
  85. package/dist/cjs/utils/throttle.d.ts +11 -0
  86. package/dist/cjs/utils/throttle.js +43 -0
  87. package/dist/cjs/utils/typeGuards.d.ts +5 -0
  88. package/dist/cjs/utils/typeGuards.js +13 -0
  89. package/dist/esm/hooks/index.d.ts +13 -0
  90. package/dist/esm/hooks/index.js +13 -0
  91. package/dist/esm/hooks/useCopyToClipboard.d.ts +9 -0
  92. package/dist/esm/hooks/useCopyToClipboard.js +24 -0
  93. package/dist/esm/hooks/useDataPersist/index.d.ts +2 -0
  94. package/dist/esm/hooks/useDataPersist/index.js +2 -0
  95. package/dist/esm/hooks/useDataPersist/sources/baseSource.d.ts +11 -0
  96. package/dist/esm/hooks/useDataPersist/sources/baseSource.js +31 -0
  97. package/dist/esm/hooks/useDataPersist/sources/index.d.ts +3 -0
  98. package/dist/esm/hooks/useDataPersist/sources/index.js +3 -0
  99. package/dist/esm/hooks/useDataPersist/sources/localStorageSource.d.ts +5 -0
  100. package/dist/esm/hooks/useDataPersist/sources/localStorageSource.js +15 -0
  101. package/dist/esm/hooks/useDataPersist/sources/queryParamSource.d.ts +5 -0
  102. package/dist/esm/hooks/useDataPersist/sources/queryParamSource.js +17 -0
  103. package/dist/esm/hooks/useDataPersist/useDataPersist.d.ts +16 -0
  104. package/dist/esm/hooks/useDataPersist/useDataPersist.js +22 -0
  105. package/dist/esm/hooks/useDataPersist/useSource.d.ts +8 -0
  106. package/dist/esm/hooks/useDataPersist/useSource.js +11 -0
  107. package/dist/esm/hooks/useDebounce.d.ts +6 -0
  108. package/dist/esm/hooks/useDebounce.js +22 -0
  109. package/dist/esm/hooks/useDynamicList.d.ts +18 -0
  110. package/dist/esm/hooks/useDynamicList.js +96 -0
  111. package/dist/esm/hooks/useEventHandler.d.ts +6 -0
  112. package/dist/esm/hooks/useEventHandler.js +20 -0
  113. package/dist/esm/hooks/useHierarchicalSelection.d.ts +12 -0
  114. package/dist/esm/hooks/useHierarchicalSelection.js +8 -0
  115. package/dist/esm/hooks/useIsomorphicLayoutEffect.d.ts +7 -0
  116. package/dist/esm/hooks/useIsomorphicLayoutEffect.js +9 -0
  117. package/dist/esm/hooks/useLocalStorage.d.ts +1 -0
  118. package/dist/esm/hooks/useLocalStorage.js +12 -0
  119. package/dist/esm/hooks/useModalOpenState.d.ts +18 -0
  120. package/dist/esm/hooks/useModalOpenState.js +24 -0
  121. package/dist/esm/hooks/usePopstateSubscription.d.ts +1 -0
  122. package/dist/esm/hooks/usePopstateSubscription.js +11 -0
  123. package/dist/esm/hooks/useSwipeable.d.ts +21 -0
  124. package/dist/esm/hooks/useSwipeable.js +46 -0
  125. package/dist/esm/hooks/useUncontrolledProp.d.ts +21 -0
  126. package/dist/esm/hooks/useUncontrolledProp.js +20 -0
  127. package/dist/esm/hooks/useValueControl.d.ts +20 -0
  128. package/dist/esm/hooks/useValueControl.js +17 -0
  129. package/dist/esm/index.d.ts +3 -0
  130. package/dist/esm/index.js +3 -0
  131. package/dist/esm/types/index.d.ts +2 -0
  132. package/dist/esm/types/index.js +2 -0
  133. package/dist/esm/types/polymorphic.d.ts +3 -0
  134. package/dist/esm/types/polymorphic.js +1 -0
  135. package/dist/esm/types/valueOf.d.ts +5 -0
  136. package/dist/esm/types/valueOf.js +1 -0
  137. package/dist/esm/utils/componentPropsProcessors.d.ts +19 -0
  138. package/dist/esm/utils/componentPropsProcessors.js +38 -0
  139. package/dist/esm/utils/copyToClipboard.d.ts +12 -0
  140. package/dist/esm/utils/copyToClipboard.js +44 -0
  141. package/dist/esm/utils/debounce.d.ts +11 -0
  142. package/dist/esm/utils/debounce.js +21 -0
  143. package/dist/esm/utils/hierarchicalSelection/applySelectionDiff.d.ts +4 -0
  144. package/dist/esm/utils/hierarchicalSelection/applySelectionDiff.js +13 -0
  145. package/dist/esm/utils/hierarchicalSelection/checkGroupSelection.d.ts +5 -0
  146. package/dist/esm/utils/hierarchicalSelection/checkGroupSelection.js +15 -0
  147. package/dist/esm/utils/hierarchicalSelection/createHierarchicalSelectionHandlers.d.ts +19 -0
  148. package/dist/esm/utils/hierarchicalSelection/createHierarchicalSelectionHandlers.js +17 -0
  149. package/dist/esm/utils/hierarchicalSelection/getNodeSelectionState.d.ts +8 -0
  150. package/dist/esm/utils/hierarchicalSelection/getNodeSelectionState.js +19 -0
  151. package/dist/esm/utils/hierarchicalSelection/index.d.ts +6 -0
  152. package/dist/esm/utils/hierarchicalSelection/index.js +6 -0
  153. package/dist/esm/utils/hierarchicalSelection/toggleHierarchicalSelection.d.ts +5 -0
  154. package/dist/esm/utils/hierarchicalSelection/toggleHierarchicalSelection.js +66 -0
  155. package/dist/esm/utils/hierarchicalSelection/types.d.ts +23 -0
  156. package/dist/esm/utils/hierarchicalSelection/types.js +1 -0
  157. package/dist/esm/utils/index.d.ts +10 -0
  158. package/dist/esm/utils/index.js +10 -0
  159. package/dist/esm/utils/innerRef.d.ts +26 -0
  160. package/dist/esm/utils/innerRef.js +32 -0
  161. package/dist/esm/utils/isBrowser.d.ts +6 -0
  162. package/dist/esm/utils/isBrowser.js +9 -0
  163. package/dist/esm/utils/keyboard.d.ts +41 -0
  164. package/dist/esm/utils/keyboard.js +58 -0
  165. package/dist/esm/utils/preventEvent.d.ts +4 -0
  166. package/dist/esm/utils/preventEvent.js +10 -0
  167. package/dist/esm/utils/stopEventPropagation.d.ts +2 -0
  168. package/dist/esm/utils/stopEventPropagation.js +3 -0
  169. package/dist/esm/utils/throttle.d.ts +11 -0
  170. package/dist/esm/utils/throttle.js +40 -0
  171. package/dist/esm/utils/typeGuards.d.ts +5 -0
  172. package/dist/esm/utils/typeGuards.js +5 -0
  173. package/dist/tsconfig.cjs.tsbuildinfo +1 -0
  174. package/dist/tsconfig.esm.tsbuildinfo +1 -0
  175. package/package.json +38 -0
  176. package/src/hooks/index.ts +13 -0
  177. package/src/hooks/useCopyToClipboard.ts +38 -0
  178. package/src/hooks/useDataPersist/index.ts +2 -0
  179. package/src/hooks/useDataPersist/sources/baseSource.ts +36 -0
  180. package/src/hooks/useDataPersist/sources/index.ts +3 -0
  181. package/src/hooks/useDataPersist/sources/localStorageSource.ts +17 -0
  182. package/src/hooks/useDataPersist/sources/queryParamSource.ts +19 -0
  183. package/src/hooks/useDataPersist/useDataPersist.ts +51 -0
  184. package/src/hooks/useDataPersist/useSource.ts +23 -0
  185. package/src/hooks/useDebounce.ts +25 -0
  186. package/src/hooks/useDynamicList.ts +137 -0
  187. package/src/hooks/useEventHandler.ts +26 -0
  188. package/src/hooks/useHierarchicalSelection.ts +13 -0
  189. package/src/hooks/useIsomorphicLayoutEffect.ts +11 -0
  190. package/src/hooks/useLocalStorage.ts +19 -0
  191. package/src/hooks/useModalOpenState.ts +37 -0
  192. package/src/hooks/usePopstateSubscription.ts +14 -0
  193. package/src/hooks/useSwipeable.ts +85 -0
  194. package/src/hooks/useUncontrolledProp.ts +65 -0
  195. package/src/hooks/useValueControl.ts +41 -0
  196. package/src/index.ts +3 -0
  197. package/src/types/index.ts +2 -0
  198. package/src/types/polymorphic.ts +4 -0
  199. package/src/types/valueOf.ts +5 -0
  200. package/src/utils/componentPropsProcessors.ts +55 -0
  201. package/src/utils/copyToClipboard.ts +46 -0
  202. package/src/utils/debounce.ts +31 -0
  203. package/src/utils/hierarchicalSelection/applySelectionDiff.ts +20 -0
  204. package/src/utils/hierarchicalSelection/checkGroupSelection.ts +22 -0
  205. package/src/utils/hierarchicalSelection/createHierarchicalSelectionHandlers.ts +35 -0
  206. package/src/utils/hierarchicalSelection/getNodeSelectionState.ts +29 -0
  207. package/src/utils/hierarchicalSelection/index.ts +6 -0
  208. package/src/utils/hierarchicalSelection/toggleHierarchicalSelection.ts +109 -0
  209. package/src/utils/hierarchicalSelection/types.ts +26 -0
  210. package/src/utils/index.ts +10 -0
  211. package/src/utils/innerRef.ts +37 -0
  212. package/src/utils/isBrowser.ts +9 -0
  213. package/src/utils/keyboard.ts +68 -0
  214. package/src/utils/stopEventPropagation.ts +5 -0
  215. package/src/utils/throttle.ts +55 -0
  216. package/src/utils/typeGuards.ts +9 -0
@@ -0,0 +1,26 @@
1
+ import { useCallback, useRef } from 'react';
2
+
3
+ import { useLayoutEffect } from './useIsomorphicLayoutEffect';
4
+
5
+ /**
6
+ * Хук для создания обработчика событий с неизменной функцией.
7
+ * @function React hook
8
+ * @param handler коллбек события, без параметров
9
+ */
10
+ export function useEventHandler<T extends (...args: never[]) => unknown>(handler: T) {
11
+ const handlerRef = useRef<T | null>(null);
12
+
13
+ useLayoutEffect(() => {
14
+ handlerRef.current = handler;
15
+ });
16
+
17
+ return useCallback((...args: Parameters<T>) => {
18
+ const handler = handlerRef.current;
19
+
20
+ if (handler === null) {
21
+ console.warn('The event handler cannot be called during render');
22
+ }
23
+
24
+ return handler?.(...args);
25
+ }, []) as T;
26
+ }
@@ -0,0 +1,13 @@
1
+ import { useMemo } from 'react';
2
+
3
+ import {
4
+ createHierarchicalSelectionHandlers,
5
+ HierarchicalSelectionConfig,
6
+ } from '../utils/hierarchicalSelection/createHierarchicalSelectionHandlers';
7
+
8
+ /**
9
+ * Возвращает стабильные обработчики иерархического выбора с единым `includeParentsInValue`.
10
+ */
11
+ export function useHierarchicalSelection({ includeParentsInValue }: HierarchicalSelectionConfig) {
12
+ return useMemo(() => createHierarchicalSelectionHandlers({ includeParentsInValue }), [includeParentsInValue]);
13
+ }
@@ -0,0 +1,11 @@
1
+ // eslint-disable-next-line no-restricted-imports
2
+ import { useEffect, useLayoutEffect as useLayoutEffectInternal } from 'react';
3
+
4
+ import { isBrowser } from '../utils';
5
+
6
+ /**
7
+ * Хук используется вместо стандартного useLayoutEffect-а,
8
+ * нужен для корректной работы SSR
9
+ * @function React hook
10
+ */
11
+ export const useLayoutEffect = isBrowser() ? useLayoutEffectInternal : useEffect;
@@ -0,0 +1,19 @@
1
+ import { useCallback, useState } from 'react';
2
+
3
+ import { isBrowser } from '../utils';
4
+
5
+ export function useLocalStorage<T extends string = string>(key: string, defaultValue: T): [T, (newValue: T) => void] {
6
+ const [value, setValueState] = useState<T>(((isBrowser() ? localStorage.getItem(key) : null) ?? defaultValue) as T);
7
+
8
+ const setValue = useCallback(
9
+ (newValue: T) => {
10
+ if (isBrowser()) {
11
+ localStorage.setItem(key, newValue);
12
+ }
13
+ setValueState(newValue);
14
+ },
15
+ [key],
16
+ );
17
+
18
+ return [value, setValue];
19
+ }
@@ -0,0 +1,37 @@
1
+ import { useEffect } from 'react';
2
+
3
+ import { useEventHandler } from './useEventHandler';
4
+ import { usePopstateSubscription } from './usePopstateSubscription';
5
+
6
+ type UseModalOpenStateOptions = {
7
+ /** закрывать модалку при переходе по истории браузера (по умолчанию false) */
8
+ closeOnPopstate?: boolean;
9
+ /** закрывать модалку через CloseWatcher (по умолчанию true) */
10
+ closeByCloseWatcher?: boolean;
11
+ };
12
+
13
+ /**
14
+ * Хук для управления состоянием модалки
15
+ * @returns [open, onClose]
16
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/CloseWatcher
17
+ * @function hook
18
+ */
19
+ export function useModalOpenState(
20
+ /** состояние модалки */
21
+ open: boolean,
22
+ /** колбек закрытия */
23
+ onClose: () => void,
24
+ { closeOnPopstate = false, closeByCloseWatcher = true }: UseModalOpenStateOptions,
25
+ ) {
26
+ const closeHandler = useEventHandler(onClose);
27
+
28
+ usePopstateSubscription(() => open && closeHandler(), closeOnPopstate);
29
+
30
+ useEffect(() => {
31
+ if (open && 'CloseWatcher' in window && closeByCloseWatcher) {
32
+ const watcher = new CloseWatcher();
33
+ watcher.onclose = () => closeHandler();
34
+ return () => watcher.destroy();
35
+ }
36
+ }, [open, closeHandler, closeByCloseWatcher]);
37
+ }
@@ -0,0 +1,14 @@
1
+ import { useEffect } from 'react';
2
+
3
+ import { useEventHandler } from './useEventHandler';
4
+
5
+ export function usePopstateSubscription(callback: (e: PopStateEvent) => void, enabled: boolean = true) {
6
+ const handler = useEventHandler(callback);
7
+
8
+ useEffect(() => {
9
+ if (!enabled) return;
10
+
11
+ window.addEventListener('popstate', handler);
12
+ return () => window.removeEventListener('popstate', handler);
13
+ }, [enabled, handler]);
14
+ }
@@ -0,0 +1,85 @@
1
+ import { useRef } from 'react';
2
+ import {
3
+ SwipeableHandlers,
4
+ SwipeableProps,
5
+ SwipeCallback,
6
+ SwipeDirections,
7
+ SwipeEventData,
8
+ useSwipeable as useSwipeableInternal,
9
+ } from 'react-swipeable';
10
+
11
+ export const DATA_SWIPE_DIRECTIONS_ATTRIBUTE = 'data-swipe-directions';
12
+
13
+ export type UseSwipeProps = {
14
+ /**
15
+ * Включен ли свайп
16
+ */
17
+ enabled?: boolean;
18
+ /**
19
+ * Направления, в которых будет работать свайп.
20
+ * Укажите это свойство, чтобы предотвратить конфликты и заблокировать свайп в родительских элементах по этим направлениям.
21
+ */
22
+ availableDirections?: SwipeDirections[];
23
+ } & SwipeableProps;
24
+
25
+ export type UseSwipeReturnType = SwipeableHandlers & {
26
+ [DATA_SWIPE_DIRECTIONS_ATTRIBUTE]?: string;
27
+ };
28
+
29
+ /**
30
+ * Хук для работы с событиями свайпа
31
+ */
32
+ export function useSwipeable({ availableDirections, enabled = true, ...options }: UseSwipeProps): UseSwipeReturnType {
33
+ const canSwipe = useRef(true);
34
+
35
+ const onSwipeStart: SwipeCallback = eventData => {
36
+ if (availableDirections) {
37
+ if (!availableDirections.includes(eventData.dir)) {
38
+ canSwipe.current = false;
39
+ return;
40
+ }
41
+
42
+ if (!options.onSwipeStart) {
43
+ return;
44
+ }
45
+
46
+ eventData.event.stopPropagation();
47
+ }
48
+
49
+ options.onSwipeStart?.(eventData);
50
+ };
51
+
52
+ const onSwiping: SwipeCallback = eventData => {
53
+ if (availableDirections) {
54
+ if (!canSwipe.current) {
55
+ return;
56
+ }
57
+
58
+ eventData.event.stopPropagation();
59
+ }
60
+
61
+ options.onSwiping?.(eventData);
62
+ };
63
+
64
+ const onSwiped: SwipeCallback = eventData => {
65
+ if (availableDirections) {
66
+ if (!canSwipe.current) {
67
+ canSwipe.current = true;
68
+ return;
69
+ }
70
+
71
+ eventData.event.stopPropagation();
72
+ }
73
+
74
+ options.onSwiped?.(eventData);
75
+ };
76
+
77
+ const handlers = useSwipeableInternal(enabled ? { ...options, onSwipeStart, onSwiping, onSwiped } : {});
78
+
79
+ return {
80
+ ...handlers,
81
+ [DATA_SWIPE_DIRECTIONS_ATTRIBUTE]: enabled && availableDirections ? availableDirections.join(' ') : undefined,
82
+ };
83
+ }
84
+
85
+ export type { SwipeCallback, SwipeDirections, SwipeEventData };
@@ -0,0 +1,65 @@
1
+ import { useCallback, useRef, useState } from 'react';
2
+
3
+ /* eslint-disable @typescript-eslint/no-explicit-any */
4
+ /**
5
+ * Сигнатура колбэка смены значения.
6
+ *
7
+ * `any` здесь — не небрежность, а воспроизведение публичного типа пакета `uncontrollable`,
8
+ * на месте которого стоит этот хук. Свободный тип позволяет и колбэки с дополнительными
9
+ * аргументами (`onSelect(key, node)`), и функциональные апдейтеры (`setValue(prev => …)`,
10
+ * которые дальше применяет `useState`). Сузить его нельзя, не переписав типы `ToggleGroup`,
11
+ * `SelectionProvider` и `TreeContext`, — это отдельная задача.
12
+ */
13
+ export type UncontrolledHandler = (value: any, ...args: any[]) => any;
14
+ /* eslint-enable @typescript-eslint/no-explicit-any */
15
+
16
+ /**
17
+ * Пара «значение + сеттер» для пропа, который работает и в controlled, и в uncontrolled режиме.
18
+ * Пока `propValue` задан — источник истины он, а сеттер только зовёт `handler`. Как только
19
+ * `propValue` становится `undefined`, состояние переходит во внутренний `useState`
20
+ * и сбрасывается на `defaultValue`.
21
+ *
22
+ * Сеттер стабилен между рендерами, пока не меняется `handler`.
23
+ * @function React hook
24
+ */
25
+ export function useUncontrolledProp<TValue, THandler extends UncontrolledHandler = UncontrolledHandler>(
26
+ propValue: TValue | undefined,
27
+ defaultValue: TValue,
28
+ handler?: THandler,
29
+ ): readonly [TValue, THandler];
30
+ export function useUncontrolledProp<TValue, THandler extends UncontrolledHandler = UncontrolledHandler>(
31
+ propValue: TValue | undefined,
32
+ defaultValue?: TValue,
33
+ handler?: THandler,
34
+ ): readonly [TValue | undefined, (...args: Parameters<THandler>) => ReturnType<THandler> | void];
35
+ export function useUncontrolledProp<TValue, THandler extends UncontrolledHandler = UncontrolledHandler>(
36
+ propValue: TValue | undefined,
37
+ defaultValue?: TValue,
38
+ handler?: THandler,
39
+ ): readonly [TValue | undefined, (...args: Parameters<THandler>) => ReturnType<THandler> | void] {
40
+ const isProp = propValue !== undefined;
41
+ const wasPropRef = useRef(isProp);
42
+ const [stateValue, setStateValue] = useState(defaultValue);
43
+
44
+ const wasProp = wasPropRef.current;
45
+ wasPropRef.current = isProp;
46
+
47
+ // Переход controlled -> uncontrolled: внутреннее состояние отстало от `defaultValue`,
48
+ // возвращаем его к дефолту, иначе компонент «залипнет» на последнем controlled-значении.
49
+ if (!isProp && wasProp && stateValue !== defaultValue) {
50
+ setStateValue(defaultValue);
51
+ }
52
+
53
+ const setValue = useCallback(
54
+ (...args: Parameters<THandler>) => {
55
+ const [value, ...rest] = args;
56
+ const returnValue = handler?.(value, ...rest);
57
+ setStateValue(value);
58
+
59
+ return returnValue;
60
+ },
61
+ [handler],
62
+ );
63
+
64
+ return [isProp ? propValue : stateValue, setValue];
65
+ }
@@ -0,0 +1,41 @@
1
+ import { useCallback, useRef } from 'react';
2
+
3
+ import { useUncontrolledProp } from './useUncontrolledProp';
4
+
5
+ type UseValueControl<TValue> = {
6
+ /**
7
+ * Значение состояния
8
+ */
9
+ value?: TValue;
10
+ /**
11
+ * Значение по умолчанию
12
+ */
13
+ defaultValue?: TValue;
14
+ /**
15
+ * Колбек, вызываемый на смену состояния
16
+ */
17
+ onChange?(value: TValue): void;
18
+ };
19
+
20
+ /**
21
+ * Хук для работы с состоянием.
22
+ * Нужен для поддержки controlled/uncontrolled поведения, в зависимости от того были ли переданы входные аргументы
23
+ */
24
+ export function useValueControl<TValue>({ value, onChange, defaultValue }: UseValueControl<TValue>) {
25
+ // Значение читаем из ref: иначе handler пересоздавался бы на каждую смену `value`,
26
+ // а вместе с ним — и сеттер из `useUncontrolledProp`.
27
+ const valueRef = useRef(value);
28
+ valueRef.current = value;
29
+
30
+ const handleChange = useCallback(
31
+ (newValue: TValue) => {
32
+ const newState =
33
+ typeof newValue === 'function' ? (newValue as (prev?: TValue) => TValue)(valueRef.current) : newValue;
34
+
35
+ onChange?.(newState);
36
+ },
37
+ [onChange],
38
+ );
39
+
40
+ return useUncontrolledProp<TValue>(value, defaultValue, handleChange);
41
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './hooks';
2
+ export * from './utils';
3
+ export * from './types';
@@ -0,0 +1,2 @@
1
+ export * from './polymorphic';
2
+ export * from './valueOf';
@@ -0,0 +1,4 @@
1
+ import { ComponentPropsWithRef, ElementType } from 'react';
2
+
3
+ /** Тип ref'а для полиморфного компонента: то, что примет элемент/компонент, переданный в `as`. */
4
+ export type PolymorphicRef<T extends ElementType> = ComponentPropsWithRef<T>['ref'];
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Utility-тип, преобразующий константные значения объекта в union
3
+ * @function type
4
+ */
5
+ export type ValueOf<T> = T[keyof T];
@@ -0,0 +1,55 @@
1
+ import { AriaAttributes } from 'react';
2
+ const DATA_TEST_REGEXP = /^data-test-/;
3
+ const DATA_AND_ARIA_REGEXP = /^(data|aria)-/;
4
+
5
+ function excludeProps<T extends Record<string, unknown>>(props: T, regexp: RegExp) {
6
+ return Object.keys(props)
7
+ .filter(prop => !prop.match(regexp))
8
+ .reduce(
9
+ (nextProps, prop) => ({
10
+ ...nextProps,
11
+ [prop]: props[prop],
12
+ }),
13
+ {},
14
+ );
15
+ }
16
+
17
+ function extractProps<T extends Record<string, unknown>>(props: T, regex: RegExp) {
18
+ return Object.keys(props).reduce(
19
+ (nextProps, prop) => {
20
+ if (prop.match(regex)) nextProps[prop] = props[prop];
21
+
22
+ return nextProps;
23
+ },
24
+ {} as Record<string, unknown>,
25
+ );
26
+ }
27
+
28
+ export type WithSupportProps<T> = {
29
+ 'data-test-id'?: string;
30
+ } & AriaAttributes &
31
+ T;
32
+
33
+ /**
34
+ * Функция для исключения вспомогательных свойств (data-test-id & aria-*)
35
+ * @function helper
36
+ */
37
+ export function excludeSupportProps<T extends Record<string, unknown>>(props: T) {
38
+ return excludeProps(props, DATA_AND_ARIA_REGEXP);
39
+ }
40
+
41
+ /**
42
+ * Функция для отбора свойства data-test-id
43
+ * @function helper
44
+ */
45
+ export function extractDataTestProps<T extends Record<string, unknown>>(props: T) {
46
+ return extractProps(props, DATA_TEST_REGEXP);
47
+ }
48
+
49
+ /**
50
+ * Функция для отбора вспомогательных свойств (data-test-id & aria-*)
51
+ * @function helper
52
+ */
53
+ export function extractSupportProps<T extends Record<string, unknown>>(props: T) {
54
+ return extractProps(props, DATA_AND_ARIA_REGEXP);
55
+ }
@@ -0,0 +1,46 @@
1
+ import { isBrowser } from './isBrowser';
2
+
3
+ /**
4
+ * Копирует текст в буфер обмена. Сначала пробует Async Clipboard API, при отказе
5
+ * (нет разрешения, insecure-context) падает на `execCommand('copy')` со скрытым textarea.
6
+ *
7
+ * Возвращает `true`, если запись удалась — по этому признаку поля и кнопки показывают
8
+ * индикатор «скопировано».
9
+ *
10
+ * Проверки `isBrowser()` продублированы внутри веток намеренно: правило
11
+ * `@cloud-ru/ssr-safe-react/domApi` требует, чтобы доступ к `navigator`/`document` стоял
12
+ * непосредственно внутри такого условия, а ранний выход оно не учитывает.
13
+ */
14
+ export async function copyToClipboard(value: string): Promise<boolean> {
15
+ if (!isBrowser()) return false;
16
+
17
+ if (isBrowser() && navigator.clipboard?.writeText) {
18
+ try {
19
+ await navigator.clipboard.writeText(value);
20
+
21
+ return true;
22
+ } catch {
23
+ // fall through to execCommand fallback
24
+ }
25
+ }
26
+
27
+ if (isBrowser()) {
28
+ const textarea = document.createElement('textarea');
29
+ textarea.value = value;
30
+ textarea.setAttribute('readonly', '');
31
+ textarea.style.position = 'fixed';
32
+ textarea.style.opacity = '0';
33
+ document.body.appendChild(textarea);
34
+ textarea.select();
35
+
36
+ try {
37
+ return document.execCommand('copy');
38
+ } catch {
39
+ return false;
40
+ } finally {
41
+ document.body.removeChild(textarea);
42
+ }
43
+ }
44
+
45
+ return false;
46
+ }
@@ -0,0 +1,31 @@
1
+ /** Функция-обёртка `debounce`: вызывается как исходная, плюс умеет отменить отложенный вызов. */
2
+ export type Debounced<TArgs extends unknown[]> = {
3
+ (...args: TArgs): void;
4
+ /** Отменяет запланированный вызов. Нужен в cleanup эффекта, чтобы не стрелять после размонтирования. */
5
+ cancel(): void;
6
+ };
7
+
8
+ /**
9
+ * Откладывает вызов `callback` на `wait` мс, перезапуская таймер на каждом новом вызове.
10
+ * Срабатывает по заднему фронту — с аргументами последнего вызова.
11
+ */
12
+ export function debounce<TArgs extends unknown[]>(callback: (...args: TArgs) => void, wait = 0): Debounced<TArgs> {
13
+ let timer: ReturnType<typeof setTimeout> | undefined;
14
+
15
+ const cancel = () => {
16
+ if (timer !== undefined) {
17
+ clearTimeout(timer);
18
+ timer = undefined;
19
+ }
20
+ };
21
+
22
+ const debounced = (...args: TArgs): void => {
23
+ cancel();
24
+ timer = setTimeout(() => {
25
+ timer = undefined;
26
+ callback(...args);
27
+ }, wait);
28
+ };
29
+
30
+ return Object.assign(debounced, { cancel });
31
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Применяет diff к набору выбранных id.
3
+ */
4
+ export function applySelectionDiff<TId extends string | number>(
5
+ selectedIds: TId[],
6
+ added: TId[],
7
+ removed: TId[],
8
+ ): TId[] {
9
+ const result = new Set(selectedIds);
10
+
11
+ for (const id of added) {
12
+ result.add(id);
13
+ }
14
+
15
+ for (const id of removed) {
16
+ result.delete(id);
17
+ }
18
+
19
+ return [...result];
20
+ }
@@ -0,0 +1,22 @@
1
+ import { GroupSelectionState } from './types';
2
+
3
+ /**
4
+ * Проверяет состояние выбора группы по flat-списку дочерних id.
5
+ */
6
+ export function checkGroupSelection<TId extends string | number>(
7
+ childIds: TId[],
8
+ selectedIds: TId[],
9
+ ): GroupSelectionState {
10
+ if (!childIds.length) {
11
+ return { allSelected: false, someSelected: false };
12
+ }
13
+
14
+ const selectedCount = childIds.filter(id => selectedIds.includes(id)).length;
15
+ const someSelected = selectedCount > 0;
16
+ const allSelected = someSelected && selectedCount === childIds.length;
17
+
18
+ return {
19
+ allSelected,
20
+ someSelected: !allSelected && someSelected,
21
+ };
22
+ }
@@ -0,0 +1,35 @@
1
+ import { checkGroupSelection } from './checkGroupSelection';
2
+ import { getNodeSelectionState } from './getNodeSelectionState';
3
+ import { toggleHierarchicalSelection } from './toggleHierarchicalSelection';
4
+ import { GetNodeSelectionStateParams, ToggleHierarchicalSelectionParams } from './types';
5
+
6
+ export type HierarchicalSelectionConfig = {
7
+ includeParentsInValue: boolean;
8
+ };
9
+
10
+ export type HierarchicalSelectionNodeParams<TId extends string | number = string> = Omit<
11
+ GetNodeSelectionStateParams<TId>,
12
+ 'includeParentsInValue'
13
+ >;
14
+
15
+ export type HierarchicalSelectionToggleParams<TId extends string | number = string> = Omit<
16
+ ToggleHierarchicalSelectionParams<TId>,
17
+ 'includeParentsInValue'
18
+ >;
19
+
20
+ /**
21
+ * Создаёт набор обработчиков иерархического выбора с единым `includeParentsInValue`.
22
+ */
23
+ export function createHierarchicalSelectionHandlers({ includeParentsInValue }: HierarchicalSelectionConfig) {
24
+ return {
25
+ getSelectionState<TId extends string | number>(params: HierarchicalSelectionNodeParams<TId>) {
26
+ return getNodeSelectionState({ ...params, includeParentsInValue });
27
+ },
28
+ toggleSelection<TId extends string | number>(params: HierarchicalSelectionToggleParams<TId>) {
29
+ return toggleHierarchicalSelection({ ...params, includeParentsInValue });
30
+ },
31
+ checkGroupSelection,
32
+ };
33
+ }
34
+
35
+ export type HierarchicalSelectionHandlers = ReturnType<typeof createHierarchicalSelectionHandlers>;
@@ -0,0 +1,29 @@
1
+ import { checkGroupSelection } from './checkGroupSelection';
2
+ import { GetNodeSelectionStateParams } from './types';
3
+
4
+ /**
5
+ * Единая формула checked/indeterminate для иерархического мультивыбора.
6
+ */
7
+ export function getNodeSelectionState<TId extends string | number>({
8
+ nodeId,
9
+ childIds,
10
+ selectedIds,
11
+ includeParentsInValue,
12
+ }: GetNodeSelectionStateParams<TId>) {
13
+ const { allSelected, someSelected } = checkGroupSelection(childIds, selectedIds);
14
+
15
+ if (!includeParentsInValue) {
16
+ return {
17
+ checked: allSelected,
18
+ indeterminate: someSelected,
19
+ };
20
+ }
21
+
22
+ const inValue = selectedIds.includes(nodeId);
23
+ const checked = inValue || allSelected;
24
+
25
+ return {
26
+ checked,
27
+ indeterminate: !checked && someSelected,
28
+ };
29
+ }
@@ -0,0 +1,6 @@
1
+ export * from './applySelectionDiff';
2
+ export * from './checkGroupSelection';
3
+ export * from './createHierarchicalSelectionHandlers';
4
+ export * from './getNodeSelectionState';
5
+ export * from './toggleHierarchicalSelection';
6
+ export * from './types';