@cloud-ru/ds-fields 2.2.2 → 2.2.3-preview-0c4119af.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 (60) hide show
  1. package/README.md +3 -1
  2. package/dist/cjs/components/FieldSelect/FieldSelect.js +81 -41
  3. package/dist/cjs/components/FieldSelect/constants.d.ts +11 -0
  4. package/dist/cjs/components/FieldSelect/constants.js +20 -1
  5. package/dist/cjs/components/FieldSelect/hooks.d.ts +32 -0
  6. package/dist/cjs/components/FieldSelect/hooks.js +83 -0
  7. package/dist/cjs/components/FieldSelect/types.d.ts +26 -3
  8. package/dist/cjs/components/FieldSelect/utils/customOption.d.ts +21 -0
  9. package/dist/cjs/components/FieldSelect/utils/customOption.js +55 -0
  10. package/dist/cjs/components/FieldSelect/utils/extract.d.ts +11 -0
  11. package/dist/cjs/components/FieldSelect/utils/extract.js +39 -0
  12. package/dist/cjs/components/FieldSelect/utils/filter.d.ts +3 -0
  13. package/dist/cjs/components/FieldSelect/utils/filter.js +40 -0
  14. package/dist/cjs/components/FieldSelect/utils/index.d.ts +6 -0
  15. package/dist/cjs/components/FieldSelect/utils/index.js +22 -0
  16. package/dist/cjs/components/FieldSelect/utils/items.d.ts +9 -0
  17. package/dist/cjs/components/FieldSelect/utils/items.js +47 -0
  18. package/dist/cjs/components/FieldSelect/utils/selection.d.ts +11 -0
  19. package/dist/cjs/components/FieldSelect/utils/selection.js +18 -0
  20. package/dist/cjs/components/FieldSelect/utils/tagSize.d.ts +4 -0
  21. package/dist/cjs/components/FieldSelect/utils/tagSize.js +5 -0
  22. package/dist/esm/components/FieldSelect/FieldSelect.js +82 -42
  23. package/dist/esm/components/FieldSelect/constants.d.ts +11 -0
  24. package/dist/esm/components/FieldSelect/constants.js +19 -0
  25. package/dist/esm/components/FieldSelect/hooks.d.ts +32 -0
  26. package/dist/esm/components/FieldSelect/hooks.js +80 -0
  27. package/dist/esm/components/FieldSelect/types.d.ts +26 -3
  28. package/dist/esm/components/FieldSelect/utils/customOption.d.ts +21 -0
  29. package/dist/esm/components/FieldSelect/utils/customOption.js +48 -0
  30. package/dist/esm/components/FieldSelect/utils/extract.d.ts +11 -0
  31. package/dist/esm/components/FieldSelect/utils/extract.js +34 -0
  32. package/dist/esm/components/FieldSelect/utils/filter.d.ts +3 -0
  33. package/dist/esm/components/FieldSelect/utils/filter.js +36 -0
  34. package/dist/esm/components/FieldSelect/utils/index.d.ts +6 -0
  35. package/dist/esm/components/FieldSelect/utils/index.js +6 -0
  36. package/dist/esm/components/FieldSelect/utils/items.d.ts +9 -0
  37. package/dist/esm/components/FieldSelect/utils/items.js +41 -0
  38. package/dist/esm/components/FieldSelect/utils/selection.d.ts +11 -0
  39. package/dist/esm/components/FieldSelect/utils/selection.js +14 -0
  40. package/dist/esm/components/FieldSelect/utils/tagSize.d.ts +4 -0
  41. package/dist/esm/components/FieldSelect/utils/tagSize.js +2 -0
  42. package/dist/tsconfig.cjs.tsbuildinfo +1 -1
  43. package/dist/tsconfig.esm.tsbuildinfo +1 -1
  44. package/package.json +20 -20
  45. package/src/components/FieldSelect/FieldSelect.tsx +129 -61
  46. package/src/components/FieldSelect/constants.ts +22 -0
  47. package/src/components/FieldSelect/hooks.ts +162 -0
  48. package/src/components/FieldSelect/types.ts +82 -48
  49. package/src/components/FieldSelect/utils/customOption.ts +77 -0
  50. package/src/components/FieldSelect/utils/extract.ts +49 -0
  51. package/src/components/FieldSelect/utils/filter.ts +51 -0
  52. package/src/components/FieldSelect/utils/index.ts +6 -0
  53. package/src/components/FieldSelect/utils/items.ts +58 -0
  54. package/src/components/FieldSelect/utils/selection.ts +25 -0
  55. package/src/components/FieldSelect/utils/tagSize.ts +5 -0
  56. package/dist/cjs/components/FieldSelect/utils.d.ts +0 -19
  57. package/dist/cjs/components/FieldSelect/utils.js +0 -103
  58. package/dist/esm/components/FieldSelect/utils.d.ts +0 -19
  59. package/dist/esm/components/FieldSelect/utils.js +0 -92
  60. package/src/components/FieldSelect/utils.ts +0 -132
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloud-ru/ds-fields",
3
- "version": "2.2.2",
3
+ "version": "2.2.3-preview-0c4119af.0",
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-button": "^1.0.2",
44
- "@cloud-ru/ds-bottom-sheet": "^1.1.1",
45
- "@cloud-ru/ds-calendar": "^1.0.14",
46
- "@cloud-ru/ds-color-picker": "^1.0.6",
47
- "@cloud-ru/ds-dropdown": "^1.0.8",
48
- "@cloud-ru/ds-divider": "^1.0.1",
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-loader": "^1.0.1",
53
- "@cloud-ru/ds-list": "^2.1.3",
54
- "@cloud-ru/ds-scroll": "^1.0.2",
55
- "@cloud-ru/ds-materials": "^1.0.1",
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-tooltip": "^1.0.4",
60
- "@cloud-ru/ds-utils": "^1.1.0"
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-calendar": "1.0.15-preview-0c4119af.0",
46
+ "@cloud-ru/ds-color-picker": "1.0.6",
47
+ "@cloud-ru/ds-divider": "1.0.1",
48
+ "@cloud-ru/ds-field-decorator": "1.0.5",
49
+ "@cloud-ru/ds-icons": "1.1.0",
50
+ "@cloud-ru/ds-dropdown": "1.0.8",
51
+ "@cloud-ru/ds-list": "2.1.4-preview-0c4119af.0",
52
+ "@cloud-ru/ds-input-private": "1.0.4",
53
+ "@cloud-ru/ds-loader": "1.0.1",
54
+ "@cloud-ru/ds-materials": "1.0.1",
55
+ "@cloud-ru/ds-scroll": "1.0.2",
56
+ "@cloud-ru/ds-tooltip": "1.0.4",
57
+ "@cloud-ru/ds-slider": "1.0.4",
58
+ "@cloud-ru/ds-skeleton": "1.0.1",
59
+ "@cloud-ru/ds-tag": "1.1.1",
60
+ "@cloud-ru/ds-utils": "1.1.0"
61
61
  }
62
62
  }
@@ -34,6 +34,7 @@ import {
34
34
  useCopyButton,
35
35
  } from '../shared';
36
36
  import fieldStyles from '../shared/styles.module.scss';
37
+ import { useCustomOption } from './hooks';
37
38
  import styles from './styles.module.scss';
38
39
  import { FieldSelectProps } from './types';
39
40
  import {
@@ -42,6 +43,8 @@ import {
42
43
  filterItems,
43
44
  findItem,
44
45
  flatten,
46
+ getMissingSelectedItems,
47
+ getSelectedIds,
45
48
  isMultiple,
46
49
  TAG_SIZE_MAP,
47
50
  WithIdContent,
@@ -111,6 +114,8 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
111
114
  const chips = multiple ? (props.chips ?? true) : false;
112
115
  const removeByBackspace = multiple ? (props.removeByBackspace ?? true) : false;
113
116
  const closeDroplistOnItemClick = props.closeDroplistOnItemClick ?? !multiple;
117
+ const allowCustomOption = props.allowCustomOption === true;
118
+ const addCustomOptionTriggers = allowCustomOption ? props.addCustomOptionTriggers : undefined;
114
119
 
115
120
  const resolvedAutoFocus = useAdaptiveAutoFocus(autoFocus, layoutPresets);
116
121
 
@@ -177,18 +182,31 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
177
182
 
178
183
  const allItems = useMemo(() => [...(pinTop ?? []), ...items, ...(pinBottom ?? [])], [items, pinTop, pinBottom]);
179
184
 
185
+ const selectedIds = useMemo(
186
+ () => getSelectedIds({ multiple, multipleValue, singleValue }),
187
+ [multiple, multipleValue, singleValue],
188
+ );
189
+
190
+ // Выбранное, которого нет в `items` (кастом по allowCustomOption, ленивая подгрузка) — в список, не только в чип.
191
+ const missingSelectedItems = useMemo(() => getMissingSelectedItems(selectedIds, allItems), [selectedIds, allItems]);
192
+
193
+ const itemsWithPlaceholders = useMemo(
194
+ () => (missingSelectedItems.length > 0 ? [...missingSelectedItems, ...items] : items),
195
+ [missingSelectedItems, items],
196
+ );
197
+
180
198
  // Выбранное значение переживает смену `items` (серверный поиск, ленивая подгрузка).
181
199
  const seenItems = useRef(new Map<ItemId, WithIdContent>());
182
200
 
183
201
  const resolveItem = useMemo(() => {
184
- for (const item of flatten(allItems)) {
202
+ for (const item of flatten([...missingSelectedItems, ...allItems])) {
185
203
  if (item.id !== undefined) {
186
204
  seenItems.current.set(item.id, item);
187
205
  }
188
206
  }
189
207
 
190
- return (id: ItemId) => findItem(allItems, id) ?? seenItems.current.get(id);
191
- }, [allItems]);
208
+ return (id: ItemId) => findItem(allItems, id) ?? findItem(missingSelectedItems, id) ?? seenItems.current.get(id);
209
+ }, [allItems, missingSelectedItems]);
192
210
 
193
211
  const selectedPairs = useMemo(() => {
194
212
  if (!multiple || multipleValue.length === 0) {
@@ -265,14 +283,14 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
265
283
  setInputValue(selectedLabel);
266
284
  }, [selectedLabel, typing, resetSearchOnOptionSelection, setInputValue]);
267
285
 
268
- // При autocomplete фильтрует сервер — items берутся как есть.
286
+ // При autocomplete фильтрует сервер — items берутся как есть, плюс выбранное вне списка.
269
287
  const filteredItems = useMemo(() => {
270
288
  if (!searchable || autocomplete || !typing) {
271
- return items;
289
+ return itemsWithPlaceholders;
272
290
  }
273
291
 
274
- return filterItems(items, inputValue, enableFuzzySearch);
275
- }, [searchable, autocomplete, typing, items, inputValue, enableFuzzySearch]);
292
+ return filterItems(itemsWithPlaceholders, inputValue, enableFuzzySearch);
293
+ }, [searchable, autocomplete, typing, itemsWithPlaceholders, inputValue, enableFuzzySearch]);
276
294
 
277
295
  const filteredPinTop = useMemo(() => {
278
296
  if (!searchable || autocomplete || !typing || !pinTop) {
@@ -290,61 +308,112 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
290
308
  return filterItems(pinBottom, inputValue, enableFuzzySearch);
291
309
  }, [searchable, autocomplete, typing, pinBottom, inputValue, enableFuzzySearch]);
292
310
 
293
- const handleOpenChange = useCallback(
294
- (next: boolean) => {
295
- // readonly/disabled поле открывать нельзя, закрывать — можно.
296
- if (next && (disabled || readOnly)) return;
297
- if (openProp === undefined) {
298
- setOpenLocal(next);
311
+ const handleSingleChange = useCallback(
312
+ (next: ItemId | undefined) => {
313
+ if (multiple) {
314
+ return;
299
315
  }
300
316
 
301
- onOpenChange?.(next);
317
+ if (props.value === undefined) {
318
+ setSingleLocal(next);
319
+ }
302
320
 
303
- if (!next) {
321
+ props.onChange?.(next);
322
+ setTyping(false);
323
+ },
324
+ [multiple, props],
325
+ );
326
+
327
+ const handleMultipleChange = useCallback(
328
+ (next: ItemId[]) => {
329
+ if (!multiple) {
330
+ return;
331
+ }
332
+
333
+ if (props.value === undefined) {
334
+ setMultipleLocal(next);
335
+ }
336
+
337
+ props.onChange?.(next);
338
+
339
+ if (searchable) {
304
340
  setTyping(false);
305
341
 
306
342
  if (resetSearchOnOptionSelection) {
307
- setInputValue(selectedLabel);
343
+ setInputValue('');
308
344
  }
309
345
  }
310
346
  },
311
- [openProp, onOpenChange, disabled, readOnly, resetSearchOnOptionSelection, setInputValue, selectedLabel],
347
+ [multiple, props, searchable, resetSearchOnOptionSelection, setInputValue],
312
348
  );
313
349
 
314
- const handleSingleChange = (next: ItemId | undefined) => {
315
- if (multiple) {
316
- return;
317
- }
318
-
319
- if (props.value === undefined) {
320
- setSingleLocal(next);
321
- }
322
-
323
- props.onChange?.(next);
350
+ const resetCustomOptionSearch = useCallback(() => {
324
351
  setTyping(false);
325
- };
326
352
 
327
- const handleMultipleChange = (next: ItemId[]) => {
328
- if (!multiple) {
329
- return;
353
+ if (resetSearchOnOptionSelection) {
354
+ setInputValue(multiple ? '' : selectedLabel);
330
355
  }
356
+ }, [resetSearchOnOptionSelection, setInputValue, multiple, selectedLabel]);
331
357
 
332
- if (props.value === undefined) {
333
- setMultipleLocal(next);
334
- }
358
+ const {
359
+ listRef,
360
+ suppressCloseCommit,
361
+ enableCloseCommit,
362
+ tryCommitOnClose,
363
+ tryCommitOnBlur,
364
+ tryCommitFromKeyboard,
365
+ handleListSingleChange,
366
+ handleListMultipleChange,
367
+ } = useCustomOption({
368
+ allowCustomOption,
369
+ addCustomOptionTriggers,
370
+ addOptionByEnter,
371
+ multiple,
372
+ inputValue,
373
+ multipleValue,
374
+ commitSingle: handleSingleChange,
375
+ commitMultiple: handleMultipleChange,
376
+ resetSearch: resetCustomOptionSearch,
377
+ inputElementRef,
378
+ clearButtonRef,
379
+ copyButtonRef,
380
+ footerActiveElementsRefs,
381
+ });
382
+
383
+ const handleOpenChange = useCallback(
384
+ (next: boolean) => {
385
+ // readonly/disabled поле открывать нельзя, закрывать — можно.
386
+ if (next && (disabled || readOnly)) return;
387
+ if (openProp === undefined) {
388
+ setOpenLocal(next);
389
+ }
335
390
 
336
- props.onChange?.(next);
391
+ onOpenChange?.(next);
337
392
 
338
- if (searchable) {
339
- setTyping(false);
393
+ if (!next) {
394
+ setTyping(false);
395
+ const committed = tryCommitOnClose();
340
396
 
341
- if (resetSearchOnOptionSelection) {
342
- setInputValue('');
397
+ // Кастомная опция не зафиксирована — вернуть лейбл выбранного значения.
398
+ if (resetSearchOnOptionSelection && !committed) {
399
+ setInputValue(selectedLabel);
400
+ }
343
401
  }
344
- }
345
- };
402
+ },
403
+ [
404
+ openProp,
405
+ onOpenChange,
406
+ disabled,
407
+ readOnly,
408
+ resetSearchOnOptionSelection,
409
+ setInputValue,
410
+ selectedLabel,
411
+ tryCommitOnClose,
412
+ ],
413
+ );
346
414
 
347
415
  const handleInputChange = (next: string) => {
416
+ enableCloseCommit();
348
417
  setInputValue(next);
349
418
  setTyping(true);
350
419
 
@@ -471,21 +540,13 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
471
540
 
472
541
  if (event.key === 'Escape' && open) {
473
542
  event.preventDefault();
543
+ suppressCloseCommit();
474
544
  handleOpenChange(false);
475
545
  }
476
546
 
477
- if (addOptionByEnter && event.key === 'Enter' && inputValue !== '') {
478
- // Введённый текст становится новым значением (создание опции «на лету»).
547
+ if (tryCommitFromKeyboard(event)) {
479
548
  event.preventDefault();
480
-
481
- if (multiple) {
482
- if (!multipleValue.includes(inputValue)) {
483
- handleMultipleChange([...multipleValue, inputValue]);
484
- }
485
- } else {
486
- handleSingleChange(inputValue);
487
- }
488
-
549
+ event.stopPropagation();
489
550
  return;
490
551
  }
491
552
 
@@ -536,17 +597,23 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
536
597
  [searchable, onFocusProp],
537
598
  );
538
599
 
539
- const handleInputBlur = useCallback(
540
- (event: FocusEvent<HTMLInputElement>) => {
541
- setFocusVisible(false);
542
- onBlurProp?.(event);
543
- },
544
- [onBlurProp],
545
- );
600
+ const handleInputBlur = (event: FocusEvent<HTMLInputElement>) => {
601
+ setFocusVisible(false);
602
+ const blurResult = tryCommitOnBlur(event);
603
+
604
+ // Закрытый список не получит onOpenChange — вернуть лейбл выбранного значения.
605
+ // Клик по айтему открытого списка даёт relatedTarget === null, восстановление делает закрытие.
606
+ if (resetSearchOnOptionSelection && blurResult === 'skipped' && (!open || event.relatedTarget !== null)) {
607
+ setTyping(false);
608
+ setInputValue(selectedLabel);
609
+ }
610
+
611
+ onBlurProp?.(event);
612
+ };
546
613
 
547
614
  const droplistSelection = multiple
548
- ? { mode: 'multiple' as const, value: multipleValue, onChange: handleMultipleChange }
549
- : { mode: 'single' as const, value: singleValue, onChange: handleSingleChange };
615
+ ? { mode: 'multiple' as const, value: multipleValue, onChange: handleListMultipleChange }
616
+ : { mode: 'single' as const, value: singleValue, onChange: handleListSingleChange };
550
617
 
551
618
  const hasChips = chips && selectedPairs.length > 0;
552
619
 
@@ -698,6 +765,7 @@ export const FieldSelect = forwardRef<HTMLInputElement, FieldSelectProps>(functi
698
765
  placement={placement}
699
766
  widthStrategy={widthStrategy}
700
767
  triggerElemRef={inputRef}
768
+ listRef={listRef}
701
769
  size={size}
702
770
  open={open}
703
771
  onOpenChange={handleOpenChange}
@@ -2,3 +2,25 @@ 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;
@@ -0,0 +1,162 @@
1
+ import { ItemId } from '@cloud-ru/ds-list';
2
+ import { isBrowser } from '@cloud-ru/ds-utils';
3
+ import { FocusEvent, KeyboardEvent, RefObject, useCallback, useMemo, useRef } from 'react';
4
+
5
+ import {
6
+ ADD_CUSTOM_OPTION_TRIGGER,
7
+ ADD_CUSTOM_OPTION_TRIGGERS_MULTIPLE,
8
+ ADD_CUSTOM_OPTION_TRIGGERS_SINGLE,
9
+ } from './constants';
10
+ import { FieldSelectAddCustomOptionTrigger } from './types';
11
+ import {
12
+ getCustomOptionTriggerByCode,
13
+ isCustomOptionBlurInsideField,
14
+ resolveAddCustomOptionTriggers,
15
+ shouldCommitCustomOptionOnClose,
16
+ shouldHandleCustomOptionTrigger,
17
+ } from './utils';
18
+
19
+ /** Результат blur: фокус остался внутри поля, ввод зафиксирован, или коммитить было нечего. */
20
+ type CustomOptionBlurResult = 'inside' | 'committed' | 'skipped';
21
+
22
+ type UseCustomOptionParams = {
23
+ allowCustomOption: boolean;
24
+ addCustomOptionTriggers: readonly FieldSelectAddCustomOptionTrigger[] | undefined;
25
+ addOptionByEnter: boolean;
26
+ multiple: boolean;
27
+ inputValue: string;
28
+ multipleValue: ItemId[];
29
+ commitSingle(value: ItemId | undefined): void;
30
+ commitMultiple(value: ItemId[]): void;
31
+ resetSearch(): void;
32
+ inputElementRef: RefObject<HTMLInputElement | null>;
33
+ clearButtonRef: RefObject<HTMLButtonElement | null>;
34
+ copyButtonRef: RefObject<HTMLButtonElement | null>;
35
+ footerActiveElementsRefs?: RefObject<HTMLElement>[];
36
+ };
37
+
38
+ /** Фиксация произвольного ввода как выбранной опции: триггеры, blur, закрытие списка. */
39
+ export function useCustomOption({
40
+ allowCustomOption,
41
+ addCustomOptionTriggers,
42
+ addOptionByEnter,
43
+ multiple,
44
+ inputValue,
45
+ multipleValue,
46
+ commitSingle,
47
+ commitMultiple,
48
+ resetSearch,
49
+ inputElementRef,
50
+ clearButtonRef,
51
+ copyButtonRef,
52
+ footerActiveElementsRefs,
53
+ }: UseCustomOptionParams) {
54
+ const listRef = useRef<HTMLElement>(null);
55
+ const skipCloseCommitRef = useRef(false);
56
+
57
+ const resolvedTriggers = useMemo(
58
+ () =>
59
+ resolveAddCustomOptionTriggers({
60
+ allowCustomOption,
61
+ addCustomOptionTriggers,
62
+ addOptionByEnter,
63
+ defaultTriggers: multiple ? ADD_CUSTOM_OPTION_TRIGGERS_MULTIPLE : ADD_CUSTOM_OPTION_TRIGGERS_SINGLE,
64
+ }),
65
+ [allowCustomOption, addCustomOptionTriggers, addOptionByEnter, multiple],
66
+ );
67
+
68
+ const tryCommit = useCallback(
69
+ (trigger: FieldSelectAddCustomOptionTrigger) => {
70
+ if (!shouldHandleCustomOptionTrigger(trigger, resolvedTriggers) || inputValue === '') {
71
+ return false;
72
+ }
73
+
74
+ if (multiple) {
75
+ if (multipleValue.includes(inputValue)) {
76
+ resetSearch();
77
+ } else {
78
+ commitMultiple([...multipleValue, inputValue]);
79
+ }
80
+ } else {
81
+ commitSingle(inputValue);
82
+ }
83
+
84
+ return true;
85
+ },
86
+ [resolvedTriggers, inputValue, multiple, multipleValue, commitMultiple, commitSingle, resetSearch],
87
+ );
88
+
89
+ const suppressCloseCommit = useCallback(() => {
90
+ skipCloseCommitRef.current = true;
91
+ }, []);
92
+
93
+ const enableCloseCommit = useCallback(() => {
94
+ skipCloseCommitRef.current = false;
95
+ }, []);
96
+
97
+ const tryCommitOnClose = useCallback(() => {
98
+ const shouldCommit = shouldCommitCustomOptionOnClose({
99
+ skip: skipCloseCommitRef.current,
100
+ inputElement: inputElementRef.current,
101
+ activeElement: isBrowser() ? document.activeElement : null,
102
+ });
103
+
104
+ skipCloseCommitRef.current = false;
105
+
106
+ return shouldCommit && tryCommit(ADD_CUSTOM_OPTION_TRIGGER.Blur);
107
+ }, [inputElementRef, tryCommit]);
108
+
109
+ const tryCommitOnBlur = useCallback(
110
+ (event: FocusEvent<HTMLInputElement>): CustomOptionBlurResult => {
111
+ if (
112
+ isCustomOptionBlurInsideField(event.relatedTarget, [
113
+ listRef.current,
114
+ clearButtonRef.current,
115
+ copyButtonRef.current,
116
+ ...(footerActiveElementsRefs?.map(ref => ref.current) ?? []),
117
+ ])
118
+ ) {
119
+ return 'inside';
120
+ }
121
+
122
+ return tryCommit(ADD_CUSTOM_OPTION_TRIGGER.Blur) ? 'committed' : 'skipped';
123
+ },
124
+ [clearButtonRef, copyButtonRef, footerActiveElementsRefs, tryCommit],
125
+ );
126
+
127
+ const tryCommitFromKeyboard = useCallback(
128
+ (event: KeyboardEvent<HTMLInputElement>) => {
129
+ const trigger = getCustomOptionTriggerByCode(event.code, event.key);
130
+
131
+ return trigger !== undefined && tryCommit(trigger);
132
+ },
133
+ [tryCommit],
134
+ );
135
+
136
+ const handleListSingleChange = useCallback(
137
+ (next: ItemId | undefined) => {
138
+ skipCloseCommitRef.current = true;
139
+ commitSingle(next);
140
+ },
141
+ [commitSingle],
142
+ );
143
+
144
+ const handleListMultipleChange = useCallback(
145
+ (next: ItemId[]) => {
146
+ skipCloseCommitRef.current = true;
147
+ commitMultiple(next);
148
+ },
149
+ [commitMultiple],
150
+ );
151
+
152
+ return {
153
+ listRef,
154
+ suppressCloseCommit,
155
+ enableCloseCommit,
156
+ tryCommitOnClose,
157
+ tryCommitOnBlur,
158
+ tryCommitFromKeyboard,
159
+ handleListSingleChange,
160
+ handleListMultipleChange,
161
+ };
162
+ }