@cloud-ru/uikit-product-mobile-fields 0.11.24

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 (65) hide show
  1. package/CHANGELOG.md +1300 -0
  2. package/LICENSE +201 -0
  3. package/README.md +8 -0
  4. package/package.json +66 -0
  5. package/src/components/AdaptiveField/AdaptiveField.tsx +88 -0
  6. package/src/components/AdaptiveField/index.ts +1 -0
  7. package/src/components/MobileFieldDate/MobileFieldDate.tsx +375 -0
  8. package/src/components/MobileFieldDate/constants.ts +33 -0
  9. package/src/components/MobileFieldDate/index.ts +2 -0
  10. package/src/components/MobileFieldDate/styles.module.scss +75 -0
  11. package/src/components/MobileFieldDate/types.ts +6 -0
  12. package/src/components/MobileFieldDate/utils.ts +49 -0
  13. package/src/components/MobileFieldSelect/MobileFieldSelect.tsx +18 -0
  14. package/src/components/MobileFieldSelect/MobileFieldSelectMultiple.tsx +331 -0
  15. package/src/components/MobileFieldSelect/MobileFieldSelectSingle.tsx +300 -0
  16. package/src/components/MobileFieldSelect/hooks.tsx +195 -0
  17. package/src/components/MobileFieldSelect/index.ts +13 -0
  18. package/src/components/MobileFieldSelect/legacy/components/Items/hooks.tsx +53 -0
  19. package/src/components/MobileFieldSelect/legacy/components/index.ts +1 -0
  20. package/src/components/MobileFieldSelect/legacy/hooks.ts +38 -0
  21. package/src/components/MobileFieldSelect/legacy/index.ts +3 -0
  22. package/src/components/MobileFieldSelect/legacy/utils.ts +176 -0
  23. package/src/components/MobileFieldSelect/styles.module.scss +176 -0
  24. package/src/components/MobileFieldSelect/types.ts +156 -0
  25. package/src/components/MobileFieldSelect/utils/extractFieldDecoratorProps.ts +35 -0
  26. package/src/components/MobileFieldSelect/utils/extractListProps.ts +30 -0
  27. package/src/components/MobileFieldSelect/utils/getArrowIcon.ts +15 -0
  28. package/src/components/MobileFieldSelect/utils/index.ts +6 -0
  29. package/src/components/MobileFieldSelect/utils/options.tsx +88 -0
  30. package/src/components/MobileFieldSelect/utils/typeGuards.ts +38 -0
  31. package/src/components/MobileFieldSelect/utils/updateItems.ts +121 -0
  32. package/src/components/index.ts +3 -0
  33. package/src/constants/allFields.ts +11 -0
  34. package/src/constants/dateFields.ts +127 -0
  35. package/src/constants/index.ts +2 -0
  36. package/src/helperComponents/ButtonCopyValue/ButtonCopyValue.tsx +79 -0
  37. package/src/helperComponents/ButtonCopyValue/helpers.tsx +19 -0
  38. package/src/helperComponents/ButtonCopyValue/index.ts +1 -0
  39. package/src/helperComponents/ButtonCopyValue/styles.module.scss +5 -0
  40. package/src/helperComponents/FieldContainerPrivate/FieldContainerPrivate.tsx +79 -0
  41. package/src/helperComponents/FieldContainerPrivate/index.ts +1 -0
  42. package/src/helperComponents/FieldContainerPrivate/styles.module.scss +131 -0
  43. package/src/helperComponents/ItemContent/ItemContent.tsx +37 -0
  44. package/src/helperComponents/ItemContent/index.ts +1 -0
  45. package/src/helperComponents/ItemContent/styles.module.scss +80 -0
  46. package/src/helperComponents/index.ts +3 -0
  47. package/src/hooks/dateHandlers/index.ts +3 -0
  48. package/src/hooks/dateHandlers/useDateField.ts +275 -0
  49. package/src/hooks/dateHandlers/useDateFieldHelpersForMode.ts +145 -0
  50. package/src/hooks/dateHandlers/useFocusHandlers.ts +46 -0
  51. package/src/hooks/dateHandlers/useHandlers.ts +15 -0
  52. package/src/hooks/index.ts +5 -0
  53. package/src/hooks/styles.module.scss +17 -0
  54. package/src/hooks/useCopyButton.tsx +47 -0
  55. package/src/hooks/usePostfix.tsx +21 -0
  56. package/src/hooks/usePrefix.tsx +21 -0
  57. package/src/hooks/useValueControl.ts +15 -0
  58. package/src/index.ts +3 -0
  59. package/src/styles.module.scss +55 -0
  60. package/src/types/allFields.ts +9 -0
  61. package/src/types/dateFields.ts +14 -0
  62. package/src/types/index.ts +2 -0
  63. package/src/utils/adaptiveField.tsx +19 -0
  64. package/src/utils/dateFields.ts +75 -0
  65. package/src/utils/getValidationState.ts +6 -0
@@ -0,0 +1,275 @@
1
+ import { ChangeEvent, FocusEventHandler, KeyboardEvent, RefObject, useCallback, useMemo, useRef } from 'react';
2
+
3
+ import { TimePickerProps } from '@snack-uikit/calendar';
4
+ import { isBrowser } from '@snack-uikit/utils';
5
+
6
+ import {
7
+ DEFAULT_LOCALE,
8
+ FocusSlot,
9
+ MASK,
10
+ MODES,
11
+ NO_SECONDS_MODE,
12
+ SLOT_ORDER,
13
+ SlotKey,
14
+ SLOTS,
15
+ SLOTS_PLACEHOLDER,
16
+ } from '../../constants';
17
+ import { Mode, TimeMode } from '../../types';
18
+ import { parseDate } from '../../utils/dateFields';
19
+ import { useDateFieldHelpersForMode } from './useDateFieldHelpersForMode';
20
+
21
+ type BaseProps = {
22
+ inputRef: RefObject<HTMLInputElement>;
23
+ readonly?: boolean;
24
+ locale?: Intl.Locale;
25
+ setIsOpen(v: boolean): void;
26
+ showSeconds?: boolean;
27
+ };
28
+
29
+ type UseDateFieldProps =
30
+ | ({
31
+ mode: Mode;
32
+ onChange?(value: Date | undefined): void;
33
+ } & BaseProps)
34
+ | ({
35
+ mode: TimeMode;
36
+ onChange?: TimePickerProps['onChangeValue'];
37
+ } & BaseProps);
38
+
39
+ export function useDateField({
40
+ inputRef,
41
+ onChange,
42
+ readonly,
43
+ locale = DEFAULT_LOCALE,
44
+ setIsOpen,
45
+ mode,
46
+ showSeconds,
47
+ }: UseDateFieldProps) {
48
+ const dateTimeMode = mode === MODES.DateTime && !showSeconds ? NO_SECONDS_MODE : mode;
49
+ const slotsInfo = SLOTS[dateTimeMode];
50
+ const mask = MASK[dateTimeMode][locale.baseName] || MASK[dateTimeMode][DEFAULT_LOCALE.baseName];
51
+ const slotsPlaceholder =
52
+ SLOTS_PLACEHOLDER[dateTimeMode][locale.baseName] || SLOTS_PLACEHOLDER[dateTimeMode][DEFAULT_LOCALE.baseName];
53
+ const slotOrder = SLOT_ORDER[dateTimeMode];
54
+ const {
55
+ getNextSlotKey,
56
+ getPrevSlotKey,
57
+ getSlotKeyFromIndex,
58
+ setFocus,
59
+ updateSlot,
60
+ getSlot,
61
+ isLikeDate,
62
+ isAllSelected,
63
+ tryToCompleteInput,
64
+ isValidInput,
65
+ } = useDateFieldHelpersForMode({ inputRef, mode: dateTimeMode });
66
+
67
+ const focusSlotKey = useMemo(() => slotOrder[0], [slotOrder]);
68
+ const focusSlotRef = useRef<FocusSlot>(focusSlotKey);
69
+
70
+ const setInputFocus = useCallback(
71
+ (focusSlot?: FocusSlot) => {
72
+ if (!inputRef.current || readonly) {
73
+ return;
74
+ }
75
+
76
+ if (isBrowser() && document.activeElement !== inputRef.current) {
77
+ focusSlotRef.current = focusSlot || focusSlotKey;
78
+ inputRef.current.focus();
79
+ return;
80
+ }
81
+
82
+ const focusSlotValue = focusSlot || focusSlotRef.current;
83
+
84
+ if (isLikeDate() && focusSlotValue === focusSlotKey) {
85
+ return;
86
+ }
87
+
88
+ if (!inputRef.current.value) {
89
+ inputRef.current.value = mask;
90
+ setFocus(focusSlotKey);
91
+ return;
92
+ }
93
+
94
+ if (focusSlot !== 'auto') {
95
+ setFocus(focusSlotValue);
96
+ return;
97
+ }
98
+
99
+ const slotKey = getSlotKeyFromIndex(inputRef.current.selectionStart);
100
+
101
+ if (slotKey) {
102
+ const { start, end } = slotsInfo[slotKey];
103
+ inputRef.current.setSelectionRange(start, end);
104
+ }
105
+ },
106
+ [inputRef, readonly, isLikeDate, focusSlotKey, getSlotKeyFromIndex, mask, setFocus, slotsInfo],
107
+ );
108
+
109
+ const handleClick = useCallback(() => {
110
+ setInputFocus('auto');
111
+ }, [setInputFocus]);
112
+
113
+ const handleChange: (value: string, e?: ChangeEvent<HTMLInputElement> | undefined) => void = () => {
114
+ onChange && isLikeDate() && onChange(parseDate(inputRef.current?.value || ''));
115
+ };
116
+
117
+ const checkInputAndGoNext = useCallback(
118
+ (slotKey: SlotKey | undefined) => {
119
+ if (slotKey === slotOrder[slotOrder.length - 1] && tryToCompleteInput()) {
120
+ return;
121
+ }
122
+
123
+ if (isValidInput()) {
124
+ setFocus(getNextSlotKey(slotKey));
125
+ return;
126
+ }
127
+
128
+ switch (slotKey) {
129
+ case SlotKey.Day:
130
+ updateSlot(SlotKey.Month, slotsPlaceholder?.[SlotKey.Month] ?? '');
131
+ setFocus(SlotKey.Month);
132
+ return;
133
+ case SlotKey.Year:
134
+ case SlotKey.Month:
135
+ updateSlot(SlotKey.Day, slotsPlaceholder?.[SlotKey.Day] ?? '');
136
+ setFocus(SlotKey.Day);
137
+ return;
138
+ default:
139
+ setFocus(getNextSlotKey(slotKey));
140
+ }
141
+ },
142
+ [slotOrder, tryToCompleteInput, isValidInput, setFocus, getNextSlotKey, updateSlot, slotsPlaceholder],
143
+ );
144
+
145
+ const handleKeyDown = useCallback(
146
+ (e: KeyboardEvent<HTMLInputElement>) => {
147
+ if (inputRef.current && !readonly) {
148
+ if (e.key !== 'Tab') {
149
+ e.preventDefault();
150
+ e.stopPropagation();
151
+ }
152
+
153
+ if (e.key !== 'ArrowDown') {
154
+ setIsOpen(false);
155
+ }
156
+
157
+ if (e.key === 'Escape') {
158
+ inputRef.current.blur();
159
+ return;
160
+ }
161
+
162
+ if (e.key === 'Enter') {
163
+ tryToCompleteInput();
164
+ }
165
+
166
+ const clickIndex = inputRef.current.selectionStart;
167
+ const slotKey = getSlotKeyFromIndex(clickIndex);
168
+
169
+ if (slotKey) {
170
+ const value = getSlot(slotKey);
171
+ const { max } = slotsInfo[slotKey];
172
+
173
+ const numberValue = Number(value) || 0;
174
+
175
+ if (e.key === 'ArrowRight') {
176
+ if (isAllSelected() || slotKey === slotOrder[slotOrder.length - 1]) {
177
+ inputRef.current.selectionStart = inputRef.current.value.length;
178
+ return;
179
+ }
180
+ setFocus(getNextSlotKey(slotKey));
181
+ return;
182
+ }
183
+
184
+ if (e.key === 'ArrowLeft') {
185
+ setFocus(getPrevSlotKey(slotKey));
186
+ return;
187
+ }
188
+
189
+ if (e.key === 'Backspace') {
190
+ if (isAllSelected()) {
191
+ inputRef.current.value = mask;
192
+ setFocus(focusSlotKey);
193
+ } else {
194
+ updateSlot(slotKey, slotsPlaceholder[slotKey] ?? '');
195
+ }
196
+ }
197
+
198
+ if (/^\d+$/.test(e.key)) {
199
+ const slotValue = parseInt(numberValue.toString() + e.key, 10) || 0;
200
+
201
+ const valueLength = slotValue.toString().length;
202
+ const maxLength = max.toString().length;
203
+
204
+ if (valueLength < maxLength) {
205
+ slotValue && updateSlot(slotKey, slotValue);
206
+
207
+ if (slotValue * 10 > max) {
208
+ checkInputAndGoNext(slotKey);
209
+ }
210
+ } else if (valueLength > maxLength) {
211
+ if (Number(e.key) * 10 > max) {
212
+ updateSlot(slotKey, e.key);
213
+ checkInputAndGoNext(slotKey);
214
+ } else {
215
+ Number(e.key) && updateSlot(slotKey, e.key);
216
+ }
217
+ } else {
218
+ if (slotValue <= max) {
219
+ updateSlot(slotKey, slotValue);
220
+ checkInputAndGoNext(slotKey);
221
+ } else {
222
+ if (Number(e.key) * 10 > max) {
223
+ updateSlot(slotKey, e.key);
224
+ checkInputAndGoNext(slotKey);
225
+ } else {
226
+ Number(e.key) && updateSlot(slotKey, e.key);
227
+ }
228
+ }
229
+ }
230
+ }
231
+ const newDate = parseDate(isLikeDate() ? inputRef.current.value : '');
232
+ onChange?.(newDate);
233
+ }
234
+ }
235
+ },
236
+ [
237
+ inputRef,
238
+ readonly,
239
+ getSlotKeyFromIndex,
240
+ setIsOpen,
241
+ tryToCompleteInput,
242
+ getSlot,
243
+ slotsInfo,
244
+ isLikeDate,
245
+ onChange,
246
+ isAllSelected,
247
+ slotOrder,
248
+ setFocus,
249
+ getNextSlotKey,
250
+ getPrevSlotKey,
251
+ mask,
252
+ focusSlotKey,
253
+ updateSlot,
254
+ slotsPlaceholder,
255
+ checkInputAndGoNext,
256
+ ],
257
+ );
258
+
259
+ const handleBlur: FocusEventHandler<HTMLInputElement> = useCallback(() => {
260
+ if (!readonly && inputRef.current?.value === mask) {
261
+ inputRef.current.value = '';
262
+ }
263
+ focusSlotRef.current = focusSlotKey;
264
+ }, [inputRef, mask, readonly, focusSlotKey]);
265
+
266
+ return {
267
+ handleKeyDown,
268
+ handleChange,
269
+ handleClick,
270
+ setInputFocus,
271
+ value: inputRef.current?.value,
272
+ mask,
273
+ handleBlur,
274
+ };
275
+ }
@@ -0,0 +1,145 @@
1
+ import { RefObject, useCallback, useMemo } from 'react';
2
+
3
+ import { MODES, NO_SECONDS_MODE, SlotKey, SLOTS, TIME_MODES } from '../../constants';
4
+ import { Mode, NoSecondsMode, TimeMode } from '../../types';
5
+ import { getNextSlotKeyHandler, getPrevSlotKeyHandler, getSlotKeyFromIndexHandler } from '../../utils/dateFields';
6
+
7
+ export function useDateFieldHelpersForMode({
8
+ inputRef,
9
+ mode,
10
+ }: {
11
+ inputRef: RefObject<HTMLInputElement>;
12
+ mode: Mode | TimeMode | NoSecondsMode;
13
+ }) {
14
+ const setFocus = useCallback(
15
+ (slotKey: string) => {
16
+ if (inputRef.current) {
17
+ const { start, end } = SLOTS[mode][slotKey];
18
+
19
+ inputRef.current.setSelectionRange(start, end);
20
+ }
21
+ },
22
+ [mode, inputRef],
23
+ );
24
+
25
+ const isAllSelected = useCallback(
26
+ () => inputRef.current?.value.length === inputRef.current?.selectionEnd && inputRef.current?.selectionStart === 0,
27
+ [inputRef],
28
+ );
29
+
30
+ const getSlot = useCallback(
31
+ (slotKey: string) => {
32
+ if (inputRef.current) {
33
+ return inputRef.current.value.slice(SLOTS[mode][slotKey].start, SLOTS[mode][slotKey].end);
34
+ }
35
+
36
+ return '';
37
+ },
38
+ [mode, inputRef],
39
+ );
40
+
41
+ const isLikeDate = useCallback(() => {
42
+ if (inputRef.current) {
43
+ return Object.keys(SLOTS[mode]).every(slotKey => getSlot(slotKey) && Number.isInteger(Number(getSlot(slotKey))));
44
+ }
45
+ return false;
46
+ }, [mode, getSlot, inputRef]);
47
+
48
+ const isValidInput = useCallback((): boolean => {
49
+ const isTimeMode = Object.values(TIME_MODES).includes(mode as TimeMode);
50
+
51
+ if (isTimeMode) {
52
+ return true;
53
+ }
54
+
55
+ const day = parseInt(getSlot(SlotKey.Day), 10);
56
+ const month = parseInt(getSlot(SlotKey.Month), 10);
57
+ const year = parseInt(getSlot(SlotKey.Year), 10);
58
+
59
+ if (!month || !day) {
60
+ return true;
61
+ }
62
+
63
+ const date = new Date(year || /* високосный год = */ 2020, month - 1, day);
64
+
65
+ return date.getDate() === day;
66
+ }, [getSlot, mode]);
67
+
68
+ const tryToCompleteInput = useCallback((): boolean => {
69
+ let isCompleted: boolean;
70
+
71
+ const parsedSlotsData = Object.keys(SLOTS[mode]).reduce(
72
+ (res, key) => {
73
+ const slotKey = key as SlotKey;
74
+ res[slotKey] = parseInt(getSlot(slotKey), 10);
75
+ return res;
76
+ },
77
+ {} as Record<SlotKey, number>,
78
+ );
79
+
80
+ const {
81
+ [SlotKey.Day]: day,
82
+ [SlotKey.Month]: month,
83
+ [SlotKey.Year]: year,
84
+ [SlotKey.Hours]: hours,
85
+ [SlotKey.Minutes]: minutes,
86
+ [SlotKey.Seconds]: seconds,
87
+ } = parsedSlotsData;
88
+
89
+ const yearSlotMeta = SLOTS[mode][SlotKey.Year];
90
+ const isDateCompleted = Boolean(day && month && year >= yearSlotMeta?.min && year <= yearSlotMeta?.max);
91
+ const isTimeCompleted = [
92
+ hours,
93
+ minutes,
94
+ ...(mode === MODES.DateTime || mode === TIME_MODES.FullTime ? [seconds] : []),
95
+ ].every(value => value !== undefined);
96
+
97
+ if (mode === MODES.DateTime || mode === NO_SECONDS_MODE) {
98
+ isCompleted = isDateCompleted && isTimeCompleted;
99
+ } else if (mode === TIME_MODES.FullTime || mode === TIME_MODES.NoSeconds) {
100
+ isCompleted = isTimeCompleted;
101
+ } else {
102
+ isCompleted = isDateCompleted;
103
+ }
104
+
105
+ if (isCompleted && inputRef.current) {
106
+ const lastPosition = inputRef.current?.value.length;
107
+ inputRef.current.selectionStart = lastPosition;
108
+ inputRef.current.selectionEnd = lastPosition;
109
+ }
110
+
111
+ return isCompleted;
112
+ }, [getSlot, inputRef, mode]);
113
+
114
+ const updateSlot = useCallback(
115
+ (slotKey: string, slotValue: number | string) => {
116
+ if (inputRef.current) {
117
+ const { start, end, max } = SLOTS[mode][slotKey];
118
+
119
+ inputRef.current.value =
120
+ inputRef.current.value.slice(0, start) +
121
+ slotValue.toString().padStart(max.toString().length, '0') +
122
+ inputRef.current.value.slice(end);
123
+ setFocus(slotKey);
124
+ }
125
+ },
126
+ [inputRef, setFocus, mode],
127
+ );
128
+
129
+ const getNextSlotKey = useMemo(() => getNextSlotKeyHandler(mode), [mode]);
130
+ const getPrevSlotKey = useMemo(() => getPrevSlotKeyHandler(mode), [mode]);
131
+ const getSlotKeyFromIndex = useMemo(() => getSlotKeyFromIndexHandler(mode), [mode]);
132
+
133
+ return {
134
+ isAllSelected,
135
+ isValidInput,
136
+ tryToCompleteInput,
137
+ getSlot,
138
+ updateSlot,
139
+ setFocus,
140
+ isLikeDate,
141
+ getNextSlotKey,
142
+ getPrevSlotKey,
143
+ getSlotKeyFromIndex,
144
+ };
145
+ }
@@ -0,0 +1,46 @@
1
+ import { FocusEvent, useCallback, useRef } from 'react';
2
+
3
+ type Options = {
4
+ onFocusByKeyboard?: (e: FocusEvent<HTMLInputElement>) => void;
5
+ onFocusByClick?: (e: FocusEvent<HTMLInputElement>) => void;
6
+ };
7
+
8
+ /**
9
+ * Позволяет разделить обработку получения фокуса в результате клика или с клавиатуры.
10
+ * Основано на том факте что событию фокуса в результате клика предшествует событие onMouseDown.
11
+ */
12
+ export function useFocusHandlers({ onFocusByKeyboard, onFocusByClick }: Options) {
13
+ const isFocused = useRef(false);
14
+ const isMouseDown = useRef(false);
15
+
16
+ const onFocus = useCallback(
17
+ (e: FocusEvent<HTMLInputElement>) => {
18
+ isFocused.current = true;
19
+
20
+ if (isMouseDown.current) {
21
+ onFocusByClick?.(e);
22
+ isMouseDown.current = false;
23
+ } else {
24
+ onFocusByKeyboard?.(e);
25
+ }
26
+ },
27
+ [isMouseDown, onFocusByClick, onFocusByKeyboard],
28
+ );
29
+
30
+ const onBlur = useCallback(() => {
31
+ isFocused.current = false;
32
+ isMouseDown.current = false;
33
+ }, []);
34
+
35
+ const onMouseDown = useCallback(() => {
36
+ if (!isFocused.current) {
37
+ isMouseDown.current = true;
38
+ }
39
+ }, [isFocused]);
40
+
41
+ return {
42
+ onFocus,
43
+ onBlur,
44
+ onMouseDown,
45
+ };
46
+ }
@@ -0,0 +1,15 @@
1
+ import { useCallback } from 'react';
2
+
3
+ type Handler<T> = (e: T) => void;
4
+
5
+ /**
6
+ * Вызывает обработчики события по порядку
7
+ */
8
+ export function useHandlers<T>(handlers: (Handler<T> | undefined)[]) {
9
+ return useCallback(
10
+ (e: T) => {
11
+ handlers.forEach(handler => handler?.(e));
12
+ },
13
+ [handlers],
14
+ );
15
+ }
@@ -0,0 +1,5 @@
1
+ export * from './dateHandlers';
2
+ export * from './useCopyButton';
3
+ export * from './usePostfix';
4
+ export * from './usePrefix';
5
+ export * from './useValueControl';
@@ -0,0 +1,17 @@
1
+ @use '@sbercloud/figma-tokens-cloud-platform/build/scss/components/styles-tokens-fields';
2
+
3
+ .prefix {
4
+ color: styles-tokens-fields.$sys-neutral-text-support;
5
+
6
+ &[data-disabled] {
7
+ opacity: styles-tokens-fields.$opacity-a056;
8
+ }
9
+ }
10
+
11
+ .postfix {
12
+ color: styles-tokens-fields.$sys-neutral-text-support;
13
+
14
+ &[data-disabled] {
15
+ opacity: styles-tokens-fields.$opacity-a056;
16
+ }
17
+ }
@@ -0,0 +1,47 @@
1
+ import { RefObject, useMemo } from 'react';
2
+
3
+ import { BUTTON_SIZE_MAP, ButtonProps, Size } from '@snack-uikit/input-private';
4
+
5
+ import { ButtonCopyValue } from '../helperComponents';
6
+ import { AsyncValueRequest } from '../types';
7
+
8
+ type UseCopyButtonProps = {
9
+ copyButtonRef: RefObject<HTMLButtonElement>;
10
+ showCopyButton: boolean;
11
+ valueToCopy: string;
12
+ size: Size;
13
+ onValueRequest?(): AsyncValueRequest;
14
+ disabled?: boolean;
15
+ prefix?: string;
16
+ postfix?: string;
17
+ };
18
+
19
+ export function useCopyButton({
20
+ copyButtonRef,
21
+ showCopyButton,
22
+ size,
23
+ valueToCopy,
24
+ onValueRequest,
25
+ disabled,
26
+ prefix = '',
27
+ postfix = '',
28
+ }: UseCopyButtonProps): ButtonProps {
29
+ return useMemo(
30
+ () => ({
31
+ id: 'copy',
32
+ active: true,
33
+ ref: copyButtonRef,
34
+ show: showCopyButton,
35
+ render: props => (
36
+ <ButtonCopyValue
37
+ {...props}
38
+ size={BUTTON_SIZE_MAP[size]}
39
+ valueToCopy={(prefix ?? '') + valueToCopy + (postfix ?? '')}
40
+ onValueRequest={onValueRequest}
41
+ disabled={disabled}
42
+ />
43
+ ),
44
+ }),
45
+ [copyButtonRef, disabled, onValueRequest, showCopyButton, size, valueToCopy, prefix, postfix],
46
+ );
47
+ }
@@ -0,0 +1,21 @@
1
+ import { ReactNode, useMemo } from 'react';
2
+
3
+ import { InactiveItem } from '@snack-uikit/input-private';
4
+
5
+ import styles from './styles.module.scss';
6
+
7
+ export function usePostfix({ postfix, disabled }: { postfix?: ReactNode; disabled?: boolean }): InactiveItem {
8
+ return useMemo(
9
+ () => ({
10
+ id: 'postfix',
11
+ active: false,
12
+ show: Boolean(postfix),
13
+ render: props => (
14
+ <div {...props} className={styles.postfix} data-test-id='field-postfix' data-disabled={disabled || undefined}>
15
+ {postfix}
16
+ </div>
17
+ ),
18
+ }),
19
+ [disabled, postfix],
20
+ );
21
+ }
@@ -0,0 +1,21 @@
1
+ import { ReactNode, useMemo } from 'react';
2
+
3
+ import { InactiveItem } from '@snack-uikit/input-private';
4
+
5
+ import styles from './styles.module.scss';
6
+
7
+ export function usePrefix({ prefix, disabled }: { prefix?: ReactNode; disabled?: boolean }): InactiveItem {
8
+ return useMemo(
9
+ () => ({
10
+ id: 'prefix',
11
+ active: false,
12
+ show: Boolean(prefix),
13
+ render: props => (
14
+ <div {...props} data-test-id='field-prefix' className={styles.prefix} data-disabled={disabled || undefined}>
15
+ {prefix}
16
+ </div>
17
+ ),
18
+ }),
19
+ [disabled, prefix],
20
+ );
21
+ }
@@ -0,0 +1,15 @@
1
+ import { useUncontrolledProp } from 'uncontrollable';
2
+
3
+ type UseValueControl<TValue> = {
4
+ value?: TValue;
5
+ onChange?(value: TValue): void;
6
+ defaultValue?: TValue;
7
+ };
8
+
9
+ export function useValueControl<TValue>({ value, onChange, defaultValue }: UseValueControl<TValue>) {
10
+ return useUncontrolledProp<TValue>(value, defaultValue, (newValue: TValue) => {
11
+ const newState = typeof newValue === 'function' ? newValue(value) : newValue;
12
+
13
+ onChange?.(newState);
14
+ });
15
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './components';
2
+ export * from './utils/adaptiveField';
3
+ export { setNonce } from '@snack-uikit/fields';
@@ -0,0 +1,55 @@
1
+ @use "@sbercloud/figma-tokens-cloud-platform/build/scss/components/styles-tokens-element";
2
+ @use "@sbercloud/figma-tokens-cloud-platform/build/scss/components/styles-tokens-fields";
3
+
4
+ $sizes: 's', 'm';
5
+ $icon-sizes: (
6
+ 's': styles-tokens-element.$icon-xs,
7
+ 'm': styles-tokens-element.$icon-s
8
+ );
9
+
10
+ @mixin button-styles {
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+
15
+ margin: 0;
16
+ padding: 0;
17
+
18
+ color: styles-tokens-fields.$sys-neutral-text-light;
19
+
20
+ background-color: transparent;
21
+ border: none;
22
+
23
+ @each $size in $sizes {
24
+ &[data-size='#{$size}'] {
25
+ @include styles-tokens-fields.composite-var(styles-tokens-fields.$fields, 'buttons', $size);
26
+
27
+ svg {
28
+ width: styles-tokens-fields.simple-var($icon-sizes, $size) !important; /* stylelint-disable-line declaration-no-important */
29
+ height: styles-tokens-fields.simple-var($icon-sizes, $size) !important; /* stylelint-disable-line declaration-no-important */
30
+ }
31
+ }
32
+ }
33
+
34
+ &:hover {
35
+ cursor: pointer;
36
+ color: styles-tokens-fields.$sys-neutral-text-support;
37
+ }
38
+
39
+ &:focus-visible {
40
+ @include styles-tokens-fields.outline-var(styles-tokens-element.$container-focused-s);
41
+
42
+ color: styles-tokens-fields.$sys-neutral-text-support;
43
+ outline-color: styles-tokens-fields.$sys-available-complementary;
44
+ outline-offset: styles-tokens-fields.$spacing-state-focus-offset;
45
+ }
46
+
47
+ &:active {
48
+ color: styles-tokens-fields.$sys-neutral-text-main;
49
+ }
50
+
51
+ &[data-disabled] {
52
+ cursor: not-allowed;
53
+ color: styles-tokens-fields.$sys-neutral-text-disabled;
54
+ }
55
+ }
@@ -0,0 +1,9 @@
1
+ import { ValueOf } from '@snack-uikit/utils';
2
+
3
+ import { CONTAINER_VARIANT, VALIDATION_STATE } from '../constants';
4
+
5
+ export type AsyncValueRequest = Promise<{ success: boolean; value?: string }>;
6
+
7
+ export type ValidationState = ValueOf<typeof VALIDATION_STATE>;
8
+
9
+ export type ContainerVariant = ValueOf<typeof CONTAINER_VARIANT>;
@@ -0,0 +1,14 @@
1
+ import { ValueOf } from '@snack-uikit/utils';
2
+
3
+ import { MODES, NO_SECONDS_MODE, TIME_MODES } from '../constants';
4
+
5
+ export type Slot = {
6
+ start: number;
7
+ end: number;
8
+ max: number;
9
+ min: number;
10
+ };
11
+
12
+ export type Mode = ValueOf<typeof MODES>;
13
+ export type TimeMode = ValueOf<typeof TIME_MODES>;
14
+ export type NoSecondsMode = typeof NO_SECONDS_MODE;