@cloud-ru/ds-fields 2.2.3-preview-a0540d7f.0 → 2.2.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 +4 -0
- package/README.md +1 -3
- package/dist/cjs/components/FieldSelect/FieldSelect.js +43 -98
- package/dist/cjs/components/FieldSelect/constants.d.ts +0 -11
- package/dist/cjs/components/FieldSelect/constants.js +1 -20
- package/dist/cjs/components/FieldSelect/types.d.ts +3 -26
- package/dist/cjs/components/FieldSelect/utils.d.ts +19 -0
- package/dist/cjs/components/FieldSelect/utils.js +103 -0
- package/dist/esm/components/FieldSelect/FieldSelect.js +45 -100
- package/dist/esm/components/FieldSelect/constants.d.ts +0 -11
- package/dist/esm/components/FieldSelect/constants.js +0 -19
- package/dist/esm/components/FieldSelect/types.d.ts +3 -26
- package/dist/esm/components/FieldSelect/utils.d.ts +19 -0
- package/dist/esm/components/FieldSelect/utils.js +92 -0
- package/dist/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/package.json +20 -20
- package/src/components/FieldSelect/FieldSelect.tsx +63 -151
- package/src/components/FieldSelect/constants.ts +0 -22
- package/src/components/FieldSelect/types.ts +48 -82
- package/src/components/FieldSelect/utils.ts +132 -0
- package/dist/cjs/components/FieldSelect/hooks.d.ts +0 -33
- package/dist/cjs/components/FieldSelect/hooks.js +0 -95
- package/dist/cjs/components/FieldSelect/utils/customOption.d.ts +0 -21
- package/dist/cjs/components/FieldSelect/utils/customOption.js +0 -55
- package/dist/cjs/components/FieldSelect/utils/extract.d.ts +0 -11
- package/dist/cjs/components/FieldSelect/utils/extract.js +0 -39
- package/dist/cjs/components/FieldSelect/utils/filter.d.ts +0 -3
- package/dist/cjs/components/FieldSelect/utils/filter.js +0 -40
- package/dist/cjs/components/FieldSelect/utils/index.d.ts +0 -6
- package/dist/cjs/components/FieldSelect/utils/index.js +0 -22
- package/dist/cjs/components/FieldSelect/utils/items.d.ts +0 -9
- package/dist/cjs/components/FieldSelect/utils/items.js +0 -47
- package/dist/cjs/components/FieldSelect/utils/selection.d.ts +0 -11
- package/dist/cjs/components/FieldSelect/utils/selection.js +0 -18
- package/dist/cjs/components/FieldSelect/utils/tagSize.d.ts +0 -4
- package/dist/cjs/components/FieldSelect/utils/tagSize.js +0 -5
- package/dist/esm/components/FieldSelect/hooks.d.ts +0 -33
- package/dist/esm/components/FieldSelect/hooks.js +0 -92
- package/dist/esm/components/FieldSelect/utils/customOption.d.ts +0 -21
- package/dist/esm/components/FieldSelect/utils/customOption.js +0 -48
- package/dist/esm/components/FieldSelect/utils/extract.d.ts +0 -11
- package/dist/esm/components/FieldSelect/utils/extract.js +0 -34
- package/dist/esm/components/FieldSelect/utils/filter.d.ts +0 -3
- package/dist/esm/components/FieldSelect/utils/filter.js +0 -36
- package/dist/esm/components/FieldSelect/utils/index.d.ts +0 -6
- package/dist/esm/components/FieldSelect/utils/index.js +0 -6
- package/dist/esm/components/FieldSelect/utils/items.d.ts +0 -9
- package/dist/esm/components/FieldSelect/utils/items.js +0 -41
- package/dist/esm/components/FieldSelect/utils/selection.d.ts +0 -11
- package/dist/esm/components/FieldSelect/utils/selection.js +0 -14
- package/dist/esm/components/FieldSelect/utils/tagSize.d.ts +0 -4
- package/dist/esm/components/FieldSelect/utils/tagSize.js +0 -2
- package/src/components/FieldSelect/hooks.ts +0 -177
- package/src/components/FieldSelect/utils/customOption.ts +0 -77
- package/src/components/FieldSelect/utils/extract.ts +0 -49
- package/src/components/FieldSelect/utils/filter.ts +0 -51
- package/src/components/FieldSelect/utils/index.ts +0 -6
- package/src/components/FieldSelect/utils/items.ts +0 -58
- package/src/components/FieldSelect/utils/selection.ts +0 -25
- package/src/components/FieldSelect/utils/tagSize.ts +0 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloud-ru/ds-fields",
|
|
3
|
-
"version": "2.2.3
|
|
3
|
+
"version": "2.2.3",
|
|
4
4
|
"description": "Поля ввода с единой обёрткой label/caption/hint/error и общими осями size/validationState — текст, пароль, число, ползунок, дата, время, выбор, цвет.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -39,24 +39,24 @@
|
|
|
39
39
|
"classnames": "^2.5.1",
|
|
40
40
|
"merge-refs": "^2.0.0",
|
|
41
41
|
"react-textarea-autosize": "^8.5.9",
|
|
42
|
-
"@cloud-ru/ds-adaptive": "1.0.1",
|
|
43
|
-
"@cloud-ru/ds-
|
|
44
|
-
"@cloud-ru/ds-
|
|
45
|
-
"@cloud-ru/ds-
|
|
46
|
-
"@cloud-ru/ds-
|
|
47
|
-
"@cloud-ru/ds-
|
|
48
|
-
"@cloud-ru/ds-dropdown": "1.0.8",
|
|
49
|
-
"@cloud-ru/ds-
|
|
50
|
-
"@cloud-ru/ds-
|
|
51
|
-
"@cloud-ru/ds-input-private": "1.0.4",
|
|
52
|
-
"@cloud-ru/ds-
|
|
53
|
-
"@cloud-ru/ds-
|
|
54
|
-
"@cloud-ru/ds-
|
|
55
|
-
"@cloud-ru/ds-scroll": "1.0.2",
|
|
56
|
-
"@cloud-ru/ds-skeleton": "1.0.1",
|
|
57
|
-
"@cloud-ru/ds-slider": "1.0.4",
|
|
58
|
-
"@cloud-ru/ds-tag": "1.1.1",
|
|
59
|
-
"@cloud-ru/ds-
|
|
60
|
-
"@cloud-ru/ds-
|
|
42
|
+
"@cloud-ru/ds-adaptive": "^1.0.1",
|
|
43
|
+
"@cloud-ru/ds-button": "^1.0.2",
|
|
44
|
+
"@cloud-ru/ds-bottom-sheet": "^1.1.1",
|
|
45
|
+
"@cloud-ru/ds-color-picker": "^1.0.6",
|
|
46
|
+
"@cloud-ru/ds-divider": "^1.0.1",
|
|
47
|
+
"@cloud-ru/ds-calendar": "^1.0.15",
|
|
48
|
+
"@cloud-ru/ds-dropdown": "^1.0.8",
|
|
49
|
+
"@cloud-ru/ds-field-decorator": "^1.0.5",
|
|
50
|
+
"@cloud-ru/ds-icons": "^1.1.0",
|
|
51
|
+
"@cloud-ru/ds-input-private": "^1.0.4",
|
|
52
|
+
"@cloud-ru/ds-list": "^2.1.3",
|
|
53
|
+
"@cloud-ru/ds-materials": "^1.0.1",
|
|
54
|
+
"@cloud-ru/ds-loader": "^1.0.1",
|
|
55
|
+
"@cloud-ru/ds-scroll": "^1.0.2",
|
|
56
|
+
"@cloud-ru/ds-skeleton": "^1.0.1",
|
|
57
|
+
"@cloud-ru/ds-slider": "^1.0.4",
|
|
58
|
+
"@cloud-ru/ds-tag": "^1.1.1",
|
|
59
|
+
"@cloud-ru/ds-utils": "^1.1.0",
|
|
60
|
+
"@cloud-ru/ds-tooltip": "^1.0.4"
|
|
61
61
|
}
|
|
62
62
|
}
|
|
@@ -2,7 +2,7 @@ import { ChevronDownSVG } from '@cloud-ru/ds-icons/interface/system';
|
|
|
2
2
|
import { InputPrivate, useButtonNavigation, useClearButton } from '@cloud-ru/ds-input-private';
|
|
3
3
|
import { Droplist, ItemId } from '@cloud-ru/ds-list';
|
|
4
4
|
import { Tag } from '@cloud-ru/ds-tag';
|
|
5
|
-
import {
|
|
5
|
+
import { useLayoutEffect } from '@cloud-ru/ds-utils';
|
|
6
6
|
import cn from 'classnames';
|
|
7
7
|
import mergeRefs from 'merge-refs';
|
|
8
8
|
import {
|
|
@@ -34,7 +34,6 @@ import {
|
|
|
34
34
|
useCopyButton,
|
|
35
35
|
} from '../shared';
|
|
36
36
|
import fieldStyles from '../shared/styles.module.scss';
|
|
37
|
-
import { useCustomOption } from './hooks';
|
|
38
37
|
import styles from './styles.module.scss';
|
|
39
38
|
import { FieldSelectProps } from './types';
|
|
40
39
|
import {
|
|
@@ -43,8 +42,6 @@ import {
|
|
|
43
42
|
filterItems,
|
|
44
43
|
findItem,
|
|
45
44
|
flatten,
|
|
46
|
-
getMissingSelectedItems,
|
|
47
|
-
getSelectedIds,
|
|
48
45
|
isMultiple,
|
|
49
46
|
TAG_SIZE_MAP,
|
|
50
47
|
WithIdContent,
|
|
@@ -114,15 +111,12 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
|
|
|
114
111
|
const chips = multiple ? (props.chips ?? true) : false;
|
|
115
112
|
const removeByBackspace = multiple ? (props.removeByBackspace ?? true) : false;
|
|
116
113
|
const closeDroplistOnItemClick = props.closeDroplistOnItemClick ?? !multiple;
|
|
117
|
-
const allowCustomOption = props.allowCustomOption === true;
|
|
118
|
-
const addCustomOptionTriggers = allowCustomOption ? props.addCustomOptionTriggers : undefined;
|
|
119
114
|
|
|
120
115
|
const resolvedAutoFocus = useAdaptiveAutoFocus(autoFocus, layoutPresets);
|
|
121
116
|
|
|
122
117
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
123
118
|
// Отдельный ref на реальный <input>: `inputRef.current` перезаписывается span-обёрткой PopoverPrivate.
|
|
124
119
|
const inputElementRef = useRef<HTMLInputElement>(null);
|
|
125
|
-
const restoreInputFocusRef = useRef(false);
|
|
126
120
|
const clearButtonRef = useRef<HTMLButtonElement>(null);
|
|
127
121
|
const copyButtonRef = useRef<HTMLButtonElement>(null);
|
|
128
122
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
@@ -183,31 +177,18 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
|
|
|
183
177
|
|
|
184
178
|
const allItems = useMemo(() => [...(pinTop ?? []), ...items, ...(pinBottom ?? [])], [items, pinTop, pinBottom]);
|
|
185
179
|
|
|
186
|
-
const selectedIds = useMemo(
|
|
187
|
-
() => getSelectedIds({ multiple, multipleValue, singleValue }),
|
|
188
|
-
[multiple, multipleValue, singleValue],
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
// Выбранное, которого нет в `items` (кастом по allowCustomOption, ленивая подгрузка) — в список, не только в чип.
|
|
192
|
-
const missingSelectedItems = useMemo(() => getMissingSelectedItems(selectedIds, allItems), [selectedIds, allItems]);
|
|
193
|
-
|
|
194
|
-
const itemsWithPlaceholders = useMemo(
|
|
195
|
-
() => (missingSelectedItems.length > 0 ? [...missingSelectedItems, ...items] : items),
|
|
196
|
-
[missingSelectedItems, items],
|
|
197
|
-
);
|
|
198
|
-
|
|
199
180
|
// Выбранное значение переживает смену `items` (серверный поиск, ленивая подгрузка).
|
|
200
181
|
const seenItems = useRef(new Map<ItemId, WithIdContent>());
|
|
201
182
|
|
|
202
183
|
const resolveItem = useMemo(() => {
|
|
203
|
-
for (const item of flatten(
|
|
184
|
+
for (const item of flatten(allItems)) {
|
|
204
185
|
if (item.id !== undefined) {
|
|
205
186
|
seenItems.current.set(item.id, item);
|
|
206
187
|
}
|
|
207
188
|
}
|
|
208
189
|
|
|
209
|
-
return (id: ItemId) => findItem(allItems, id) ??
|
|
210
|
-
}, [allItems
|
|
190
|
+
return (id: ItemId) => findItem(allItems, id) ?? seenItems.current.get(id);
|
|
191
|
+
}, [allItems]);
|
|
211
192
|
|
|
212
193
|
const selectedPairs = useMemo(() => {
|
|
213
194
|
if (!multiple || multipleValue.length === 0) {
|
|
@@ -284,14 +265,14 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
|
|
|
284
265
|
setInputValue(selectedLabel);
|
|
285
266
|
}, [selectedLabel, typing, resetSearchOnOptionSelection, setInputValue]);
|
|
286
267
|
|
|
287
|
-
// При autocomplete фильтрует сервер — items берутся как
|
|
268
|
+
// При autocomplete фильтрует сервер — items берутся как есть.
|
|
288
269
|
const filteredItems = useMemo(() => {
|
|
289
270
|
if (!searchable || autocomplete || !typing) {
|
|
290
|
-
return
|
|
271
|
+
return items;
|
|
291
272
|
}
|
|
292
273
|
|
|
293
|
-
return filterItems(
|
|
294
|
-
}, [searchable, autocomplete, typing,
|
|
274
|
+
return filterItems(items, inputValue, enableFuzzySearch);
|
|
275
|
+
}, [searchable, autocomplete, typing, items, inputValue, enableFuzzySearch]);
|
|
295
276
|
|
|
296
277
|
const filteredPinTop = useMemo(() => {
|
|
297
278
|
if (!searchable || autocomplete || !typing || !pinTop) {
|
|
@@ -309,115 +290,61 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
|
|
|
309
290
|
return filterItems(pinBottom, inputValue, enableFuzzySearch);
|
|
310
291
|
}, [searchable, autocomplete, typing, pinBottom, inputValue, enableFuzzySearch]);
|
|
311
292
|
|
|
312
|
-
const
|
|
313
|
-
(next:
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if (props.value === undefined) {
|
|
319
|
-
setSingleLocal(next);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
props.onChange?.(next);
|
|
323
|
-
setTyping(false);
|
|
324
|
-
},
|
|
325
|
-
[multiple, props],
|
|
326
|
-
);
|
|
327
|
-
|
|
328
|
-
const handleMultipleChange = useCallback(
|
|
329
|
-
(next: ItemId[]) => {
|
|
330
|
-
if (!multiple) {
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
restoreInputFocusRef.current = isBrowser() && inputElementRef.current === document.activeElement;
|
|
335
|
-
|
|
336
|
-
if (props.value === undefined) {
|
|
337
|
-
setMultipleLocal(next);
|
|
293
|
+
const handleOpenChange = useCallback(
|
|
294
|
+
(next: boolean) => {
|
|
295
|
+
// readonly/disabled поле открывать нельзя, закрывать — можно.
|
|
296
|
+
if (next && (disabled || readOnly)) return;
|
|
297
|
+
if (openProp === undefined) {
|
|
298
|
+
setOpenLocal(next);
|
|
338
299
|
}
|
|
339
300
|
|
|
340
|
-
|
|
301
|
+
onOpenChange?.(next);
|
|
341
302
|
|
|
342
|
-
if (
|
|
303
|
+
if (!next) {
|
|
343
304
|
setTyping(false);
|
|
344
305
|
|
|
345
306
|
if (resetSearchOnOptionSelection) {
|
|
346
|
-
setInputValue(
|
|
307
|
+
setInputValue(selectedLabel);
|
|
347
308
|
}
|
|
348
309
|
}
|
|
349
310
|
},
|
|
350
|
-
[
|
|
311
|
+
[openProp, onOpenChange, disabled, readOnly, resetSearchOnOptionSelection, setInputValue, selectedLabel],
|
|
351
312
|
);
|
|
352
313
|
|
|
353
|
-
const
|
|
354
|
-
|
|
314
|
+
const handleSingleChange = (next: ItemId | undefined) => {
|
|
315
|
+
if (multiple) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
355
318
|
|
|
356
|
-
if (
|
|
357
|
-
|
|
319
|
+
if (props.value === undefined) {
|
|
320
|
+
setSingleLocal(next);
|
|
358
321
|
}
|
|
359
|
-
}, [resetSearchOnOptionSelection, setInputValue, multiple, selectedLabel]);
|
|
360
322
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
enableCloseCommit,
|
|
365
|
-
tryCommitOnClose,
|
|
366
|
-
tryCommitOnBlur,
|
|
367
|
-
tryCommitFromKeyboard,
|
|
368
|
-
handleListSingleChange,
|
|
369
|
-
handleListMultipleChange,
|
|
370
|
-
} = useCustomOption({
|
|
371
|
-
allowCustomOption,
|
|
372
|
-
addCustomOptionTriggers,
|
|
373
|
-
addOptionByEnter,
|
|
374
|
-
multiple,
|
|
375
|
-
inputValue,
|
|
376
|
-
typing,
|
|
377
|
-
multipleValue,
|
|
378
|
-
commitSingle: handleSingleChange,
|
|
379
|
-
commitMultiple: handleMultipleChange,
|
|
380
|
-
resetSearch: resetCustomOptionSearch,
|
|
381
|
-
inputElementRef,
|
|
382
|
-
clearButtonRef,
|
|
383
|
-
copyButtonRef,
|
|
384
|
-
footerActiveElementsRefs,
|
|
385
|
-
});
|
|
323
|
+
props.onChange?.(next);
|
|
324
|
+
setTyping(false);
|
|
325
|
+
};
|
|
386
326
|
|
|
387
|
-
const
|
|
388
|
-
(
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
if (openProp === undefined) {
|
|
392
|
-
setOpenLocal(next);
|
|
393
|
-
}
|
|
327
|
+
const handleMultipleChange = (next: ItemId[]) => {
|
|
328
|
+
if (!multiple) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
394
331
|
|
|
395
|
-
|
|
332
|
+
if (props.value === undefined) {
|
|
333
|
+
setMultipleLocal(next);
|
|
334
|
+
}
|
|
396
335
|
|
|
397
|
-
|
|
398
|
-
setTyping(false);
|
|
399
|
-
const committed = tryCommitOnClose();
|
|
336
|
+
props.onChange?.(next);
|
|
400
337
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
338
|
+
if (searchable) {
|
|
339
|
+
setTyping(false);
|
|
340
|
+
|
|
341
|
+
if (resetSearchOnOptionSelection) {
|
|
342
|
+
setInputValue('');
|
|
405
343
|
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
openProp,
|
|
409
|
-
onOpenChange,
|
|
410
|
-
disabled,
|
|
411
|
-
readOnly,
|
|
412
|
-
resetSearchOnOptionSelection,
|
|
413
|
-
setInputValue,
|
|
414
|
-
selectedLabel,
|
|
415
|
-
tryCommitOnClose,
|
|
416
|
-
],
|
|
417
|
-
);
|
|
344
|
+
}
|
|
345
|
+
};
|
|
418
346
|
|
|
419
347
|
const handleInputChange = (next: string) => {
|
|
420
|
-
enableCloseCommit();
|
|
421
348
|
setInputValue(next);
|
|
422
349
|
setTyping(true);
|
|
423
350
|
|
|
@@ -544,13 +471,21 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
|
|
|
544
471
|
|
|
545
472
|
if (event.key === 'Escape' && open) {
|
|
546
473
|
event.preventDefault();
|
|
547
|
-
suppressCloseCommit();
|
|
548
474
|
handleOpenChange(false);
|
|
549
475
|
}
|
|
550
476
|
|
|
551
|
-
if (
|
|
477
|
+
if (addOptionByEnter && event.key === 'Enter' && inputValue !== '') {
|
|
478
|
+
// Введённый текст становится новым значением (создание опции «на лету»).
|
|
552
479
|
event.preventDefault();
|
|
553
|
-
|
|
480
|
+
|
|
481
|
+
if (multiple) {
|
|
482
|
+
if (!multipleValue.includes(inputValue)) {
|
|
483
|
+
handleMultipleChange([...multipleValue, inputValue]);
|
|
484
|
+
}
|
|
485
|
+
} else {
|
|
486
|
+
handleSingleChange(inputValue);
|
|
487
|
+
}
|
|
488
|
+
|
|
554
489
|
return;
|
|
555
490
|
}
|
|
556
491
|
|
|
@@ -566,12 +501,6 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
|
|
|
566
501
|
}
|
|
567
502
|
};
|
|
568
503
|
|
|
569
|
-
const handleTriggerKeyDownCapture = (event: KeyboardEvent<HTMLElement>) => {
|
|
570
|
-
if (event.key === 'Escape') {
|
|
571
|
-
suppressCloseCommit();
|
|
572
|
-
}
|
|
573
|
-
};
|
|
574
|
-
|
|
575
504
|
const handleTriggerMouseEnter = useCallback(() => {
|
|
576
505
|
if (!readOnly && !disabled) {
|
|
577
506
|
setHover(true);
|
|
@@ -607,35 +536,20 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
|
|
|
607
536
|
[searchable, onFocusProp],
|
|
608
537
|
);
|
|
609
538
|
|
|
610
|
-
const handleInputBlur = (
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
setTyping(false);
|
|
618
|
-
setInputValue(selectedLabel);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
onBlurProp?.(event);
|
|
622
|
-
};
|
|
539
|
+
const handleInputBlur = useCallback(
|
|
540
|
+
(event: FocusEvent<HTMLInputElement>) => {
|
|
541
|
+
setFocusVisible(false);
|
|
542
|
+
onBlurProp?.(event);
|
|
543
|
+
},
|
|
544
|
+
[onBlurProp],
|
|
545
|
+
);
|
|
623
546
|
|
|
624
547
|
const droplistSelection = multiple
|
|
625
|
-
? { mode: 'multiple' as const, value: multipleValue, onChange:
|
|
626
|
-
: { mode: 'single' as const, value: singleValue, onChange:
|
|
548
|
+
? { mode: 'multiple' as const, value: multipleValue, onChange: handleMultipleChange }
|
|
549
|
+
: { mode: 'single' as const, value: singleValue, onChange: handleSingleChange };
|
|
627
550
|
|
|
628
551
|
const hasChips = chips && selectedPairs.length > 0;
|
|
629
552
|
|
|
630
|
-
useLayoutEffect(() => {
|
|
631
|
-
if (!restoreInputFocusRef.current) {
|
|
632
|
-
return;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
restoreInputFocusRef.current = false;
|
|
636
|
-
inputElementRef.current?.focus();
|
|
637
|
-
}, [multipleValue.length]);
|
|
638
|
-
|
|
639
553
|
// minWidth поискового input'а = ширина введённого текста (`.inputPlug`), но не больше строки чипов.
|
|
640
554
|
// Замер до paint, иначе input мигает при вводе.
|
|
641
555
|
useLayoutEffect(() => {
|
|
@@ -669,7 +583,7 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
|
|
|
669
583
|
);
|
|
670
584
|
|
|
671
585
|
const searchInput = hasChips ? (
|
|
672
|
-
<div
|
|
586
|
+
<div className={styles.inputWrapper} style={{ minWidth: chipInputMinWidth } as CSSProperties}>
|
|
673
587
|
{searchInputField}
|
|
674
588
|
</div>
|
|
675
589
|
) : (
|
|
@@ -691,7 +605,6 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
|
|
|
691
605
|
data-test-id={dataTestId}
|
|
692
606
|
onMouseEnter={handleTriggerMouseEnter}
|
|
693
607
|
onMouseLeave={handleTriggerMouseLeave}
|
|
694
|
-
onKeyDownCapture={handleTriggerKeyDownCapture}
|
|
695
608
|
onClick={handleTriggerClick}
|
|
696
609
|
>
|
|
697
610
|
<div className={fieldStyles.backgroundWrapper}>
|
|
@@ -785,7 +698,6 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
|
|
|
785
698
|
placement={placement}
|
|
786
699
|
widthStrategy={widthStrategy}
|
|
787
700
|
triggerElemRef={inputRef}
|
|
788
|
-
listRef={listRef}
|
|
789
701
|
size={size}
|
|
790
702
|
open={open}
|
|
791
703
|
onOpenChange={handleOpenChange}
|
|
@@ -2,25 +2,3 @@ export const SELECTION_MODE = {
|
|
|
2
2
|
Single: 'single',
|
|
3
3
|
Multiple: 'multiple',
|
|
4
4
|
} as const;
|
|
5
|
-
|
|
6
|
-
/** События, по которым произвольный ввод фиксируется как выбранная опция. */
|
|
7
|
-
export const ADD_CUSTOM_OPTION_TRIGGER = {
|
|
8
|
-
Enter: 'enter',
|
|
9
|
-
Blur: 'blur',
|
|
10
|
-
Space: 'space',
|
|
11
|
-
Comma: 'comma',
|
|
12
|
-
} as const;
|
|
13
|
-
|
|
14
|
-
/** Триггеры по умолчанию для `selection='single'` при `allowCustomOption`. */
|
|
15
|
-
export const ADD_CUSTOM_OPTION_TRIGGERS_SINGLE = [
|
|
16
|
-
ADD_CUSTOM_OPTION_TRIGGER.Enter,
|
|
17
|
-
ADD_CUSTOM_OPTION_TRIGGER.Blur,
|
|
18
|
-
] as const;
|
|
19
|
-
|
|
20
|
-
/** Триггеры по умолчанию для `selection='multiple'` при `allowCustomOption`. */
|
|
21
|
-
export const ADD_CUSTOM_OPTION_TRIGGERS_MULTIPLE = [
|
|
22
|
-
ADD_CUSTOM_OPTION_TRIGGER.Enter,
|
|
23
|
-
ADD_CUSTOM_OPTION_TRIGGER.Blur,
|
|
24
|
-
ADD_CUSTOM_OPTION_TRIGGER.Space,
|
|
25
|
-
ADD_CUSTOM_OPTION_TRIGGER.Comma,
|
|
26
|
-
] as const;
|
|
@@ -5,41 +5,11 @@ import { ValueOf } from '@cloud-ru/ds-utils';
|
|
|
5
5
|
import { FocusEvent, KeyboardEvent, ReactNode } from 'react';
|
|
6
6
|
|
|
7
7
|
import { FieldLayoutPresets } from '../../hooks';
|
|
8
|
-
import {
|
|
9
|
-
ADD_CUSTOM_OPTION_TRIGGER,
|
|
10
|
-
ADD_CUSTOM_OPTION_TRIGGERS_MULTIPLE,
|
|
11
|
-
ADD_CUSTOM_OPTION_TRIGGERS_SINGLE,
|
|
12
|
-
SELECTION_MODE,
|
|
13
|
-
} from './constants';
|
|
8
|
+
import { SELECTION_MODE } from './constants';
|
|
14
9
|
|
|
15
10
|
/** Режим выбора FieldSelect: одно значение или несколько. */
|
|
16
11
|
export type Selection = ValueOf<typeof SELECTION_MODE>;
|
|
17
12
|
|
|
18
|
-
/** Событие, по которому произвольный ввод фиксируется как выбранная опция. */
|
|
19
|
-
export type FieldSelectAddCustomOptionTrigger = ValueOf<typeof ADD_CUSTOM_OPTION_TRIGGER>;
|
|
20
|
-
|
|
21
|
-
/** Триггеры кастомной опции в режиме `single`. */
|
|
22
|
-
export type FieldSelectSingleAddCustomOptionTrigger = (typeof ADD_CUSTOM_OPTION_TRIGGERS_SINGLE)[number];
|
|
23
|
-
|
|
24
|
-
/** Триггеры кастомной опции в режиме `multiple`. */
|
|
25
|
-
export type FieldSelectMultipleAddCustomOptionTrigger = (typeof ADD_CUSTOM_OPTION_TRIGGERS_MULTIPLE)[number];
|
|
26
|
-
|
|
27
|
-
type CustomOptionProps<TTrigger extends FieldSelectAddCustomOptionTrigger> = {
|
|
28
|
-
/**
|
|
29
|
-
* Разрешить фиксировать произвольный ввод из строки поиска как выбранную опцию
|
|
30
|
-
* (значение, которого нет в `items`).
|
|
31
|
-
* @default false
|
|
32
|
-
*/
|
|
33
|
-
allowCustomOption?: boolean;
|
|
34
|
-
/**
|
|
35
|
-
* События, по которым произвольный ввод фиксируется как выбранная опция.
|
|
36
|
-
* Учитывается только при `allowCustomOption={true}`.
|
|
37
|
-
* Если не задан — используются все триггеры режима (`single`: enter, blur; `multiple`: enter, blur, space, comma).
|
|
38
|
-
* Триггеры, недоступные в текущем `selection`, игнорируются.
|
|
39
|
-
*/
|
|
40
|
-
addCustomOptionTriggers?: TTrigger[];
|
|
41
|
-
};
|
|
42
|
-
|
|
43
13
|
// ItemProps — union с рекурсивными группами, поэтому раздаём поле по всем веткам.
|
|
44
14
|
type WithTagAppearance<T> = T extends { items: ItemProps[] }
|
|
45
15
|
? Omit<T, 'items'> & { appearance?: TagAppearance; items: WithTagAppearance<ItemProps>[] }
|
|
@@ -145,8 +115,6 @@ type CommonSelectProps = FieldSelectDecoratorProps &
|
|
|
145
115
|
autocomplete?: boolean;
|
|
146
116
|
/**
|
|
147
117
|
* Зафиксировать введённый текст как новый выбор по `Enter` (создание опции «на лету»).
|
|
148
|
-
*
|
|
149
|
-
* @deprecated Используйте `allowCustomOption`. `true` эквивалентен `allowCustomOption` с триггером `enter`.
|
|
150
118
|
* @default false
|
|
151
119
|
*/
|
|
152
120
|
addOptionByEnter?: boolean;
|
|
@@ -201,55 +169,53 @@ type CommonSelectProps = FieldSelectDecoratorProps &
|
|
|
201
169
|
'data-test-id'?: string;
|
|
202
170
|
};
|
|
203
171
|
|
|
204
|
-
export type FieldSelectSingleProps = CommonSelectProps &
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
};
|
|
172
|
+
export type FieldSelectSingleProps = CommonSelectProps & {
|
|
173
|
+
/** Режим выбора. По умолчанию `'single'`. */
|
|
174
|
+
selection?: typeof SELECTION_MODE.Single;
|
|
175
|
+
/** Управляемое значение. Пустая строка трактуется как «значение не выбрано». */
|
|
176
|
+
value?: ItemId;
|
|
177
|
+
/** Неуправляемое значение по умолчанию. Пустая строка трактуется как «значение не выбрано». */
|
|
178
|
+
defaultValue?: ItemId;
|
|
179
|
+
/** Колбек смены значения */
|
|
180
|
+
onChange?(value: ItemId | undefined): void;
|
|
181
|
+
/**
|
|
182
|
+
* Закрывать дроплист после клика на айтем.
|
|
183
|
+
* @default true
|
|
184
|
+
*/
|
|
185
|
+
closeDroplistOnItemClick?: boolean;
|
|
186
|
+
};
|
|
220
187
|
|
|
221
|
-
export type FieldSelectMultipleProps = CommonSelectProps &
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
};
|
|
188
|
+
export type FieldSelectMultipleProps = CommonSelectProps & {
|
|
189
|
+
/** Режим выбора */
|
|
190
|
+
selection: typeof SELECTION_MODE.Multiple;
|
|
191
|
+
/** Управляемые значения */
|
|
192
|
+
value?: ItemId[];
|
|
193
|
+
/** Неуправляемые значения по умолчанию */
|
|
194
|
+
defaultValue?: ItemId[];
|
|
195
|
+
/** Колбек смены значений */
|
|
196
|
+
onChange?(value: ItemId[]): void;
|
|
197
|
+
/**
|
|
198
|
+
* Форматтер строки выбранных значений (используется, если `chips=false`).
|
|
199
|
+
* @default — список лейблов через `, `
|
|
200
|
+
*/
|
|
201
|
+
formatSelected?(selected: { id: ItemId; label: string }[]): string;
|
|
202
|
+
/**
|
|
203
|
+
* Отображать выбранные значения как чипы (`@cloud-ru/ds-tag`) внутри поля. Если `false`,
|
|
204
|
+
* показывает строку из `formatSelected` либо comma-joined.
|
|
205
|
+
* @default true
|
|
206
|
+
*/
|
|
207
|
+
chips?: boolean;
|
|
208
|
+
/**
|
|
209
|
+
* Удалять последний чип по `Backspace`, когда строка ввода пустая.
|
|
210
|
+
* Работает только при `chips=true` и `searchable=true`.
|
|
211
|
+
* @default true
|
|
212
|
+
*/
|
|
213
|
+
removeByBackspace?: boolean;
|
|
214
|
+
/**
|
|
215
|
+
* Закрывать дроплист после клика на айтем.
|
|
216
|
+
* @default false
|
|
217
|
+
*/
|
|
218
|
+
closeDroplistOnItemClick?: boolean;
|
|
219
|
+
};
|
|
254
220
|
|
|
255
221
|
export type FieldSelectProps = FieldSelectSingleProps | FieldSelectMultipleProps;
|