@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloud-ru/ds-fields",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.3",
|
|
4
4
|
"description": "Поля ввода с единой обёрткой label/caption/hint/error и общими осями size/validationState — текст, пароль, число, ползунок, дата, время, выбор, цвет.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -42,21 +42,21 @@
|
|
|
42
42
|
"@cloud-ru/ds-adaptive": "^1.0.1",
|
|
43
43
|
"@cloud-ru/ds-bottom-sheet": "^1.1.1",
|
|
44
44
|
"@cloud-ru/ds-button": "^1.0.2",
|
|
45
|
-
"@cloud-ru/ds-calendar": "^1.0.
|
|
45
|
+
"@cloud-ru/ds-calendar": "^1.0.13",
|
|
46
46
|
"@cloud-ru/ds-color-picker": "^1.0.5",
|
|
47
47
|
"@cloud-ru/ds-divider": "^1.0.1",
|
|
48
|
+
"@cloud-ru/ds-field-decorator": "^1.0.5",
|
|
48
49
|
"@cloud-ru/ds-dropdown": "^1.0.8",
|
|
49
50
|
"@cloud-ru/ds-icons": "^1.1.0",
|
|
50
|
-
"@cloud-ru/ds-field-decorator": "^1.0.5",
|
|
51
51
|
"@cloud-ru/ds-input-private": "^1.0.4",
|
|
52
|
-
"@cloud-ru/ds-list": "^2.1.
|
|
52
|
+
"@cloud-ru/ds-list": "^2.1.2",
|
|
53
53
|
"@cloud-ru/ds-loader": "^1.0.1",
|
|
54
54
|
"@cloud-ru/ds-materials": "^1.0.1",
|
|
55
55
|
"@cloud-ru/ds-scroll": "^1.0.2",
|
|
56
56
|
"@cloud-ru/ds-skeleton": "^1.0.1",
|
|
57
57
|
"@cloud-ru/ds-tag": "^1.0.9",
|
|
58
|
-
"@cloud-ru/ds-slider": "^1.0.4",
|
|
59
58
|
"@cloud-ru/ds-tooltip": "^1.0.4",
|
|
60
|
-
"@cloud-ru/ds-utils": "^1.1.0"
|
|
59
|
+
"@cloud-ru/ds-utils": "^1.1.0",
|
|
60
|
+
"@cloud-ru/ds-slider": "^1.0.4"
|
|
61
61
|
}
|
|
62
62
|
}
|
|
@@ -18,8 +18,7 @@ import {
|
|
|
18
18
|
useState,
|
|
19
19
|
} from 'react';
|
|
20
20
|
|
|
21
|
-
// Минимальная ширина поискового input'а в режиме
|
|
22
|
-
// к этому значению и прижимается к последнему чипу (паритет с легаси BASE_MIN_WIDTH).
|
|
21
|
+
// Минимальная ширина поискового input'а в режиме чипов (паритет с легаси BASE_MIN_WIDTH).
|
|
23
22
|
const SEARCH_INPUT_PLUG_MIN_WIDTH = 4;
|
|
24
23
|
|
|
25
24
|
import { FieldDecorator, SIZE, VALIDATION_STATE } from '@cloud-ru/ds-field-decorator';
|
|
@@ -116,14 +115,10 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
|
|
|
116
115
|
const resolvedAutoFocus = useAdaptiveAutoFocus(autoFocus, layoutPresets);
|
|
117
116
|
|
|
118
117
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
119
|
-
//
|
|
120
|
-
// перезаписывает его `.current` своей span-обёрткой (см. MR!101). Отдельный ref на сам input
|
|
121
|
-
// нужен для `useButtonNavigation` (курсор/фокус) и возврата фокуса после очистки.
|
|
118
|
+
// Отдельный ref на реальный <input>: `inputRef.current` перезаписывается span-обёрткой PopoverPrivate.
|
|
122
119
|
const inputElementRef = useRef<HTMLInputElement>(null);
|
|
123
120
|
const clearButtonRef = useRef<HTMLButtonElement>(null);
|
|
124
121
|
const copyButtonRef = useRef<HTMLButtonElement>(null);
|
|
125
|
-
// Контейнер чипов+input (замер доступной ширины) и скрытый плаг (замер ширины введённого текста)
|
|
126
|
-
// для расчёта minWidth поискового input'а в режиме чипов — см. searchInput ниже.
|
|
127
122
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
128
123
|
const inputPlugRef = useRef<HTMLSpanElement>(null);
|
|
129
124
|
const [chipInputMinWidth, setChipInputMinWidth] = useState<number | undefined>(undefined);
|
|
@@ -136,15 +131,11 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
|
|
|
136
131
|
!multiple ? (props.defaultValue as ItemId | undefined) : undefined,
|
|
137
132
|
);
|
|
138
133
|
const [multipleLocal, setMultipleLocal] = useState<ItemId[]>(
|
|
139
|
-
//
|
|
140
|
-
// но «расхлябанный» вызов (например, Storybook-spread, где single-default остаётся
|
|
141
|
-
// строкой при переключении selection) не должен ронять компонент на `.map` строки.
|
|
134
|
+
// Страховка от вызова без типов (Storybook-spread): при multiple defaultValue может прийти строкой.
|
|
142
135
|
multiple && Array.isArray(props.defaultValue) ? (props.defaultValue as ItemId[]) : [],
|
|
143
136
|
);
|
|
144
137
|
|
|
145
|
-
// Controlled-режим «залипает»:
|
|
146
|
-
// не читается. Иначе очистка (потребитель ставит `undefined`) откатывалась бы на устаревший
|
|
147
|
-
// локальный выбор и воскрешала прошлое значение.
|
|
138
|
+
// Controlled-режим «залипает»: иначе очистка через `value: undefined` откатится на локальный стейт.
|
|
148
139
|
const singleControlled = useRef(false);
|
|
149
140
|
const multipleControlled = useRef(false);
|
|
150
141
|
|
|
@@ -245,30 +236,36 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
|
|
|
245
236
|
return selectedPairs.map(formatPair).join(', ');
|
|
246
237
|
}, [multiple, singleValue, selectedPairs, chips, props, formatPair, resolveItem]);
|
|
247
238
|
|
|
248
|
-
// Строка поиска: controlled через `search.value`, иначе локальный state. Auto-sync к выбранному
|
|
249
|
-
// значению делаем через СТАБИЛЬНЫЙ setLocalInput, а не через setInputValue в deps эффекта —
|
|
250
|
-
// иначе нестабильная ссылка сеттера зацикливает эффект (сбрасывала бы значение постоянно).
|
|
251
239
|
const [localInput, setLocalInput] = useState(search?.defaultValue ?? selectedLabel);
|
|
252
240
|
const inputValue = search?.value !== undefined ? search.value : localInput;
|
|
253
241
|
const [typing, setTyping] = useState(false);
|
|
254
242
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
243
|
+
// `search` — объектный литерал с новой идентичностью каждый рендер; ref делает `setInputValue` стабильным.
|
|
244
|
+
const searchRef = useRef(search);
|
|
245
|
+
searchRef.current = search;
|
|
246
|
+
|
|
247
|
+
const setInputValue = useCallback((next: string) => {
|
|
248
|
+
const currentSearch = searchRef.current;
|
|
249
|
+
|
|
250
|
+
if (currentSearch?.value === undefined) {
|
|
251
|
+
setLocalInput(next);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
currentSearch?.onChange?.(next);
|
|
255
|
+
}, []);
|
|
256
|
+
|
|
257
|
+
const syncedLabelRef = useRef(selectedLabel);
|
|
264
258
|
|
|
265
259
|
useEffect(() => {
|
|
266
|
-
if (
|
|
267
|
-
|
|
260
|
+
if (typing || !resetSearchOnOptionSelection || syncedLabelRef.current === selectedLabel) {
|
|
261
|
+
return;
|
|
268
262
|
}
|
|
269
|
-
}, [selectedLabel, typing, resetSearchOnOptionSelection, search]);
|
|
270
263
|
|
|
271
|
-
|
|
264
|
+
syncedLabelRef.current = selectedLabel;
|
|
265
|
+
setInputValue(selectedLabel);
|
|
266
|
+
}, [selectedLabel, typing, resetSearchOnOptionSelection, setInputValue]);
|
|
267
|
+
|
|
268
|
+
// При autocomplete фильтрует сервер — items берутся как есть.
|
|
272
269
|
const filteredItems = useMemo(() => {
|
|
273
270
|
if (!searchable || autocomplete || !typing) {
|
|
274
271
|
return items;
|
|
@@ -295,8 +292,7 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
|
|
|
295
292
|
|
|
296
293
|
const handleOpenChange = useCallback(
|
|
297
294
|
(next: boolean) => {
|
|
298
|
-
//
|
|
299
|
-
// readonly/disabled поле открывать нельзя — блокируем открытие, закрытие разрешаем.
|
|
295
|
+
// readonly/disabled поле открывать нельзя, закрывать — можно.
|
|
300
296
|
if (next && (disabled || readOnly)) return;
|
|
301
297
|
if (openProp === undefined) {
|
|
302
298
|
setOpenLocal(next);
|
|
@@ -428,9 +424,7 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
|
|
|
428
424
|
return copied;
|
|
429
425
|
}, [onCopyButtonClick, valueToCopy]);
|
|
430
426
|
|
|
431
|
-
// Clear/Copy
|
|
432
|
-
// кнопку, ArrowLeft — обратно в поле. Паритет с FieldCombo. `inputElementRef` — реальный input,
|
|
433
|
-
// т.к. `inputRef` клобберится popover'ом.
|
|
427
|
+
// Clear/Copy — roving-postfix (ArrowRight/ArrowLeft), паритет с FieldCombo.
|
|
434
428
|
const clearButtonSettings = useClearButton({
|
|
435
429
|
clearButtonRef,
|
|
436
430
|
showClearButton: showClear,
|
|
@@ -458,10 +452,9 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
|
|
|
458
452
|
});
|
|
459
453
|
|
|
460
454
|
const handleTriggerKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
|
461
|
-
//
|
|
455
|
+
// Consumer-обработчик идёт до внутренней обработки (паритет с легаси useHandleOnKeyDown).
|
|
462
456
|
onKeyDownProp?.(event);
|
|
463
457
|
|
|
464
|
-
// Roving-навигация по clear/copy (ArrowRight/ArrowLeft). ArrowDown/Enter ниже не перехватываются.
|
|
465
458
|
onInputKeyDown(event);
|
|
466
459
|
|
|
467
460
|
if (disabled || readOnly) {
|
|
@@ -479,7 +472,7 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
|
|
|
479
472
|
}
|
|
480
473
|
|
|
481
474
|
if (addOptionByEnter && event.key === 'Enter' && inputValue !== '') {
|
|
482
|
-
//
|
|
475
|
+
// Введённый текст становится новым значением (создание опции «на лету»).
|
|
483
476
|
event.preventDefault();
|
|
484
477
|
|
|
485
478
|
if (multiple) {
|
|
@@ -554,10 +547,8 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
|
|
|
554
547
|
|
|
555
548
|
const hasChips = chips && selectedPairs.length > 0;
|
|
556
549
|
|
|
557
|
-
// minWidth поискового input'а
|
|
558
|
-
//
|
|
559
|
-
// пустой ввод → ~4px (input прижат к последнему чипу), длинный ввод → перенос на новую строку
|
|
560
|
-
// через flex-wrap. Без чипов minWidth не задаётся (input занимает всю строку как single-select).
|
|
550
|
+
// minWidth поискового input'а = ширина введённого текста (`.inputPlug`), но не больше строки чипов.
|
|
551
|
+
// Замер до paint, иначе input мигает при вводе.
|
|
561
552
|
useLayoutEffect(() => {
|
|
562
553
|
if (!hasChips) {
|
|
563
554
|
setChipInputMinWidth(undefined);
|
|
@@ -588,8 +579,6 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
|
|
|
588
579
|
/>
|
|
589
580
|
);
|
|
590
581
|
|
|
591
|
-
// В режиме чипов input заворачивается в `.inputWrapper` с динамической minWidth (флоу за чипами);
|
|
592
|
-
// без чипов — обычный `inputArea` на всю строку.
|
|
593
582
|
const searchInput = hasChips ? (
|
|
594
583
|
<div className={styles.inputWrapper} style={{ minWidth: chipInputMinWidth } as CSSProperties}>
|
|
595
584
|
{searchInputField}
|
|
@@ -599,7 +588,7 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
|
|
|
599
588
|
);
|
|
600
589
|
|
|
601
590
|
const trigger = (
|
|
602
|
-
//
|
|
591
|
+
// Focusable-элемент — InputPrivate внутри, обёртка только собирает combobox.
|
|
603
592
|
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
|
604
593
|
<div
|
|
605
594
|
className={cn(fieldStyles.fieldWrapper, styles.trigger, fieldClassName)}
|
|
@@ -636,9 +625,7 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
|
|
|
636
625
|
{iconBefore && <div className={fieldStyles.iconSlot}>{iconBefore}</div>}
|
|
637
626
|
<div className={fieldStyles.inputLine}>
|
|
638
627
|
{prefix && <span className={fieldStyles.prefix}>{prefix}</span>}
|
|
639
|
-
{/* Чипы и
|
|
640
|
-
FieldSelect .contentWrapper): input течёт сразу за последним чипом и
|
|
641
|
-
переносится на новую строку только когда не влезает. */}
|
|
628
|
+
{/* Чипы и input в одном wrap-контейнере: input течёт за последним чипом. */}
|
|
642
629
|
{hasChips ? (
|
|
643
630
|
<div className={styles.chipsRow} ref={contentRef} data-test-id={TEST_IDS.fieldSelectChips}>
|
|
644
631
|
{selectedPairs.map(pair => (
|
|
@@ -651,8 +638,7 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
|
|
|
651
638
|
/>
|
|
652
639
|
))}
|
|
653
640
|
{searchInput}
|
|
654
|
-
{/*
|
|
655
|
-
для minWidth поискового input'а (см. layout-эффект выше). aria-hidden — вне дерева. */}
|
|
641
|
+
{/* Скрытая копия введённого текста — по её ширине считается minWidth input'а. */}
|
|
656
642
|
<span ref={inputPlugRef} className={styles.inputPlug} aria-hidden>
|
|
657
643
|
{inputValue}
|
|
658
644
|
</span>
|
|
@@ -660,9 +646,7 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
|
|
|
660
646
|
) : (
|
|
661
647
|
searchInput
|
|
662
648
|
)}
|
|
663
|
-
{/*
|
|
664
|
-
Слот-обёртка гасит всплытие mousedown/click к Droplist-триггеру; test-id и onClick
|
|
665
|
-
живут на <Button> внутри (адресуемый = интерактивный). */}
|
|
649
|
+
{/* Обёртка гасит всплытие mousedown/click к Droplist-триггеру. */}
|
|
666
650
|
{postfixButtons && (
|
|
667
651
|
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
|
668
652
|
<span
|