@skbkontur/react-ui 6.1.7-65057.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 +28 -24
  9. package/components/TimePicker/TimePicker.js +205 -131
  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,65 +262,59 @@ 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
279
  var handleUnexpectedInput = useCallback(function (unexpectedValue) {
238
- if (onUnexpectedInput) {
239
- onUnexpectedInput(unexpectedValue, blinkInputValue);
280
+ if (!onUnexpectedInput) {
281
+ blinkInput();
282
+ return;
283
+ }
284
+ var returnedValue = onUnexpectedInput(unexpectedValue, blinkInput);
285
+ if (returnedValue === undefined) {
286
+ return;
287
+ }
288
+ if (returnedValue === null) {
289
+ commitValue(EMPTY_VALUE);
240
290
  return;
241
291
  }
242
- blinkInputValue();
243
- }, [blinkInputValue, onUnexpectedInput]);
292
+ commitItem(returnedValue);
293
+ }, [blinkInput, commitItem, commitValue, onUnexpectedInput]);
294
+ /** Закрывает открытое меню: мобильный попап заодно завершает сессию ввода, как потеря фокуса. */
244
295
  var closeOpenedDropdown = useCallback(function () {
245
- var _a;
246
- if (canUseMobileDropdownTimePicker && isDropdownOpened) {
296
+ var _a, _b;
297
+ if (isMobileDropdownActive) {
298
+ // Фокус может остаться в любом из двух полей: инпут попапа не всегда получает его —
299
+ // например мобильный браузер отказывается переводить фокус без жеста пользователя.
300
+ // Гасим оба, чтобы закрытие попапа всегда давало событие потери фокуса.
247
301
  (_a = mobileInputRef.current) === null || _a === void 0 ? void 0 : _a.blur();
302
+ (_b = inputRef.current) === null || _b === void 0 ? void 0 : _b.blur();
248
303
  setIsInputFocused(false);
249
304
  resetMouseInteractionState();
250
305
  closeDropdown();
251
306
  commitEditingValue();
252
307
  var blurEvent = mobileBlurEventRef.current;
253
308
  mobileBlurEventRef.current = null;
254
- if (blurEvent) {
255
- onBlur === null || onBlur === void 0 ? void 0 : onBlur(blurEvent);
256
- }
309
+ emitBlur(blurEvent);
257
310
  return;
258
311
  }
259
312
  closeDropdown();
260
- }, [
261
- canUseMobileDropdownTimePicker,
262
- closeDropdown,
263
- commitEditingValue,
264
- isDropdownOpened,
265
- onBlur,
266
- resetMouseInteractionState,
267
- ]);
313
+ }, [closeDropdown, commitEditingValue, emitBlur, isMobileDropdownActive, resetMouseInteractionState]);
268
314
  var selectItem = useCallback(function (item) {
269
315
  commitItem(item);
270
- if (canUseMobileDropdownTimePicker) {
271
- closeOpenedDropdown();
272
- }
273
- else {
274
- closeDropdown();
275
- }
276
- }, [canUseMobileDropdownTimePicker, closeDropdown, closeOpenedDropdown, commitItem]);
316
+ closeOpenedDropdown();
317
+ }, [closeOpenedDropdown, commitItem]);
277
318
  var isSelectHighlightedItem = useCallback(function (event) {
278
319
  var _a, _b;
279
320
  if (highlightedItemIndex === null) {
@@ -321,27 +362,20 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
321
362
  }, [canUseMobileNativeTimePicker, disabled, getActiveInput, openDropdown, openNativeTimePicker]);
322
363
  var blurInput = useCallback(function () {
323
364
  var _a;
324
- if (canUseMobileDropdownTimePicker && isDropdownOpened) {
365
+ if (isMobileDropdownActive) {
325
366
  closeOpenedDropdown();
326
367
  return;
327
368
  }
328
369
  (_a = getActiveInput()) === null || _a === void 0 ? void 0 : _a.blur();
329
- }, [canUseMobileDropdownTimePicker, closeOpenedDropdown, getActiveInput, isDropdownOpened]);
330
- var closeInputDropdown = useCallback(function () {
331
- closeOpenedDropdown();
332
- }, [closeOpenedDropdown]);
333
- var blinkInput = useCallback(function () {
334
- var _a;
335
- (_a = getActiveInput()) === null || _a === void 0 ? void 0 : _a.blink();
336
- }, [getActiveInput]);
370
+ }, [closeOpenedDropdown, getActiveInput, isMobileDropdownActive]);
337
371
  useImperativeHandle(ref, function () { return ({
338
372
  focus: focusInput,
339
373
  blur: blurInput,
340
374
  blink: blinkInput,
341
375
  open: openInputDropdown,
342
- close: closeInputDropdown,
376
+ close: closeOpenedDropdown,
343
377
  getRootNode: function () { return rootRef.current; },
344
- }); }, [blinkInput, blurInput, closeInputDropdown, focusInput, openInputDropdown]);
378
+ }); }, [blinkInput, blurInput, closeOpenedDropdown, focusInput, openInputDropdown]);
345
379
  useLayoutEffect(function () {
346
380
  if (!isDropdownOpened) {
347
381
  return;
@@ -363,7 +397,7 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
363
397
  if (!disabled) {
364
398
  return;
365
399
  }
366
- if (canUseMobileDropdownTimePicker && isDropdownOpened) {
400
+ if (isMobileDropdownActive) {
367
401
  closeOpenedDropdown();
368
402
  return;
369
403
  }
@@ -376,94 +410,145 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
376
410
  commitEditingValue();
377
411
  }
378
412
  }, [
379
- canUseMobileDropdownTimePicker,
380
413
  closeDropdown,
381
414
  closeOpenedDropdown,
382
415
  commitEditingValue,
383
416
  disabled,
384
417
  isDropdownOpened,
385
418
  isInputFocused,
419
+ isMobileDropdownActive,
386
420
  resetMouseInteractionState,
387
421
  ]);
422
+ var previousIsMobileLayoutRef = useRef(isMobileLayout);
423
+ /**
424
+ * Смена верстки переносит открытое меню в нужную версию: как в ComboBox, меню не закрывается.
425
+ * Начатый ввод переезжает вместе с ним, а если переезжать некуда — коммитится, как при потере фокуса.
426
+ */
388
427
  useEffect(function () {
389
- setIsMobileDevice(Boolean(isMobile));
390
- setCanUseMobileNativeTimePicker(useMobileNativeTimePicker && Boolean(isMobile));
391
- }, [useMobileNativeTimePicker]);
392
- var handleCloseMobileDropdown = useCallback(function () {
393
- closeOpenedDropdown();
394
- }, [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
+ };
395
473
  var handleFocusBySource = function (event, isMobilePopupInput) {
396
474
  var shouldSkipOpenDropdown = shouldSkipOpenOnFocusRef.current;
397
475
  shouldSkipOpenOnFocusRef.current = false;
476
+ var isFocusMovedInsideControl = isControlFocusedRef.current;
398
477
  if (canUseMobileNativeTimePicker) {
399
478
  if (!shouldSkipOpenDropdown) {
400
479
  openNativeTimePicker();
401
480
  }
402
- onFocus === null || onFocus === void 0 ? void 0 : onFocus(event);
481
+ emitFocus(event);
403
482
  return;
404
483
  }
405
484
  if (canUseMobileDropdownTimePicker && !isMobilePopupInput) {
406
485
  if (!shouldSkipOpenDropdown) {
407
486
  openDropdown();
408
487
  }
488
+ // Дальше ввод идет в инпуте попапа, но фокус контрол получает здесь.
489
+ emitFocus(event);
409
490
  return;
410
491
  }
411
492
  setIsInputFocused(true);
412
493
  if (!shouldSkipOpenDropdown) {
413
494
  openDropdown();
414
495
  }
415
- if (isMouseDownOnFragmentRef.current && !isInputFocused) {
416
- if (isTimeDisplayEmpty(editingValueRef.current)) {
417
- selectSegment('hours');
418
- }
419
- else if (mouseDownSegmentRef.current) {
420
- selectSegment(mouseDownSegmentRef.current);
421
- }
496
+ if (mouseDownSegmentRef.current && !isInputFocused) {
497
+ // По пустому полю ввод всегда начинается с часов, а по заполненному — с сегмента под курсором.
498
+ selectSegment(isTimeDisplayEmpty(editingValueRef.current) ? 'hours' : mouseDownSegmentRef.current);
422
499
  }
423
500
  else {
424
- selectSegment('hours');
501
+ // Переезд фокуса внутри контрола продолжает начатый ввод, поэтому сегмент сохраняется:
502
+ // иначе следующая цифра ушла бы в часы и затерла бы набранное время.
503
+ selectSegment(isFocusMovedInsideControl ? selectedSegment : 'hours');
425
504
  }
426
- onFocus === null || onFocus === void 0 ? void 0 : onFocus(event);
505
+ emitFocus(event);
427
506
  };
428
507
  var handleFocus = function (event) { return handleFocusBySource(event, false); };
429
508
  var handleMobileFocus = function (event) { return handleFocusBySource(event, true); };
430
509
  var handleBlurBySource = function (event, isMobilePopupInput) {
431
- if (canUseMobileNativeTimePicker) {
432
- 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;
433
518
  return;
434
519
  }
435
- if (canUseMobileDropdownTimePicker && !isMobilePopupInput) {
520
+ if (canUseMobileNativeTimePicker) {
521
+ // Ввод в мобильной версии не идет через поле, но незакоммиченное значение могло остаться
522
+ // от десктопной версии, если верстка сменилась посреди ввода.
523
+ if (isInputFocused) {
524
+ setIsInputFocused(false);
525
+ resetMouseInteractionState();
526
+ commitEditingValue();
527
+ }
528
+ emitBlur(event);
436
529
  return;
437
530
  }
531
+ // Инпут закрытого попапа фокуса уже не держит, но его blur может прийти на размонтировании.
438
532
  if (isMobilePopupInput) {
439
- event.persist();
440
- mobileBlurEventRef.current = event;
441
533
  return;
442
534
  }
443
535
  setIsInputFocused(false);
444
536
  resetMouseInteractionState();
445
537
  closeDropdown();
446
538
  commitEditingValue();
447
- onBlur === null || onBlur === void 0 ? void 0 : onBlur(event);
539
+ emitBlur(event);
448
540
  };
449
541
  var handleBlur = function (event) { return handleBlurBySource(event, false); };
450
542
  var handleMobileBlur = function (event) { return handleBlurBySource(event, true); };
451
- var handleClickBySource = function (event, isMobilePopupInput) {
543
+ var handleClick = function (event) {
452
544
  if (canUseMobileNativeTimePicker) {
453
545
  openNativeTimePicker();
454
- onClick === null || onClick === void 0 ? void 0 : onClick(event);
455
- return;
456
546
  }
457
- if (canUseMobileDropdownTimePicker && !isMobilePopupInput) {
547
+ else {
458
548
  openDropdown();
459
- onClick === null || onClick === void 0 ? void 0 : onClick(event);
460
- return;
461
549
  }
462
- openDropdown();
463
550
  onClick === null || onClick === void 0 ? void 0 : onClick(event);
464
551
  };
465
- var handleClick = function (event) { return handleClickBySource(event, false); };
466
- var handleMobileClick = function (event) { return handleClickBySource(event, true); };
467
552
  var handleMouseDownCaptureBySource = function (event, isMobilePopupInput) {
468
553
  var _a, _b;
469
554
  if (canUseMobileNativeTimePicker || (canUseMobileDropdownTimePicker && !isMobilePopupInput)) {
@@ -471,12 +556,10 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
471
556
  }
472
557
  var currentInputRef = isMobilePopupInput ? mobileInputRef : inputRef;
473
558
  var segment = (_b = (_a = currentInputRef.current) === null || _a === void 0 ? void 0 : _a.getSegment(event.target)) !== null && _b !== void 0 ? _b : null;
474
- var isFragment = segment !== null;
475
- if (isInputFocused && !isFragment) {
559
+ if (isInputFocused && segment === null) {
476
560
  event.preventDefault();
477
561
  }
478
562
  isMouseFocusRef.current = !isInputFocused;
479
- isMouseDownOnFragmentRef.current = isFragment;
480
563
  mouseDownSegmentRef.current = segment;
481
564
  };
482
565
  var handleMouseDownCapture = function (event) {
@@ -637,12 +720,7 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
637
720
  if (isKeyEscape(event)) {
638
721
  if (isDropdownOpened) {
639
722
  event.preventDefault();
640
- if (canUseMobileDropdownTimePicker) {
641
- closeOpenedDropdown();
642
- }
643
- else {
644
- closeDropdown();
645
- }
723
+ closeOpenedDropdown();
646
724
  }
647
725
  return;
648
726
  }
@@ -651,15 +729,14 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
651
729
  event.preventDefault();
652
730
  return;
653
731
  }
654
- if (canUseMobileDropdownTimePicker && isDropdownOpened) {
732
+ if (isMobileDropdownActive) {
655
733
  event.preventDefault();
656
734
  closeOpenedDropdown();
657
735
  return;
658
736
  }
659
- // Enter не выбирает элемент, а лишь подтверждает введенное значение,
660
- // поэтому коммитим значение как при потере фокуса и подсвечиваем поле.
737
+ // Enter не выбирает элемент, а лишь подтверждает введенное значение, как потеря фокуса.
661
738
  commitEditingValue();
662
- blinkInputValue();
739
+ blinkInput();
663
740
  if (isDropdownOpened) {
664
741
  event.preventDefault();
665
742
  closeDropdown();
@@ -711,19 +788,16 @@ export var TimePicker = Object.assign(forwardRefAndName('TimePicker', function (
711
788
  };
712
789
  var handlePaste = function (event) { return handlePasteBySource(event, false); };
713
790
  var handleMobilePaste = function (event) { return handlePasteBySource(event, true); };
714
- var handleNativeValueChange = useCallback(function (nextValue) {
715
- commitValue(nextValue);
716
- }, [commitValue]);
717
791
  return (React.createElement(CommonWrapper, __assign({}, props),
718
792
  React.createElement("span", { ref: rootRef, className: styles.root(), "data-tid": TimePickerDataTids.root },
719
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 })),
720
- canUseMobileNativeTimePicker && (React.createElement(NativeTimeInput, { "data-tid": TimePickerDataTids.nativeInput, disabled: disabled, ref: nativeInputRef, format: format, minTime: minTime, maxTime: maxTime, value: editingValue, onValueChange: handleNativeValueChange })),
721
- 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 })),
722
796
  hasDropdown &&
723
797
  !canUseMobileNativeTimePicker &&
724
798
  !canUseMobileDropdownTimePicker &&
725
799
  isDropdownOpened &&
726
800
  hasDropdownContent &&
727
- 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 })))));
728
802
  }), { validate: validateTimePicker });
729
803
  //# sourceMappingURL=TimePicker.js.map