@skbkontur/react-ui 6.1.7-1c902.0 → 6.1.7-4c72d.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 (72) hide show
  1. package/components/MenuItem/MenuItem.d.ts +1 -1
  2. package/components/MenuItem/MenuItem.js +0 -1
  3. package/components/MenuItem/MenuItem.js.map +1 -1
  4. package/components/MenuItem/MenuItem.styles.d.ts +0 -1
  5. package/components/MenuItem/MenuItem.styles.js +7 -10
  6. package/components/MenuItem/MenuItem.styles.js.map +1 -1
  7. package/components/TimePicker/TimePicker.d.ts +44 -25
  8. package/components/TimePicker/TimePicker.js +373 -191
  9. package/components/TimePicker/TimePicker.js.map +1 -1
  10. package/components/TimePicker/TimePicker.styles.d.ts +3 -1
  11. package/components/TimePicker/TimePicker.styles.js +9 -3
  12. package/components/TimePicker/TimePicker.styles.js.map +1 -1
  13. package/components/TimePicker/TimePickerItems.d.ts +5 -1
  14. package/components/TimePicker/TimePickerItems.js +39 -29
  15. package/components/TimePicker/TimePickerItems.js.map +1 -1
  16. package/components/TimePicker/TimePickerMobilePopup.d.ts +5 -2
  17. package/components/TimePicker/TimePickerMobilePopup.js +4 -4
  18. package/components/TimePicker/TimePickerMobilePopup.js.map +1 -1
  19. package/components/TimePicker/TimePickerPopup.d.ts +5 -1
  20. package/components/TimePicker/TimePickerPopup.js +4 -4
  21. package/components/TimePicker/TimePickerPopup.js.map +1 -1
  22. package/components/TimePicker/helpers/TimePicker.constants.d.ts +8 -1
  23. package/components/TimePicker/helpers/TimePicker.constants.js +8 -7
  24. package/components/TimePicker/helpers/TimePicker.constants.js.map +1 -1
  25. package/components/TimePicker/helpers/TimePicker.editing.d.ts +1 -5
  26. package/components/TimePicker/helpers/TimePicker.editing.js +8 -35
  27. package/components/TimePicker/helpers/TimePicker.editing.js.map +1 -1
  28. package/components/TimePicker/helpers/TimePicker.shared.d.ts +20 -22
  29. package/components/TimePicker/helpers/TimePicker.shared.js +27 -24
  30. package/components/TimePicker/helpers/TimePicker.shared.js.map +1 -1
  31. package/components/TimePicker/helpers/TimePicker.value.d.ts +22 -11
  32. package/components/TimePicker/helpers/TimePicker.value.js +69 -37
  33. package/components/TimePicker/helpers/TimePicker.value.js.map +1 -1
  34. package/components/TimePicker/helpers/scrollSelectedItemIntoView.d.ts +2 -0
  35. package/components/TimePicker/helpers/scrollSelectedItemIntoView.js +2 -0
  36. package/components/TimePicker/helpers/scrollSelectedItemIntoView.js.map +1 -1
  37. package/components/TimePicker/helpers/validateTimePicker.js +2 -4
  38. package/components/TimePicker/helpers/validateTimePicker.js.map +1 -1
  39. package/components/TimePicker/hooks/useTimePickerDropdown.d.ts +6 -1
  40. package/components/TimePicker/hooks/useTimePickerDropdown.js +15 -33
  41. package/components/TimePicker/hooks/useTimePickerDropdown.js.map +1 -1
  42. package/components/TimePicker/hooks/useTimePickerSource.d.ts +29 -0
  43. package/components/TimePicker/hooks/useTimePickerSource.js +67 -0
  44. package/components/TimePicker/hooks/useTimePickerSource.js.map +1 -0
  45. package/components/TimePicker/hooks/useTimePickerValue.d.ts +15 -4
  46. package/components/TimePicker/hooks/useTimePickerValue.js +40 -20
  47. package/components/TimePicker/hooks/useTimePickerValue.js.map +1 -1
  48. package/components/TimePicker/index.d.ts +3 -1
  49. package/components/TimePicker/index.js +1 -0
  50. package/components/TimePicker/index.js.map +1 -1
  51. package/components/TimePicker/locale/locales/en.js +2 -0
  52. package/components/TimePicker/locale/locales/en.js.map +1 -1
  53. package/components/TimePicker/locale/locales/ru.js +2 -0
  54. package/components/TimePicker/locale/locales/ru.js.map +1 -1
  55. package/components/TimePicker/locale/types.d.ts +2 -0
  56. package/components/TimePicker/locale/types.js.map +1 -1
  57. package/internal/NativeTimeInput/NativeTimeInput.d.ts +4 -0
  58. package/internal/NativeTimeInput/NativeTimeInput.js +7 -2
  59. package/internal/NativeTimeInput/NativeTimeInput.js.map +1 -1
  60. package/internal/NativeTimeInput/NativeTimeInput.utils.d.ts +2 -0
  61. package/internal/NativeTimeInput/NativeTimeInput.utils.js +4 -8
  62. package/internal/NativeTimeInput/NativeTimeInput.utils.js.map +1 -1
  63. package/internal/TimeInput/TimeFragments.d.ts +0 -1
  64. package/internal/TimeInput/TimeFragments.js +30 -8
  65. package/internal/TimeInput/TimeFragments.js.map +1 -1
  66. package/internal/TimeInput/TimeFragments.styles.d.ts +15 -0
  67. package/internal/TimeInput/TimeFragments.styles.js +39 -9
  68. package/internal/TimeInput/TimeFragments.styles.js.map +1 -1
  69. package/internal/TimeInput/TimeInput.d.ts +0 -1
  70. package/internal/TimeInput/TimeInput.js +0 -1
  71. package/internal/TimeInput/TimeInput.js.map +1 -1
  72. package/package.json +1 -1
@@ -22,103 +22,169 @@ var __rest = (this && this.__rest) || function (s, e) {
22
22
  };
23
23
  import React, { useCallback, useContext, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from 'react';
24
24
  import { CommonWrapper } from '../../internal/CommonWrapper/index.js';
25
+ import { extractCommonProps } from '../../internal/CommonWrapper/utils/extractCommonProps.js';
25
26
  import { LoadingIcon } from '../../internal/icons2022/LoadingIcon.js';
26
27
  import { NativeTimeInput } from '../../internal/NativeTimeInput/index.js';
27
28
  import { TimeInput } from '../../internal/TimeInput/index.js';
28
- import { isIOS, isMobile } from '../../lib/client.js';
29
+ import { isIOS } from '../../lib/client.js';
29
30
  import { isKeyArrowDown, isKeyArrowLeft, isKeyArrowRight, isKeyArrowUp, isKeyBackspace, isKeyChar, isKeyDelete, isKeyEnter, isKeyEscape, isKeySpace, isKeyTab, isModShift, isShortcutSelectAll, } from '../../lib/events/keyboard/identifiers.js';
30
31
  import { forwardRefAndName } from '../../lib/forwardRefAndName.js';
31
32
  import { useLocaleForControl } from '../../lib/locale/useLocaleForControl.js';
32
33
  import { useStyles } from '../../lib/renderEnvironment/index.js';
33
34
  import { ThemeContext } from '../../lib/theming/ThemeContext.js';
34
35
  import { getRandomID } from '../../lib/utils.js';
36
+ import { useResponsiveLayout } from '../ResponsiveLayout/index.js';
35
37
  import { scrollSelectedItemIntoView } from './helpers/scrollSelectedItemIntoView.js';
36
38
  import { DIGIT_REGEXP, EMPTY_SEGMENT, EMPTY_VALUE, TimePickerDataTids } from './helpers/TimePicker.constants.js';
37
39
  import { commitTimeSegmentOnLeave, deleteTimeSegmentDigit, formatDigitToTimeSegment, shiftTimeSegmentValue, } from './helpers/TimePicker.editing.js';
38
40
  import { getTimePickerPopupMaxHeight } from './helpers/TimePicker.layout.js';
39
41
  import { getNextTimeSegment, getPreviousTimeSegment } from './helpers/TimePicker.selection.js';
40
- import { isTimeItem, isTimeItemObject, isTimeMenuItem, timeValueToItem, } from './helpers/TimePicker.shared.js';
41
- import { dedupeTimeItems, filterTimeItems, getEmptyDisplayValue, getTimeFilterQuery, getTimeValueString, isTimeDisplayEmpty, normalizeTimeValue, parsePastedTimeValue, replaceTimeSegment, resolveTimeItems, } from './helpers/TimePicker.value.js';
42
+ import { isNavigableMenuElement, isTimeItem, isTimeItemObject, isTimeMenuItem, timeValueToItem, } from './helpers/TimePicker.shared.js';
43
+ import { filterTimeItems, getEmptyDisplayValue, getTimeFilterQuery, getTimeValueString, isTimeDisplayEmpty, normalizeTimeValue, parsePastedTimeValue, replaceTimeSegment, resolveTimeItems, } from './helpers/TimePicker.value.js';
42
44
  import { validateTimePicker } from './helpers/validateTimePicker.js';
43
45
  import { useTimePickerDropdown } from './hooks/useTimePickerDropdown.js';
44
46
  import { useTimePickerSelection } from './hooks/useTimePickerSelection.js';
47
+ import { useTimePickerSource } from './hooks/useTimePickerSource.js';
45
48
  import { useTimePickerValue } from './hooks/useTimePickerValue.js';
46
49
  import { TimePickerLocaleHelper } from './locale/index.js';
47
50
  import { getStyles } from './TimePicker.styles.js';
48
51
  import { TimePickerMobilePopup } from './TimePickerMobilePopup.js';
49
52
  import { TimePickerPopup } from './TimePickerPopup.js';
53
+ /** Пустой список для клавиатуры: не создает новую ссылку на каждый рендер. */
54
+ var EMPTY_MENU_ITEMS = [];
50
55
  /** Поле с временем помогает пользователю быстро и удобно указать время в правильном формате.
51
56
  * В поле можно ввести время с клавиатуры либо выбрать из выпадающего списка,
52
57
  * передав массив или функцию в проп `source`.
53
58
  */
54
59
  export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (props, ref) {
55
- var disabled = props.disabled, _a = props.useMobileNativeTimePicker, useMobileNativeTimePicker = _a === void 0 ? false : _a, _b = props.format, format = _b === void 0 ? 'HH:mm' : _b, _c = props.size, size = _c === void 0 ? 'small' : _c, source = props.source, renderItem = props.renderItem, menuPos = props.menuPos, menuAlign = props.menuAlign, menuWidth = props.menuWidth, minTime = props.minTime, maxTime = props.maxTime, rightIcon = props.rightIcon, suffix = props.suffix, corners = props.corners, value = props.value, valueToItem = props.valueToItem, onValueChange = props.onValueChange, onInputValueChange = props.onInputValueChange, onUnexpectedInput = props.onUnexpectedInput, onFocus = props.onFocus, onBlur = props.onBlur, onClick = props.onClick, onKeyDown = props.onKeyDown, onPaste = props.onPaste, inputProps = __rest(props, ["disabled", "useMobileNativeTimePicker", "format", "size", "source", "renderItem", "menuPos", "menuAlign", "menuWidth", "minTime", "maxTime", "rightIcon", "suffix", "corners", "value", "valueToItem", "onValueChange", "onInputValueChange", "onUnexpectedInput", "onFocus", "onBlur", "onClick", "onKeyDown", "onPaste"]);
60
+ var _a;
61
+ // `className`, `style` и data-атрибуты применяет `CommonWrapper` к корневому элементу,
62
+ // поэтому в поле ввода они не должны уехать повторно.
63
+ var _b = extractCommonProps(props), notCommonProps = _b[1];
64
+ var disabled = notCommonProps.disabled, _c = notCommonProps.useMobileNativeTimePicker, useMobileNativeTimePicker = _c === void 0 ? false : _c, _d = notCommonProps.format, format = _d === void 0 ? 'HH:mm' : _d, _e = notCommonProps.size, size = _e === void 0 ? 'small' : _e, source = notCommonProps.source, renderItem = notCommonProps.renderItem, menuPos = notCommonProps.menuPos, menuAlign = notCommonProps.menuAlign, menuWidth = notCommonProps.menuWidth, minTime = notCommonProps.minTime, maxTime = notCommonProps.maxTime, rightIcon = notCommonProps.rightIcon, suffix = notCommonProps.suffix, corners = notCommonProps.corners, value = notCommonProps.value, valueToItem = notCommonProps.valueToItem, onValueChange = notCommonProps.onValueChange, onInputValueChange = notCommonProps.onInputValueChange, onUnexpectedInput = notCommonProps.onUnexpectedInput, onFocus = notCommonProps.onFocus, onBlur = notCommonProps.onBlur, onClick = notCommonProps.onClick, onKeyDown = notCommonProps.onKeyDown, onPaste = notCommonProps.onPaste, inputProps = __rest(notCommonProps, ["disabled", "useMobileNativeTimePicker", "format", "size", "source", "renderItem", "menuPos", "menuAlign", "menuWidth", "minTime", "maxTime", "rightIcon", "suffix", "corners", "value", "valueToItem", "onValueChange", "onInputValueChange", "onUnexpectedInput", "onFocus", "onBlur", "onClick", "onKeyDown", "onPaste"]);
56
65
  var theme = useContext(ThemeContext);
57
66
  var styles = useStyles(getStyles);
58
67
  var locale = useLocaleForControl('TimePicker', TimePickerLocaleHelper);
59
- var _d = useState(false), isInputFocused = _d[0], setIsInputFocused = _d[1];
68
+ var _f = useState(false), isInputFocused = _f[0], setIsInputFocused = _f[1];
60
69
  var inputRef = useRef(null);
61
70
  var mobileInputRef = useRef(null);
62
71
  var nativeInputRef = useRef(null);
63
72
  var rootRef = useRef(null);
64
73
  var itemRefs = useRef(new Map());
65
74
  var isMouseFocusRef = useRef(false);
66
- var isMouseDownOnFragmentRef = useRef(false);
75
+ /** Сегмент, по которому нажали мышью, или `null`, если нажали мимо сегментов. */
67
76
  var mouseDownSegmentRef = useRef(null);
68
77
  var mobileBlurEventRef = useRef(null);
78
+ var shouldSkipOpenOnFocusRef = useRef(false);
79
+ /**
80
+ * Контрол считает себя сфокусированным: о фокусе сообщено наружу, а сессия ввода не закончена.
81
+ * Внутри одной сессии фокус может переезжать между полем и инпутом мобильного попапа —
82
+ * например вместе со сменой верстки, — и такой переезд не начинает сессию заново.
83
+ */
84
+ var isControlFocusedRef = useRef(false);
85
+ /** Фокус, который контрол сам переводит в скрытый нативный инпут, открывая системный пикер. */
86
+ var isMovingFocusToNativePickerRef = useRef(false);
69
87
  var popupIdRef = useRef(TimePickerDataTids.popup + getRandomID());
70
- var _e = useState(false), isMobileDevice = _e[0], setIsMobileDevice = _e[1];
71
- var _f = useState(false), canUseMobileNativeTimePicker = _f[0], setCanUseMobileNativeTimePicker = _f[1];
88
+ /**
89
+ * Мобильная версия включается медиавыражением темы, как в ComboBox:
90
+ * поле переключается между мобильной и десктопной версией вместе с версткой страницы.
91
+ */
92
+ var isMobileLayout = useResponsiveLayout().isMobile;
93
+ var canUseMobileNativeTimePicker = useMobileNativeTimePicker && isMobileLayout;
72
94
  var isSourceFunction = typeof source === 'function';
73
- var sourceItems = useMemo(function () { return (Array.isArray(source) ? source : []); }, [source]);
74
- var hasDropdown = isSourceFunction || sourceItems.length > 0;
75
- var _g = useState([]), fetchedItems = _g[0], setFetchedItems = _g[1];
76
- var _h = useState(null), fetchedItemsQuery = _h[0], setFetchedItemsQuery = _h[1];
77
- var _j = useState(false), isLoadingItems = _j[0], setIsLoadingItems = _j[1];
78
- var _k = useState(false), isEditedAfterOpen = _k[0], setIsEditedAfterOpen = _k[1];
79
- var sourceValueItems = isSourceFunction ? fetchedItems : sourceItems;
95
+ var hasDropdown = isSourceFunction || (Array.isArray(source) && source.length > 0);
96
+ var _g = useState(false), isDropdownOpened = _g[0], setIsDropdownOpened = _g[1];
97
+ var _h = useState(false), isEditedAfterOpen = _h[0], setIsEditedAfterOpen = _h[1];
98
+ /**
99
+ * Форма элементов функции-источника: последняя, о которой компонент знает.
100
+ * Форма запоминается, потому что элементы функции очищаются при закрытии меню,
101
+ * а коммит значения после закрытия должен отдать наружу элемент той же формы.
102
+ * Для массива форма всегда считается по текущим элементам — запоминать нечего.
103
+ */
104
+ var lastSourceItemShapeRef = useRef(null);
80
105
  /**
81
106
  * Форма элемента, который компонент собирает для времени без элемента.
82
107
  * Повторяет форму элементов источника, а если их нет — форму внешнего `value`,
83
108
  * чтобы наружу уходило то же, что пришло внутрь.
109
+ * Значение читается при коммите, поэтому обновляется в эффекте: так его не меняет отброшенный рендер.
84
110
  */
85
- var shouldBuildItemAsObject = sourceValueItems.some(function (item) { return isTimeItem(item) && isTimeItemObject(item); }) ||
86
- (value !== null && value !== undefined && isTimeItemObject(value));
111
+ var shouldBuildItemAsObjectRef = useRef(false);
87
112
  var valueToItemRef = useRef(valueToItem);
88
113
  valueToItemRef.current = valueToItem;
89
- var shouldBuildItemAsObjectRef = useRef(shouldBuildItemAsObject);
90
- shouldBuildItemAsObjectRef.current = shouldBuildItemAsObject;
91
- var handleCommitValue = useCallback(function (committedValue, item) {
114
+ var handleCommitValue = useCallback(function (nextValue, item) {
92
115
  var _a, _b;
93
116
  if (!onValueChange) {
94
117
  return;
95
118
  }
96
- if (committedValue === EMPTY_VALUE) {
119
+ if (nextValue === EMPTY_VALUE) {
97
120
  onValueChange(null, EMPTY_VALUE);
98
121
  return;
99
122
  }
100
- var nextItem = (_b = item !== null && item !== void 0 ? item : (_a = valueToItemRef.current) === null || _a === void 0 ? void 0 : _a.call(valueToItemRef, committedValue)) !== null && _b !== void 0 ? _b : timeValueToItem(committedValue, shouldBuildItemAsObjectRef.current);
101
- onValueChange(nextItem, committedValue);
123
+ var nextItem = (_b = item !== null && item !== void 0 ? item : (_a = valueToItemRef.current) === null || _a === void 0 ? void 0 : _a.call(valueToItemRef, nextValue)) !== null && _b !== void 0 ? _b : timeValueToItem(nextValue, shouldBuildItemAsObjectRef.current);
124
+ onValueChange(nextItem, nextValue);
102
125
  }, [onValueChange]);
103
- var _l = useTimePickerValue({
126
+ var onBlurRef = useRef(onBlur);
127
+ onBlurRef.current = onBlur;
128
+ var pendingBlurEventRef = useRef(null);
129
+ var _j = useState(0), blurRequestId = _j[0], setBlurRequestId = _j[1];
130
+ /**
131
+ * Завершает сессию фокуса и откладывает `onBlur` до конца рендера, как это делает ComboBox.
132
+ * Значение коммитится синхронно с потерей фокуса, поэтому вызванный тут же `onBlur`
133
+ * попал бы в обработчик с состоянием родителя до изменения значения — например с пустым временем.
134
+ * Событие может отсутствовать: сессия все равно закрывается, но наружу ничего не уходит.
135
+ */
136
+ var emitBlur = useCallback(function (event) {
137
+ if (!isControlFocusedRef.current) {
138
+ return;
139
+ }
140
+ isControlFocusedRef.current = false;
141
+ if (!event || !onBlurRef.current) {
142
+ return;
143
+ }
144
+ event.persist();
145
+ pendingBlurEventRef.current = event;
146
+ setBlurRequestId(function (currentId) { return currentId + 1; });
147
+ }, []);
148
+ var flushPendingBlur = useCallback(function () {
149
+ var _a;
150
+ var pendingBlurEvent = pendingBlurEventRef.current;
151
+ if (!pendingBlurEvent) {
152
+ return;
153
+ }
154
+ pendingBlurEventRef.current = null;
155
+ (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, pendingBlurEvent);
156
+ }, []);
157
+ useEffect(flushPendingBlur, [blurRequestId, flushPendingBlur]);
158
+ /** Родитель мог убрать поле в ответ на коммит значения — отложенное событие все равно должно уйти. */
159
+ useEffect(function () { return flushPendingBlur; }, [flushPendingBlur]);
160
+ var _k = useTimePickerValue({
104
161
  isInputFocused: isInputFocused,
105
162
  format: format,
106
163
  externalValue: getTimeValueString(value),
164
+ isValueControlled: value !== undefined,
107
165
  onCommitValue: handleCommitValue,
108
166
  onInputValueChange: onInputValueChange,
109
- }), editingValue = _l.editingValue, editingValueRef = _l.editingValueRef, updateEditingValue = _l.updateEditingValue, commitEditingValue = _l.commitEditingValue, commitValue = _l.commitValue, commitItem = _l.commitItem;
167
+ }), editingValue = _k.editingValue, editingValueRef = _k.editingValueRef, committedValue = _k.committedValue, updateEditingValue = _k.updateEditingValue, commitEditingValue = _k.commitEditingValue, commitValue = _k.commitValue, commitItem = _k.commitItem;
110
168
  var filterQuery = isEditedAfterOpen ? getTimeFilterQuery(editingValue, format) : EMPTY_VALUE;
111
- var filteredItems = useMemo(function () { return filterTimeItems(dedupeTimeItems(sourceValueItems, format), filterQuery, format); }, [filterQuery, format, sourceValueItems]);
112
- /**
113
- * Элементы для отрисовки: время и подпись достаются из элемента,
114
- * а `disabled` дополняется проверкой диапазона `minTime`/`maxTime`.
115
- * Исходный элемент сохраняется как есть — именно он уходит в `onValueChange`.
116
- */
117
- var resolvedItems = useMemo(function () { return resolveTimeItems(filteredItems, format, minTime, maxTime); }, [maxTime, minTime, format, filteredItems]);
169
+ var _l = useTimePickerSource({ source: source, format: format, isDropdownOpened: isDropdownOpened, filterQuery: filterQuery }), sourceValueItems = _l.items, fetchedItemsQuery = _l.fetchedQuery, isLoadingItems = _l.isLoading, isSourceFailed = _l.isFailed, retrySource = _l.retry;
170
+ useEffect(function () {
171
+ if (!isSourceFunction || sourceValueItems.some(isTimeItem)) {
172
+ lastSourceItemShapeRef.current = sourceValueItems.some(function (item) { return isTimeItem(item) && isTimeItemObject(item); });
173
+ }
174
+ shouldBuildItemAsObjectRef.current =
175
+ lastSourceItemShapeRef.current === true || (value !== null && value !== undefined && isTimeItemObject(value));
176
+ }, [isSourceFunction, sourceValueItems, value]);
177
+ var resolvedItems = useMemo(function () { return resolveTimeItems(filterTimeItems(sourceValueItems, filterQuery, format), format, minTime, maxTime); }, [filterQuery, format, maxTime, minTime, sourceValueItems]);
118
178
  var popupMaxHeight = getTimePickerPopupMaxHeight(size, theme);
119
179
  var ariaPlaceholder = format === 'HH:mm' ? locale.ariaPlaceholderHHMM : locale.ariaPlaceholderHHMMSS;
120
- var normalizedValue = normalizeTimeValue(editingValue, format);
121
- var selectedItemIndex = resolvedItems.findIndex(function (item) { return isTimeMenuItem(item) && normalizeTimeValue(item.value, format) === normalizedValue; });
180
+ /**
181
+ * Время, отмеченное в меню выбранным.
182
+ * Пока пользователь не начал править значение при открытом меню, это то же время, что в поле.
183
+ * С началом правки отмечено остается закоммиченное значение: набор фильтра — например `10` —
184
+ * не должен отмечать элемент `10:00` как уже выбранный.
185
+ */
186
+ var selectedValue = normalizeTimeValue(isEditedAfterOpen ? committedValue : editingValue, format);
187
+ var selectedItemIndex = resolvedItems.findIndex(function (item) { return isTimeMenuItem(item) && normalizeTimeValue(item.value, format) === selectedValue; });
122
188
  var autoHighlightKey = null;
123
189
  if (filterQuery !== EMPTY_VALUE) {
124
190
  if (isSourceFunction) {
@@ -130,28 +196,42 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
130
196
  autoHighlightKey = "source-array:".concat(filterQuery);
131
197
  }
132
198
  }
133
- var canUseMobileDropdownTimePicker = hasDropdown && isMobileDevice && !canUseMobileNativeTimePicker;
134
- var hasDropdownContent = resolvedItems.length > 0 || isLoadingItems;
199
+ var canUseMobileDropdownTimePicker = hasDropdown && isMobileLayout && !canUseMobileNativeTimePicker;
200
+ /** Ввод идет в мобильном попапе: он открыт, а нативного пикера в этом режиме нет. */
201
+ var isMobileDropdownActive = canUseMobileDropdownTimePicker && isDropdownOpened;
202
+ /**
203
+ * Меню с одними лишь заголовками и разделителями показывать не о чем,
204
+ * поэтому при активном фильтре нужен хотя бы один подошедший элемент со временем.
205
+ */
206
+ var hasDropdownContent = isLoadingItems ||
207
+ isSourceFailed ||
208
+ (filterQuery === EMPTY_VALUE ? resolvedItems.length > 0 : resolvedItems.some(isTimeMenuItem));
209
+ /**
210
+ * Элементы, с которыми работает клавиатура.
211
+ * Пока меню скрыто, их нет: иначе стрелки ходили бы по невидимому списку,
212
+ * а Enter выбирал бы невидимый элемент вместо коммита введенного времени.
213
+ */
214
+ var menuItems = hasDropdownContent ? resolvedItems : EMPTY_MENU_ITEMS;
135
215
  var inputRightIcon = isLoadingItems && resolvedItems.length > 0 ? (React.createElement("span", { "data-tid": TimePickerDataTids.inputLoading },
136
216
  React.createElement(LoadingIcon, { size: size }))) : (rightIcon);
137
- var displayValue = editingValue;
138
- if (isInputFocused) {
139
- displayValue = editingValue === EMPTY_VALUE ? getEmptyDisplayValue(format) : editingValue;
140
- }
141
- else if (isTimeDisplayEmpty(editingValue)) {
142
- displayValue = EMPTY_VALUE;
143
- }
217
+ /** Сфокусированное поле всегда показывает сегменты, а пустое без фокуса — ничего. */
218
+ var getDisplayValue = function () {
219
+ if (isInputFocused) {
220
+ return editingValue || getEmptyDisplayValue(format);
221
+ }
222
+ return isTimeDisplayEmpty(editingValue) ? EMPTY_VALUE : editingValue;
223
+ };
224
+ var displayValue = getDisplayValue();
144
225
  var resetMouseInteractionState = useCallback(function () {
145
226
  isMouseFocusRef.current = false;
146
- isMouseDownOnFragmentRef.current = false;
147
227
  mouseDownSegmentRef.current = null;
148
228
  }, []);
149
229
  var updateEditingValueAfterUserEdit = useCallback(function (nextEditingValue) {
150
- if (editingValueRef.current !== nextEditingValue) {
230
+ if (hasDropdown && editingValueRef.current !== nextEditingValue) {
151
231
  setIsEditedAfterOpen(true);
152
232
  }
153
233
  updateEditingValue(nextEditingValue);
154
- }, [editingValueRef, updateEditingValue]);
234
+ }, [editingValueRef, hasDropdown, updateEditingValue]);
155
235
  var normalizeCurrentSegmentIfNeeded = useCallback(function (segment) {
156
236
  var nextDisplayValue = commitTimeSegmentOnLeave(editingValue === EMPTY_VALUE ? getEmptyDisplayValue(format) : editingValue, segment, format);
157
237
  updateEditingValueAfterUserEdit(nextDisplayValue);
@@ -161,149 +241,159 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
161
241
  if (!canUseMobileNativeTimePicker || disabled) {
162
242
  return;
163
243
  }
244
+ // На iOS системный пикер открывается только фокусом, поэтому фокус уходит из поля в скрытый инпут.
245
+ isMovingFocusToNativePickerRef.current = true;
164
246
  if (isIOS) {
165
247
  (_a = nativeInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
166
248
  }
167
249
  else {
168
250
  (_b = nativeInputRef.current) === null || _b === void 0 ? void 0 : _b.click();
169
251
  }
252
+ isMovingFocusToNativePickerRef.current = false;
170
253
  }, [canUseMobileNativeTimePicker, disabled]);
254
+ /** Фокус ушел в скрытый нативный инпут: для внешнего мира контрол остается активным. */
255
+ var isFocusMovedToNativePicker = function (event) {
256
+ return isMovingFocusToNativePickerRef.current ||
257
+ (nativeInputRef.current !== null && event.relatedTarget === nativeInputRef.current.getNode());
258
+ };
171
259
  var _m = useTimePickerDropdown({
172
260
  disabled: disabled,
173
261
  hasDropdown: hasDropdown,
174
- items: resolvedItems,
262
+ items: menuItems,
175
263
  selectedItemIndex: selectedItemIndex >= 0 ? selectedItemIndex : null,
176
264
  autoHighlightKey: autoHighlightKey,
177
- }), isDropdownOpened = _m.isDropdownOpened, highlightedItemIndex = _m.highlightedItemIndex, openDropdown = _m.openDropdown, closeDropdown = _m.closeDropdown, resetHighlightedItem = _m.resetHighlightedItem, tryNavigateItems = _m.tryNavigateItems;
178
- var sourceRef = useRef(source);
179
- sourceRef.current = source;
180
- useEffect(function () {
181
- var currentSource = sourceRef.current;
182
- if (typeof currentSource !== 'function' || !isDropdownOpened) {
183
- setIsLoadingItems(false);
184
- return;
185
- }
186
- var cancelled = false;
187
- setIsLoadingItems(true);
188
- new Promise(function (resolve) { return resolve(currentSource(filterQuery)); }).then(function (nextItems) {
189
- if (!cancelled) {
190
- setFetchedItems(nextItems);
191
- setFetchedItemsQuery(filterQuery);
192
- setIsLoadingItems(false);
193
- }
194
- }, function () {
195
- if (!cancelled) {
196
- setFetchedItems([]);
197
- setFetchedItemsQuery(filterQuery);
198
- setIsLoadingItems(false);
199
- }
200
- });
201
- return function () {
202
- cancelled = true;
203
- };
204
- }, [filterQuery, isDropdownOpened, isSourceFunction]);
265
+ isDropdownOpened: isDropdownOpened,
266
+ setIsDropdownOpened: setIsDropdownOpened,
267
+ }), highlightedItemIndex = _m.highlightedItemIndex, openDropdown = _m.openDropdown, closeDropdown = _m.closeDropdown, resetHighlightedItem = _m.resetHighlightedItem, tryNavigateItems = _m.tryNavigateItems;
268
+ /**
269
+ * Фильтрация начинается только после правки значения при открытом меню.
270
+ * Состояние живет ровно столько, сколько открыто меню: правка, которая заново открывает меню,
271
+ * сохраняет свой запрос, а правка при закрытом меню — например стрелками — на список не влияет.
272
+ */
205
273
  useEffect(function () {
206
- setIsEditedAfterOpen(false);
207
- if (!isDropdownOpened && isSourceFunction) {
208
- setFetchedItems([]);
209
- setFetchedItemsQuery(null);
274
+ if (!isDropdownOpened && isEditedAfterOpen) {
275
+ setIsEditedAfterOpen(false);
210
276
  }
211
- }, [isDropdownOpened, isSourceFunction]);
277
+ }, [isDropdownOpened, isEditedAfterOpen]);
212
278
  var popupId = popupIdRef.current;
213
- var activeDescendantId = hasDropdown && highlightedItemIndex !== null ? "".concat(popupId, "-item-").concat(highlightedItemIndex) : undefined;
214
- var isMobileDropdownActive = canUseMobileDropdownTimePicker && isDropdownOpened;
215
- var getActiveInput = useCallback(function () { return (canUseMobileDropdownTimePicker && isDropdownOpened ? mobileInputRef.current : inputRef.current); }, [canUseMobileDropdownTimePicker, isDropdownOpened]);
279
+ var highlightedMenuItem = highlightedItemIndex === null ? undefined : menuItems[highlightedItemIndex];
280
+ /** Кастомный `MenuItem` может принести свой `id` — тогда указывать нужно именно на него. */
281
+ var activeDescendantId = highlightedMenuItem
282
+ ? ((_a = (isNavigableMenuElement(highlightedMenuItem) ? highlightedMenuItem.props.id : undefined)) !== null && _a !== void 0 ? _a : "".concat(popupId, "-item-").concat(highlightedItemIndex))
283
+ : undefined;
284
+ var getActiveInput = useCallback(function () { return (isMobileDropdownActive ? mobileInputRef.current : inputRef.current); }, [isMobileDropdownActive]);
216
285
  var _o = useTimePickerSelection({
217
286
  isInputFocused: isInputFocused,
218
287
  format: format,
219
288
  displayValue: displayValue,
220
289
  getInput: getActiveInput,
221
290
  }), selection = _o.selection, selectedSegment = _o.selectedSegment, selectSegment = _o.selectSegment, selectAll = _o.selectAll, syncSelectionWithDOM = _o.syncSelectionWithDOM;
222
- var blinkInputValue = useCallback(function () {
291
+ var blinkInput = useCallback(function () {
223
292
  var _a;
224
293
  (_a = getActiveInput()) === null || _a === void 0 ? void 0 : _a.blink();
225
294
  }, [getActiveInput]);
226
- /**
227
- * Сообщает о некорректном вводе: нажатой клавише или вставленном значении, которое не может быть временем.
228
- * Если обработчик не задан, поле вспыхивает само.
229
- */
230
295
  var handleUnexpectedInput = useCallback(function (unexpectedValue) {
231
- if (onUnexpectedInput) {
232
- onUnexpectedInput(unexpectedValue, blinkInputValue);
296
+ if (!onUnexpectedInput) {
297
+ blinkInput();
298
+ return;
299
+ }
300
+ var returnedValue = onUnexpectedInput(unexpectedValue, blinkInput);
301
+ if (returnedValue === undefined) {
302
+ return;
303
+ }
304
+ if (returnedValue === null) {
305
+ commitValue(EMPTY_VALUE);
233
306
  return;
234
307
  }
235
- blinkInputValue();
236
- }, [blinkInputValue, onUnexpectedInput]);
308
+ commitItem(returnedValue);
309
+ }, [blinkInput, commitItem, commitValue, onUnexpectedInput]);
310
+ /** Закрывает открытое меню: мобильный попап заодно завершает сессию ввода, как потеря фокуса. */
237
311
  var closeOpenedDropdown = useCallback(function () {
238
- var _a;
239
- if (canUseMobileDropdownTimePicker && isDropdownOpened) {
312
+ var _a, _b;
313
+ if (isMobileDropdownActive) {
314
+ // Фокус может остаться в любом из двух полей: инпут попапа не всегда получает его —
315
+ // например мобильный браузер отказывается переводить фокус без жеста пользователя.
316
+ // Гасим оба, чтобы закрытие попапа всегда давало событие потери фокуса.
240
317
  (_a = mobileInputRef.current) === null || _a === void 0 ? void 0 : _a.blur();
318
+ (_b = inputRef.current) === null || _b === void 0 ? void 0 : _b.blur();
241
319
  setIsInputFocused(false);
242
320
  resetMouseInteractionState();
243
321
  closeDropdown();
244
322
  commitEditingValue();
245
323
  var blurEvent = mobileBlurEventRef.current;
246
324
  mobileBlurEventRef.current = null;
247
- if (blurEvent) {
248
- onBlur === null || onBlur === void 0 ? void 0 : onBlur(blurEvent);
249
- }
325
+ emitBlur(blurEvent);
250
326
  return;
251
327
  }
252
328
  closeDropdown();
253
- }, [
254
- canUseMobileDropdownTimePicker,
255
- closeDropdown,
256
- commitEditingValue,
257
- isDropdownOpened,
258
- onBlur,
259
- resetMouseInteractionState,
260
- ]);
329
+ }, [closeDropdown, commitEditingValue, emitBlur, isMobileDropdownActive, resetMouseInteractionState]);
261
330
  var selectItem = useCallback(function (item) {
262
331
  commitItem(item);
263
- if (canUseMobileDropdownTimePicker) {
264
- closeOpenedDropdown();
265
- }
266
- else {
267
- closeDropdown();
268
- }
269
- }, [canUseMobileDropdownTimePicker, closeDropdown, closeOpenedDropdown, commitItem]);
270
- var isSelectHighlightedItem = useCallback(function () {
332
+ closeOpenedDropdown();
333
+ }, [closeOpenedDropdown, commitItem]);
334
+ var isSelectHighlightedItem = useCallback(function (event) {
335
+ var _a, _b;
271
336
  if (highlightedItemIndex === null) {
272
337
  return false;
273
338
  }
274
- var resolvedItem = resolvedItems[highlightedItemIndex];
275
- if (!resolvedItem || !isTimeMenuItem(resolvedItem) || resolvedItem.disabled) {
339
+ var resolvedItem = menuItems[highlightedItemIndex];
340
+ if (!resolvedItem) {
341
+ return true;
342
+ }
343
+ if (isNavigableMenuElement(resolvedItem)) {
344
+ (_b = (_a = resolvedItem.props).onClick) === null || _b === void 0 ? void 0 : _b.call(_a, event);
345
+ return true;
346
+ }
347
+ if (!isTimeMenuItem(resolvedItem) || resolvedItem.disabled) {
276
348
  return true;
277
349
  }
278
350
  selectItem(resolvedItem.item);
279
351
  return true;
280
- }, [highlightedItemIndex, resolvedItems, selectItem]);
281
- var focusInput = useCallback(function () {
352
+ }, [highlightedItemIndex, menuItems, selectItem]);
353
+ var focusInput = useCallback(function (options) {
282
354
  var _a;
355
+ if (disabled) {
356
+ return;
357
+ }
358
+ shouldSkipOpenOnFocusRef.current = Boolean(options === null || options === void 0 ? void 0 : options.withoutOpenDropdown);
283
359
  (_a = getActiveInput()) === null || _a === void 0 ? void 0 : _a.focus();
284
- }, [getActiveInput]);
285
- var blurInput = useCallback(function () {
360
+ shouldSkipOpenOnFocusRef.current = false;
361
+ }, [disabled, getActiveInput]);
362
+ /**
363
+ * Открывает выпадающее меню так же, как это делает пользователь: через фокус в поле.
364
+ * Меню закрывается по потере фокуса, поэтому открытое без фокуса меню было бы нечем закрыть.
365
+ * В режиме нативного пикера открывается системный пикер — своего меню в этом режиме нет.
366
+ */
367
+ var openInputDropdown = useCallback(function () {
286
368
  var _a;
287
- if (canUseMobileDropdownTimePicker && isDropdownOpened) {
369
+ if (disabled) {
370
+ return;
371
+ }
372
+ if (canUseMobileNativeTimePicker) {
373
+ openNativeTimePicker();
374
+ return;
375
+ }
376
+ (_a = getActiveInput()) === null || _a === void 0 ? void 0 : _a.focus();
377
+ openDropdown();
378
+ }, [canUseMobileNativeTimePicker, disabled, getActiveInput, openDropdown, openNativeTimePicker]);
379
+ var blurInput = useCallback(function () {
380
+ var _a, _b;
381
+ if (isMobileDropdownActive) {
288
382
  closeOpenedDropdown();
289
383
  return;
290
384
  }
291
- (_a = getActiveInput()) === null || _a === void 0 ? void 0 : _a.blur();
292
- }, [canUseMobileDropdownTimePicker, closeOpenedDropdown, getActiveInput, isDropdownOpened]);
293
- var closeInputDropdown = useCallback(function () {
294
- closeOpenedDropdown();
295
- }, [closeOpenedDropdown]);
296
- var blinkInput = useCallback(function () {
297
- var _a;
298
- (_a = getActiveInput()) === null || _a === void 0 ? void 0 : _a.blink();
299
- }, [getActiveInput]);
385
+ // В нативном режиме фокус держит скрытый инпут системного пикера.
386
+ (_a = nativeInputRef.current) === null || _a === void 0 ? void 0 : _a.blur();
387
+ (_b = getActiveInput()) === null || _b === void 0 ? void 0 : _b.blur();
388
+ }, [closeOpenedDropdown, getActiveInput, isMobileDropdownActive]);
300
389
  useImperativeHandle(ref, function () { return ({
301
390
  focus: focusInput,
302
391
  blur: blurInput,
303
392
  blink: blinkInput,
304
- close: closeInputDropdown,
393
+ open: openInputDropdown,
394
+ close: closeOpenedDropdown,
305
395
  getRootNode: function () { return rootRef.current; },
306
- }); }, [blinkInput, blurInput, closeInputDropdown, focusInput, getActiveInput]);
396
+ }); }, [blinkInput, blurInput, closeOpenedDropdown, focusInput, openInputDropdown]);
307
397
  useLayoutEffect(function () {
308
398
  if (!isDropdownOpened) {
309
399
  return;
@@ -317,82 +407,170 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
317
407
  scrollSelectedItemIntoView(targetNode);
318
408
  }
319
409
  }, [highlightedItemIndex, isDropdownOpened, selectedItemIndex]);
410
+ /**
411
+ * Блокировка контрола посреди ввода завершает его так же, как потеря фокуса:
412
+ * меню закрывается, а недоведенное значение коммитится, а не остается висеть в поле.
413
+ */
320
414
  useEffect(function () {
321
- if (disabled && isDropdownOpened) {
415
+ if (!disabled) {
416
+ return;
417
+ }
418
+ if (isMobileDropdownActive) {
322
419
  closeOpenedDropdown();
420
+ return;
421
+ }
422
+ if (isDropdownOpened) {
423
+ closeDropdown();
424
+ }
425
+ if (isInputFocused) {
426
+ setIsInputFocused(false);
427
+ resetMouseInteractionState();
428
+ commitEditingValue();
323
429
  }
324
- }, [closeOpenedDropdown, disabled, isDropdownOpened]);
430
+ }, [
431
+ closeDropdown,
432
+ closeOpenedDropdown,
433
+ commitEditingValue,
434
+ disabled,
435
+ isDropdownOpened,
436
+ isInputFocused,
437
+ isMobileDropdownActive,
438
+ resetMouseInteractionState,
439
+ ]);
440
+ var previousIsMobileLayoutRef = useRef(isMobileLayout);
441
+ /**
442
+ * Смена верстки переносит открытое меню в нужную версию: как в ComboBox, меню не закрывается.
443
+ * Начатый ввод переезжает вместе с ним, а если переезжать некуда — коммитится, как при потере фокуса.
444
+ */
325
445
  useEffect(function () {
326
- setIsMobileDevice(Boolean(isMobile));
327
- setCanUseMobileNativeTimePicker(useMobileNativeTimePicker && Boolean(isMobile));
328
- }, [useMobileNativeTimePicker]);
329
- var handleCloseMobileDropdown = useCallback(function () {
330
- closeOpenedDropdown();
331
- }, [closeOpenedDropdown]);
446
+ if (previousIsMobileLayoutRef.current === isMobileLayout) {
447
+ return;
448
+ }
449
+ previousIsMobileLayoutRef.current = isMobileLayout;
450
+ // Запомненный blur остался от прошлой верстки: как потеря фокуса контрола он больше не годится.
451
+ mobileBlurEventRef.current = null;
452
+ if (isMobileLayout) {
453
+ // Ввод продолжится в инпуте попапа, только если попап открывается вместе со сменой верстки.
454
+ if (isInputFocused && !isMobileDropdownActive) {
455
+ setIsInputFocused(false);
456
+ resetMouseInteractionState();
457
+ commitEditingValue();
458
+ }
459
+ // В режиме нативного пикера своего меню нет.
460
+ if (isDropdownOpened && canUseMobileNativeTimePicker) {
461
+ closeDropdown();
462
+ }
463
+ return;
464
+ }
465
+ /**
466
+ * Десктопное меню закрывается по потере фокуса поля, поэтому без фокуса в контроле
467
+ * его нечем было бы закрыть: мобильный попап мог быть открыт и без фокуса в своем инпуте.
468
+ * Если же фокус был, его вернет в поле `useTimePickerSelection`.
469
+ */
470
+ if (isDropdownOpened && !isInputFocused) {
471
+ closeDropdown();
472
+ }
473
+ }, [
474
+ canUseMobileNativeTimePicker,
475
+ closeDropdown,
476
+ commitEditingValue,
477
+ isDropdownOpened,
478
+ isInputFocused,
479
+ isMobileDropdownActive,
480
+ isMobileLayout,
481
+ resetMouseInteractionState,
482
+ ]);
483
+ /** Сообщает о фокусе один раз за сессию: переезд фокуса внутри контрола наружу не виден. */
484
+ var emitFocus = function (event) {
485
+ if (isControlFocusedRef.current) {
486
+ return;
487
+ }
488
+ isControlFocusedRef.current = true;
489
+ onFocus === null || onFocus === void 0 ? void 0 : onFocus(event);
490
+ };
332
491
  var handleFocusBySource = function (event, isMobilePopupInput) {
492
+ var shouldSkipOpenDropdown = shouldSkipOpenOnFocusRef.current;
493
+ shouldSkipOpenOnFocusRef.current = false;
494
+ var isFocusMovedInsideControl = isControlFocusedRef.current;
333
495
  if (canUseMobileNativeTimePicker) {
334
- openNativeTimePicker();
335
- onFocus === null || onFocus === void 0 ? void 0 : onFocus(event);
496
+ // Фокус сообщается до открытия пикера: иначе увод фокуса в нативный инпут закрыл бы сессию.
497
+ emitFocus(event);
498
+ if (!shouldSkipOpenDropdown) {
499
+ openNativeTimePicker();
500
+ }
336
501
  return;
337
502
  }
338
503
  if (canUseMobileDropdownTimePicker && !isMobilePopupInput) {
339
- openDropdown();
504
+ if (!shouldSkipOpenDropdown) {
505
+ openDropdown();
506
+ }
507
+ // Дальше ввод идет в инпуте попапа, но фокус контрол получает здесь.
508
+ emitFocus(event);
340
509
  return;
341
510
  }
342
511
  setIsInputFocused(true);
343
- openDropdown();
344
- if (isMouseDownOnFragmentRef.current && !isInputFocused) {
345
- if (isTimeDisplayEmpty(editingValueRef.current)) {
346
- selectSegment('hours');
347
- }
348
- else if (mouseDownSegmentRef.current) {
349
- selectSegment(mouseDownSegmentRef.current);
350
- }
512
+ if (!shouldSkipOpenDropdown) {
513
+ openDropdown();
514
+ }
515
+ if (mouseDownSegmentRef.current && !isInputFocused) {
516
+ // По пустому полю ввод всегда начинается с часов, а по заполненному — с сегмента под курсором.
517
+ selectSegment(isTimeDisplayEmpty(editingValueRef.current) ? 'hours' : mouseDownSegmentRef.current);
351
518
  }
352
519
  else {
353
- selectSegment('hours');
520
+ // Переезд фокуса внутри контрола продолжает начатый ввод, поэтому сегмент сохраняется:
521
+ // иначе следующая цифра ушла бы в часы и затерла бы набранное время.
522
+ selectSegment(isFocusMovedInsideControl ? selectedSegment : 'hours');
354
523
  }
355
- onFocus === null || onFocus === void 0 ? void 0 : onFocus(event);
524
+ emitFocus(event);
356
525
  };
357
526
  var handleFocus = function (event) { return handleFocusBySource(event, false); };
358
527
  var handleMobileFocus = function (event) { return handleFocusBySource(event, true); };
359
528
  var handleBlurBySource = function (event, isMobilePopupInput) {
360
- if (canUseMobileNativeTimePicker) {
361
- onBlur === null || onBlur === void 0 ? void 0 : onBlur(event);
529
+ /**
530
+ * Пока открыт мобильный попап, фокус ходит между полем и его инпутом: контрол остается активным,
531
+ * поэтому наружу о таком blur сообщать нечего. Событие запоминается на случай,
532
+ * если фокус в инпуте попапа так и не оказался — тогда именно оно уйдет в `onBlur` при закрытии.
533
+ */
534
+ if (isMobileDropdownActive) {
535
+ event.persist();
536
+ mobileBlurEventRef.current = event;
362
537
  return;
363
538
  }
364
- if (canUseMobileDropdownTimePicker && !isMobilePopupInput) {
539
+ if (canUseMobileNativeTimePicker) {
540
+ if (isFocusMovedToNativePicker(event)) {
541
+ return;
542
+ }
543
+ // Ввод в мобильной версии не идет через поле, но незакоммиченное значение могло остаться
544
+ // от десктопной версии, если верстка сменилась посреди ввода.
545
+ if (isInputFocused) {
546
+ setIsInputFocused(false);
547
+ resetMouseInteractionState();
548
+ commitEditingValue();
549
+ }
550
+ emitBlur(event);
365
551
  return;
366
552
  }
553
+ // Инпут закрытого попапа фокуса уже не держит, но его blur может прийти на размонтировании.
367
554
  if (isMobilePopupInput) {
368
- event.persist();
369
- mobileBlurEventRef.current = event;
370
555
  return;
371
556
  }
372
557
  setIsInputFocused(false);
373
558
  resetMouseInteractionState();
374
559
  closeDropdown();
375
560
  commitEditingValue();
376
- onBlur === null || onBlur === void 0 ? void 0 : onBlur(event);
561
+ emitBlur(event);
377
562
  };
378
563
  var handleBlur = function (event) { return handleBlurBySource(event, false); };
379
564
  var handleMobileBlur = function (event) { return handleBlurBySource(event, true); };
380
- var handleClickBySource = function (event, isMobilePopupInput) {
565
+ var handleClick = function (event) {
381
566
  if (canUseMobileNativeTimePicker) {
382
567
  openNativeTimePicker();
383
- onClick === null || onClick === void 0 ? void 0 : onClick(event);
384
- return;
385
568
  }
386
- if (canUseMobileDropdownTimePicker && !isMobilePopupInput) {
569
+ else {
387
570
  openDropdown();
388
- onClick === null || onClick === void 0 ? void 0 : onClick(event);
389
- return;
390
571
  }
391
- openDropdown();
392
572
  onClick === null || onClick === void 0 ? void 0 : onClick(event);
393
573
  };
394
- var handleClick = function (event) { return handleClickBySource(event, false); };
395
- var handleMobileClick = function (event) { return handleClickBySource(event, true); };
396
574
  var handleMouseDownCaptureBySource = function (event, isMobilePopupInput) {
397
575
  var _a, _b;
398
576
  if (canUseMobileNativeTimePicker || (canUseMobileDropdownTimePicker && !isMobilePopupInput)) {
@@ -400,12 +578,10 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
400
578
  }
401
579
  var currentInputRef = isMobilePopupInput ? mobileInputRef : inputRef;
402
580
  var segment = (_b = (_a = currentInputRef.current) === null || _a === void 0 ? void 0 : _a.getSegment(event.target)) !== null && _b !== void 0 ? _b : null;
403
- var isFragment = segment !== null;
404
- if (isInputFocused && !isFragment) {
581
+ if (isInputFocused && segment === null) {
405
582
  event.preventDefault();
406
583
  }
407
584
  isMouseFocusRef.current = !isInputFocused;
408
- isMouseDownOnFragmentRef.current = isFragment;
409
585
  mouseDownSegmentRef.current = segment;
410
586
  };
411
587
  var handleMouseDownCapture = function (event) {
@@ -477,20 +653,24 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
477
653
  }
478
654
  if (shouldHandleAsAllSelection && (isKeyBackspace(event) || isKeyDelete(event))) {
479
655
  event.preventDefault();
656
+ openDropdown();
480
657
  updateEditingValueAfterUserEdit(getEmptyDisplayValue(format));
481
658
  selectSegment('hours');
482
659
  return;
483
660
  }
484
661
  if (DIGIT_REGEXP.test(event.key)) {
485
662
  event.preventDefault();
486
- resetHighlightedItem();
663
+ openDropdown();
487
664
  var nextDigitInputValue = shouldHandleAsAllSelection ? getEmptyDisplayValue(format) : currentDisplayValue;
488
665
  var nextDigitInputSegment = shouldHandleAsAllSelection ? 'hours' : currentSegment;
489
666
  var result = formatDigitToTimeSegment(nextDigitInputValue, nextDigitInputSegment, event.key, format);
667
+ // Отклоненная цифра не меняет ни значение, ни список, поэтому и подсветку трогать нельзя:
668
+ // автоподсветка не восстановится, а Enter выбрал бы не тот элемент, который видит пользователь.
490
669
  if (result.shouldBlink) {
491
670
  handleUnexpectedInput(event.key);
492
671
  return;
493
672
  }
673
+ resetHighlightedItem();
494
674
  updateEditingValueAfterUserEdit(result.nextValue);
495
675
  selectSegment(result.selectedSegment);
496
676
  return;
@@ -538,6 +718,7 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
538
718
  }
539
719
  if (isKeyBackspace(event)) {
540
720
  event.preventDefault();
721
+ openDropdown();
541
722
  var nextDisplayValue = deleteTimeSegmentDigit(currentDisplayValue, currentSegment, format);
542
723
  var nextSegment = currentSegment;
543
724
  if (nextDisplayValue === currentDisplayValue) {
@@ -553,6 +734,7 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
553
734
  }
554
735
  if (isKeyDelete(event)) {
555
736
  event.preventDefault();
737
+ openDropdown();
556
738
  updateEditingValueAfterUserEdit(replaceTimeSegment(currentDisplayValue, currentSegment, EMPTY_SEGMENT, format));
557
739
  selectSegment(currentSegment);
558
740
  return;
@@ -560,29 +742,28 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
560
742
  if (isKeyEscape(event)) {
561
743
  if (isDropdownOpened) {
562
744
  event.preventDefault();
563
- if (canUseMobileDropdownTimePicker) {
564
- closeOpenedDropdown();
565
- }
566
- else {
567
- closeDropdown();
568
- }
745
+ closeOpenedDropdown();
569
746
  }
570
747
  return;
571
748
  }
572
749
  if (isKeyEnter(event)) {
573
- if (isSelectHighlightedItem()) {
750
+ if (isSourceFailed) {
574
751
  event.preventDefault();
752
+ retrySource();
575
753
  return;
576
754
  }
577
- if (canUseMobileDropdownTimePicker && isDropdownOpened) {
755
+ if (isSelectHighlightedItem(event)) {
756
+ event.preventDefault();
757
+ return;
758
+ }
759
+ if (isMobileDropdownActive) {
578
760
  event.preventDefault();
579
761
  closeOpenedDropdown();
580
762
  return;
581
763
  }
582
- // Enter не выбирает элемент, а лишь подтверждает введенное значение,
583
- // поэтому коммитим значение как при потере фокуса и подсвечиваем поле.
764
+ // Enter не выбирает элемент, а лишь подтверждает введенное значение, как потеря фокуса.
584
765
  commitEditingValue();
585
- blinkInputValue();
766
+ blinkInput();
586
767
  if (isDropdownOpened) {
587
768
  event.preventDefault();
588
769
  closeDropdown();
@@ -610,7 +791,7 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
610
791
  var handleKeyDown = function (event) { return handleKeyDownBySource(event, false); };
611
792
  var handleMobileKeyDown = function (event) { return handleKeyDownBySource(event, true); };
612
793
  var handlePasteBySource = function (event, isMobilePopupInput) {
613
- var _a;
794
+ var _a, _b;
614
795
  onPaste === null || onPaste === void 0 ? void 0 : onPaste(event);
615
796
  if (event.defaultPrevented || disabled) {
616
797
  return;
@@ -618,31 +799,32 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
618
799
  if (canUseMobileNativeTimePicker || (canUseMobileDropdownTimePicker && !isMobilePopupInput)) {
619
800
  return;
620
801
  }
621
- var pastedValue = (_a = event.clipboardData) === null || _a === void 0 ? void 0 : _a.getData('text');
622
- var nextDisplayValue = parsePastedTimeValue(pastedValue, format);
802
+ var pastedValue = (_b = (_a = event.clipboardData) === null || _a === void 0 ? void 0 : _a.getData('text')) !== null && _b !== void 0 ? _b : EMPTY_VALUE;
623
803
  event.preventDefault();
804
+ if (pastedValue.trim() === EMPTY_VALUE) {
805
+ return;
806
+ }
807
+ var nextDisplayValue = parsePastedTimeValue(pastedValue, format);
624
808
  if (isTimeDisplayEmpty(nextDisplayValue)) {
625
809
  handleUnexpectedInput(pastedValue);
626
810
  return;
627
811
  }
812
+ openDropdown();
628
813
  updateEditingValueAfterUserEdit(nextDisplayValue);
629
814
  selectSegment('hours');
630
815
  };
631
816
  var handlePaste = function (event) { return handlePasteBySource(event, false); };
632
817
  var handleMobilePaste = function (event) { return handlePasteBySource(event, true); };
633
- var handleNativeValueChange = useCallback(function (nextValue) {
634
- commitValue(nextValue);
635
- }, [commitValue]);
636
818
  return (React.createElement(CommonWrapper, __assign({}, props),
637
819
  React.createElement("span", { ref: rootRef, className: styles.root(), "data-tid": TimePickerDataTids.root },
638
820
  React.createElement(TimeInput, __assign({}, inputProps, { "data-tid": TimePickerDataTids.input, ref: inputRef, disabled: disabled, hasDropdown: hasDropdown, size: size, format: format, corners: corners, rightIcon: inputRightIcon, suffix: suffix, value: displayValue, "aria-haspopup": hasDropdown ? 'listbox' : undefined, "aria-expanded": !isMobileDropdownActive && hasDropdown && isDropdownOpened && hasDropdownContent ? true : undefined, "aria-controls": !isMobileDropdownActive && hasDropdown && isDropdownOpened && hasDropdownContent ? popupId : undefined, "aria-activedescendant": isMobileDropdownActive ? undefined : activeDescendantId, "aria-placeholder": ariaPlaceholder, "aria-busy": isLoadingItems || undefined, onClick: handleClick, onBlur: handleBlur, onFocus: handleFocus, onMouseDownCapture: handleMouseDownCapture, onMouseUp: handleMouseUp, onKeyDown: handleKeyDown, onSelectSegment: handleSelectSegmentByMouse, onPaste: handlePaste })),
639
- canUseMobileNativeTimePicker && (React.createElement(NativeTimeInput, { "data-tid": TimePickerDataTids.nativeInput, disabled: disabled, ref: nativeInputRef, format: format, minTime: minTime, maxTime: maxTime, value: editingValue, onValueChange: handleNativeValueChange })),
640
- canUseMobileDropdownTimePicker && isDropdownOpened && (React.createElement(TimePickerMobilePopup, { id: popupId, opened: isDropdownOpened, value: displayValue, inputRef: mobileInputRef, disabled: disabled, format: format, size: size, rightIcon: inputRightIcon, suffix: suffix, resolvedItems: resolvedItems, renderItem: renderItem, isLoading: isLoadingItems, highlightedItemIndex: highlightedItemIndex, normalizedValue: normalizedValue, itemRefs: itemRefs, error: props.error, warning: props.warning, "aria-describedby": props['aria-describedby'], "aria-label": props['aria-label'], "aria-placeholder": ariaPlaceholder, "aria-haspopup": 'listbox', "aria-expanded": hasDropdownContent ? true : undefined, "aria-controls": hasDropdownContent ? popupId : undefined, "aria-activedescendant": activeDescendantId, onFocus: handleMobileFocus, onBlur: handleMobileBlur, onClick: handleMobileClick, onKeyDown: handleMobileKeyDown, onMouseDownCapture: handleMobileMouseDownCapture, onMouseUp: handleMobileMouseUp, onPaste: handleMobilePaste, onSelectSegment: handleMobileSelectSegmentByMouse, onSelectItem: selectItem, onCloseRequest: handleCloseMobileDropdown })),
821
+ canUseMobileNativeTimePicker && (React.createElement(NativeTimeInput, { "data-tid": TimePickerDataTids.nativeInput, disabled: disabled, ref: nativeInputRef, format: format, minTime: minTime, maxTime: maxTime, value: editingValue, onValueChange: commitValue, onBlur: emitBlur })),
822
+ isMobileDropdownActive && (React.createElement(TimePickerMobilePopup, { id: popupId, value: displayValue, inputRef: mobileInputRef, disabled: disabled, format: format, size: size, rightIcon: inputRightIcon, suffix: suffix, resolvedItems: resolvedItems, renderItem: renderItem, isLoading: isLoadingItems, isFailed: isSourceFailed, errorNetworkButton: locale.errorNetworkButton, errorNetworkMessage: locale.errorNetworkMessage, highlightedItemIndex: highlightedItemIndex, selectedValue: selectedValue, itemRefs: itemRefs, error: props.error, warning: props.warning, "aria-describedby": props['aria-describedby'], "aria-label": props['aria-label'], "aria-placeholder": ariaPlaceholder, "aria-haspopup": 'listbox', "aria-expanded": resolvedItems.length > 0 || isLoadingItems || isSourceFailed ? true : undefined, "aria-controls": resolvedItems.length > 0 || isLoadingItems || isSourceFailed ? popupId : undefined, "aria-activedescendant": activeDescendantId, onFocus: handleMobileFocus, onBlur: handleMobileBlur, onClick: handleClick, onKeyDown: handleMobileKeyDown, onMouseDownCapture: handleMobileMouseDownCapture, onMouseUp: handleMobileMouseUp, onPaste: handleMobilePaste, onSelectSegment: handleMobileSelectSegmentByMouse, onRetry: retrySource, onSelectItem: selectItem, onCloseRequest: closeOpenedDropdown })),
641
823
  hasDropdown &&
642
824
  !canUseMobileNativeTimePicker &&
643
825
  !canUseMobileDropdownTimePicker &&
644
826
  isDropdownOpened &&
645
827
  hasDropdownContent &&
646
- rootRef.current && (React.createElement(TimePickerPopup, { id: popupId, anchorElement: rootRef.current, menuPos: menuPos, menuAlign: menuAlign, menuWidth: menuWidth, popupMaxHeight: popupMaxHeight, format: format, size: size, resolvedItems: resolvedItems, renderItem: renderItem, isLoading: isLoadingItems, highlightedItemIndex: highlightedItemIndex, normalizedValue: normalizedValue, itemRefs: itemRefs, onSelectItem: selectItem })))));
828
+ rootRef.current && (React.createElement(TimePickerPopup, { id: popupId, anchorElement: rootRef.current, menuPos: menuPos, menuAlign: menuAlign, menuWidth: menuWidth, popupMaxHeight: popupMaxHeight, format: format, size: size, resolvedItems: resolvedItems, renderItem: renderItem, isLoading: isLoadingItems, isFailed: isSourceFailed, errorNetworkButton: locale.errorNetworkButton, errorNetworkMessage: locale.errorNetworkMessage, highlightedItemIndex: highlightedItemIndex, selectedValue: selectedValue, itemRefs: itemRefs, onRetry: retrySource, onSelectItem: selectItem })))));
647
829
  }), { validate: validateTimePicker });
648
830
  //# sourceMappingURL=TimePicker.js.map