@skbkontur/react-ui 6.1.7-c386f.0 → 6.1.7-e032b.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 (58) hide show
  1. package/components/Button/Button.d.ts +1 -1
  2. package/components/Button/Button.js +4 -7
  3. package/components/Button/Button.js.map +1 -1
  4. package/components/FxInput/FxInputRestoreBtn.js +1 -1
  5. package/components/FxInput/FxInputRestoreBtn.js.map +1 -1
  6. package/components/Switcher/Switcher.js +0 -2
  7. package/components/Switcher/Switcher.js.map +1 -1
  8. package/components/TimePicker/TimePicker.d.ts +24 -23
  9. package/components/TimePicker/TimePicker.js +196 -132
  10. package/components/TimePicker/TimePicker.js.map +1 -1
  11. package/components/TimePicker/TimePickerItems.d.ts +1 -1
  12. package/components/TimePicker/TimePickerItems.js +5 -8
  13. package/components/TimePicker/TimePickerItems.js.map +1 -1
  14. package/components/TimePicker/TimePickerMobilePopup.d.ts +1 -2
  15. package/components/TimePicker/TimePickerMobilePopup.js +3 -3
  16. package/components/TimePicker/TimePickerMobilePopup.js.map +1 -1
  17. package/components/TimePicker/TimePickerPopup.d.ts +1 -1
  18. package/components/TimePicker/TimePickerPopup.js +2 -2
  19. package/components/TimePicker/TimePickerPopup.js.map +1 -1
  20. package/components/TimePicker/helpers/TimePicker.constants.d.ts +2 -1
  21. package/components/TimePicker/helpers/TimePicker.constants.js +2 -7
  22. package/components/TimePicker/helpers/TimePicker.constants.js.map +1 -1
  23. package/components/TimePicker/helpers/TimePicker.editing.d.ts +1 -5
  24. package/components/TimePicker/helpers/TimePicker.editing.js +8 -35
  25. package/components/TimePicker/helpers/TimePicker.editing.js.map +1 -1
  26. package/components/TimePicker/helpers/TimePicker.shared.d.ts +13 -24
  27. package/components/TimePicker/helpers/TimePicker.shared.js +13 -27
  28. package/components/TimePicker/helpers/TimePicker.shared.js.map +1 -1
  29. package/components/TimePicker/helpers/TimePicker.value.d.ts +3 -13
  30. package/components/TimePicker/helpers/TimePicker.value.js +13 -43
  31. package/components/TimePicker/helpers/TimePicker.value.js.map +1 -1
  32. package/components/TimePicker/helpers/scrollSelectedItemIntoView.d.ts +2 -0
  33. package/components/TimePicker/helpers/scrollSelectedItemIntoView.js +2 -0
  34. package/components/TimePicker/helpers/scrollSelectedItemIntoView.js.map +1 -1
  35. package/components/TimePicker/helpers/validateTimePicker.js +2 -4
  36. package/components/TimePicker/helpers/validateTimePicker.js.map +1 -1
  37. package/components/TimePicker/hooks/useTimePickerDropdown.js +9 -25
  38. package/components/TimePicker/hooks/useTimePickerDropdown.js.map +1 -1
  39. package/components/TimePicker/hooks/useTimePickerSource.js +2 -1
  40. package/components/TimePicker/hooks/useTimePickerSource.js.map +1 -1
  41. package/components/TimePicker/hooks/useTimePickerValue.d.ts +9 -4
  42. package/components/TimePicker/hooks/useTimePickerValue.js +22 -23
  43. package/components/TimePicker/hooks/useTimePickerValue.js.map +1 -1
  44. package/components/TimePicker/index.d.ts +1 -1
  45. package/components/TimePicker/index.js.map +1 -1
  46. package/internal/NativeTimeInput/NativeTimeInput.utils.d.ts +2 -0
  47. package/internal/NativeTimeInput/NativeTimeInput.utils.js +4 -8
  48. package/internal/NativeTimeInput/NativeTimeInput.utils.js.map +1 -1
  49. package/internal/TimeInput/TimeFragments.d.ts +0 -1
  50. package/internal/TimeInput/TimeFragments.js +27 -6
  51. package/internal/TimeInput/TimeFragments.js.map +1 -1
  52. package/internal/TimeInput/TimeFragments.styles.d.ts +14 -0
  53. package/internal/TimeInput/TimeFragments.styles.js +36 -9
  54. package/internal/TimeInput/TimeFragments.styles.js.map +1 -1
  55. package/internal/TimeInput/TimeInput.d.ts +0 -1
  56. package/internal/TimeInput/TimeInput.js +0 -1
  57. package/internal/TimeInput/TimeInput.js.map +1 -1
  58. package/package.json +1 -1
@@ -25,20 +25,21 @@ import { CommonWrapper } from '../../internal/CommonWrapper/index.js';
25
25
  import { LoadingIcon } from '../../internal/icons2022/LoadingIcon.js';
26
26
  import { NativeTimeInput } from '../../internal/NativeTimeInput/index.js';
27
27
  import { TimeInput } from '../../internal/TimeInput/index.js';
28
- import { isIOS, isMobile } from '../../lib/client.js';
28
+ import { isIOS } from '../../lib/client.js';
29
29
  import { isKeyArrowDown, isKeyArrowLeft, isKeyArrowRight, isKeyArrowUp, isKeyBackspace, isKeyChar, isKeyDelete, isKeyEnter, isKeyEscape, isKeySpace, isKeyTab, isModShift, isShortcutSelectAll, } from '../../lib/events/keyboard/identifiers.js';
30
30
  import { forwardRefAndName } from '../../lib/forwardRefAndName.js';
31
31
  import { useLocaleForControl } from '../../lib/locale/useLocaleForControl.js';
32
32
  import { useStyles } from '../../lib/renderEnvironment/index.js';
33
33
  import { ThemeContext } from '../../lib/theming/ThemeContext.js';
34
34
  import { getRandomID } from '../../lib/utils.js';
35
+ import { useResponsiveLayout } from '../ResponsiveLayout/index.js';
35
36
  import { scrollSelectedItemIntoView } from './helpers/scrollSelectedItemIntoView.js';
36
37
  import { DIGIT_REGEXP, EMPTY_SEGMENT, EMPTY_VALUE, TimePickerDataTids } from './helpers/TimePicker.constants.js';
37
38
  import { commitTimeSegmentOnLeave, deleteTimeSegmentDigit, formatDigitToTimeSegment, shiftTimeSegmentValue, } from './helpers/TimePicker.editing.js';
38
39
  import { getTimePickerPopupMaxHeight } from './helpers/TimePicker.layout.js';
39
40
  import { getNextTimeSegment, getPreviousTimeSegment } from './helpers/TimePicker.selection.js';
40
41
  import { isNavigableMenuElement, 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 { filterTimeItems, getEmptyDisplayValue, getTimeFilterQuery, getTimeValueString, isTimeDisplayEmpty, normalizeTimeValue, parsePastedTimeValue, replaceTimeSegment, resolveTimeItems, } from './helpers/TimePicker.value.js';
42
43
  import { validateTimePicker } from './helpers/validateTimePicker.js';
43
44
  import { useTimePickerDropdown } from './hooks/useTimePickerDropdown.js';
44
45
  import { useTimePickerSelection } from './hooks/useTimePickerSelection.js';
@@ -67,17 +68,27 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
67
68
  var rootRef = useRef(null);
68
69
  var itemRefs = useRef(new Map());
69
70
  var isMouseFocusRef = useRef(false);
70
- var isMouseDownOnFragmentRef = useRef(false);
71
+ /** Сегмент, по которому нажали мышью, или `null`, если нажали мимо сегментов. */
71
72
  var mouseDownSegmentRef = useRef(null);
72
73
  var mobileBlurEventRef = useRef(null);
73
74
  var shouldSkipOpenOnFocusRef = useRef(false);
75
+ /**
76
+ * Контрол считает себя сфокусированным: о фокусе сообщено наружу, а сессия ввода не закончена.
77
+ * Внутри одной сессии фокус может переезжать между полем и инпутом мобильного попапа —
78
+ * например вместе со сменой верстки, — и такой переезд не начинает сессию заново.
79
+ */
80
+ var isControlFocusedRef = useRef(false);
74
81
  var popupIdRef = useRef(TimePickerDataTids.popup + getRandomID());
75
- var _f = useState(false), isMobileDevice = _f[0], setIsMobileDevice = _f[1];
76
- var _g = useState(false), canUseMobileNativeTimePicker = _g[0], setCanUseMobileNativeTimePicker = _g[1];
82
+ /**
83
+ * Мобильная версия включается медиавыражением темы, как в ComboBox:
84
+ * поле переключается между мобильной и десктопной версией вместе с версткой страницы.
85
+ */
86
+ var isMobileLayout = useResponsiveLayout().isMobile;
87
+ var canUseMobileNativeTimePicker = useMobileNativeTimePicker && isMobileLayout;
77
88
  var isSourceFunction = typeof source === 'function';
78
89
  var hasDropdown = isSourceFunction || (Array.isArray(source) && source.length > 0);
79
- var _h = useState(false), isDropdownOpened = _h[0], setIsDropdownOpened = _h[1];
80
- var _j = useState(false), isEditedAfterOpen = _j[0], setIsEditedAfterOpen = _j[1];
90
+ var _f = useState(false), isDropdownOpened = _f[0], setIsDropdownOpened = _f[1];
91
+ var _g = useState(false), isEditedAfterOpen = _g[0], setIsEditedAfterOpen = _g[1];
81
92
  /**
82
93
  * Форма элементов функции-источника: последняя, о которой компонент знает.
83
94
  * Форма запоминается, потому что элементы функции очищаются при закрытии меню,
@@ -94,28 +105,62 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
94
105
  var shouldBuildItemAsObjectRef = useRef(false);
95
106
  var valueToItemRef = useRef(valueToItem);
96
107
  valueToItemRef.current = valueToItem;
97
- var handleCommitValue = useCallback(function (committedValue, item) {
108
+ var handleCommitValue = useCallback(function (nextValue, item) {
98
109
  var _a, _b;
99
110
  if (!onValueChange) {
100
111
  return;
101
112
  }
102
- if (committedValue === EMPTY_VALUE) {
113
+ if (nextValue === EMPTY_VALUE) {
103
114
  onValueChange(null, EMPTY_VALUE);
104
115
  return;
105
116
  }
106
- 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);
107
- onValueChange(nextItem, committedValue);
117
+ 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);
118
+ onValueChange(nextItem, nextValue);
108
119
  }, [onValueChange]);
109
- var _k = useTimePickerValue({
120
+ var onBlurRef = useRef(onBlur);
121
+ onBlurRef.current = onBlur;
122
+ var pendingBlurEventRef = useRef(null);
123
+ var _h = useState(0), blurRequestId = _h[0], setBlurRequestId = _h[1];
124
+ /**
125
+ * Завершает сессию фокуса и откладывает `onBlur` до конца рендера, как это делает ComboBox.
126
+ * Значение коммитится синхронно с потерей фокуса, поэтому вызванный тут же `onBlur`
127
+ * попал бы в обработчик с состоянием родителя до изменения значения — например с пустым временем.
128
+ * Событие может отсутствовать: сессия все равно закрывается, но наружу ничего не уходит.
129
+ */
130
+ var emitBlur = useCallback(function (event) {
131
+ if (!isControlFocusedRef.current) {
132
+ return;
133
+ }
134
+ isControlFocusedRef.current = false;
135
+ if (!event || !onBlurRef.current) {
136
+ return;
137
+ }
138
+ event.persist();
139
+ pendingBlurEventRef.current = event;
140
+ setBlurRequestId(function (currentId) { return currentId + 1; });
141
+ }, []);
142
+ var flushPendingBlur = useCallback(function () {
143
+ var _a;
144
+ var pendingBlurEvent = pendingBlurEventRef.current;
145
+ if (!pendingBlurEvent) {
146
+ return;
147
+ }
148
+ pendingBlurEventRef.current = null;
149
+ (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, pendingBlurEvent);
150
+ }, []);
151
+ useEffect(flushPendingBlur, [blurRequestId, flushPendingBlur]);
152
+ /** Родитель мог убрать поле в ответ на коммит значения — отложенное событие все равно должно уйти. */
153
+ useEffect(function () { return flushPendingBlur; }, [flushPendingBlur]);
154
+ var _j = useTimePickerValue({
110
155
  isInputFocused: isInputFocused,
111
156
  format: format,
112
157
  externalValue: getTimeValueString(value),
113
158
  isValueControlled: value !== undefined,
114
159
  onCommitValue: handleCommitValue,
115
160
  onInputValueChange: onInputValueChange,
116
- }), editingValue = _k.editingValue, editingValueRef = _k.editingValueRef, updateEditingValue = _k.updateEditingValue, commitEditingValue = _k.commitEditingValue, commitValue = _k.commitValue, commitItem = _k.commitItem;
161
+ }), editingValue = _j.editingValue, editingValueRef = _j.editingValueRef, committedValue = _j.committedValue, updateEditingValue = _j.updateEditingValue, commitEditingValue = _j.commitEditingValue, commitValue = _j.commitValue, commitItem = _j.commitItem;
117
162
  var filterQuery = isEditedAfterOpen ? getTimeFilterQuery(editingValue, format) : EMPTY_VALUE;
118
- var _l = useTimePickerSource({ source: source, format: format, isDropdownOpened: isDropdownOpened, filterQuery: filterQuery }), sourceValueItems = _l.items, fetchedItemsQuery = _l.fetchedQuery, isLoadingItems = _l.isLoading;
163
+ var _k = useTimePickerSource({ source: source, format: format, isDropdownOpened: isDropdownOpened, filterQuery: filterQuery }), sourceValueItems = _k.items, fetchedItemsQuery = _k.fetchedQuery, isLoadingItems = _k.isLoading;
119
164
  useEffect(function () {
120
165
  if (!isSourceFunction || sourceValueItems.some(isTimeItem)) {
121
166
  lastSourceItemShapeRef.current = sourceValueItems.some(function (item) { return isTimeItem(item) && isTimeItemObject(item); });
@@ -123,17 +168,17 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
123
168
  shouldBuildItemAsObjectRef.current =
124
169
  lastSourceItemShapeRef.current === true || (value !== null && value !== undefined && isTimeItemObject(value));
125
170
  }, [isSourceFunction, sourceValueItems, value]);
126
- var filteredItems = useMemo(function () { return filterTimeItems(dedupeTimeItems(sourceValueItems, format), filterQuery, format); }, [filterQuery, format, sourceValueItems]);
127
- /**
128
- * Элементы для отрисовки: время и подпись достаются из элемента,
129
- * а `disabled` дополняется проверкой диапазона `minTime`/`maxTime`.
130
- * Исходный элемент сохраняется как есть — именно он уходит в `onValueChange`.
131
- */
132
- var resolvedItems = useMemo(function () { return resolveTimeItems(filteredItems, format, minTime, maxTime); }, [maxTime, minTime, format, filteredItems]);
171
+ var resolvedItems = useMemo(function () { return resolveTimeItems(filterTimeItems(sourceValueItems, filterQuery, format), format, minTime, maxTime); }, [filterQuery, format, maxTime, minTime, sourceValueItems]);
133
172
  var popupMaxHeight = getTimePickerPopupMaxHeight(size, theme);
134
173
  var ariaPlaceholder = format === 'HH:mm' ? locale.ariaPlaceholderHHMM : locale.ariaPlaceholderHHMMSS;
135
- var normalizedValue = normalizeTimeValue(editingValue, format);
136
- var selectedItemIndex = resolvedItems.findIndex(function (item) { return isTimeMenuItem(item) && normalizeTimeValue(item.value, format) === normalizedValue; });
174
+ /**
175
+ * Время, отмеченное в меню выбранным.
176
+ * Пока пользователь не начал править значение при открытом меню, это то же время, что в поле.
177
+ * С началом правки отмечено остается закоммиченное значение: набор фильтра — например `10` —
178
+ * не должен отмечать элемент `10:00` как уже выбранный.
179
+ */
180
+ var selectedValue = normalizeTimeValue(isEditedAfterOpen ? committedValue : editingValue, format);
181
+ var selectedItemIndex = resolvedItems.findIndex(function (item) { return isTimeMenuItem(item) && normalizeTimeValue(item.value, format) === selectedValue; });
137
182
  var autoHighlightKey = null;
138
183
  if (filterQuery !== EMPTY_VALUE) {
139
184
  if (isSourceFunction) {
@@ -145,7 +190,9 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
145
190
  autoHighlightKey = "source-array:".concat(filterQuery);
146
191
  }
147
192
  }
148
- var canUseMobileDropdownTimePicker = hasDropdown && isMobileDevice && !canUseMobileNativeTimePicker;
193
+ var canUseMobileDropdownTimePicker = hasDropdown && isMobileLayout && !canUseMobileNativeTimePicker;
194
+ /** Ввод идет в мобильном попапе: он открыт, а нативного пикера в этом режиме нет. */
195
+ var isMobileDropdownActive = canUseMobileDropdownTimePicker && isDropdownOpened;
149
196
  /**
150
197
  * Меню с одними лишь заголовками и разделителями показывать не о чем,
151
198
  * поэтому при активном фильтре нужен хотя бы один подошедший элемент со временем.
@@ -159,16 +206,16 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
159
206
  var menuItems = hasDropdownContent ? resolvedItems : EMPTY_MENU_ITEMS;
160
207
  var inputRightIcon = isLoadingItems && resolvedItems.length > 0 ? (React.createElement("span", { "data-tid": TimePickerDataTids.inputLoading },
161
208
  React.createElement(LoadingIcon, { size: size }))) : (rightIcon);
162
- var displayValue = editingValue;
163
- if (isInputFocused) {
164
- displayValue = editingValue === EMPTY_VALUE ? getEmptyDisplayValue(format) : editingValue;
165
- }
166
- else if (isTimeDisplayEmpty(editingValue)) {
167
- displayValue = EMPTY_VALUE;
168
- }
209
+ /** Сфокусированное поле всегда показывает сегменты, а пустое без фокуса — ничего. */
210
+ var getDisplayValue = function () {
211
+ if (isInputFocused) {
212
+ return editingValue || getEmptyDisplayValue(format);
213
+ }
214
+ return isTimeDisplayEmpty(editingValue) ? EMPTY_VALUE : editingValue;
215
+ };
216
+ var displayValue = getDisplayValue();
169
217
  var resetMouseInteractionState = useCallback(function () {
170
218
  isMouseFocusRef.current = false;
171
- isMouseDownOnFragmentRef.current = false;
172
219
  mouseDownSegmentRef.current = null;
173
220
  }, []);
174
221
  var updateEditingValueAfterUserEdit = useCallback(function (nextEditingValue) {
@@ -193,7 +240,7 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
193
240
  (_b = nativeInputRef.current) === null || _b === void 0 ? void 0 : _b.click();
194
241
  }
195
242
  }, [canUseMobileNativeTimePicker, disabled]);
196
- var _m = useTimePickerDropdown({
243
+ var _l = useTimePickerDropdown({
197
244
  disabled: disabled,
198
245
  hasDropdown: hasDropdown,
199
246
  items: menuItems,
@@ -201,7 +248,7 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
201
248
  autoHighlightKey: autoHighlightKey,
202
249
  isDropdownOpened: isDropdownOpened,
203
250
  setIsDropdownOpened: setIsDropdownOpened,
204
- }), highlightedItemIndex = _m.highlightedItemIndex, openDropdown = _m.openDropdown, closeDropdown = _m.closeDropdown, resetHighlightedItem = _m.resetHighlightedItem, tryNavigateItems = _m.tryNavigateItems;
251
+ }), highlightedItemIndex = _l.highlightedItemIndex, openDropdown = _l.openDropdown, closeDropdown = _l.closeDropdown, resetHighlightedItem = _l.resetHighlightedItem, tryNavigateItems = _l.tryNavigateItems;
205
252
  /**
206
253
  * Фильтрация начинается только после правки значения при открытом меню.
207
254
  * Состояние живет ровно столько, сколько открыто меню: правка, которая заново открывает меню,
@@ -215,33 +262,26 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
215
262
  var popupId = popupIdRef.current;
216
263
  var highlightedMenuItem = highlightedItemIndex === null ? undefined : menuItems[highlightedItemIndex];
217
264
  /** Кастомный `MenuItem` может принести свой `id` — тогда указывать нужно именно на него. */
218
- var activeDescendantId = hasDropdown && highlightedItemIndex !== null && highlightedMenuItem
265
+ var activeDescendantId = highlightedMenuItem
219
266
  ? ((_a = (isNavigableMenuElement(highlightedMenuItem) ? highlightedMenuItem.props.id : undefined)) !== null && _a !== void 0 ? _a : "".concat(popupId, "-item-").concat(highlightedItemIndex))
220
267
  : undefined;
221
- var isMobileDropdownActive = canUseMobileDropdownTimePicker && isDropdownOpened;
222
- var getActiveInput = useCallback(function () { return (canUseMobileDropdownTimePicker && isDropdownOpened ? mobileInputRef.current : inputRef.current); }, [canUseMobileDropdownTimePicker, isDropdownOpened]);
223
- var _o = useTimePickerSelection({
268
+ var getActiveInput = useCallback(function () { return (isMobileDropdownActive ? mobileInputRef.current : inputRef.current); }, [isMobileDropdownActive]);
269
+ var _m = useTimePickerSelection({
224
270
  isInputFocused: isInputFocused,
225
271
  format: format,
226
272
  displayValue: displayValue,
227
273
  getInput: getActiveInput,
228
- }), selection = _o.selection, selectedSegment = _o.selectedSegment, selectSegment = _o.selectSegment, selectAll = _o.selectAll, syncSelectionWithDOM = _o.syncSelectionWithDOM;
229
- var blinkInputValue = useCallback(function () {
274
+ }), selection = _m.selection, selectedSegment = _m.selectedSegment, selectSegment = _m.selectSegment, selectAll = _m.selectAll, syncSelectionWithDOM = _m.syncSelectionWithDOM;
275
+ var blinkInput = useCallback(function () {
230
276
  var _a;
231
277
  (_a = getActiveInput()) === null || _a === void 0 ? void 0 : _a.blink();
232
278
  }, [getActiveInput]);
233
- /**
234
- * Сообщает о некорректном вводе: нажатой клавише или вставленном значении, которое не может быть временем.
235
- * Если обработчик не задан, поле вспыхивает само.
236
- * Обработчик может подставить значение вместо некорректного ввода:
237
- * элемент коммитится как есть, `null` очищает поле, а `undefined` оставляет значение прежним.
238
- */
239
279
  var handleUnexpectedInput = useCallback(function (unexpectedValue) {
240
280
  if (!onUnexpectedInput) {
241
- blinkInputValue();
281
+ blinkInput();
242
282
  return;
243
283
  }
244
- var returnedValue = onUnexpectedInput(unexpectedValue, blinkInputValue);
284
+ var returnedValue = onUnexpectedInput(unexpectedValue, blinkInput);
245
285
  if (returnedValue === undefined) {
246
286
  return;
247
287
  }
@@ -250,40 +290,31 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
250
290
  return;
251
291
  }
252
292
  commitItem(returnedValue);
253
- }, [blinkInputValue, commitItem, commitValue, onUnexpectedInput]);
293
+ }, [blinkInput, commitItem, commitValue, onUnexpectedInput]);
294
+ /** Закрывает открытое меню: мобильный попап заодно завершает сессию ввода, как потеря фокуса. */
254
295
  var closeOpenedDropdown = useCallback(function () {
255
- var _a;
256
- if (canUseMobileDropdownTimePicker && isDropdownOpened) {
296
+ var _a, _b;
297
+ if (isMobileDropdownActive) {
298
+ // Фокус может остаться в любом из двух полей: инпут попапа не всегда получает его —
299
+ // например мобильный браузер отказывается переводить фокус без жеста пользователя.
300
+ // Гасим оба, чтобы закрытие попапа всегда давало событие потери фокуса.
257
301
  (_a = mobileInputRef.current) === null || _a === void 0 ? void 0 : _a.blur();
302
+ (_b = inputRef.current) === null || _b === void 0 ? void 0 : _b.blur();
258
303
  setIsInputFocused(false);
259
304
  resetMouseInteractionState();
260
305
  closeDropdown();
261
306
  commitEditingValue();
262
307
  var blurEvent = mobileBlurEventRef.current;
263
308
  mobileBlurEventRef.current = null;
264
- if (blurEvent) {
265
- onBlur === null || onBlur === void 0 ? void 0 : onBlur(blurEvent);
266
- }
309
+ emitBlur(blurEvent);
267
310
  return;
268
311
  }
269
312
  closeDropdown();
270
- }, [
271
- canUseMobileDropdownTimePicker,
272
- closeDropdown,
273
- commitEditingValue,
274
- isDropdownOpened,
275
- onBlur,
276
- resetMouseInteractionState,
277
- ]);
313
+ }, [closeDropdown, commitEditingValue, emitBlur, isMobileDropdownActive, resetMouseInteractionState]);
278
314
  var selectItem = useCallback(function (item) {
279
315
  commitItem(item);
280
- if (canUseMobileDropdownTimePicker) {
281
- closeOpenedDropdown();
282
- }
283
- else {
284
- closeDropdown();
285
- }
286
- }, [canUseMobileDropdownTimePicker, closeDropdown, closeOpenedDropdown, commitItem]);
316
+ closeOpenedDropdown();
317
+ }, [closeOpenedDropdown, commitItem]);
287
318
  var isSelectHighlightedItem = useCallback(function (event) {
288
319
  var _a, _b;
289
320
  if (highlightedItemIndex === null) {
@@ -331,27 +362,20 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
331
362
  }, [canUseMobileNativeTimePicker, disabled, getActiveInput, openDropdown, openNativeTimePicker]);
332
363
  var blurInput = useCallback(function () {
333
364
  var _a;
334
- if (canUseMobileDropdownTimePicker && isDropdownOpened) {
365
+ if (isMobileDropdownActive) {
335
366
  closeOpenedDropdown();
336
367
  return;
337
368
  }
338
369
  (_a = getActiveInput()) === null || _a === void 0 ? void 0 : _a.blur();
339
- }, [canUseMobileDropdownTimePicker, closeOpenedDropdown, getActiveInput, isDropdownOpened]);
340
- var closeInputDropdown = useCallback(function () {
341
- closeOpenedDropdown();
342
- }, [closeOpenedDropdown]);
343
- var blinkInput = useCallback(function () {
344
- var _a;
345
- (_a = getActiveInput()) === null || _a === void 0 ? void 0 : _a.blink();
346
- }, [getActiveInput]);
370
+ }, [closeOpenedDropdown, getActiveInput, isMobileDropdownActive]);
347
371
  useImperativeHandle(ref, function () { return ({
348
372
  focus: focusInput,
349
373
  blur: blurInput,
350
374
  blink: blinkInput,
351
375
  open: openInputDropdown,
352
- close: closeInputDropdown,
376
+ close: closeOpenedDropdown,
353
377
  getRootNode: function () { return rootRef.current; },
354
- }); }, [blinkInput, blurInput, closeInputDropdown, focusInput, openInputDropdown]);
378
+ }); }, [blinkInput, blurInput, closeOpenedDropdown, focusInput, openInputDropdown]);
355
379
  useLayoutEffect(function () {
356
380
  if (!isDropdownOpened) {
357
381
  return;
@@ -373,7 +397,7 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
373
397
  if (!disabled) {
374
398
  return;
375
399
  }
376
- if (canUseMobileDropdownTimePicker && isDropdownOpened) {
400
+ if (isMobileDropdownActive) {
377
401
  closeOpenedDropdown();
378
402
  return;
379
403
  }
@@ -386,94 +410,145 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
386
410
  commitEditingValue();
387
411
  }
388
412
  }, [
389
- canUseMobileDropdownTimePicker,
390
413
  closeDropdown,
391
414
  closeOpenedDropdown,
392
415
  commitEditingValue,
393
416
  disabled,
394
417
  isDropdownOpened,
395
418
  isInputFocused,
419
+ isMobileDropdownActive,
396
420
  resetMouseInteractionState,
397
421
  ]);
422
+ var previousIsMobileLayoutRef = useRef(isMobileLayout);
423
+ /**
424
+ * Смена верстки переносит открытое меню в нужную версию: как в ComboBox, меню не закрывается.
425
+ * Начатый ввод переезжает вместе с ним, а если переезжать некуда — коммитится, как при потере фокуса.
426
+ */
398
427
  useEffect(function () {
399
- setIsMobileDevice(Boolean(isMobile));
400
- setCanUseMobileNativeTimePicker(useMobileNativeTimePicker && Boolean(isMobile));
401
- }, [useMobileNativeTimePicker]);
402
- var handleCloseMobileDropdown = useCallback(function () {
403
- closeOpenedDropdown();
404
- }, [closeOpenedDropdown]);
428
+ if (previousIsMobileLayoutRef.current === isMobileLayout) {
429
+ return;
430
+ }
431
+ previousIsMobileLayoutRef.current = isMobileLayout;
432
+ // Запомненный blur остался от прошлой верстки: как потеря фокуса контрола он больше не годится.
433
+ mobileBlurEventRef.current = null;
434
+ if (isMobileLayout) {
435
+ // Ввод продолжится в инпуте попапа, только если попап открывается вместе со сменой верстки.
436
+ if (isInputFocused && !isMobileDropdownActive) {
437
+ setIsInputFocused(false);
438
+ resetMouseInteractionState();
439
+ commitEditingValue();
440
+ }
441
+ // В режиме нативного пикера своего меню нет.
442
+ if (isDropdownOpened && canUseMobileNativeTimePicker) {
443
+ closeDropdown();
444
+ }
445
+ return;
446
+ }
447
+ /**
448
+ * Десктопное меню закрывается по потере фокуса поля, поэтому без фокуса в контроле
449
+ * его нечем было бы закрыть: мобильный попап мог быть открыт и без фокуса в своем инпуте.
450
+ * Если же фокус был, его вернет в поле `useTimePickerSelection`.
451
+ */
452
+ if (isDropdownOpened && !isInputFocused) {
453
+ closeDropdown();
454
+ }
455
+ }, [
456
+ canUseMobileNativeTimePicker,
457
+ closeDropdown,
458
+ commitEditingValue,
459
+ isDropdownOpened,
460
+ isInputFocused,
461
+ isMobileDropdownActive,
462
+ isMobileLayout,
463
+ resetMouseInteractionState,
464
+ ]);
465
+ /** Сообщает о фокусе один раз за сессию: переезд фокуса внутри контрола наружу не виден. */
466
+ var emitFocus = function (event) {
467
+ if (isControlFocusedRef.current) {
468
+ return;
469
+ }
470
+ isControlFocusedRef.current = true;
471
+ onFocus === null || onFocus === void 0 ? void 0 : onFocus(event);
472
+ };
405
473
  var handleFocusBySource = function (event, isMobilePopupInput) {
406
474
  var shouldSkipOpenDropdown = shouldSkipOpenOnFocusRef.current;
407
475
  shouldSkipOpenOnFocusRef.current = false;
476
+ var isFocusMovedInsideControl = isControlFocusedRef.current;
408
477
  if (canUseMobileNativeTimePicker) {
409
478
  if (!shouldSkipOpenDropdown) {
410
479
  openNativeTimePicker();
411
480
  }
412
- onFocus === null || onFocus === void 0 ? void 0 : onFocus(event);
481
+ emitFocus(event);
413
482
  return;
414
483
  }
415
484
  if (canUseMobileDropdownTimePicker && !isMobilePopupInput) {
416
485
  if (!shouldSkipOpenDropdown) {
417
486
  openDropdown();
418
487
  }
488
+ // Дальше ввод идет в инпуте попапа, но фокус контрол получает здесь.
489
+ emitFocus(event);
419
490
  return;
420
491
  }
421
492
  setIsInputFocused(true);
422
493
  if (!shouldSkipOpenDropdown) {
423
494
  openDropdown();
424
495
  }
425
- if (isMouseDownOnFragmentRef.current && !isInputFocused) {
426
- if (isTimeDisplayEmpty(editingValueRef.current)) {
427
- selectSegment('hours');
428
- }
429
- else if (mouseDownSegmentRef.current) {
430
- selectSegment(mouseDownSegmentRef.current);
431
- }
496
+ if (mouseDownSegmentRef.current && !isInputFocused) {
497
+ // По пустому полю ввод всегда начинается с часов, а по заполненному — с сегмента под курсором.
498
+ selectSegment(isTimeDisplayEmpty(editingValueRef.current) ? 'hours' : mouseDownSegmentRef.current);
432
499
  }
433
500
  else {
434
- selectSegment('hours');
501
+ // Переезд фокуса внутри контрола продолжает начатый ввод, поэтому сегмент сохраняется:
502
+ // иначе следующая цифра ушла бы в часы и затерла бы набранное время.
503
+ selectSegment(isFocusMovedInsideControl ? selectedSegment : 'hours');
435
504
  }
436
- onFocus === null || onFocus === void 0 ? void 0 : onFocus(event);
505
+ emitFocus(event);
437
506
  };
438
507
  var handleFocus = function (event) { return handleFocusBySource(event, false); };
439
508
  var handleMobileFocus = function (event) { return handleFocusBySource(event, true); };
440
509
  var handleBlurBySource = function (event, isMobilePopupInput) {
441
- if (canUseMobileNativeTimePicker) {
442
- onBlur === null || onBlur === void 0 ? void 0 : onBlur(event);
510
+ /**
511
+ * Пока открыт мобильный попап, фокус ходит между полем и его инпутом: контрол остается активным,
512
+ * поэтому наружу о таком blur сообщать нечего. Событие запоминается на случай,
513
+ * если фокус в инпуте попапа так и не оказался — тогда именно оно уйдет в `onBlur` при закрытии.
514
+ */
515
+ if (isMobileDropdownActive) {
516
+ event.persist();
517
+ mobileBlurEventRef.current = event;
443
518
  return;
444
519
  }
445
- if (canUseMobileDropdownTimePicker && !isMobilePopupInput) {
520
+ if (canUseMobileNativeTimePicker) {
521
+ // Ввод в мобильной версии не идет через поле, но незакоммиченное значение могло остаться
522
+ // от десктопной версии, если верстка сменилась посреди ввода.
523
+ if (isInputFocused) {
524
+ setIsInputFocused(false);
525
+ resetMouseInteractionState();
526
+ commitEditingValue();
527
+ }
528
+ emitBlur(event);
446
529
  return;
447
530
  }
531
+ // Инпут закрытого попапа фокуса уже не держит, но его blur может прийти на размонтировании.
448
532
  if (isMobilePopupInput) {
449
- event.persist();
450
- mobileBlurEventRef.current = event;
451
533
  return;
452
534
  }
453
535
  setIsInputFocused(false);
454
536
  resetMouseInteractionState();
455
537
  closeDropdown();
456
538
  commitEditingValue();
457
- onBlur === null || onBlur === void 0 ? void 0 : onBlur(event);
539
+ emitBlur(event);
458
540
  };
459
541
  var handleBlur = function (event) { return handleBlurBySource(event, false); };
460
542
  var handleMobileBlur = function (event) { return handleBlurBySource(event, true); };
461
- var handleClickBySource = function (event, isMobilePopupInput) {
543
+ var handleClick = function (event) {
462
544
  if (canUseMobileNativeTimePicker) {
463
545
  openNativeTimePicker();
464
- onClick === null || onClick === void 0 ? void 0 : onClick(event);
465
- return;
466
546
  }
467
- if (canUseMobileDropdownTimePicker && !isMobilePopupInput) {
547
+ else {
468
548
  openDropdown();
469
- onClick === null || onClick === void 0 ? void 0 : onClick(event);
470
- return;
471
549
  }
472
- openDropdown();
473
550
  onClick === null || onClick === void 0 ? void 0 : onClick(event);
474
551
  };
475
- var handleClick = function (event) { return handleClickBySource(event, false); };
476
- var handleMobileClick = function (event) { return handleClickBySource(event, true); };
477
552
  var handleMouseDownCaptureBySource = function (event, isMobilePopupInput) {
478
553
  var _a, _b;
479
554
  if (canUseMobileNativeTimePicker || (canUseMobileDropdownTimePicker && !isMobilePopupInput)) {
@@ -481,12 +556,10 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
481
556
  }
482
557
  var currentInputRef = isMobilePopupInput ? mobileInputRef : inputRef;
483
558
  var segment = (_b = (_a = currentInputRef.current) === null || _a === void 0 ? void 0 : _a.getSegment(event.target)) !== null && _b !== void 0 ? _b : null;
484
- var isFragment = segment !== null;
485
- if (isInputFocused && !isFragment) {
559
+ if (isInputFocused && segment === null) {
486
560
  event.preventDefault();
487
561
  }
488
562
  isMouseFocusRef.current = !isInputFocused;
489
- isMouseDownOnFragmentRef.current = isFragment;
490
563
  mouseDownSegmentRef.current = segment;
491
564
  };
492
565
  var handleMouseDownCapture = function (event) {
@@ -647,12 +720,7 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
647
720
  if (isKeyEscape(event)) {
648
721
  if (isDropdownOpened) {
649
722
  event.preventDefault();
650
- if (canUseMobileDropdownTimePicker) {
651
- closeOpenedDropdown();
652
- }
653
- else {
654
- closeDropdown();
655
- }
723
+ closeOpenedDropdown();
656
724
  }
657
725
  return;
658
726
  }
@@ -661,15 +729,14 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
661
729
  event.preventDefault();
662
730
  return;
663
731
  }
664
- if (canUseMobileDropdownTimePicker && isDropdownOpened) {
732
+ if (isMobileDropdownActive) {
665
733
  event.preventDefault();
666
734
  closeOpenedDropdown();
667
735
  return;
668
736
  }
669
- // Enter не выбирает элемент, а лишь подтверждает введенное значение,
670
- // поэтому коммитим значение как при потере фокуса и подсвечиваем поле.
737
+ // Enter не выбирает элемент, а лишь подтверждает введенное значение, как потеря фокуса.
671
738
  commitEditingValue();
672
- blinkInputValue();
739
+ blinkInput();
673
740
  if (isDropdownOpened) {
674
741
  event.preventDefault();
675
742
  closeDropdown();
@@ -721,19 +788,16 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
721
788
  };
722
789
  var handlePaste = function (event) { return handlePasteBySource(event, false); };
723
790
  var handleMobilePaste = function (event) { return handlePasteBySource(event, true); };
724
- var handleNativeValueChange = useCallback(function (nextValue) {
725
- commitValue(nextValue);
726
- }, [commitValue]);
727
791
  return (React.createElement(CommonWrapper, __assign({}, props),
728
792
  React.createElement("span", { ref: rootRef, className: styles.root(), "data-tid": TimePickerDataTids.root },
729
793
  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 })),
730
- canUseMobileNativeTimePicker && (React.createElement(NativeTimeInput, { "data-tid": TimePickerDataTids.nativeInput, disabled: disabled, ref: nativeInputRef, format: format, minTime: minTime, maxTime: maxTime, value: editingValue, onValueChange: handleNativeValueChange })),
731
- 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": resolvedItems.length > 0 || isLoadingItems ? true : undefined, "aria-controls": resolvedItems.length > 0 || isLoadingItems ? 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 })),
794
+ canUseMobileNativeTimePicker && (React.createElement(NativeTimeInput, { "data-tid": TimePickerDataTids.nativeInput, disabled: disabled, ref: nativeInputRef, format: format, minTime: minTime, maxTime: maxTime, value: editingValue, onValueChange: commitValue })),
795
+ 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, 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 ? true : undefined, "aria-controls": resolvedItems.length > 0 || isLoadingItems ? popupId : undefined, "aria-activedescendant": activeDescendantId, onFocus: handleMobileFocus, onBlur: handleMobileBlur, onClick: handleClick, onKeyDown: handleMobileKeyDown, onMouseDownCapture: handleMobileMouseDownCapture, onMouseUp: handleMobileMouseUp, onPaste: handleMobilePaste, onSelectSegment: handleMobileSelectSegmentByMouse, onSelectItem: selectItem, onCloseRequest: closeOpenedDropdown })),
732
796
  hasDropdown &&
733
797
  !canUseMobileNativeTimePicker &&
734
798
  !canUseMobileDropdownTimePicker &&
735
799
  isDropdownOpened &&
736
800
  hasDropdownContent &&
737
- 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 })))));
801
+ 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, selectedValue: selectedValue, itemRefs: itemRefs, onSelectItem: selectItem })))));
738
802
  }), { validate: validateTimePicker });
739
803
  //# sourceMappingURL=TimePicker.js.map