@cloud-ru/ds-fields 2.1.1 → 2.1.3
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.
- package/CHANGELOG.md +13 -0
- package/dist/cjs/components/FieldDate/styles.module.css +3 -0
- package/dist/cjs/components/FieldSelect/FieldSelect.js +25 -38
- package/dist/cjs/style.css +3 -0
- package/dist/esm/components/FieldDate/styles.module.css +3 -0
- package/dist/esm/components/FieldSelect/FieldSelect.js +25 -38
- package/dist/esm/style.css +3 -0
- package/dist/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/src/components/FieldDate/styles.module.scss +4 -0
- package/src/components/FieldSelect/FieldSelect.tsx +36 -52
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## 2.1.3 (2026-09-02)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
- **FF-8959:** sync FieldSelect search to the selected value when search is controlled ([2f29e15](https://github.com/cloud-ru-tech/snack-v2/commit/2f29e15eeb3e1dd54bb119fd27ab68d5d84e383e))
|
|
11
|
+
|
|
12
|
+
## 2.1.2 (2026-09-02)
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
- **PDS-4414:** fix FieldDate calendar interactions ([668e3c3](https://github.com/cloud-ru-tech/snack-v2/commit/668e3c3309265b6ad2fef903eb2b9b05b89f7655))
|
|
17
|
+
- **PDS-4414:** keep FieldSelect open when expanding collapse group ([179fd2b](https://github.com/cloud-ru-tech/snack-v2/commit/179fd2bc891677cc4cb47f466aefed5a818ea1d0))
|
|
18
|
+
|
|
6
19
|
## 2.1.1 (2026-09-01)
|
|
7
20
|
|
|
8
21
|
**Note:** Version bump only for package @ds/fields
|
|
@@ -13,8 +13,7 @@ const utils_1 = require("@cloud-ru/ds-utils");
|
|
|
13
13
|
const classnames_1 = __importDefault(require("classnames"));
|
|
14
14
|
const merge_refs_1 = __importDefault(require("merge-refs"));
|
|
15
15
|
const react_1 = require("react");
|
|
16
|
-
// Минимальная ширина поискового input'а в режиме
|
|
17
|
-
// к этому значению и прижимается к последнему чипу (паритет с легаси BASE_MIN_WIDTH).
|
|
16
|
+
// Минимальная ширина поискового input'а в режиме чипов (паритет с легаси BASE_MIN_WIDTH).
|
|
18
17
|
const SEARCH_INPUT_PLUG_MIN_WIDTH = 4;
|
|
19
18
|
const field_decorator_1 = require("@cloud-ru/ds-field-decorator");
|
|
20
19
|
const constants_1 = require("../../constants");
|
|
@@ -31,14 +30,10 @@ exports.FieldSelect = (0, react_1.forwardRef)(function FieldSelect(props, ref) {
|
|
|
31
30
|
const closeDroplistOnItemClick = props.closeDroplistOnItemClick ?? !multiple;
|
|
32
31
|
const resolvedAutoFocus = (0, hooks_1.useAdaptiveAutoFocus)(autoFocus, layoutPresets);
|
|
33
32
|
const inputRef = (0, react_1.useRef)(null);
|
|
34
|
-
//
|
|
35
|
-
// перезаписывает его `.current` своей span-обёрткой (см. MR!101). Отдельный ref на сам input
|
|
36
|
-
// нужен для `useButtonNavigation` (курсор/фокус) и возврата фокуса после очистки.
|
|
33
|
+
// Отдельный ref на реальный <input>: `inputRef.current` перезаписывается span-обёрткой PopoverPrivate.
|
|
37
34
|
const inputElementRef = (0, react_1.useRef)(null);
|
|
38
35
|
const clearButtonRef = (0, react_1.useRef)(null);
|
|
39
36
|
const copyButtonRef = (0, react_1.useRef)(null);
|
|
40
|
-
// Контейнер чипов+input (замер доступной ширины) и скрытый плаг (замер ширины введённого текста)
|
|
41
|
-
// для расчёта minWidth поискового input'а в режиме чипов — см. searchInput ниже.
|
|
42
37
|
const contentRef = (0, react_1.useRef)(null);
|
|
43
38
|
const inputPlugRef = (0, react_1.useRef)(null);
|
|
44
39
|
const [chipInputMinWidth, setChipInputMinWidth] = (0, react_1.useState)(undefined);
|
|
@@ -48,13 +43,9 @@ exports.FieldSelect = (0, react_1.forwardRef)(function FieldSelect(props, ref) {
|
|
|
48
43
|
const open = openProp ?? openLocal;
|
|
49
44
|
const [singleLocal, setSingleLocal] = (0, react_1.useState)(!multiple ? props.defaultValue : undefined);
|
|
50
45
|
const [multipleLocal, setMultipleLocal] = (0, react_1.useState)(
|
|
51
|
-
//
|
|
52
|
-
// но «расхлябанный» вызов (например, Storybook-spread, где single-default остаётся
|
|
53
|
-
// строкой при переключении selection) не должен ронять компонент на `.map` строки.
|
|
46
|
+
// Страховка от вызова без типов (Storybook-spread): при multiple defaultValue может прийти строкой.
|
|
54
47
|
multiple && Array.isArray(props.defaultValue) ? props.defaultValue : []);
|
|
55
|
-
// Controlled-режим «залипает»:
|
|
56
|
-
// не читается. Иначе очистка (потребитель ставит `undefined`) откатывалась бы на устаревший
|
|
57
|
-
// локальный выбор и воскрешала прошлое значение.
|
|
48
|
+
// Controlled-режим «залипает»: иначе очистка через `value: undefined` откатится на локальный стейт.
|
|
58
49
|
const singleControlled = (0, react_1.useRef)(false);
|
|
59
50
|
const multipleControlled = (0, react_1.useRef)(false);
|
|
60
51
|
if (!multiple && props.value !== undefined) {
|
|
@@ -125,24 +116,28 @@ exports.FieldSelect = (0, react_1.forwardRef)(function FieldSelect(props, ref) {
|
|
|
125
116
|
}
|
|
126
117
|
return selectedPairs.map(formatPair).join(', ');
|
|
127
118
|
}, [multiple, singleValue, selectedPairs, chips, props, formatPair, resolveItem]);
|
|
128
|
-
// Строка поиска: controlled через `search.value`, иначе локальный state. Auto-sync к выбранному
|
|
129
|
-
// значению делаем через СТАБИЛЬНЫЙ setLocalInput, а не через setInputValue в deps эффекта —
|
|
130
|
-
// иначе нестабильная ссылка сеттера зацикливает эффект (сбрасывала бы значение постоянно).
|
|
131
119
|
const [localInput, setLocalInput] = (0, react_1.useState)(search?.defaultValue ?? selectedLabel);
|
|
132
120
|
const inputValue = search?.value !== undefined ? search.value : localInput;
|
|
133
121
|
const [typing, setTyping] = (0, react_1.useState)(false);
|
|
122
|
+
// `search` — объектный литерал с новой идентичностью каждый рендер; ref делает `setInputValue` стабильным.
|
|
123
|
+
const searchRef = (0, react_1.useRef)(search);
|
|
124
|
+
searchRef.current = search;
|
|
134
125
|
const setInputValue = (0, react_1.useCallback)((next) => {
|
|
135
|
-
|
|
126
|
+
const currentSearch = searchRef.current;
|
|
127
|
+
if (currentSearch?.value === undefined) {
|
|
136
128
|
setLocalInput(next);
|
|
137
129
|
}
|
|
138
|
-
|
|
139
|
-
}, [
|
|
130
|
+
currentSearch?.onChange?.(next);
|
|
131
|
+
}, []);
|
|
132
|
+
const syncedLabelRef = (0, react_1.useRef)(selectedLabel);
|
|
140
133
|
(0, react_1.useEffect)(() => {
|
|
141
|
-
if (
|
|
142
|
-
|
|
134
|
+
if (typing || !resetSearchOnOptionSelection || syncedLabelRef.current === selectedLabel) {
|
|
135
|
+
return;
|
|
143
136
|
}
|
|
144
|
-
|
|
145
|
-
|
|
137
|
+
syncedLabelRef.current = selectedLabel;
|
|
138
|
+
setInputValue(selectedLabel);
|
|
139
|
+
}, [selectedLabel, typing, resetSearchOnOptionSelection, setInputValue]);
|
|
140
|
+
// При autocomplete фильтрует сервер — items берутся как есть.
|
|
146
141
|
const filteredItems = (0, react_1.useMemo)(() => {
|
|
147
142
|
if (!searchable || autocomplete || !typing) {
|
|
148
143
|
return items;
|
|
@@ -162,8 +157,7 @@ exports.FieldSelect = (0, react_1.forwardRef)(function FieldSelect(props, ref) {
|
|
|
162
157
|
return (0, utils_2.filterItems)(pinBottom, inputValue, enableFuzzySearch);
|
|
163
158
|
}, [searchable, autocomplete, typing, pinBottom, inputValue, enableFuzzySearch]);
|
|
164
159
|
const handleOpenChange = (0, react_1.useCallback)((next) => {
|
|
165
|
-
//
|
|
166
|
-
// readonly/disabled поле открывать нельзя — блокируем открытие, закрытие разрешаем.
|
|
160
|
+
// readonly/disabled поле открывать нельзя, закрывать — можно.
|
|
167
161
|
if (next && (disabled || readOnly))
|
|
168
162
|
return;
|
|
169
163
|
if (openProp === undefined) {
|
|
@@ -261,9 +255,7 @@ exports.FieldSelect = (0, react_1.forwardRef)(function FieldSelect(props, ref) {
|
|
|
261
255
|
}
|
|
262
256
|
return copied;
|
|
263
257
|
}, [onCopyButtonClick, valueToCopy]);
|
|
264
|
-
// Clear/Copy
|
|
265
|
-
// кнопку, ArrowLeft — обратно в поле. Паритет с FieldCombo. `inputElementRef` — реальный input,
|
|
266
|
-
// т.к. `inputRef` клобберится popover'ом.
|
|
258
|
+
// Clear/Copy — roving-postfix (ArrowRight/ArrowLeft), паритет с FieldCombo.
|
|
267
259
|
const clearButtonSettings = (0, input_private_1.useClearButton)({
|
|
268
260
|
clearButtonRef,
|
|
269
261
|
showClearButton: showClear,
|
|
@@ -288,9 +280,8 @@ exports.FieldSelect = (0, react_1.forwardRef)(function FieldSelect(props, ref) {
|
|
|
288
280
|
submitKeys: ['Enter', 'Space', 'Tab'],
|
|
289
281
|
});
|
|
290
282
|
const handleTriggerKeyDown = (event) => {
|
|
291
|
-
//
|
|
283
|
+
// Consumer-обработчик идёт до внутренней обработки (паритет с легаси useHandleOnKeyDown).
|
|
292
284
|
onKeyDownProp?.(event);
|
|
293
|
-
// Roving-навигация по clear/copy (ArrowRight/ArrowLeft). ArrowDown/Enter ниже не перехватываются.
|
|
294
285
|
onInputKeyDown(event);
|
|
295
286
|
if (disabled || readOnly) {
|
|
296
287
|
return;
|
|
@@ -304,7 +295,7 @@ exports.FieldSelect = (0, react_1.forwardRef)(function FieldSelect(props, ref) {
|
|
|
304
295
|
handleOpenChange(false);
|
|
305
296
|
}
|
|
306
297
|
if (addOptionByEnter && event.key === 'Enter' && inputValue !== '') {
|
|
307
|
-
//
|
|
298
|
+
// Введённый текст становится новым значением (создание опции «на лету»).
|
|
308
299
|
event.preventDefault();
|
|
309
300
|
if (multiple) {
|
|
310
301
|
if (!multipleValue.includes(inputValue)) {
|
|
@@ -358,10 +349,8 @@ exports.FieldSelect = (0, react_1.forwardRef)(function FieldSelect(props, ref) {
|
|
|
358
349
|
? { mode: 'multiple', value: multipleValue, onChange: handleMultipleChange }
|
|
359
350
|
: { mode: 'single', value: singleValue, onChange: handleSingleChange };
|
|
360
351
|
const hasChips = chips && selectedPairs.length > 0;
|
|
361
|
-
// minWidth поискового input'а
|
|
362
|
-
//
|
|
363
|
-
// пустой ввод → ~4px (input прижат к последнему чипу), длинный ввод → перенос на новую строку
|
|
364
|
-
// через flex-wrap. Без чипов minWidth не задаётся (input занимает всю строку как single-select).
|
|
352
|
+
// minWidth поискового input'а = ширина введённого текста (`.inputPlug`), но не больше строки чипов.
|
|
353
|
+
// Замер до paint, иначе input мигает при вводе.
|
|
365
354
|
(0, utils_1.useLayoutEffect)(() => {
|
|
366
355
|
if (!hasChips) {
|
|
367
356
|
setChipInputMinWidth(undefined);
|
|
@@ -372,11 +361,9 @@ exports.FieldSelect = (0, react_1.forwardRef)(function FieldSelect(props, ref) {
|
|
|
372
361
|
setChipInputMinWidth(Math.min(content, Math.max(plug, SEARCH_INPUT_PLUG_MIN_WIDTH)));
|
|
373
362
|
}, [hasChips, inputValue, selectedPairs.length]);
|
|
374
363
|
const searchInputField = ((0, jsx_runtime_1.jsx)(input_private_1.InputPrivate, { ref: (0, merge_refs_1.default)(ref, inputRef, inputElementRef), className: (0, classnames_1.default)(styles_module_scss_1.default.fieldInput, !searchable && styles_module_scss_2.default.readonlyCursor), value: inputValue, onChange: searchable ? handleInputChange : undefined, placeholder: selectedLabel || hasChips ? '' : placeholder, disabled: disabled, readonly: !searchable || readOnly, id: id, name: name, autoFocus: resolvedAutoFocus, tabIndex: inputTabIndex, onFocus: handleInputFocus, onBlur: handleInputBlur, onKeyDown: handleTriggerKeyDown, "data-test-id": constants_1.TEST_IDS.fieldSelectInput }));
|
|
375
|
-
// В режиме чипов input заворачивается в `.inputWrapper` с динамической minWidth (флоу за чипами);
|
|
376
|
-
// без чипов — обычный `inputArea` на всю строку.
|
|
377
364
|
const searchInput = hasChips ? ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_2.default.inputWrapper, style: { minWidth: chipInputMinWidth }, children: searchInputField })) : ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.inputArea, children: searchInputField }));
|
|
378
365
|
const trigger = (
|
|
379
|
-
//
|
|
366
|
+
// Focusable-элемент — InputPrivate внутри, обёртка только собирает combobox.
|
|
380
367
|
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
|
381
368
|
(0, jsx_runtime_1.jsxs)("div", { className: (0, classnames_1.default)(styles_module_scss_1.default.fieldWrapper, styles_module_scss_2.default.trigger, fieldClassName), "data-size": size, "data-validation-state": effectiveValidationState, "data-disabled": disabled || undefined, "data-readonly": readOnly || undefined, "data-withbackground": background || undefined, "data-focusvisible": focusVisible || open || undefined, "data-hover": !readOnly && !disabled && hover ? true : undefined, "data-test-id": dataTestId, onMouseEnter: handleTriggerMouseEnter, onMouseLeave: handleTriggerMouseLeave, onClick: handleTriggerClick, children: [(0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_1.default.backgroundWrapper, children: [(0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.materialLayer, ...(0, shared_1.getAcrylicProps)({
|
|
382
369
|
validationState: effectiveValidationState,
|
package/dist/cjs/style.css
CHANGED
|
@@ -7,8 +7,7 @@ import { useLayoutEffect } from '@cloud-ru/ds-utils';
|
|
|
7
7
|
import cn from 'classnames';
|
|
8
8
|
import mergeRefs from 'merge-refs';
|
|
9
9
|
import { forwardRef, useCallback, useEffect, useMemo, useRef, useState, } from 'react';
|
|
10
|
-
// Минимальная ширина поискового input'а в режиме
|
|
11
|
-
// к этому значению и прижимается к последнему чипу (паритет с легаси BASE_MIN_WIDTH).
|
|
10
|
+
// Минимальная ширина поискового input'а в режиме чипов (паритет с легаси BASE_MIN_WIDTH).
|
|
12
11
|
const SEARCH_INPUT_PLUG_MIN_WIDTH = 4;
|
|
13
12
|
import { FieldDecorator, SIZE, VALIDATION_STATE } from '@cloud-ru/ds-field-decorator';
|
|
14
13
|
import { TEST_IDS } from '../../constants.js';
|
|
@@ -25,14 +24,10 @@ export const FieldSelect = forwardRef(function FieldSelect(props, ref) {
|
|
|
25
24
|
const closeDroplistOnItemClick = props.closeDroplistOnItemClick ?? !multiple;
|
|
26
25
|
const resolvedAutoFocus = useAdaptiveAutoFocus(autoFocus, layoutPresets);
|
|
27
26
|
const inputRef = useRef(null);
|
|
28
|
-
//
|
|
29
|
-
// перезаписывает его `.current` своей span-обёрткой (см. MR!101). Отдельный ref на сам input
|
|
30
|
-
// нужен для `useButtonNavigation` (курсор/фокус) и возврата фокуса после очистки.
|
|
27
|
+
// Отдельный ref на реальный <input>: `inputRef.current` перезаписывается span-обёрткой PopoverPrivate.
|
|
31
28
|
const inputElementRef = useRef(null);
|
|
32
29
|
const clearButtonRef = useRef(null);
|
|
33
30
|
const copyButtonRef = useRef(null);
|
|
34
|
-
// Контейнер чипов+input (замер доступной ширины) и скрытый плаг (замер ширины введённого текста)
|
|
35
|
-
// для расчёта minWidth поискового input'а в режиме чипов — см. searchInput ниже.
|
|
36
31
|
const contentRef = useRef(null);
|
|
37
32
|
const inputPlugRef = useRef(null);
|
|
38
33
|
const [chipInputMinWidth, setChipInputMinWidth] = useState(undefined);
|
|
@@ -42,13 +37,9 @@ export const FieldSelect = forwardRef(function FieldSelect(props, ref) {
|
|
|
42
37
|
const open = openProp ?? openLocal;
|
|
43
38
|
const [singleLocal, setSingleLocal] = useState(!multiple ? props.defaultValue : undefined);
|
|
44
39
|
const [multipleLocal, setMultipleLocal] = useState(
|
|
45
|
-
//
|
|
46
|
-
// но «расхлябанный» вызов (например, Storybook-spread, где single-default остаётся
|
|
47
|
-
// строкой при переключении selection) не должен ронять компонент на `.map` строки.
|
|
40
|
+
// Страховка от вызова без типов (Storybook-spread): при multiple defaultValue может прийти строкой.
|
|
48
41
|
multiple && Array.isArray(props.defaultValue) ? props.defaultValue : []);
|
|
49
|
-
// Controlled-режим «залипает»:
|
|
50
|
-
// не читается. Иначе очистка (потребитель ставит `undefined`) откатывалась бы на устаревший
|
|
51
|
-
// локальный выбор и воскрешала прошлое значение.
|
|
42
|
+
// Controlled-режим «залипает»: иначе очистка через `value: undefined` откатится на локальный стейт.
|
|
52
43
|
const singleControlled = useRef(false);
|
|
53
44
|
const multipleControlled = useRef(false);
|
|
54
45
|
if (!multiple && props.value !== undefined) {
|
|
@@ -119,24 +110,28 @@ export const FieldSelect = forwardRef(function FieldSelect(props, ref) {
|
|
|
119
110
|
}
|
|
120
111
|
return selectedPairs.map(formatPair).join(', ');
|
|
121
112
|
}, [multiple, singleValue, selectedPairs, chips, props, formatPair, resolveItem]);
|
|
122
|
-
// Строка поиска: controlled через `search.value`, иначе локальный state. Auto-sync к выбранному
|
|
123
|
-
// значению делаем через СТАБИЛЬНЫЙ setLocalInput, а не через setInputValue в deps эффекта —
|
|
124
|
-
// иначе нестабильная ссылка сеттера зацикливает эффект (сбрасывала бы значение постоянно).
|
|
125
113
|
const [localInput, setLocalInput] = useState(search?.defaultValue ?? selectedLabel);
|
|
126
114
|
const inputValue = search?.value !== undefined ? search.value : localInput;
|
|
127
115
|
const [typing, setTyping] = useState(false);
|
|
116
|
+
// `search` — объектный литерал с новой идентичностью каждый рендер; ref делает `setInputValue` стабильным.
|
|
117
|
+
const searchRef = useRef(search);
|
|
118
|
+
searchRef.current = search;
|
|
128
119
|
const setInputValue = useCallback((next) => {
|
|
129
|
-
|
|
120
|
+
const currentSearch = searchRef.current;
|
|
121
|
+
if (currentSearch?.value === undefined) {
|
|
130
122
|
setLocalInput(next);
|
|
131
123
|
}
|
|
132
|
-
|
|
133
|
-
}, [
|
|
124
|
+
currentSearch?.onChange?.(next);
|
|
125
|
+
}, []);
|
|
126
|
+
const syncedLabelRef = useRef(selectedLabel);
|
|
134
127
|
useEffect(() => {
|
|
135
|
-
if (
|
|
136
|
-
|
|
128
|
+
if (typing || !resetSearchOnOptionSelection || syncedLabelRef.current === selectedLabel) {
|
|
129
|
+
return;
|
|
137
130
|
}
|
|
138
|
-
|
|
139
|
-
|
|
131
|
+
syncedLabelRef.current = selectedLabel;
|
|
132
|
+
setInputValue(selectedLabel);
|
|
133
|
+
}, [selectedLabel, typing, resetSearchOnOptionSelection, setInputValue]);
|
|
134
|
+
// При autocomplete фильтрует сервер — items берутся как есть.
|
|
140
135
|
const filteredItems = useMemo(() => {
|
|
141
136
|
if (!searchable || autocomplete || !typing) {
|
|
142
137
|
return items;
|
|
@@ -156,8 +151,7 @@ export const FieldSelect = forwardRef(function FieldSelect(props, ref) {
|
|
|
156
151
|
return filterItems(pinBottom, inputValue, enableFuzzySearch);
|
|
157
152
|
}, [searchable, autocomplete, typing, pinBottom, inputValue, enableFuzzySearch]);
|
|
158
153
|
const handleOpenChange = useCallback((next) => {
|
|
159
|
-
//
|
|
160
|
-
// readonly/disabled поле открывать нельзя — блокируем открытие, закрытие разрешаем.
|
|
154
|
+
// readonly/disabled поле открывать нельзя, закрывать — можно.
|
|
161
155
|
if (next && (disabled || readOnly))
|
|
162
156
|
return;
|
|
163
157
|
if (openProp === undefined) {
|
|
@@ -255,9 +249,7 @@ export const FieldSelect = forwardRef(function FieldSelect(props, ref) {
|
|
|
255
249
|
}
|
|
256
250
|
return copied;
|
|
257
251
|
}, [onCopyButtonClick, valueToCopy]);
|
|
258
|
-
// Clear/Copy
|
|
259
|
-
// кнопку, ArrowLeft — обратно в поле. Паритет с FieldCombo. `inputElementRef` — реальный input,
|
|
260
|
-
// т.к. `inputRef` клобберится popover'ом.
|
|
252
|
+
// Clear/Copy — roving-postfix (ArrowRight/ArrowLeft), паритет с FieldCombo.
|
|
261
253
|
const clearButtonSettings = useClearButton({
|
|
262
254
|
clearButtonRef,
|
|
263
255
|
showClearButton: showClear,
|
|
@@ -282,9 +274,8 @@ export const FieldSelect = forwardRef(function FieldSelect(props, ref) {
|
|
|
282
274
|
submitKeys: ['Enter', 'Space', 'Tab'],
|
|
283
275
|
});
|
|
284
276
|
const handleTriggerKeyDown = (event) => {
|
|
285
|
-
//
|
|
277
|
+
// Consumer-обработчик идёт до внутренней обработки (паритет с легаси useHandleOnKeyDown).
|
|
286
278
|
onKeyDownProp?.(event);
|
|
287
|
-
// Roving-навигация по clear/copy (ArrowRight/ArrowLeft). ArrowDown/Enter ниже не перехватываются.
|
|
288
279
|
onInputKeyDown(event);
|
|
289
280
|
if (disabled || readOnly) {
|
|
290
281
|
return;
|
|
@@ -298,7 +289,7 @@ export const FieldSelect = forwardRef(function FieldSelect(props, ref) {
|
|
|
298
289
|
handleOpenChange(false);
|
|
299
290
|
}
|
|
300
291
|
if (addOptionByEnter && event.key === 'Enter' && inputValue !== '') {
|
|
301
|
-
//
|
|
292
|
+
// Введённый текст становится новым значением (создание опции «на лету»).
|
|
302
293
|
event.preventDefault();
|
|
303
294
|
if (multiple) {
|
|
304
295
|
if (!multipleValue.includes(inputValue)) {
|
|
@@ -352,10 +343,8 @@ export const FieldSelect = forwardRef(function FieldSelect(props, ref) {
|
|
|
352
343
|
? { mode: 'multiple', value: multipleValue, onChange: handleMultipleChange }
|
|
353
344
|
: { mode: 'single', value: singleValue, onChange: handleSingleChange };
|
|
354
345
|
const hasChips = chips && selectedPairs.length > 0;
|
|
355
|
-
// minWidth поискового input'а
|
|
356
|
-
//
|
|
357
|
-
// пустой ввод → ~4px (input прижат к последнему чипу), длинный ввод → перенос на новую строку
|
|
358
|
-
// через flex-wrap. Без чипов minWidth не задаётся (input занимает всю строку как single-select).
|
|
346
|
+
// minWidth поискового input'а = ширина введённого текста (`.inputPlug`), но не больше строки чипов.
|
|
347
|
+
// Замер до paint, иначе input мигает при вводе.
|
|
359
348
|
useLayoutEffect(() => {
|
|
360
349
|
if (!hasChips) {
|
|
361
350
|
setChipInputMinWidth(undefined);
|
|
@@ -366,11 +355,9 @@ export const FieldSelect = forwardRef(function FieldSelect(props, ref) {
|
|
|
366
355
|
setChipInputMinWidth(Math.min(content, Math.max(plug, SEARCH_INPUT_PLUG_MIN_WIDTH)));
|
|
367
356
|
}, [hasChips, inputValue, selectedPairs.length]);
|
|
368
357
|
const searchInputField = (_jsx(InputPrivate, { ref: mergeRefs(ref, inputRef, inputElementRef), className: cn(fieldStyles.fieldInput, !searchable && styles.readonlyCursor), value: inputValue, onChange: searchable ? handleInputChange : undefined, placeholder: selectedLabel || hasChips ? '' : placeholder, disabled: disabled, readonly: !searchable || readOnly, id: id, name: name, autoFocus: resolvedAutoFocus, tabIndex: inputTabIndex, onFocus: handleInputFocus, onBlur: handleInputBlur, onKeyDown: handleTriggerKeyDown, "data-test-id": TEST_IDS.fieldSelectInput }));
|
|
369
|
-
// В режиме чипов input заворачивается в `.inputWrapper` с динамической minWidth (флоу за чипами);
|
|
370
|
-
// без чипов — обычный `inputArea` на всю строку.
|
|
371
358
|
const searchInput = hasChips ? (_jsx("div", { className: styles.inputWrapper, style: { minWidth: chipInputMinWidth }, children: searchInputField })) : (_jsx("div", { className: fieldStyles.inputArea, children: searchInputField }));
|
|
372
359
|
const trigger = (
|
|
373
|
-
//
|
|
360
|
+
// Focusable-элемент — InputPrivate внутри, обёртка только собирает combobox.
|
|
374
361
|
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
|
375
362
|
_jsxs("div", { className: cn(fieldStyles.fieldWrapper, styles.trigger, fieldClassName), "data-size": size, "data-validation-state": effectiveValidationState, "data-disabled": disabled || undefined, "data-readonly": readOnly || undefined, "data-withbackground": background || undefined, "data-focusvisible": focusVisible || open || undefined, "data-hover": !readOnly && !disabled && hover ? true : undefined, "data-test-id": dataTestId, onMouseEnter: handleTriggerMouseEnter, onMouseLeave: handleTriggerMouseLeave, onClick: handleTriggerClick, children: [_jsxs("div", { className: fieldStyles.backgroundWrapper, children: [_jsx("div", { className: fieldStyles.materialLayer, ...getAcrylicProps({
|
|
376
363
|
validationState: effectiveValidationState,
|