@vkontakte/vkui 7.2.0 → 7.2.1

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 (153) hide show
  1. package/dist/components/Calendar/Calendar.d.ts +2 -2
  2. package/dist/components/Calendar/Calendar.d.ts.map +1 -1
  3. package/dist/components/Calendar/Calendar.js +1 -1
  4. package/dist/components/Calendar/Calendar.js.map +1 -1
  5. package/dist/components/CalendarDays/CalendarDays.d.ts +1 -1
  6. package/dist/components/CalendarDays/CalendarDays.d.ts.map +1 -1
  7. package/dist/components/CalendarDays/CalendarDays.js.map +1 -1
  8. package/dist/components/CalendarRange/CalendarRange.d.ts +2 -2
  9. package/dist/components/CalendarRange/CalendarRange.d.ts.map +1 -1
  10. package/dist/components/CalendarRange/CalendarRange.js +4 -1
  11. package/dist/components/CalendarRange/CalendarRange.js.map +1 -1
  12. package/dist/components/CalendarTime/CalendarTime.d.ts.map +1 -1
  13. package/dist/components/CalendarTime/CalendarTime.js +16 -13
  14. package/dist/components/CalendarTime/CalendarTime.js.map +1 -1
  15. package/dist/components/ChipsInputBase/ChipsInputBase.d.ts.map +1 -1
  16. package/dist/components/ChipsInputBase/ChipsInputBase.js +1 -0
  17. package/dist/components/ChipsInputBase/ChipsInputBase.js.map +1 -1
  18. package/dist/components/ChipsInputBase/helpers.d.ts +1 -1
  19. package/dist/components/ChipsInputBase/helpers.d.ts.map +1 -1
  20. package/dist/components/ChipsInputBase/helpers.js +4 -0
  21. package/dist/components/ChipsInputBase/helpers.js.map +1 -1
  22. package/dist/components/ChipsInputBase/types.d.ts +1 -1
  23. package/dist/components/ChipsInputBase/types.d.ts.map +1 -1
  24. package/dist/components/ChipsInputBase/types.js.map +1 -1
  25. package/dist/components/ChipsSelect/ChipsSelect.d.ts.map +1 -1
  26. package/dist/components/ChipsSelect/ChipsSelect.js +6 -1
  27. package/dist/components/ChipsSelect/ChipsSelect.js.map +1 -1
  28. package/dist/components/CustomSelect/CustomSelect.d.ts.map +1 -1
  29. package/dist/components/CustomSelect/CustomSelect.js +3 -2
  30. package/dist/components/CustomSelect/CustomSelect.js.map +1 -1
  31. package/dist/components/CustomSelectDropdown/CustomSelectDropdown.d.ts.map +1 -1
  32. package/dist/components/CustomSelectDropdown/CustomSelectDropdown.js +1 -0
  33. package/dist/components/CustomSelectDropdown/CustomSelectDropdown.js.map +1 -1
  34. package/dist/components/DateInput/DateInput.d.ts +9 -1
  35. package/dist/components/DateInput/DateInput.d.ts.map +1 -1
  36. package/dist/components/DateInput/DateInput.js +14 -4
  37. package/dist/components/DateInput/DateInput.js.map +1 -1
  38. package/dist/components/DateInput/hooks.d.ts +7 -6
  39. package/dist/components/DateInput/hooks.d.ts.map +1 -1
  40. package/dist/components/DateInput/hooks.js +14 -7
  41. package/dist/components/DateInput/hooks.js.map +1 -1
  42. package/dist/components/DateRangeInput/DateRangeInput.d.ts +9 -1
  43. package/dist/components/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  44. package/dist/components/DateRangeInput/DateRangeInput.js +13 -3
  45. package/dist/components/DateRangeInput/DateRangeInput.js.map +1 -1
  46. package/dist/components/ModalCard/ModalCard.d.ts.map +1 -1
  47. package/dist/components/ModalCard/ModalCard.js +4 -12
  48. package/dist/components/ModalCard/ModalCard.js.map +1 -1
  49. package/dist/components/ModalPage/ModalPage.d.ts.map +1 -1
  50. package/dist/components/ModalPage/ModalPage.js +5 -12
  51. package/dist/components/ModalPage/ModalPage.js.map +1 -1
  52. package/dist/components/ModalRoot/types.d.ts +1 -0
  53. package/dist/components/ModalRoot/types.d.ts.map +1 -1
  54. package/dist/components/ModalRoot/types.js.map +1 -1
  55. package/dist/components/ModalRoot/useModalManager.d.ts +4 -2
  56. package/dist/components/ModalRoot/useModalManager.d.ts.map +1 -1
  57. package/dist/components/ModalRoot/useModalManager.js +12 -3
  58. package/dist/components/ModalRoot/useModalManager.js.map +1 -1
  59. package/dist/components/ModalRoot/useModalRootContext.js +1 -0
  60. package/dist/components/ModalRoot/useModalRootContext.js.map +1 -1
  61. package/dist/components/Touch/Touch.d.ts.map +1 -1
  62. package/dist/components/Touch/Touch.js +2 -2
  63. package/dist/components/Touch/Touch.js.map +1 -1
  64. package/dist/components.css +1 -1
  65. package/dist/components.css.map +1 -1
  66. package/dist/cssm/components/Calendar/Calendar.js +1 -1
  67. package/dist/cssm/components/Calendar/Calendar.js.map +1 -1
  68. package/dist/cssm/components/CalendarDays/CalendarDays.js.map +1 -1
  69. package/dist/cssm/components/CalendarRange/CalendarRange.js +4 -1
  70. package/dist/cssm/components/CalendarRange/CalendarRange.js.map +1 -1
  71. package/dist/cssm/components/CalendarTime/CalendarTime.js +16 -13
  72. package/dist/cssm/components/CalendarTime/CalendarTime.js.map +1 -1
  73. package/dist/cssm/components/ChipsInputBase/ChipsInputBase.js +1 -0
  74. package/dist/cssm/components/ChipsInputBase/ChipsInputBase.js.map +1 -1
  75. package/dist/cssm/components/ChipsInputBase/helpers.js +4 -0
  76. package/dist/cssm/components/ChipsInputBase/helpers.js.map +1 -1
  77. package/dist/cssm/components/ChipsInputBase/types.js.map +1 -1
  78. package/dist/cssm/components/ChipsSelect/ChipsSelect.js +6 -1
  79. package/dist/cssm/components/ChipsSelect/ChipsSelect.js.map +1 -1
  80. package/dist/cssm/components/CustomSelect/CustomSelect.js +3 -1
  81. package/dist/cssm/components/CustomSelect/CustomSelect.js.map +1 -1
  82. package/dist/cssm/components/CustomSelectDropdown/CustomSelectDropdown.js +1 -0
  83. package/dist/cssm/components/CustomSelectDropdown/CustomSelectDropdown.js.map +1 -1
  84. package/dist/cssm/components/DateInput/DateInput.js +12 -4
  85. package/dist/cssm/components/DateInput/DateInput.js.map +1 -1
  86. package/dist/cssm/components/DateInput/hooks.js +14 -7
  87. package/dist/cssm/components/DateInput/hooks.js.map +1 -1
  88. package/dist/cssm/components/DateRangeInput/DateRangeInput.js +11 -3
  89. package/dist/cssm/components/DateRangeInput/DateRangeInput.js.map +1 -1
  90. package/dist/cssm/components/FormItem/FormItem.module.css +1 -0
  91. package/dist/cssm/components/ModalCard/ModalCard.js +2 -11
  92. package/dist/cssm/components/ModalCard/ModalCard.js.map +1 -1
  93. package/dist/cssm/components/ModalPage/ModalPage.js +3 -11
  94. package/dist/cssm/components/ModalPage/ModalPage.js.map +1 -1
  95. package/dist/cssm/components/ModalRoot/types.js.map +1 -1
  96. package/dist/cssm/components/ModalRoot/useModalManager.js +12 -3
  97. package/dist/cssm/components/ModalRoot/useModalManager.js.map +1 -1
  98. package/dist/cssm/components/ModalRoot/useModalRootContext.js +1 -0
  99. package/dist/cssm/components/ModalRoot/useModalRootContext.js.map +1 -1
  100. package/dist/cssm/components/Tappable/Tappable.module.css +1 -1
  101. package/dist/cssm/components/Touch/Touch.js +2 -2
  102. package/dist/cssm/components/Touch/Touch.js.map +1 -1
  103. package/dist/cssm/hooks/useCalendar.js.map +1 -1
  104. package/dist/cssm/hooks/useDateInput.js +3 -3
  105. package/dist/cssm/hooks/useDateInput.js.map +1 -1
  106. package/dist/cssm/lib/date.js +6 -0
  107. package/dist/cssm/lib/date.js.map +1 -1
  108. package/dist/cssm/lib/dom.js +6 -0
  109. package/dist/cssm/lib/dom.js.map +1 -1
  110. package/dist/cssm/styles/constants.css +2 -2
  111. package/dist/hooks/useCalendar.d.ts +1 -1
  112. package/dist/hooks/useCalendar.d.ts.map +1 -1
  113. package/dist/hooks/useCalendar.js.map +1 -1
  114. package/dist/hooks/useDateInput.d.ts +4 -4
  115. package/dist/hooks/useDateInput.d.ts.map +1 -1
  116. package/dist/hooks/useDateInput.js +3 -3
  117. package/dist/hooks/useDateInput.js.map +1 -1
  118. package/dist/lib/date.d.ts.map +1 -1
  119. package/dist/lib/date.js +6 -0
  120. package/dist/lib/date.js.map +1 -1
  121. package/dist/lib/dom.d.ts +1 -0
  122. package/dist/lib/dom.d.ts.map +1 -1
  123. package/dist/lib/dom.js +6 -0
  124. package/dist/lib/dom.js.map +1 -1
  125. package/dist/vkui.css +1 -1
  126. package/dist/vkui.css.map +1 -1
  127. package/package.json +1 -1
  128. package/src/components/Calendar/Calendar.tsx +7 -7
  129. package/src/components/CalendarDays/CalendarDays.tsx +1 -1
  130. package/src/components/CalendarRange/CalendarRange.tsx +11 -6
  131. package/src/components/CalendarTime/CalendarTime.tsx +17 -9
  132. package/src/components/ChipsInputBase/ChipsInputBase.tsx +1 -0
  133. package/src/components/ChipsInputBase/helpers.ts +5 -1
  134. package/src/components/ChipsInputBase/types.ts +1 -1
  135. package/src/components/ChipsSelect/ChipsSelect.tsx +6 -1
  136. package/src/components/CustomSelect/CustomSelect.tsx +2 -0
  137. package/src/components/CustomSelectDropdown/CustomSelectDropdown.tsx +1 -0
  138. package/src/components/DateInput/DateInput.tsx +37 -10
  139. package/src/components/DateInput/hooks.ts +32 -23
  140. package/src/components/DateRangeInput/DateRangeInput.tsx +26 -5
  141. package/src/components/FormItem/FormItem.module.css +1 -0
  142. package/src/components/ModalCard/ModalCard.tsx +2 -9
  143. package/src/components/ModalPage/ModalPage.tsx +3 -10
  144. package/src/components/ModalRoot/types.ts +1 -0
  145. package/src/components/ModalRoot/useModalManager.tsx +12 -5
  146. package/src/components/ModalRoot/useModalRootContext.ts +1 -1
  147. package/src/components/Tappable/Tappable.module.css +1 -1
  148. package/src/components/Touch/Touch.tsx +35 -3
  149. package/src/hooks/useCalendar.ts +1 -1
  150. package/src/hooks/useDateInput.ts +6 -6
  151. package/src/lib/date.ts +6 -0
  152. package/src/lib/dom.tsx +8 -0
  153. package/src/styles/constants.css +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "type": "module",
3
- "version": "7.2.0",
3
+ "version": "7.2.1",
4
4
  "name": "@vkontakte/vkui",
5
5
  "description": "VKUI library",
6
6
  "module": "./dist/index.js",
@@ -52,8 +52,8 @@ export interface CalendarProps
52
52
  Pick<CalendarDaysProps, 'dayProps' | 'listenDayChangesForUpdate' | 'renderDayContent'>,
53
53
  CalendarDoneButtonProps,
54
54
  CalendarTestsProps {
55
- value?: Date;
56
- defaultValue?: Date;
55
+ value?: Date | null;
56
+ defaultValue?: Date | null;
57
57
  /**
58
58
  * Запрещает выбор даты в прошлом.
59
59
  * Применяется, если не заданы `shouldDisableDate` и `disableFuture`.
@@ -70,7 +70,7 @@ export interface CalendarProps
70
70
  weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
71
71
  showNeighboringMonth?: boolean;
72
72
  size?: 's' | 'm';
73
- onChange?: (value?: Date) => void;
73
+ onChange?: (value?: Date) => void; // TODO [>=8]: поменять тип на `(value?: Date | null) => void`
74
74
  /**
75
75
  * Позволяет запретить выбор даты.
76
76
  */
@@ -155,20 +155,20 @@ export const Calendar = ({
155
155
  ...props
156
156
  }: CalendarProps): React.ReactNode => {
157
157
  const _onChange = React.useCallback(
158
- (date: Date | undefined) => {
158
+ (date: Date | null | undefined) => {
159
159
  onChange?.(convertDateFromTimeZone(date, timezone) || undefined);
160
160
  },
161
161
  [onChange, timezone],
162
162
  );
163
163
 
164
- const [value, updateValue] = useCustomEnsuredControl<Date | undefined>({
164
+ const [value, updateValue] = useCustomEnsuredControl<Date | null | undefined>({
165
165
  value: valueProp,
166
166
  defaultValue,
167
167
  onChange: _onChange,
168
168
  });
169
169
 
170
- const timeZonedValue: Date | undefined = React.useMemo(
171
- () => convertDateToTimeZone(value, timezone) || undefined,
170
+ const timeZonedValue: Date | null | undefined = React.useMemo(
171
+ () => convertDateToTimeZone(value, timezone),
172
172
  [timezone, value],
173
173
  );
174
174
 
@@ -29,7 +29,7 @@ export interface CalendarDaysProps
29
29
  extends Omit<HTMLAttributesWithRootRef<HTMLDivElement>, 'onChange'>,
30
30
  Pick<CalendarDayProps, 'renderDayContent'>,
31
31
  CalendarDaysTestsProps {
32
- value?: Date | Array<Date | null>;
32
+ value?: Date | Array<Date | null> | null;
33
33
  viewDate: Date;
34
34
  weekStartsOn: 0 | 1 | 2 | 3 | 4 | 5 | 6;
35
35
  showNeighboringMonth?: boolean;
@@ -55,19 +55,19 @@ export interface CalendarRangeProps
55
55
  >,
56
56
  Pick<CalendarDaysProps, 'listenDayChangesForUpdate' | 'renderDayContent'>,
57
57
  CalendarRangeTestsProps {
58
- value?: DateRangeType;
59
- defaultValue?: DateRangeType;
58
+ value?: DateRangeType | null;
59
+ defaultValue?: DateRangeType | null;
60
60
  disablePast?: boolean;
61
61
  disableFuture?: boolean;
62
62
  disablePickers?: boolean;
63
63
  changeDayLabel?: string;
64
64
  weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
65
- onChange?: (value: DateRangeType | undefined) => void;
65
+ onChange?: (value: DateRangeType | undefined) => void; // TODO [>=8]: поменять тип на `(value?: DateRangeType | null) => void`
66
66
  shouldDisableDate?: (value: Date) => boolean;
67
67
  onClose?: () => void;
68
68
  }
69
69
 
70
- const getIsDaySelected = (day: Date, value?: DateRangeType) => {
70
+ const getIsDaySelected = (day: Date, value?: DateRangeType | null) => {
71
71
  if (!value?.[0] || !value[1]) {
72
72
  return false;
73
73
  }
@@ -103,10 +103,15 @@ export const CalendarRange = ({
103
103
  getRootRef,
104
104
  ...props
105
105
  }: CalendarRangeProps): React.ReactNode => {
106
- const [value, updateValue] = useCustomEnsuredControl<DateRangeType | undefined>({
106
+ const _onChange = React.useCallback(
107
+ (newValue: DateRangeType | null | undefined) => onChange?.(newValue || undefined),
108
+ [onChange],
109
+ );
110
+
111
+ const [value, updateValue] = useCustomEnsuredControl<DateRangeType | null | undefined>({
107
112
  value: valueProp,
108
113
  defaultValue,
109
- onChange,
114
+ onChange: _onChange,
110
115
  });
111
116
 
112
117
  const {
@@ -137,16 +137,24 @@ export const CalendarTime = ({
137
137
 
138
138
  const onPickerKeyDown = (e: React.KeyboardEvent) => {
139
139
  const key = pressedKey(e);
140
- if (key === Keys.ENTER || key === Keys.TAB) {
141
- const steps = [hoursInputRef, minutesInputRef, doneButtonRef];
142
- const currentStepIndex = steps.findIndex((step) => step.current === e.target);
143
- const diff = e.key === 'Tab' && e.shiftKey ? -1 : 1;
144
- const nextStepIndex = currentStepIndex + diff;
145
- if (nextStepIndex < 0 || nextStepIndex >= steps.length) {
146
- return;
147
- }
140
+ /* Мы хотим иметь возможность быстро, по Enter перемещаться между
141
+ * селектами с часами и минутами, также как мы это делаем по нажатию на Tab */
142
+ if (key !== Keys.ENTER) {
143
+ return;
144
+ }
145
+
146
+ const steps = [hoursInputRef, minutesInputRef, doneButtonRef].filter((ref) =>
147
+ Boolean(ref.current),
148
+ );
149
+ const currentStepIndex = steps.findIndex((step) => step.current === e.target);
150
+ const nextStepIndex = currentStepIndex + 1;
151
+ if (nextStepIndex >= steps.length) {
152
+ return;
153
+ }
154
+ const nextStep = steps[nextStepIndex];
155
+
156
+ if (nextStep.current) {
148
157
  e.preventDefault();
149
- const nextStep = steps[nextStepIndex];
150
158
  nextStep.current?.focus();
151
159
  }
152
160
  };
@@ -260,6 +260,7 @@ export const ChipsInputBase = <O extends ChipOption>({
260
260
  // чтобы можно было легче найти этот чип в DOM
261
261
  'data-index': index,
262
262
  'data-value': option.value,
263
+ 'data-value-type': typeof option.value,
263
264
  // для a11y
264
265
  'tabIndex': lastFocusedChipOptionIndex === index ? 0 : -1,
265
266
  'role': 'option',
@@ -35,8 +35,12 @@ export const getChipOptionIndexByHTMLElement = (el: HTMLElement | null): number
35
35
  /**
36
36
  * @private
37
37
  */
38
- export const getChipOptionValueByHTMLElement = (el: HTMLElement | null): string | -1 => {
38
+ export const getChipOptionValueByHTMLElement = (el: HTMLElement | null): ChipOptionValue | -1 => {
39
39
  const value = el && el.dataset.value;
40
+ const valueType = el && el.dataset.valueType;
41
+ if (valueType === 'number') {
42
+ return Number(value);
43
+ }
40
44
  return typeof value === 'string' ? value : -1;
41
45
  };
42
46
 
@@ -102,7 +102,7 @@ export interface ChipsInputBaseProps<O extends ChipOption = ChipOption>
102
102
  *
103
103
  * @default Используется [Chip](#/Chip)
104
104
  */
105
- renderChip?: RenderChip;
105
+ renderChip?: RenderChip<O>;
106
106
  /**
107
107
  * Показывать ли кнопку для очистки значения
108
108
  */
@@ -244,6 +244,7 @@ export const ChipsSelect = <Option extends ChipOption>({
244
244
  // Связано с ChipsInputProps
245
245
  const rootRef = useExternRef(getRootRef);
246
246
  const inputRef = useExternRef(getRef, inputRefHook);
247
+ const forbidCloseByOutsideClick = React.useRef(false);
247
248
 
248
249
  // Связано с CustomSelectDropdownProps
249
250
  const [dropdownVerticalPlacement, setDropdownVerticalPlacement] = React.useState<
@@ -419,7 +420,10 @@ export const ChipsSelect = <Option extends ChipOption>({
419
420
  }, [setFocusedOptionIndex]);
420
421
 
421
422
  const handleClickOutside = React.useCallback(() => {
422
- setOpened(false);
423
+ if (!forbidCloseByOutsideClick.current) {
424
+ setOpened(false);
425
+ }
426
+ forbidCloseByOutsideClick.current = false;
423
427
  }, [setOpened]);
424
428
 
425
429
  useGlobalOnClickOutside(
@@ -492,6 +496,7 @@ export const ChipsSelect = <Option extends ChipOption>({
492
496
  if (!event.defaultPrevented) {
493
497
  closeAfterSelect && setOpened(false);
494
498
  addOption(option);
499
+ forbidCloseByOutsideClick.current = true;
495
500
  clearInput();
496
501
  }
497
502
  },
@@ -761,6 +761,7 @@ export function CustomSelect<OptionInterfaceT extends CustomSelectOptionInterfac
761
761
  // C mousemove такой проблемы нет, что позволяет реализовать поведение при наведении с клавиатуры и при наведении мышью идентично `<select>`.
762
762
  onMouseMove: (e) => focusOptionOnMouseMove(e, index),
763
763
  id: `${popupAriaId}-${option.value}`,
764
+ ...option,
764
765
  })}
765
766
  </React.Fragment>
766
767
  );
@@ -961,6 +962,7 @@ export function CustomSelect<OptionInterfaceT extends CustomSelectOptionInterfac
961
962
  {selected?.label}
962
963
  </CustomSelectInput>
963
964
  <select
965
+ tabIndex={-1}
964
966
  ref={selectElRef}
965
967
  name={name}
966
968
  onChange={onNativeSelectChange}
@@ -65,6 +65,7 @@ export const CustomSelectDropdown = ({
65
65
  getRootRef={scrollBoxRef}
66
66
  className={noMaxHeight ? undefined : styles.inWithMaxHeight}
67
67
  overscrollBehavior={overscrollBehavior}
68
+ tabIndex={-1}
68
69
  >
69
70
  {fetching ? (
70
71
  <div className={styles.fetching}>
@@ -50,6 +50,14 @@ export type DateInputPropsTestsProps = {
50
50
  * Передает атрибут `data-testid` для поля ввода минут
51
51
  */
52
52
  minuteFieldTestId?: string;
53
+ /**
54
+ * Передает атрибут `data-testid` для кнопки показа календаря.
55
+ */
56
+ showCalendarButtonTestId?: string;
57
+ /**
58
+ * Передает атрибут `data-testid` для кнопки очистки даты.
59
+ */
60
+ clearButtonTestId?: string;
53
61
  };
54
62
 
55
63
  export interface DateInputProps
@@ -209,6 +217,8 @@ export const DateInput = ({
209
217
  yearFieldTestId,
210
218
  hourFieldTestId,
211
219
  minuteFieldTestId,
220
+ showCalendarButtonTestId,
221
+ clearButtonTestId,
212
222
  id,
213
223
  onApply,
214
224
  renderCustomValue,
@@ -221,12 +231,13 @@ export const DateInput = ({
221
231
  const hoursRef = React.useRef<HTMLSpanElement>(null);
222
232
  const minutesRef = React.useRef<HTMLSpanElement>(null);
223
233
 
224
- const { value, updateValue, setInternalValue, getLastUpdatedValue } = useDateInputValue({
225
- value: valueProp,
226
- defaultValue,
227
- onChange,
228
- timezone,
229
- });
234
+ const { value, updateValue, setInternalValue, getLastUpdatedValue, clearValue } =
235
+ useDateInputValue({
236
+ value: valueProp,
237
+ defaultValue,
238
+ onChange,
239
+ timezone,
240
+ });
230
241
 
231
242
  const maxElement = enableTime ? 4 : 2;
232
243
 
@@ -277,7 +288,7 @@ export const DateInput = ({
277
288
  autoFocus,
278
289
  disabled,
279
290
  elementsConfig,
280
- onChange: updateValue,
291
+ onClear: clearValue,
281
292
  onInternalValueChange,
282
293
  getInternalValue,
283
294
  value,
@@ -299,6 +310,9 @@ export const DateInput = ({
299
310
 
300
311
  const onCalendarChange = React.useCallback(
301
312
  (value?: Date | undefined) => {
313
+ if (!value) {
314
+ return;
315
+ }
302
316
  if (enableTime) {
303
317
  setInternalValue(value);
304
318
  return;
@@ -312,13 +326,16 @@ export const DateInput = ({
312
326
  );
313
327
 
314
328
  const onDoneButtonClick = React.useCallback(() => {
329
+ if (!value) {
330
+ return;
331
+ }
315
332
  const newValue = updateValue(value);
316
333
  onApply?.(newValue);
317
334
  removeFocusFromField();
318
335
  }, [onApply, removeFocusFromField, updateValue, value]);
319
336
 
320
337
  const customValue = React.useMemo(
321
- () => !open && renderCustomValue?.(value),
338
+ () => !open && renderCustomValue?.(value || undefined),
322
339
  [open, renderCustomValue, value],
323
340
  );
324
341
 
@@ -336,11 +353,21 @@ export const DateInput = ({
336
353
  getRootRef={handleRootRef}
337
354
  after={
338
355
  value ? (
339
- <IconButton hoverMode="opacity" label={clearFieldLabel} onClick={clear}>
356
+ <IconButton
357
+ hoverMode="opacity"
358
+ label={clearFieldLabel}
359
+ onClick={clear}
360
+ data-testid={clearButtonTestId}
361
+ >
340
362
  <Icon16Clear />
341
363
  </IconButton>
342
364
  ) : (
343
- <IconButton hoverMode="opacity" label={showCalendarLabel} onClick={openCalendar}>
365
+ <IconButton
366
+ hoverMode="opacity"
367
+ label={showCalendarLabel}
368
+ onClick={openCalendar}
369
+ data-testid={showCalendarButtonTestId}
370
+ >
344
371
  <Icon20CalendarOutline />
345
372
  </IconButton>
346
373
  )
@@ -2,33 +2,34 @@ import * as React from 'react';
2
2
  import { convertDateFromTimeZone, convertDateToTimeZone } from '../../lib/date';
3
3
 
4
4
  interface UseDateInputValueOptions {
5
- value?: Date;
6
- defaultValue?: Date;
5
+ value?: Date | null;
6
+ defaultValue?: Date | null;
7
7
  onChange?: (value?: Date) => void;
8
8
  timezone?: string;
9
9
  }
10
10
 
11
11
  export interface UseDateInputValueReturn {
12
- value?: Date;
13
- updateValue: (v?: Date) => Date | undefined;
14
- setInternalValue: (v?: Date) => void;
15
- getLastUpdatedValue: () => Date | undefined;
12
+ value?: Date | null;
13
+ updateValue: (v: Date) => Date;
14
+ setInternalValue: (v?: Date | null) => void;
15
+ getLastUpdatedValue: () => Date | null | undefined;
16
+ clearValue: () => void;
16
17
  }
17
18
 
18
- const _convertDateToTimeZone = (date?: Date, timezone?: string): Date | undefined => {
19
- return convertDateToTimeZone(date, timezone) || undefined;
19
+ const _convertDateToTimeZone = (date: Date | null, timezone?: string): Date | null => {
20
+ return convertDateToTimeZone(date, timezone) || null;
20
21
  };
21
22
 
22
- const _convertDateFromTimeZone = (date?: Date, timezone?: string): Date | undefined => {
23
- return convertDateFromTimeZone(date, timezone) || undefined;
23
+ const _convertDateFromTimeZone = (date: Date, timezone?: string): Date => {
24
+ return convertDateFromTimeZone(date, timezone) as Date;
24
25
  };
25
26
 
26
27
  const getStateValue = (
27
- defaultStateValue?: Date,
28
- value?: Date,
29
- defaultValue?: Date,
28
+ defaultStateValue: Date | null,
29
+ value?: Date | null,
30
+ defaultValue?: Date | null,
30
31
  timezone?: string,
31
- ): Date | undefined => {
32
+ ): Date | null => {
32
33
  if (value !== undefined) {
33
34
  return _convertDateToTimeZone(value, timezone);
34
35
  }
@@ -44,26 +45,27 @@ export const useDateInputValue = ({
44
45
  onChange,
45
46
  timezone,
46
47
  }: UseDateInputValueOptions): UseDateInputValueReturn => {
47
- const isControlled = value !== undefined;
48
-
49
- const [internalValue, setInternalValue] = React.useState<Date | undefined>(
50
- getStateValue(undefined, value, defaultValue, timezone),
48
+ const [internalValue, setInternalValue] = React.useState<Date | null | undefined>(
49
+ getStateValue(null, value, defaultValue, timezone),
51
50
  );
52
- const lastUpdatedValueRef = React.useRef<Date | undefined>(
53
- getStateValue(undefined, value, defaultValue, timezone),
51
+ const lastUpdatedValueRef = React.useRef<Date | null | undefined>(
52
+ getStateValue(null, value, defaultValue, timezone),
54
53
  );
55
54
 
55
+ const isControlled = value !== undefined;
56
+
56
57
  React.useEffect(() => {
57
58
  if (isControlled) {
58
- setInternalValue(_convertDateToTimeZone(value, timezone));
59
- lastUpdatedValueRef.current = _convertDateToTimeZone(value, timezone);
59
+ const newInternalValue = _convertDateToTimeZone(value, timezone);
60
+ setInternalValue(newInternalValue);
61
+ lastUpdatedValueRef.current = newInternalValue;
60
62
  }
61
63
  }, [isControlled, timezone, value]);
62
64
 
63
65
  const getLastUpdatedValue = React.useCallback(() => lastUpdatedValueRef.current, []);
64
66
 
65
67
  const updateValue = React.useCallback(
66
- (newValue?: Date) => {
68
+ (newValue: Date) => {
67
69
  if (!isControlled) {
68
70
  setInternalValue(newValue);
69
71
  lastUpdatedValueRef.current = newValue;
@@ -75,10 +77,17 @@ export const useDateInputValue = ({
75
77
  [isControlled, onChange, timezone],
76
78
  );
77
79
 
80
+ const clearValue = () => {
81
+ setInternalValue(null);
82
+ lastUpdatedValueRef.current = null;
83
+ onChange?.(undefined);
84
+ };
85
+
78
86
  return {
79
87
  value: internalValue,
80
88
  updateValue,
81
89
  setInternalValue,
82
90
  getLastUpdatedValue,
91
+ clearValue,
83
92
  };
84
93
  };
@@ -57,6 +57,14 @@ export type DateRangeInputTestsProps = {
57
57
  * Передает атрибуты `data-testid` для полей ввода конечной даты
58
58
  */
59
59
  endDateTestsProps?: DateTestsProps;
60
+ /**
61
+ * Передает атрибут `data-testid` для кнопки показа календаря.
62
+ */
63
+ showCalendarButtonTestId?: string;
64
+ /**
65
+ * Передает атрибут `data-testid` для кнопки очистки даты.
66
+ */
67
+ clearButtonTestId?: string;
60
68
  };
61
69
 
62
70
  export interface DateRangeInputProps
@@ -183,6 +191,8 @@ export const DateRangeInput = ({
183
191
  calendarTestsProps,
184
192
  startDateTestsProps,
185
193
  endDateTestsProps,
194
+ clearButtonTestId,
195
+ showCalendarButtonTestId,
186
196
  id,
187
197
  ...props
188
198
  }: DateRangeInputProps): React.ReactNode => {
@@ -193,10 +203,15 @@ export const DateRangeInput = ({
193
203
  const monthsEndRef = React.useRef<HTMLSpanElement>(null);
194
204
  const yearsEndRef = React.useRef<HTMLSpanElement>(null);
195
205
 
196
- const [value, updateValue] = useCustomEnsuredControl<DateRangeType | undefined>({
206
+ const _onChange = React.useCallback(
207
+ (newValue: DateRangeType | null | undefined) => onChange?.(newValue || undefined),
208
+ [onChange],
209
+ );
210
+
211
+ const [value, updateValue] = useCustomEnsuredControl<DateRangeType | null | undefined>({
197
212
  value: valueProp,
198
213
  defaultValue,
199
- onChange,
214
+ onChange: _onChange,
200
215
  });
201
216
 
202
217
  const onInternalValueChange = React.useCallback(
@@ -248,6 +263,8 @@ export const DateRangeInput = ({
248
263
  [daysStartRef, monthsStartRef, yearsStartRef, daysEndRef, monthsEndRef, yearsEndRef],
249
264
  );
250
265
 
266
+ const onClear = React.useCallback(() => updateValue(undefined), [updateValue]);
267
+
251
268
  const {
252
269
  rootRef,
253
270
  calendarRef,
@@ -266,7 +283,7 @@ export const DateRangeInput = ({
266
283
  autoFocus,
267
284
  disabled,
268
285
  elementsConfig,
269
- onChange: updateValue,
286
+ onClear,
270
287
  onInternalValueChange,
271
288
  getInternalValue,
272
289
  value,
@@ -301,12 +318,16 @@ export const DateRangeInput = ({
301
318
  getRootRef={handleRootRef}
302
319
  after={
303
320
  value ? (
304
- <IconButton hoverMode="opacity" onClick={clear}>
321
+ <IconButton hoverMode="opacity" onClick={clear} data-testid={clearButtonTestId}>
305
322
  <VisuallyHidden>{clearFieldLabel}</VisuallyHidden>
306
323
  <Icon16Clear />
307
324
  </IconButton>
308
325
  ) : (
309
- <IconButton hoverMode="opacity" onClick={openCalendar}>
326
+ <IconButton
327
+ hoverMode="opacity"
328
+ onClick={openCalendar}
329
+ data-testid={showCalendarButtonTestId}
330
+ >
310
331
  <VisuallyHidden>{showCalendarLabel}</VisuallyHidden>
311
332
  <Icon20CalendarOutline />
312
333
  </IconButton>
@@ -49,6 +49,7 @@
49
49
  }
50
50
 
51
51
  .labelMultiline {
52
+ text-overflow: unset;
52
53
  white-space: normal;
53
54
  }
54
55
 
@@ -1,15 +1,10 @@
1
1
  'use client';
2
2
 
3
- import { useId } from 'react';
4
3
  import { ModalContext } from '../../context/ModalContext';
5
- import { getNavId } from '../../lib/getNavId';
6
- import { warnOnce } from '../../lib/warnOnce';
7
4
  import { useModalManager } from '../ModalRoot/useModalManager';
8
5
  import { ModalCardInternal } from './ModalCardInternal';
9
6
  import type { ModalCardProps } from './types';
10
7
 
11
- const warn = warnOnce('ModalCard');
12
-
13
8
  /**
14
9
  * @see https://vkcom.github.io/VKUI/#/ModalCard
15
10
  */
@@ -26,15 +21,13 @@ export const ModalCard = ({
26
21
  keepMounted = false,
27
22
  ...restProps
28
23
  }: ModalCardProps): React.ReactNode => {
29
- const generatingId = useId();
30
- const id = getNavId({ nav, id: idProp }, warn) || generatingId;
31
-
32
24
  const {
33
25
  mounted,
34
26
  shouldPreserveSnapPoint: excludedProp,
27
+ id,
35
28
  ...resolvedProps
36
29
  } = useModalManager({
37
- id,
30
+ id: nav || idProp,
38
31
  open,
39
32
  keepMounted,
40
33
  modalOverlayTestId,
@@ -1,17 +1,13 @@
1
1
  'use client';
2
2
 
3
- import { useId, useMemo } from 'react';
3
+ import { useMemo } from 'react';
4
4
  import { ModalContext } from '../../context/ModalContext';
5
5
  import { inRange } from '../../helpers/range';
6
- import { getNavId } from '../../lib/getNavId';
7
6
  import { SNAP_POINT_DETENTS, SNAP_POINT_SAFE_RANGE, type SnapPoint } from '../../lib/sheet';
8
- import { warnOnce } from '../../lib/warnOnce';
9
7
  import { useModalManager } from '../ModalRoot/useModalManager';
10
8
  import { ModalPageInternal } from './ModalPageInternal';
11
9
  import type { ModalPageProps } from './types';
12
10
 
13
- const warn = warnOnce('ModalPage');
14
-
15
11
  const snapPointCache = new Map<string, Exclude<SnapPoint, 'auto'>>();
16
12
 
17
13
  /**
@@ -33,11 +29,8 @@ export const ModalPage = ({
33
29
  keepMounted = false,
34
30
  ...restProps
35
31
  }: ModalPageProps) => {
36
- const generatingId = useId();
37
- const id = getNavId({ nav, id: idProp }, warn) || generatingId;
38
-
39
- const { mounted, shouldPreserveSnapPoint, ...resolvedProps } = useModalManager({
40
- id,
32
+ const { mounted, shouldPreserveSnapPoint, id, ...resolvedProps } = useModalManager({
33
+ id: nav || idProp,
41
34
  open,
42
35
  keepMounted,
43
36
  modalOverlayTestId,
@@ -151,5 +151,6 @@ export interface ModalRootContextInterface
151
151
  ModalRootBaseProps {}
152
152
 
153
153
  export interface UseModalRootContext extends ModalRootContextBaseInterface {
154
+ activeModal?: string | null;
154
155
  onClose?: VoidFunction;
155
156
  }
@@ -1,13 +1,16 @@
1
- import { useContext, useState } from 'react';
1
+ import { useContext, useId, useState } from 'react';
2
+ import { getNavId } from '../../lib/getNavId';
2
3
  import { useIsomorphicLayoutEffect } from '../../lib/useIsomorphicLayoutEffect';
4
+ import { warnOnce } from '../../lib/warnOnce';
3
5
  import type { AnyFunction } from '../../types';
4
6
  import { ModalOverlay, type ModalOverlayProps } from '../ModalOverlay/ModalOverlay';
5
7
  import { ModalRootContext } from './ModalRootContext';
6
8
  import { VisuallyHiddenModalOverlay } from './VisuallyHiddenModalOverlay/VisuallyHiddenModalOverlay';
7
9
  import type { ModalRootCallbackFunction } from './types';
8
10
 
11
+ const warn = warnOnce('useModalManager');
9
12
  export interface UseModalManager {
10
- id: string;
13
+ id?: string;
11
14
  open: boolean;
12
15
  keepMounted: boolean;
13
16
  modalOverlayTestId?: string;
@@ -19,6 +22,7 @@ export interface UseModalManager {
19
22
  }
20
23
 
21
24
  export interface UseModalManagerResolvedProps {
25
+ id: string;
22
26
  open: boolean;
23
27
  noFocusToDialog?: boolean;
24
28
  modalOverlayTestId?: string;
@@ -30,11 +34,11 @@ export interface UseModalManagerResolvedProps {
30
34
  }
31
35
 
32
36
  export type UseModalManagerResult =
33
- | { mounted: false; shouldPreserveSnapPoint: boolean }
37
+ | { mounted: false; shouldPreserveSnapPoint: boolean; id: UseModalManagerResolvedProps['id'] }
34
38
  | ({ mounted: true; shouldPreserveSnapPoint: boolean } & UseModalManagerResolvedProps);
35
39
 
36
40
  export const useModalManager = ({
37
- id,
41
+ id: idProp,
38
42
  open,
39
43
  keepMounted,
40
44
  modalOverlayTestId,
@@ -45,6 +49,8 @@ export const useModalManager = ({
45
49
  onClosed,
46
50
  }: UseModalManager): UseModalManagerResult => {
47
51
  const context = useContext(ModalRootContext);
52
+ const generatingId = useId();
53
+ const id = getNavId({ nav: idProp }, context.isInsideModal ? warn : undefined) || generatingId;
48
54
  const opened = context.isInsideModal ? context.activeModal === id : open;
49
55
  const shouldPreserveSnapPoint = context.isInsideModal ? context.activeModal !== null : false;
50
56
 
@@ -60,10 +66,11 @@ export const useModalManager = ({
60
66
  );
61
67
 
62
68
  if (unmounted) {
63
- return { mounted: false, shouldPreserveSnapPoint };
69
+ return { mounted: false, shouldPreserveSnapPoint, id };
64
70
  }
65
71
 
66
72
  return {
73
+ id,
67
74
  mounted: true,
68
75
  open: opened,
69
76
  shouldPreserveSnapPoint,
@@ -17,5 +17,5 @@ export const useModalRootContext = (): UseModalRootContext => {
17
17
  }
18
18
  }, [activeModal, onCloseContext]);
19
19
 
20
- return { isInsideModal, onClose, updateModalHeight, registerModal };
20
+ return { activeModal, isInsideModal, onClose, updateModalHeight, registerModal };
21
21
  };