@snack-uikit/fields 0.31.0 → 0.32.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 (134) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +179 -147
  3. package/dist/cjs/components/FieldDate/FieldDate.d.ts +15 -24
  4. package/dist/cjs/components/FieldDate/FieldDate.js +53 -41
  5. package/dist/cjs/components/FieldDate/index.d.ts +0 -1
  6. package/dist/cjs/components/FieldDate/index.js +1 -9
  7. package/dist/cjs/components/FieldSecure/FieldSecure.d.ts +1 -1
  8. package/dist/cjs/components/FieldSelect/hooks.d.ts +2 -2
  9. package/dist/cjs/components/FieldSelect/hooks.js +7 -3
  10. package/dist/cjs/components/FieldSlider/FieldSlider.d.ts +1 -1
  11. package/dist/cjs/components/FieldText/FieldText.d.ts +1 -1
  12. package/dist/cjs/components/FieldTextArea/FieldTextArea.d.ts +1 -1
  13. package/dist/cjs/components/FieldTime/FieldTime.d.ts +30 -0
  14. package/dist/cjs/components/FieldTime/FieldTime.js +298 -0
  15. package/dist/cjs/components/FieldTime/index.d.ts +1 -0
  16. package/dist/cjs/components/{FieldDate/hooks → FieldTime}/index.js +1 -1
  17. package/dist/cjs/components/FieldTime/styles.module.css +27 -0
  18. package/dist/cjs/components/index.d.ts +6 -5
  19. package/dist/cjs/components/index.js +6 -5
  20. package/dist/cjs/constants/dateFields.d.ts +24 -0
  21. package/dist/cjs/constants/dateFields.js +152 -0
  22. package/dist/cjs/constants/index.d.ts +2 -0
  23. package/dist/cjs/constants/index.js +26 -0
  24. package/dist/cjs/hooks/dateHandlers/index.d.ts +3 -0
  25. package/dist/cjs/hooks/dateHandlers/index.js +27 -0
  26. package/dist/cjs/{components/FieldDate/hooks → hooks/dateHandlers}/useDateField.d.ts +13 -5
  27. package/dist/cjs/{components/FieldDate/hooks → hooks/dateHandlers}/useDateField.js +49 -34
  28. package/dist/cjs/hooks/dateHandlers/useDateFieldHelpersForMode.d.ts +18 -0
  29. package/dist/cjs/hooks/dateHandlers/useDateFieldHelpersForMode.js +113 -0
  30. package/dist/cjs/hooks/index.d.ts +1 -0
  31. package/dist/cjs/hooks/index.js +1 -0
  32. package/dist/cjs/hooks/useCopyButton.js +1 -1
  33. package/dist/cjs/{types.d.ts → types/allFields.d.ts} +1 -1
  34. package/dist/cjs/types/dateFields.d.ts +11 -0
  35. package/dist/cjs/types/index.d.ts +2 -0
  36. package/dist/cjs/types/index.js +26 -0
  37. package/dist/cjs/utils/dateFields.d.ts +10 -0
  38. package/dist/cjs/utils/dateFields.js +71 -0
  39. package/dist/esm/components/FieldDate/FieldDate.d.ts +15 -24
  40. package/dist/esm/components/FieldDate/FieldDate.js +39 -31
  41. package/dist/esm/components/FieldDate/index.d.ts +0 -1
  42. package/dist/esm/components/FieldDate/index.js +0 -1
  43. package/dist/esm/components/FieldSecure/FieldSecure.d.ts +1 -1
  44. package/dist/esm/components/FieldSelect/hooks.d.ts +2 -2
  45. package/dist/esm/components/FieldSelect/hooks.js +9 -3
  46. package/dist/esm/components/FieldSlider/FieldSlider.d.ts +1 -1
  47. package/dist/esm/components/FieldText/FieldText.d.ts +1 -1
  48. package/dist/esm/components/FieldTextArea/FieldTextArea.d.ts +1 -1
  49. package/dist/esm/components/FieldTime/FieldTime.d.ts +30 -0
  50. package/dist/esm/components/FieldTime/FieldTime.js +161 -0
  51. package/dist/esm/components/FieldTime/index.d.ts +1 -0
  52. package/dist/esm/components/FieldTime/index.js +1 -0
  53. package/dist/esm/components/FieldTime/styles.module.css +27 -0
  54. package/dist/esm/components/index.d.ts +6 -5
  55. package/dist/esm/components/index.js +6 -5
  56. package/dist/esm/constants/dateFields.d.ts +24 -0
  57. package/dist/esm/constants/dateFields.js +103 -0
  58. package/dist/esm/constants/index.d.ts +2 -0
  59. package/dist/esm/constants/index.js +2 -0
  60. package/dist/esm/hooks/dateHandlers/index.d.ts +3 -0
  61. package/dist/esm/hooks/dateHandlers/index.js +3 -0
  62. package/dist/esm/{components/FieldDate/hooks → hooks/dateHandlers}/useDateField.d.ts +13 -5
  63. package/dist/esm/{components/FieldDate/hooks → hooks/dateHandlers}/useDateField.js +48 -35
  64. package/dist/esm/hooks/dateHandlers/useDateFieldHelpersForMode.d.ts +18 -0
  65. package/dist/esm/hooks/dateHandlers/useDateFieldHelpersForMode.js +95 -0
  66. package/dist/esm/hooks/index.d.ts +1 -0
  67. package/dist/esm/hooks/index.js +1 -0
  68. package/dist/esm/hooks/useCopyButton.js +1 -1
  69. package/dist/esm/{types.d.ts → types/allFields.d.ts} +1 -1
  70. package/dist/esm/types/dateFields.d.ts +11 -0
  71. package/dist/esm/types/index.d.ts +2 -0
  72. package/dist/esm/types/index.js +2 -0
  73. package/dist/esm/utils/dateFields.d.ts +10 -0
  74. package/dist/esm/utils/dateFields.js +59 -0
  75. package/package.json +10 -10
  76. package/src/components/FieldDate/FieldDate.tsx +72 -52
  77. package/src/components/FieldDate/index.ts +0 -1
  78. package/src/components/FieldSelect/hooks.ts +15 -3
  79. package/src/components/FieldTime/FieldTime.tsx +350 -0
  80. package/src/components/FieldTime/index.ts +1 -0
  81. package/src/components/FieldTime/styles.module.scss +41 -0
  82. package/src/components/index.ts +6 -5
  83. package/src/constants/dateFields.ts +127 -0
  84. package/src/constants/index.ts +2 -0
  85. package/src/hooks/dateHandlers/index.ts +3 -0
  86. package/src/{components/FieldDate/hooks → hooks/dateHandlers}/useDateField.ts +93 -47
  87. package/src/hooks/dateHandlers/useDateFieldHelpersForMode.ts +145 -0
  88. package/src/hooks/index.ts +1 -0
  89. package/src/hooks/useCopyButton.tsx +1 -1
  90. package/src/{types.ts → types/allFields.ts} +1 -1
  91. package/src/types/dateFields.ts +14 -0
  92. package/src/types/index.ts +2 -0
  93. package/src/utils/dateFields.ts +75 -0
  94. package/dist/cjs/components/FieldDate/constants.d.ts +0 -10
  95. package/dist/cjs/components/FieldDate/constants.js +0 -49
  96. package/dist/cjs/components/FieldDate/hooks/index.d.ts +0 -1
  97. package/dist/cjs/components/FieldDate/hooks/useDateFieldHelpers.d.ts +0 -10
  98. package/dist/cjs/components/FieldDate/hooks/useDateFieldHelpers.js +0 -82
  99. package/dist/cjs/components/FieldDate/types.d.ts +0 -6
  100. package/dist/cjs/components/FieldDate/utils.d.ts +0 -9
  101. package/dist/cjs/components/FieldDate/utils.js +0 -56
  102. package/dist/esm/components/FieldDate/constants.d.ts +0 -10
  103. package/dist/esm/components/FieldDate/constants.js +0 -28
  104. package/dist/esm/components/FieldDate/hooks/index.d.ts +0 -1
  105. package/dist/esm/components/FieldDate/hooks/index.js +0 -1
  106. package/dist/esm/components/FieldDate/hooks/useDateFieldHelpers.d.ts +0 -10
  107. package/dist/esm/components/FieldDate/hooks/useDateFieldHelpers.js +0 -66
  108. package/dist/esm/components/FieldDate/types.d.ts +0 -6
  109. package/dist/esm/components/FieldDate/utils.d.ts +0 -9
  110. package/dist/esm/components/FieldDate/utils.js +0 -43
  111. package/src/components/FieldDate/constants.ts +0 -33
  112. package/src/components/FieldDate/hooks/index.ts +0 -1
  113. package/src/components/FieldDate/hooks/useDateFieldHelpers.ts +0 -96
  114. package/src/components/FieldDate/types.ts +0 -6
  115. package/src/components/FieldDate/utils.ts +0 -49
  116. /package/dist/cjs/{constants.d.ts → constants/allFields.d.ts} +0 -0
  117. /package/dist/cjs/{constants.js → constants/allFields.js} +0 -0
  118. /package/dist/cjs/{components/FieldDate/hooks → hooks/dateHandlers}/useFocusHandlers.d.ts +0 -0
  119. /package/dist/cjs/{components/FieldDate/hooks → hooks/dateHandlers}/useFocusHandlers.js +0 -0
  120. /package/dist/cjs/{components/FieldDate/hooks → hooks/dateHandlers}/useHandlers.d.ts +0 -0
  121. /package/dist/cjs/{components/FieldDate/hooks → hooks/dateHandlers}/useHandlers.js +0 -0
  122. /package/dist/cjs/{components/FieldDate/types.js → types/allFields.js} +0 -0
  123. /package/dist/cjs/{types.js → types/dateFields.js} +0 -0
  124. /package/dist/esm/{constants.d.ts → constants/allFields.d.ts} +0 -0
  125. /package/dist/esm/{constants.js → constants/allFields.js} +0 -0
  126. /package/dist/esm/{components/FieldDate/hooks → hooks/dateHandlers}/useFocusHandlers.d.ts +0 -0
  127. /package/dist/esm/{components/FieldDate/hooks → hooks/dateHandlers}/useFocusHandlers.js +0 -0
  128. /package/dist/esm/{components/FieldDate/hooks → hooks/dateHandlers}/useHandlers.d.ts +0 -0
  129. /package/dist/esm/{components/FieldDate/hooks → hooks/dateHandlers}/useHandlers.js +0 -0
  130. /package/dist/esm/{components/FieldDate/types.js → types/allFields.js} +0 -0
  131. /package/dist/esm/{types.js → types/dateFields.js} +0 -0
  132. /package/src/{constants.ts → constants/allFields.ts} +0 -0
  133. /package/src/{components/FieldDate/hooks → hooks/dateHandlers}/useFocusHandlers.ts +0 -0
  134. /package/src/{components/FieldDate/hooks → hooks/dateHandlers}/useHandlers.ts +0 -0
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "access": "public"
5
5
  },
6
6
  "title": "Fields",
7
- "version": "0.31.0",
7
+ "version": "0.32.0",
8
8
  "sideEffects": [
9
9
  "*.css",
10
10
  "*.woff",
@@ -36,18 +36,18 @@
36
36
  "license": "Apache-2.0",
37
37
  "scripts": {},
38
38
  "dependencies": {
39
- "@snack-uikit/button": "0.19.0",
40
- "@snack-uikit/calendar": "0.10.0",
41
- "@snack-uikit/color-picker": "0.3.0",
39
+ "@snack-uikit/button": "0.19.1",
40
+ "@snack-uikit/calendar": "0.11.0",
41
+ "@snack-uikit/color-picker": "0.3.1",
42
42
  "@snack-uikit/divider": "3.2.0",
43
43
  "@snack-uikit/dropdown": "0.4.0",
44
44
  "@snack-uikit/icons": "0.24.0",
45
- "@snack-uikit/input-private": "4.2.0",
46
- "@snack-uikit/list": "0.20.0",
47
- "@snack-uikit/scroll": "0.8.0",
45
+ "@snack-uikit/input-private": "4.2.1",
46
+ "@snack-uikit/list": "0.21.0",
47
+ "@snack-uikit/scroll": "0.9.0",
48
48
  "@snack-uikit/skeleton": "0.5.0",
49
- "@snack-uikit/slider": "0.3.0",
50
- "@snack-uikit/tag": "0.11.0",
49
+ "@snack-uikit/slider": "0.3.1",
50
+ "@snack-uikit/tag": "0.11.1",
51
51
  "@snack-uikit/tooltip": "0.15.0",
52
52
  "@snack-uikit/truncate-string": "0.6.0",
53
53
  "@snack-uikit/utils": "3.5.0",
@@ -65,5 +65,5 @@
65
65
  "peerDependencies": {
66
66
  "@snack-uikit/locale": "*"
67
67
  },
68
- "gitHead": "8499829efa0c118b704de17411ae2328a024adb5"
68
+ "gitHead": "b7163c6f939105eb34cabec64c9e983ac7958c26"
69
69
  }
@@ -1,15 +1,5 @@
1
1
  import mergeRefs from 'merge-refs';
2
- import {
3
- FocusEvent,
4
- forwardRef,
5
- KeyboardEvent,
6
- MouseEvent,
7
- useCallback,
8
- useEffect,
9
- useMemo,
10
- useRef,
11
- useState,
12
- } from 'react';
2
+ import { FocusEvent, forwardRef, KeyboardEvent, MouseEvent, useCallback, useEffect, useMemo, useRef } from 'react';
13
3
  import { useUncontrolledProp } from 'uncontrollable';
14
4
 
15
5
  import { Calendar, CalendarProps } from '@snack-uikit/calendar';
@@ -22,25 +12,20 @@ import {
22
12
  InputPrivateProps,
23
13
  runAfterRerender,
24
14
  SIZE,
25
- Size,
26
15
  useButtonNavigation,
27
16
  useClearButton,
28
17
  } from '@snack-uikit/input-private';
29
18
  import { extractSupportProps, WithSupportProps } from '@snack-uikit/utils';
30
19
 
31
- import { CONTAINER_VARIANT, VALIDATION_STATE } from '../../constants';
20
+ import { CONTAINER_VARIANT, DEFAULT_LOCALE, MODES, SlotKey, VALIDATION_STATE } from '../../constants';
32
21
  import { FieldContainerPrivate } from '../../helperComponents';
33
- import { useCopyButton } from '../../hooks';
22
+ import { useCopyButton, useDateField, useFocusHandlers, useHandlers } from '../../hooks';
23
+ import { Mode } from '../../types';
34
24
  import { getValidationState } from '../../utils/getValidationState';
35
25
  import { FieldDecorator, FieldDecoratorProps } from '../FieldDecorator';
36
- import { DEFAULT_LOCALE, SlotKey } from './constants';
37
- import { useDateField } from './hooks';
38
- import { useFocusHandlers } from './hooks/useFocusHandlers';
39
- import { useHandlers } from './hooks/useHandlers';
40
26
  import styles from './styles.module.scss';
41
- import { parseDate } from './utils';
42
27
 
43
- type InputProps = Pick<InputPrivateProps, 'id' | 'name' | 'value' | 'disabled' | 'readonly' | 'onFocus' | 'onBlur'>;
28
+ type InputProps = Pick<InputPrivateProps, 'id' | 'name' | 'disabled' | 'readonly' | 'onFocus' | 'onBlur'>;
44
29
 
45
30
  type WrapperProps = Pick<
46
31
  FieldDecoratorProps,
@@ -57,13 +42,20 @@ type WrapperProps = Pick<
57
42
  | 'error'
58
43
  >;
59
44
 
45
+ type FieldDateWithSeconds = {
46
+ mode: typeof MODES.DateTime;
47
+ showSeconds?: boolean;
48
+ };
49
+
60
50
  type FieldDateOwnProps = {
61
51
  /** Открыт date-picker */
62
52
  open?: boolean;
63
53
  /** Колбек открытия пикера */
64
54
  onOpenChange?(value: boolean): void;
55
+ /** Значение поля */
56
+ value?: Date;
65
57
  /** Колбек смены значения */
66
- onChange?(value: string): void;
58
+ onChange?(value: Date | undefined): void;
67
59
  /** Отображение кнопки копирования */
68
60
  showCopyButton?: boolean;
69
61
  /**
@@ -71,18 +63,17 @@ type FieldDateOwnProps = {
71
63
  * @default true
72
64
  */
73
65
  showClearButton?: boolean;
74
- /** Текущая локаль календаря */
75
- locale?: Intl.Locale;
76
- } & Pick<CalendarProps, 'buildCellProps'>;
66
+ mode: Mode;
67
+ } & Pick<CalendarProps, 'buildCellProps'> &
68
+ (
69
+ | {
70
+ mode: typeof MODES.Date;
71
+ }
72
+ | FieldDateWithSeconds
73
+ );
77
74
 
78
75
  export type FieldDateProps = WithSupportProps<FieldDateOwnProps & InputProps & WrapperProps>;
79
76
 
80
- const CALENDAR_SIZE_MAP: Record<Size, CalendarProps['size']> = {
81
- [SIZE.S]: 's',
82
- [SIZE.M]: 'm',
83
- [SIZE.L]: 'm',
84
- };
85
-
86
77
  export const FieldDate = forwardRef<HTMLInputElement, FieldDateProps>(
87
78
  (
88
79
  {
@@ -108,15 +99,14 @@ export const FieldDate = forwardRef<HTMLInputElement, FieldDateProps>(
108
99
  showHintIcon,
109
100
  size = SIZE.S,
110
101
  validationState = VALIDATION_STATE.Default,
111
- locale = DEFAULT_LOCALE,
112
102
  buildCellProps,
113
103
  error,
104
+ mode,
114
105
  ...rest
115
106
  },
116
107
  ref,
117
108
  ) => {
118
109
  const [isOpen, setIsOpen] = useUncontrolledProp(open, false, onOpenChange);
119
- const [pickerAutofocus, setPickerAutofocus] = useState(false);
120
110
 
121
111
  const localRef = useRef<HTMLInputElement>(null);
122
112
  const clearButtonRef = useRef<HTMLButtonElement>(null);
@@ -126,20 +116,23 @@ export const FieldDate = forwardRef<HTMLInputElement, FieldDateProps>(
126
116
  const showAdditionalButton = Boolean(valueProp && !disabled);
127
117
  const showClearButton = showClearButtonProp && showAdditionalButton && !readonly;
128
118
  const showCopyButton = showCopyButtonProp && showAdditionalButton && readonly;
119
+ const showSeconds = mode === 'date-time' ? ((rest as FieldDateWithSeconds).showSeconds ?? true) : undefined;
129
120
  const fieldValidationState = getValidationState({ validationState, error });
130
121
 
122
+ const navigationStartRef: CalendarProps['navigationStartRef'] = useRef(null);
123
+
131
124
  const checkForLeavingFocus = useCallback(
132
125
  <T extends HTMLInputElement | HTMLButtonElement>(event: KeyboardEvent<T>) => {
133
126
  if (event.key === 'ArrowDown') {
134
- setPickerAutofocus(true);
135
127
  setIsOpen(true);
128
+ setTimeout(() => navigationStartRef.current?.focus(), 0);
136
129
  }
137
130
  },
138
131
  [setIsOpen],
139
132
  );
140
133
 
141
134
  const handleClear = useCallback(() => {
142
- onChange && onChange('');
135
+ onChange && onChange(undefined);
143
136
  if (localRef.current?.value) {
144
137
  localRef.current.value = '';
145
138
  }
@@ -153,8 +146,29 @@ export const FieldDate = forwardRef<HTMLInputElement, FieldDateProps>(
153
146
  }
154
147
  }, [onChange, required, setIsOpen]);
155
148
 
149
+ const getStringDateValue = useCallback(
150
+ (date: Date | undefined) => {
151
+ if (!date) return '';
152
+
153
+ if (mode === 'date') {
154
+ return date.toLocaleDateString(DEFAULT_LOCALE);
155
+ }
156
+
157
+ return date.toLocaleString(DEFAULT_LOCALE, {
158
+ year: 'numeric',
159
+ month: 'numeric',
160
+ day: 'numeric',
161
+ hour: '2-digit',
162
+ minute: '2-digit',
163
+ second: showSeconds ? '2-digit' : undefined,
164
+ });
165
+ },
166
+ [mode, showSeconds],
167
+ );
168
+
169
+ const valueToCopy = getStringDateValue(valueProp);
156
170
  const clearButtonSettings = useClearButton({ clearButtonRef, showClearButton, size, onClear: handleClear });
157
- const copyButtonSettings = useCopyButton({ copyButtonRef, showCopyButton, size, valueToCopy: valueProp || '' });
171
+ const copyButtonSettings = useCopyButton({ copyButtonRef, showCopyButton, size, valueToCopy });
158
172
  const calendarIcon: ButtonProps = useMemo(
159
173
  () => ({
160
174
  active: false,
@@ -184,11 +198,16 @@ export const FieldDate = forwardRef<HTMLInputElement, FieldDateProps>(
184
198
  inputRef: localRef,
185
199
  onChange,
186
200
  readonly,
187
- locale,
201
+ locale: DEFAULT_LOCALE,
188
202
  setIsOpen,
203
+ mode,
204
+ showSeconds,
189
205
  });
190
206
 
191
- const setInputFocusFromButtons = useCallback(() => setInputFocus(SlotKey.Year), [setInputFocus]);
207
+ const setInputFocusFromButtons = useCallback(
208
+ () => setInputFocus(mode === 'date' ? SlotKey.Year : SlotKey.Seconds),
209
+ [mode, setInputFocus],
210
+ );
192
211
 
193
212
  const {
194
213
  postfixButtons,
@@ -204,11 +223,14 @@ export const FieldDate = forwardRef<HTMLInputElement, FieldDateProps>(
204
223
  submitKeys: ['Enter', 'Space', 'Tab'],
205
224
  });
206
225
 
207
- // TODO: do not hardcode locale here
208
226
  const handleSelectDate = (date: Date) => {
209
- onChange && onChange(date.toLocaleDateString(DEFAULT_LOCALE));
227
+ onChange && onChange(date);
210
228
  localRef.current?.focus();
211
229
  setIsOpen(false);
230
+
231
+ if (localRef.current) {
232
+ localRef.current.value = getStringDateValue(date);
233
+ }
212
234
  };
213
235
 
214
236
  const handleCalendarFocusLeave: CalendarProps['onFocusLeave'] = () => {
@@ -233,11 +255,12 @@ export const FieldDate = forwardRef<HTMLInputElement, FieldDateProps>(
233
255
  }
234
256
  }, [open]);
235
257
 
258
+ // TODO input ref - determine whether to update ref based on input/non-input state
236
259
  useEffect(() => {
237
- if (localRef.current) {
238
- localRef.current.value = valueProp;
260
+ if (localRef.current && document.activeElement !== localRef.current) {
261
+ localRef.current.value = getStringDateValue(valueProp);
239
262
  }
240
- }, [valueProp]);
263
+ }, [getStringDateValue, valueProp]);
241
264
 
242
265
  const onFocusByKeyboard = useCallback(
243
266
  (e: FocusEvent<HTMLInputElement>) => {
@@ -296,20 +319,17 @@ export const FieldDate = forwardRef<HTMLInputElement, FieldDateProps>(
296
319
  content={
297
320
  <div className={styles.calendarWrapper} data-size={size}>
298
321
  <Calendar
299
- mode='date'
300
- size={CALENDAR_SIZE_MAP[size]}
301
- value={valueProp ? parseDate(valueProp) : undefined}
322
+ mode={mode}
323
+ size={size}
324
+ value={valueProp}
325
+ showSeconds={showSeconds}
302
326
  onChangeValue={handleSelectDate}
303
327
  buildCellProps={buildCellProps}
304
- navigationStartRef={element => {
305
- if (pickerAutofocus) {
306
- element?.focus();
307
- setPickerAutofocus(false);
308
- }
309
- }}
328
+ navigationStartRef={navigationStartRef}
310
329
  onFocusLeave={handleCalendarFocusLeave}
311
- locale={locale}
330
+ locale={DEFAULT_LOCALE}
312
331
  data-test-id='field-date__calendar'
332
+ fitToContainer={false}
313
333
  />
314
334
  </div>
315
335
  }
@@ -1,2 +1 @@
1
1
  export * from './FieldDate';
2
- export { parseDate } from './utils';
@@ -108,7 +108,7 @@ export function useSearchInput({
108
108
  selectedOptionFormatter,
109
109
  resetSearchOnOptionSelection = true,
110
110
  }: SearchState & { selectedOptionFormatter: SelectedOptionFormatter; resetSearchOnOptionSelection?: boolean }) {
111
- const [inputValue = '', setInputValue] = useValueControl<string>({ value, onChange, defaultValue });
111
+ const [inputValue = '', setInputValueState] = useValueControl<string>({ value, onChange, defaultValue });
112
112
 
113
113
  const prevInputValue = useRef<string>(inputValue);
114
114
 
@@ -117,12 +117,24 @@ export function useSearchInput({
117
117
  const newInputValue = selectedOptionFormatter(selectedItem);
118
118
 
119
119
  if (resetSearchOnOptionSelection && (inputValue !== newInputValue || prevInputValue.current !== newInputValue)) {
120
- setInputValue(newInputValue);
120
+ setInputValueState(newInputValue);
121
121
 
122
122
  prevInputValue.current = newInputValue;
123
123
  }
124
124
  },
125
- [inputValue, resetSearchOnOptionSelection, selectedOptionFormatter, setInputValue],
125
+ [inputValue, resetSearchOnOptionSelection, selectedOptionFormatter, setInputValueState],
126
+ );
127
+
128
+ const setInputValue = useCallback(
129
+ (value: string) => {
130
+ const updatedValue =
131
+ prevInputValue.current && value.includes(prevInputValue.current)
132
+ ? value.replace(prevInputValue.current, '')
133
+ : value;
134
+
135
+ setInputValueState(updatedValue);
136
+ },
137
+ [setInputValueState],
126
138
  );
127
139
 
128
140
  return { inputValue, setInputValue, prevInputValue, onInputValueChange: setInputValue, updateInputValue };
@@ -0,0 +1,350 @@
1
+ import mergeRefs from 'merge-refs';
2
+ import { FocusEvent, forwardRef, KeyboardEvent, MouseEvent, useCallback, useEffect, useMemo, useRef } from 'react';
3
+ import { useUncontrolledProp } from 'uncontrollable';
4
+
5
+ import { TimePicker, TimePickerProps } from '@snack-uikit/calendar';
6
+ import { Dropdown } from '@snack-uikit/dropdown';
7
+ import { WatchSVG } from '@snack-uikit/icons';
8
+ import {
9
+ ButtonProps,
10
+ ICON_SIZE,
11
+ InputPrivate,
12
+ InputPrivateProps,
13
+ runAfterRerender,
14
+ SIZE,
15
+ useButtonNavigation,
16
+ useClearButton,
17
+ } from '@snack-uikit/input-private';
18
+ import { extractSupportProps, WithSupportProps } from '@snack-uikit/utils';
19
+
20
+ import { CONTAINER_VARIANT, DEFAULT_LOCALE, SlotKey, TIME_MODES, VALIDATION_STATE } from '../../constants';
21
+ import { FieldContainerPrivate } from '../../helperComponents';
22
+ import { useCopyButton, useDateField, useFocusHandlers, useHandlers } from '../../hooks';
23
+ import { getValidationState } from '../../utils/getValidationState';
24
+ import { FieldDecorator, FieldDecoratorProps } from '../FieldDecorator';
25
+ import styles from './styles.module.scss';
26
+
27
+ type InputProps = Pick<InputPrivateProps, 'id' | 'name' | 'disabled' | 'readonly' | 'onFocus' | 'onBlur'>;
28
+
29
+ type WrapperProps = Pick<
30
+ FieldDecoratorProps,
31
+ | 'className'
32
+ | 'label'
33
+ | 'labelTooltip'
34
+ | 'required'
35
+ | 'caption'
36
+ | 'hint'
37
+ | 'showHintIcon'
38
+ | 'size'
39
+ | 'validationState'
40
+ | 'labelTooltipPlacement'
41
+ | 'error'
42
+ >;
43
+
44
+ type FieldTimeOwnProps = {
45
+ /** Открыт time-picker */
46
+ open?: boolean;
47
+ /** Колбек открытия пикера */
48
+ onOpenChange?(value: boolean): void;
49
+ /** Значение поля */
50
+ value?: TimePickerProps['value'];
51
+ /** Колбек смены значения */
52
+ onChange?: TimePickerProps['onChangeValue'];
53
+ /** Отображение кнопки копирования */
54
+ showCopyButton?: boolean;
55
+ /** Показывать ли секунды */
56
+ showSeconds?: boolean;
57
+ /**
58
+ * Отображение кнопки Очистки поля
59
+ * @default true
60
+ */
61
+ showClearButton?: boolean;
62
+ };
63
+
64
+ export type FieldTimeProps = WithSupportProps<FieldTimeOwnProps & InputProps & WrapperProps>;
65
+
66
+ const getStringTimeValue = (
67
+ time: TimePickerProps['value'],
68
+ { showSeconds, locale }: Pick<TimePickerProps, 'showSeconds'> & { locale: Intl.Locale },
69
+ ) => {
70
+ if (!time) {
71
+ return '';
72
+ }
73
+
74
+ const date = new Date();
75
+ date.setHours(time.hours ?? 0);
76
+ date.setMinutes(time.minutes ?? 0);
77
+ date.setSeconds(time.seconds ?? 0);
78
+
79
+ return date.toLocaleTimeString(locale, {
80
+ hour: 'numeric',
81
+ minute: 'numeric',
82
+ second: showSeconds ? 'numeric' : undefined,
83
+ });
84
+ };
85
+
86
+ export const FieldTime = forwardRef<HTMLInputElement, FieldTimeProps>(
87
+ (
88
+ {
89
+ id,
90
+ name,
91
+ value: valueProp,
92
+ disabled = false,
93
+ readonly = false,
94
+ showCopyButton: showCopyButtonProp = true,
95
+ showClearButton: showClearButtonProp = true,
96
+ open,
97
+ onOpenChange,
98
+ onChange,
99
+ onFocus,
100
+ onBlur: onBlurProp,
101
+ className,
102
+ label,
103
+ labelTooltip,
104
+ labelTooltipPlacement,
105
+ required = false,
106
+ caption,
107
+ hint,
108
+ showHintIcon,
109
+ showSeconds = true,
110
+ size = SIZE.S,
111
+ validationState = VALIDATION_STATE.Default,
112
+ error,
113
+ ...rest
114
+ },
115
+ ref,
116
+ ) => {
117
+ const [isOpen, setIsOpen] = useUncontrolledProp(open, false, onOpenChange);
118
+
119
+ const localRef = useRef<HTMLInputElement>(null);
120
+ const clearButtonRef = useRef<HTMLButtonElement>(null);
121
+ const copyButtonRef = useRef<HTMLButtonElement>(null);
122
+ const calendarIconSize = size === SIZE.S ? ICON_SIZE.Xs : ICON_SIZE.S;
123
+ const showDropList = isOpen && !readonly && !disabled;
124
+ const showAdditionalButton = Boolean(valueProp && !disabled);
125
+ const showClearButton = showClearButtonProp && showAdditionalButton && !readonly;
126
+ const showCopyButton = showCopyButtonProp && showAdditionalButton && readonly;
127
+ const fieldValidationState = getValidationState({ validationState, error });
128
+ const navigationStartRef: TimePickerProps['navigationStartRef'] = useRef(null);
129
+
130
+ const checkForLeavingFocus = useCallback(
131
+ <T extends HTMLInputElement | HTMLButtonElement>(event: KeyboardEvent<T>) => {
132
+ if (event.key === 'ArrowDown') {
133
+ setIsOpen(true);
134
+ setTimeout(() => navigationStartRef.current?.focus(), 0);
135
+ }
136
+ },
137
+ [setIsOpen],
138
+ );
139
+
140
+ const handleClear = useCallback(() => {
141
+ onChange && onChange(undefined);
142
+ if (localRef.current?.value) {
143
+ localRef.current.value = '';
144
+ }
145
+
146
+ if (required) {
147
+ localRef.current?.focus();
148
+ setIsOpen(true);
149
+ } else {
150
+ localRef.current?.blur();
151
+ setIsOpen(false);
152
+ }
153
+ }, [onChange, required, setIsOpen]);
154
+
155
+ const valueToCopy = getStringTimeValue(valueProp, { showSeconds, locale: DEFAULT_LOCALE });
156
+ const clearButtonSettings = useClearButton({ clearButtonRef, showClearButton, size, onClear: handleClear });
157
+ const copyButtonSettings = useCopyButton({ copyButtonRef, showCopyButton, size, valueToCopy });
158
+ const calendarIcon: ButtonProps = useMemo(
159
+ () => ({
160
+ active: false,
161
+ show: true,
162
+ id: 'watchIcon',
163
+ render: props => (
164
+ <WatchSVG {...props} size={calendarIconSize} className={styles.calendarIcon} data-size={size} />
165
+ ),
166
+ }),
167
+ [calendarIconSize, size],
168
+ );
169
+
170
+ const memorizedButtons = useMemo(
171
+ () => [clearButtonSettings, copyButtonSettings, calendarIcon],
172
+ [clearButtonSettings, copyButtonSettings, calendarIcon],
173
+ );
174
+
175
+ const {
176
+ value,
177
+ handleChange,
178
+ handleClick: timeInputClickHandler,
179
+ handleKeyDown: timeInputKeyDownHandler,
180
+ handleBlur: timeInputBlurHandler,
181
+ mask,
182
+ setInputFocus,
183
+ } = useDateField({
184
+ inputRef: localRef,
185
+ onChange,
186
+ readonly,
187
+ locale: DEFAULT_LOCALE,
188
+ setIsOpen,
189
+ mode: showSeconds ? TIME_MODES.FullTime : TIME_MODES.NoSeconds,
190
+ showSeconds,
191
+ });
192
+
193
+ const setInputFocusFromButtons = useCallback(() => setInputFocus(SlotKey.Seconds), [setInputFocus]);
194
+
195
+ const {
196
+ postfixButtons,
197
+ inputTabIndex,
198
+ onInputKeyDown: navigationInputKeyDownHandler,
199
+ setInitialTabIndices,
200
+ } = useButtonNavigation({
201
+ setInputFocus: setInputFocusFromButtons,
202
+ inputRef: localRef,
203
+ postfixButtons: memorizedButtons,
204
+ onButtonKeyDown: checkForLeavingFocus,
205
+ readonly,
206
+ submitKeys: ['Enter', 'Space', 'Tab'],
207
+ });
208
+
209
+ const handleSelectTime = (time: TimePickerProps['value']) => {
210
+ onChange && onChange(time);
211
+ localRef.current?.focus();
212
+ setIsOpen(false);
213
+
214
+ if (localRef.current) {
215
+ localRef.current.value = getStringTimeValue(time, { showSeconds, locale: DEFAULT_LOCALE });
216
+ }
217
+ };
218
+
219
+ const handleCalendarFocusLeave: TimePickerProps['onFocusLeave'] = () => {
220
+ setInitialTabIndices();
221
+ // TODO: find out why it works not as expected (focus is moved to the next element instead of the focused one)
222
+ // maybe floating-ui causes the problem
223
+ runAfterRerender(() => {
224
+ setInputFocus(SlotKey.Hours);
225
+ setIsOpen(false);
226
+ });
227
+ };
228
+
229
+ const handleInputKeyDown = useHandlers<KeyboardEvent<HTMLInputElement>>([
230
+ checkForLeavingFocus,
231
+ timeInputKeyDownHandler,
232
+ navigationInputKeyDownHandler,
233
+ ]);
234
+
235
+ useEffect(() => {
236
+ if (open) {
237
+ localRef.current?.focus();
238
+ }
239
+ }, [open]);
240
+
241
+ // TODO input ref - determine whether to update ref based on input/non-input state
242
+ useEffect(() => {
243
+ if (localRef.current && document.activeElement !== localRef.current) {
244
+ localRef.current.value = getStringTimeValue(valueProp, { showSeconds, locale: DEFAULT_LOCALE });
245
+ }
246
+ }, [showSeconds, valueProp]);
247
+
248
+ const onFocusByKeyboard = useCallback(
249
+ (e: FocusEvent<HTMLInputElement>) => {
250
+ setInputFocus();
251
+ onFocus?.(e);
252
+ },
253
+ [onFocus, setInputFocus],
254
+ );
255
+
256
+ const inputHandlers = useFocusHandlers({
257
+ onFocusByClick: onFocus,
258
+ onFocusByKeyboard,
259
+ });
260
+
261
+ const onBlur = useHandlers([timeInputBlurHandler, inputHandlers.onBlur, onBlurProp]);
262
+
263
+ const onClick = useCallback(
264
+ (e: MouseEvent<HTMLInputElement>) => {
265
+ timeInputClickHandler();
266
+ if (isOpen) {
267
+ // stop the event because want picker to stay opened
268
+ e.stopPropagation();
269
+ }
270
+ },
271
+ [timeInputClickHandler, isOpen],
272
+ );
273
+
274
+ return (
275
+ <FieldDecorator
276
+ className={className}
277
+ label={label}
278
+ labelTooltip={labelTooltip}
279
+ labelTooltipPlacement={labelTooltipPlacement}
280
+ labelFor={id}
281
+ required={required}
282
+ caption={caption}
283
+ hint={hint}
284
+ disabled={disabled}
285
+ readonly={readonly}
286
+ showHintIcon={showHintIcon}
287
+ size={size}
288
+ error={error}
289
+ validationState={fieldValidationState}
290
+ {...extractSupportProps(rest)}
291
+ >
292
+ <Dropdown
293
+ trigger='click'
294
+ triggerClassName={styles.triggerClassName}
295
+ widthStrategy='auto'
296
+ {...(readonly || disabled
297
+ ? { open: false }
298
+ : {
299
+ open: showDropList,
300
+ onOpenChange: setIsOpen,
301
+ })}
302
+ content={
303
+ <TimePicker
304
+ size={size}
305
+ value={valueProp}
306
+ onChangeValue={handleSelectTime}
307
+ navigationStartRef={navigationStartRef}
308
+ onFocusLeave={handleCalendarFocusLeave}
309
+ data-test-id='field-time__timepicker'
310
+ fitToContainer={false}
311
+ showSeconds={showSeconds}
312
+ />
313
+ }
314
+ >
315
+ <FieldContainerPrivate
316
+ className={styles.container}
317
+ size={size}
318
+ validationState={fieldValidationState}
319
+ disabled={disabled}
320
+ readonly={readonly}
321
+ variant={CONTAINER_VARIANT.SingleLine}
322
+ focused={showDropList}
323
+ inputRef={localRef}
324
+ postfix={postfixButtons}
325
+ >
326
+ <InputPrivate
327
+ ref={mergeRefs(ref, localRef)}
328
+ data-size={size}
329
+ value={value || ''}
330
+ placeholder={mask}
331
+ onChange={handleChange}
332
+ onFocus={inputHandlers.onFocus}
333
+ onMouseDown={inputHandlers.onMouseDown}
334
+ onBlur={onBlur}
335
+ onKeyDown={handleInputKeyDown}
336
+ onClick={onClick}
337
+ disabled={disabled}
338
+ readonly={readonly}
339
+ tabIndex={inputTabIndex}
340
+ type='text'
341
+ id={id}
342
+ name={name}
343
+ data-test-id='field-time__input'
344
+ />
345
+ </FieldContainerPrivate>
346
+ </Dropdown>
347
+ </FieldDecorator>
348
+ );
349
+ },
350
+ );
@@ -0,0 +1 @@
1
+ export * from './FieldTime';