@sberbusiness/triplex-next-mcp-server 1.46.0-beta → 1.50.0-beta
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/README.md +7 -7
- package/bundle/components/Amount.md +2 -1
- package/bundle/components/AmountField.md +222 -323
- package/bundle/components/Avatar.md +138 -146
- package/bundle/components/Badge.md +2 -1
- package/bundle/components/BodyPage.md +2 -1
- package/bundle/components/Button.md +5 -1
- package/bundle/components/ButtonDropdownExtended.md +3 -1
- package/bundle/components/ButtonIcon.md +3 -1
- package/bundle/components/Calendar.md +3 -1
- package/bundle/components/CardAction.md +1 -0
- package/bundle/components/CardStatic.md +402 -0
- package/bundle/components/CheckboxTree.md +157 -215
- package/bundle/components/CheckboxTreeExtended.md +7 -7
- package/bundle/components/CheckboxXGroup.md +2 -1
- package/bundle/components/Chip.md +2 -1
- package/bundle/components/ChipGroup.md +1 -1
- package/bundle/components/ChipMonthYearPicker.md +6 -5
- package/bundle/components/ChipMultiselect.md +4 -1
- package/bundle/components/ChipOptions.md +2 -1
- package/bundle/components/ChipSort.md +4 -1
- package/bundle/components/ChipSuggest.md +7 -6
- package/bundle/components/CollapsibleTree.md +10 -5
- package/bundle/components/CollapsibleTreeExtended.md +8 -6
- package/bundle/components/ComposedKeyDownListener.md +4 -3
- package/bundle/components/Confirm.md +346 -281
- package/bundle/components/DateField.md +7 -1
- package/bundle/components/DateRange.md +351 -210
- package/bundle/components/Divider.md +85 -138
- package/bundle/components/DocumentNumberEdit.md +173 -214
- package/bundle/components/Dropdown.md +11 -1
- package/bundle/components/Ellipsis.md +131 -147
- package/bundle/components/FooterPage.md +4 -1
- package/bundle/components/FormField.md +13 -5
- package/bundle/components/FormGroup.md +3 -2
- package/bundle/components/Gap.md +1 -1
- package/bundle/components/Header.md +1 -0
- package/bundle/components/HeaderPage.md +3 -1
- package/bundle/components/HelpBox.md +2 -1
- package/bundle/components/IconWrapper.md +10 -6
- package/bundle/components/ImageGallery.md +2 -1
- package/bundle/components/ImageGalleryExtended.md +2 -1
- package/bundle/components/IslandAccordion.md +6 -1
- package/bundle/components/KeyDownListener.md +3 -2
- package/bundle/components/LightBox.md +15 -1
- package/bundle/components/List.md +2 -1
- package/bundle/components/ListActionItem.md +2 -1
- package/bundle/components/ListItem.md +3 -1
- package/bundle/components/ListItemControlsButton.md +2 -1
- package/bundle/components/ListItemSelectable.md +2 -1
- package/bundle/components/ListMaster.md +2 -2
- package/bundle/components/ListMasterFooter.md +2 -1
- package/bundle/components/ListMasterHeader.md +2 -1
- package/bundle/components/ListSortable.md +2 -1
- package/bundle/components/ListSortableItem.md +2 -1
- package/bundle/components/ListTableItem.md +10 -7
- package/bundle/components/LoaderMiddle.md +5 -1
- package/bundle/components/LoaderScreen.md +7 -1
- package/bundle/components/LoaderSmall.md +10 -4
- package/bundle/components/MaskedField.md +3 -1
- package/bundle/components/MediaWidth.md +2 -1
- package/bundle/components/MobileView.md +2 -1
- package/bundle/components/ModalWindow.md +4 -1
- package/bundle/components/MonthYearField.md +19 -5
- package/bundle/components/Notification.md +1 -0
- package/bundle/components/Overlay.md +7 -2
- package/bundle/components/Pagination.md +1 -7
- package/bundle/components/Radio.md +238 -115
- package/bundle/components/RadioXGroup.md +259 -0
- package/bundle/components/RadioYGroup.md +266 -0
- package/bundle/components/SegmentedControl.md +505 -158
- package/bundle/components/SelectExtendedField.md +2 -1
- package/bundle/components/SelectField.md +4 -1
- package/bundle/components/Skeleton.md +140 -69
- package/bundle/components/Slider.md +389 -97
- package/bundle/components/SliderRange.md +486 -105
- package/bundle/components/Spoiler.md +245 -63
- package/bundle/components/StatusTracker.md +386 -94
- package/bundle/components/Step.md +257 -68
- package/bundle/components/Stepper.md +355 -102
- package/bundle/components/StepperExtended.md +633 -0
- package/bundle/components/SuggestField.md +1061 -96
- package/bundle/components/SwipeableArea.md +7 -7
- package/bundle/components/Tabs.md +266 -92
- package/bundle/components/TabsExtended.md +18 -18
- package/bundle/components/TabsLine.md +302 -127
- package/bundle/components/TagColor.md +187 -66
- package/bundle/components/TagGroup.md +184 -48
- package/bundle/components/TextField.md +8 -4
- package/bundle/components/TextareaField.md +279 -70
- package/bundle/components/ThemeProvider.md +2 -1
- package/bundle/components/Tooltip.md +6 -3
- package/bundle/components/TopOverlay.md +17 -1
- package/bundle/components/TreeView.md +16 -7
- package/bundle/components/TriggerClickOnKeyDownEvent.md +277 -0
- package/bundle/components/UploadZone.md +318 -0
- package/bundle/guides/codestyle.md +16 -1
- package/bundle/guides/commits.md +47 -1
- package/bundle/guides/context.md +4 -1
- package/bundle/guides/tests.md +23 -2
- package/bundle/manifest.json +78 -29
- package/bundle/release-notes/1.47.0.mdx +115 -0
- package/bundle/release-notes/1.48.0.mdx +68 -0
- package/bundle/release-notes/1.49.0.mdx +85 -0
- package/bundle/release-notes/1.50.0.mdx +35 -0
- package/bundle/release-notes/1.51.0.mdx +7 -0
- package/bundle/tokens.json +163 -13
- package/dist/index.js +220 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
component: SuggestField
|
|
3
|
-
category:
|
|
4
|
-
|
|
5
|
-
figma-file: ""
|
|
6
|
-
related: [Suggest, SelectField, Dropdown, TextField, MobileView, FormFieldInput]
|
|
3
|
+
category: TextFields
|
|
4
|
+
related: [Suggest, SelectField, MultiselectField, SelectExtendedField, TextField, FormField, Dropdown, LoaderSmall]
|
|
7
5
|
tokens: []
|
|
8
6
|
stories: stories/SuggestField/SuggestField.stories.tsx
|
|
9
7
|
version: "1.0"
|
|
@@ -13,140 +11,233 @@ version: "1.0"
|
|
|
13
11
|
|
|
14
12
|
## Назначение
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
для кастомизации.
|
|
14
|
+
Готовое поле ввода с выпадающим списком и поиском по введённому значению: лейбл, статусы,
|
|
15
|
+
префикс/постфикс, лоадер, кнопка очистки и подсказка-Tooltip уже собраны. Компонент
|
|
16
|
+
**не фильтрует список сам** — он сообщает введённый текст через `onFilter`, а потребитель
|
|
17
|
+
возвращает новый `options`.
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
Адаптивный: на ширине экрана < 768px вместо десктопного поля с выпадающим списком рендерится
|
|
20
|
+
мобильный вариант — поле только для чтения, по фокусу открывающее полноэкранный дропдаун с
|
|
21
|
+
собственным полем ввода. Переключение делает `MobileView`, потребителю ничего делать не нужно.
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
## Файловая структура
|
|
29
|
-
|
|
30
|
-
```text
|
|
31
|
-
src/components/SuggestField/
|
|
32
|
-
├── SuggestField.tsx # корневой — MobileView switcher
|
|
33
|
-
├── types.ts # ISuggestFieldProps, ISuggestFieldOption
|
|
34
|
-
├── desktop/
|
|
35
|
-
│ ├── SuggestFieldDesktop.tsx
|
|
36
|
-
│ ├── SuggestFieldDesktopDropdown.tsx
|
|
37
|
-
│ └── types.ts
|
|
38
|
-
├── mobile/
|
|
39
|
-
│ ├── SuggestFieldMobile.tsx
|
|
40
|
-
│ ├── SuggestFieldMobileDropdown.tsx
|
|
41
|
-
│ ├── SuggestFieldMobileDropdownHint.tsx
|
|
42
|
-
│ └── types.ts
|
|
43
|
-
└── index.ts
|
|
44
|
-
```
|
|
23
|
+
Используй когда: нужно поле с подсказками-опциями — поиск контрагента, счёта, адреса, в том
|
|
24
|
+
числе с асинхронной догрузкой (`loading`, `dropdownListLoading`, `onScrollEnd`).
|
|
45
25
|
|
|
46
|
-
|
|
47
|
-
|
|
26
|
+
Не используй когда:
|
|
27
|
+
- Список фиксированный и вводом не фильтруется — возьми `SelectField`.
|
|
28
|
+
- Нужен suggest-фильтр в ряду чипсов — возьми `ChipSuggest`.
|
|
29
|
+
- Нужна нестандартная разметка управляющего элемента или списка — собери своё на headless
|
|
30
|
+
`Suggest`; `SuggestField` жёстко задаёт композицию `TextFieldBase` + `Dropdown`.
|
|
48
31
|
|
|
49
32
|
---
|
|
50
33
|
|
|
51
|
-
##
|
|
34
|
+
## Варианты и props
|
|
52
35
|
|
|
53
|
-
|
|
36
|
+
`ISuggestFieldProps<T>` расширяет `Omit<ITextFieldProps, "onSelect">`, поэтому всё из
|
|
37
|
+
`TextField` / `FormField` доступно: `status`, `size`, `active`, `label`, `description`,
|
|
38
|
+
`counter`, `prefix`, `postfix`, `className`, `data-*`, `inputProps`.
|
|
54
39
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
Props = `Omit<ITextFieldProps, "onSelect"> & DataAttributes` + поля ниже:
|
|
40
|
+
### Обязательные props
|
|
58
41
|
|
|
59
42
|
| Prop | Тип | Описание |
|
|
60
43
|
|---|---|---|
|
|
61
|
-
| `value` | `T \| undefined` | Выбранная опция. |
|
|
62
|
-
| `options` | `T[]` | Список опций. |
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
|
72
|
-
|
|
73
|
-
| `
|
|
74
|
-
| `
|
|
75
|
-
| `
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
44
|
+
| `value` | `T \| undefined` | Выбранная опция. Контролируемое значение: при смене `value.id` поле ввода перезаписывается на `value.label` |
|
|
45
|
+
| `options` | `T[]` | Список опций. Фильтрует потребитель в ответ на `onFilter` — компонент список не фильтрует и не сортирует |
|
|
46
|
+
| `tooltipHint` | `string` | Текст подсказки («Ничего не найдено», «Введите более 3 символов»). Обязателен, даже если `tooltipOpen` всегда `false` |
|
|
47
|
+
| `tooltipOpen` | `boolean` | Показывать подсказку. Управляется снаружи: компонент сам не решает, когда список «пустой по делу» |
|
|
48
|
+
| `onSelect` | `(value: T \| undefined) => void` | Опция выбрана. Вызывается с `undefined`, когда значение сбрасывается, — на обеих платформах только кнопкой очистки (см. «Инварианты»). Blur на десктопе и закрытие дропдауна на мобильном значение не сбрасывают даже при пустом вводе. Не сбрасывай значение в `onFilter("")`: при `clearInputOnFocus` десктоп зовёт его уже на фокусе |
|
|
49
|
+
| `onFilter` | `(value: string) => void` | Текст поля ввода изменился. Ответ — обновлённый `options` |
|
|
50
|
+
| `inputProps` | `IFormFieldInputProps & { ref?: React.Ref<HTMLInputElement> }` | Свойства поля ввода. **Не опционально:** передавай хотя бы `{}`. Через `inputProps.ref` потребитель получает ref на `<input>` — собственного `forwardRef` у компонента нет |
|
|
51
|
+
|
|
52
|
+
### Опциональные props
|
|
53
|
+
|
|
54
|
+
| Prop | Тип | По умолчанию | Описание |
|
|
55
|
+
|---|---|---|---|
|
|
56
|
+
| `size` | `EComponentSize` | `EComponentSize.LG` | Размер поля и выпадающего списка |
|
|
57
|
+
| `status` | `EFormFieldStatus` | `EFormFieldStatus.DEFAULT` | `DISABLED` блокирует ввод и подавляет Tooltip |
|
|
58
|
+
| `label` | `string` | — | Лейбл над полем ввода |
|
|
59
|
+
| `placeholder` | `string` | — | Подсказка в пустом поле |
|
|
60
|
+
| `loading` | `boolean` | — | Лоадер в постфиксе поля (десктоп) и в шапке дропдауна (мобильный) |
|
|
61
|
+
| `dropdownListLoading` | `boolean` | — | Догрузка списка: лоадер под уже загруженными опциями. Пока `true`, `onScrollEnd` не вызывается |
|
|
62
|
+
| `clearInputOnFocus` | `boolean` | `false` | Очищать поле ввода при получении фокуса. На десктопе дополнительно вызывает `onFilter("")` |
|
|
63
|
+
| `onScrollEnd` | `() => void` | — | Список прокручен до конца — грузи следующую страницу опций |
|
|
64
|
+
| `onClear` | `React.MouseEventHandler<HTMLButtonElement>` | — | Обработчик очистки. **Кнопка очистки рендерится, только если он передан** |
|
|
65
|
+
| `renderInput` | `(props: IFormFieldInputProps) => JSX.Element` | `FormFieldInput` | Замена поля ввода |
|
|
66
|
+
|
|
67
|
+
Только десктопный `SuggestFieldDesktop` дополнительно принимает `renderDropdown`,
|
|
68
|
+
`renderDropdownList`, `renderDropdownListItem`.
|
|
69
|
+
|
|
70
|
+
### Опция (`ISuggestFieldOption`)
|
|
71
|
+
|
|
72
|
+
`id` (уникальный; по нему сравнивается с `value` и строится ключ списка), `label` (текст в
|
|
73
|
+
поле ввода и в списке), `content` (кастомная разметка опции в списке вместо `label`),
|
|
74
|
+
`showNotificationIcon` (значок новых уведомлений).
|
|
75
|
+
|
|
76
|
+
Тип опции расширяется дженериком: `<SuggestField<IMyOption> ...>` протаскивает `IMyOption`
|
|
77
|
+
в `value`, `options` и `onSelect`.
|
|
84
78
|
|
|
85
79
|
### `SuggestField.Input`
|
|
86
80
|
|
|
87
|
-
|
|
81
|
+
Статическое поле — это `FormFieldInput`. Нужно, чтобы потребитель собирал собственный
|
|
82
|
+
`renderInput` из того же компонента: `renderInput={(props) => <SuggestField.Input {...props} />}`.
|
|
88
83
|
|
|
89
|
-
|
|
84
|
+
### Поведение десктопного варианта
|
|
90
85
|
|
|
91
|
-
|
|
86
|
+
| Действие | Что происходит |
|
|
87
|
+
|---|---|
|
|
88
|
+
| Фокус / mousedown по полю | Список открывается, если `options` не пуст |
|
|
89
|
+
| Появились опции, пока поле в фокусе | Список открывается автоматически |
|
|
90
|
+
| `options` стал пустым | Список закрывается |
|
|
91
|
+
| Ввод текста | `onFilter(value)`, сброс `aria-activedescendant`, список снова разрешён к автооткрытию |
|
|
92
|
+
| `Escape` при открытом списке | Список закрывается, всплытие останавливается, автооткрытие подавлено до следующего ввода или mousedown |
|
|
93
|
+
| Выбор опции | `onSelect(option)`, в поле подставляется `label`, автооткрытие подавлено |
|
|
94
|
+
| Blur | В поле возвращается `label` текущего `value` (пустая строка, если `value` не задан). `onSelect` не вызывается: введённый, но не выбранный текст отбрасывается, в том числе пустой |
|
|
95
|
+
| Клик по кнопке очистки | `onSelect(undefined)` (если было значение), `onFilter("")` (если поле не пустое), затем `onClear(event)` |
|
|
92
96
|
|
|
93
|
-
|
|
97
|
+
`inputProps.onFocus` / `onBlur` / `onKeyDown` / `onMouseDown` / `onChange` вызываются
|
|
98
|
+
**после** внутренних обработчиков — подменить их через `inputProps` нельзя. С остальными
|
|
99
|
+
свойствами наоборот: `value`, `placeholder`, `data-test-id` из `inputProps` спредятся после
|
|
100
|
+
внутренних значений и перекрывают их.
|
|
94
101
|
|
|
95
|
-
|
|
96
|
-
- `ignoreAutoOpen` — предотвращает немедленное переоткрытие dropdown сразу после выбора (иначе селектнутый item → focus остаётся на input → dropdown открывается снова).
|
|
97
|
-
- Синхронизация `value?.id !== prevValue?.id` → `setInputValue(value?.label)`.
|
|
98
|
-
- `clearInputOnFocus` + focus с непустым input → `setInputValue("")` + `onFilter("")`.
|
|
99
|
-
- `activeDescendant` — id подсвеченного элемента списка (для `aria-activedescendant`).
|
|
102
|
+
### Поведение мобильного варианта
|
|
100
103
|
|
|
101
|
-
|
|
104
|
+
Поле-триггер — `readOnly`, по фокусу открывается полноэкранный `Dropdown` с собственным полем
|
|
105
|
+
ввода (автофокус). При `tooltipOpen` вместо списка показывается `SuggestFieldMobileDropdownHint`
|
|
106
|
+
с текстом `tooltipHint`. Закрытие дропдауна без выбора опции значение не сбрасывает, даже при
|
|
107
|
+
пустом поле ввода: на закрытии поле ввода дропдауна возвращается к `label` текущего `value`
|
|
108
|
+
(через `onClose` у `Dropdown`, а не только на автофокусе — при повторном открытии во время
|
|
109
|
+
анимации закрытия содержимое ещё смонтировано и автофокус не срабатывает). При следующем
|
|
110
|
+
открытии с `clearInputOnFocus` поле очищается автофокусом. Кнопка очистки работает как на
|
|
111
|
+
десктопе: `onSelect(undefined)` (если было значение), `onFilter("")` (если у значения непустой
|
|
112
|
+
`label` — его и показывает поле-триггер), затем `onClear(event)`. После закрытия поле-триггер
|
|
113
|
+
скроллится в центр экрана (в iOS открытие дропдауна уводит страницу вверх).
|
|
102
114
|
|
|
103
|
-
|
|
104
|
-
- `SuggestFieldMobileDropdownHint` — текст-подсказка над списком.
|
|
105
|
-
- Рендерится только внутри `MobileView` (viewport <= SM).
|
|
115
|
+
---
|
|
106
116
|
|
|
107
|
-
|
|
117
|
+
## Дизайн-токены
|
|
108
118
|
|
|
109
|
-
|
|
119
|
+
Собственных токенов нет. Единственный стиль компонента — `styles/SuggestFieldMobile.module.less` —
|
|
120
|
+
задаёт высоту тела мобильного дропдауна и отступ подсказки фиксированными значениями. Внешний вид
|
|
121
|
+
целиком определяют `FormField` / `TextField`, `Dropdown`, `Tooltip` и `Loader` — их токены
|
|
122
|
+
переопределяются через `ThemeProvider` в соответствующих AI.md.
|
|
110
123
|
|
|
111
124
|
---
|
|
112
125
|
|
|
113
126
|
## Инварианты
|
|
114
127
|
|
|
115
|
-
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
128
|
+
- **`forwardRef` на публичных компонентах семейства нет.** `SuggestField`, `SuggestFieldDesktop`
|
|
129
|
+
и `SuggestFieldMobile` — обычные дженерик-функции: внутренний `ref` занят корневым
|
|
130
|
+
`TextFieldBase`, и он же служит `targetRef` для `Dropdown` и `Tooltip`. `forwardRef` есть
|
|
131
|
+
только у `SuggestFieldMobileDropdown`. Добавление `forwardRef` остальным — изменение
|
|
132
|
+
публичного API, решение мейнтейнера. По той же причине `SuggestFieldMobileDropdown` намеренно
|
|
133
|
+
оставлен без `displayName`: в React DevTools он виден как `ForwardRef(SuggestFieldMobileDropdownBase)`,
|
|
134
|
+
и переименование наблюдаемо для потребителя.
|
|
135
|
+
- `SuggestField` — `Object.assign(SuggestFieldBase, { Input: FormFieldInput })`. Дженерик
|
|
136
|
+
сохраняется только у самой функции; `SuggestField.Input` — часть публичного API.
|
|
137
|
+
- **Оба варианта получают все props.** `SuggestField.tsx` отдаёт `{...props}` и в
|
|
138
|
+
`SuggestFieldDesktop`, и в `SuggestFieldMobile`. Не возвращай явный список props для мобильной
|
|
139
|
+
ветки: пропущенный prop молча терялся бы на мобильной ширине (так до 1.49.0 терялись
|
|
140
|
+
`className`, `id`, `data-test-id`, `active` и `renderInput`).
|
|
141
|
+
- **Значение сбрасывает только кнопка очистки — на обеих платформах.** Blur на десктопе и
|
|
142
|
+
закрытие дропдауна на мобильном `onSelect(undefined)` не зовут, даже при пустом вводе: иначе
|
|
143
|
+
при `clearInputOnFocus` значение терялось от простого фокуса без выбора. Кнопка очистки
|
|
144
|
+
рендерится только при `onClear`, так что без него встроенного сброса нет. Возврат сброса в
|
|
145
|
+
blur / закрытие дропдауна — изменение поведения, решение мейнтейнера.
|
|
146
|
+
- Видимость десктопного списка синхронизируется **во время рендера** (`if (inputFocused) {...}`),
|
|
147
|
+
а не в `useEffect`. Так требует правило `react-hooks/set-state-in-effect`; возврат к эффекту
|
|
148
|
+
вернёт и ошибку линтера, и лишний кадр рендера.
|
|
149
|
+
- **`options` — жёсткий инвариант, рантайм-подстраховки нет.** Оба дропдауна делают `options.map`
|
|
150
|
+
без `?.`: prop обязателен по типу, а родитель ещё до рендера списка читает `options.length`, так
|
|
151
|
+
что `undefined` упал бы всё равно выше. JS-потребитель без типов, передавший `options={undefined}`,
|
|
152
|
+
получит TypeError, а не пустой список — это сознательное решение, а не упущение.
|
|
153
|
+
- `id` выпадающего списка генерируется через `uniqueId()` из `lodash-es`, а не `React.useId` —
|
|
154
|
+
ветка `release-0` собирается на React 17.
|
|
155
|
+
- Ширина выпадающего списка жёстко `EDropdownWidth.TARGET` — список всегда по ширине поля.
|
|
156
|
+
- Выбор клавиатурой — **только `Enter`**: `keyCodesForSelection={[EVENT_KEY_CODES.ENTER]}`
|
|
157
|
+
сужает дефолт `DropdownListItem` (`Space` + `Enter`), иначе пробел в поисковом запросе
|
|
158
|
+
выбирал бы опцию вместо ввода пробела.
|
|
159
|
+
- `mousedown` по выпадающему списку отменяется (`preventDefault`) — иначе поле ввода потеряло бы
|
|
160
|
+
фокус до того, как отработает выбор опции.
|
|
161
|
+
- Суффиксы `data-test-id` берутся из `DataTestId` и используются в e2e — не менять:
|
|
162
|
+
поле ввода `{dataTestId}__input`, выпадающий список `{dataTestId}__dropdown`, элемент списка
|
|
163
|
+
`{dataTestId}__dropdown__item`, тело подсказки `{dataTestId}__tooltip`. Все они появляются,
|
|
164
|
+
только если потребитель передал `data-test-id`.
|
|
165
|
+
- Кастомный `renderDropdownListItem` **не получит ref**: тип рендер-функции — обычная функция,
|
|
166
|
+
а `DropdownList` клонирует детей и вешает на них ref для клавиатурной навигации. React напишет
|
|
167
|
+
предупреждение, стрелочная навигация по такому списку работать не будет.
|
|
122
168
|
|
|
123
169
|
---
|
|
124
170
|
|
|
125
171
|
## Accessibility
|
|
126
172
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
-
|
|
130
|
-
|
|
131
|
-
- `
|
|
173
|
+
Поле ввода — `role="combobox"` с `aria-autocomplete="list"`. Компонент сам проставляет:
|
|
174
|
+
|
|
175
|
+
- `aria-expanded` — открыт ли список. Считается как `dropdownOpen && options.length !== 0`,
|
|
176
|
+
поэтому при пустом `options` остаётся `false`, даже если внутреннее состояние «открыто».
|
|
177
|
+
- `aria-controls` — `id` выпадающего списка, общий с `DropdownList`.
|
|
178
|
+
- `aria-activedescendant` — `id` опции, выделенной стрелками. Значение приходит из
|
|
179
|
+
`DropdownListContext`, который `SuggestFieldDesktop` предоставляет, а `DropdownList` заполняет.
|
|
180
|
+
Сбрасывается при вводе текста и при закрытии списка.
|
|
181
|
+
|
|
182
|
+
Клавиатура: `ArrowUp` / `ArrowDown` — перемещение по списку (`DropdownList`), `Enter` — выбор
|
|
183
|
+
опции, `Escape` — закрытие списка со `stopPropagation`, чтобы событие не закрыло ещё и модальное
|
|
184
|
+
окно вокруг. `Escape` при закрытом списке всплывает наверх.
|
|
185
|
+
|
|
186
|
+
Подсказка (`tooltipHint`) рендерится в `Tooltip` и на десктопе показывается только пока поле
|
|
187
|
+
в фокусе и `status !== EFormFieldStatus.DISABLED`. Текст подсказки, `label` и `placeholder`
|
|
188
|
+
передаёт потребитель — библиотека мультиязычная и строк не хардкодит.
|
|
189
|
+
|
|
190
|
+
Мобильный вариант: поле-триггер `readOnly` с тем же `role="combobox"` и `aria-expanded`;
|
|
191
|
+
список внутри дропдауна — `role="listbox"` с опциями `role="option"` и `aria-selected`.
|
|
132
192
|
|
|
133
193
|
---
|
|
134
194
|
|
|
135
195
|
## Связанные компоненты
|
|
136
196
|
|
|
137
|
-
- `Suggest` — headless-основа
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
- `
|
|
141
|
-
|
|
142
|
-
|
|
197
|
+
- `Suggest` — headless-основа той же задачи: состояние и контекст без разметки. `SuggestField`
|
|
198
|
+
на него **не переведён** и держит собственную реализацию состояния (в `SuggestField.tsx`
|
|
199
|
+
стоит `TODO: Переписать через useSuggest`).
|
|
200
|
+
- `SelectField` — та же задача с другим компромиссом: список фиксированный, ввода и фильтрации
|
|
201
|
+
нет. Сюда же примыкают `MultiselectField` и `SelectExtendedField` — все трое ссылаются на
|
|
202
|
+
`SuggestField` со своей стороны.
|
|
203
|
+
- `TextField` — `ISuggestFieldProps` наследует его props (`Omit<ITextFieldProps, "onSelect">`),
|
|
204
|
+
а разметку поля даёт общий `TextFieldBase`.
|
|
205
|
+
- `FormField` — источник `FormFieldInput` (поле ввода и `SuggestField.Input`), `FormFieldClear`
|
|
206
|
+
(кнопка очистки) и статусов `EFormFieldStatus`.
|
|
207
|
+
- `Dropdown` — выпадающий список: `DropdownDesktop` + `DropdownList` + `DropdownListItem` на
|
|
208
|
+
десктопе, `DropdownMobile*` в полноэкранном мобильном варианте. `ISuggestFieldDesktopDropdownProps`
|
|
209
|
+
наследует `IDropdownDesktopProps`.
|
|
210
|
+
- `LoaderSmall` — индикатор подгрузки подсказок: `SuggestFieldDesktop` и `SuggestFieldMobile`
|
|
211
|
+
показывают его с темой `BRAND`, пока `loading`.
|
|
212
|
+
- `Tooltip` — подсказка `tooltipHint` у десктопного поля. В `related` не вынесен: связь узкая,
|
|
213
|
+
а список адресатов и так шире ориентира 2–5.
|
|
214
|
+
- `MobileView` — переключает десктопный и мобильный варианты по ширине экрана. В `related` не
|
|
215
|
+
вынесен по той же причине.
|
|
216
|
+
- `SuggestFieldDesktop` / `SuggestFieldMobile` — платформенные варианты, экспортируются из barrel
|
|
217
|
+
и принимают те же props. Отдельных AI.md не имеют: их поведение описано выше. Бери их напрямую,
|
|
218
|
+
только если адаптивное переключение `MobileView` не нужно.
|
|
219
|
+
- `SuggestFieldDesktopDropdown`, `SuggestFieldMobileDropdown`, `SuggestFieldMobileDropdownHint` —
|
|
220
|
+
внутренние части композиции. Экспортируются из barrel, чтобы их можно было переиспользовать
|
|
221
|
+
в `renderDropdown`, собственных AI.md не имеют.
|
|
143
222
|
|
|
144
223
|
---
|
|
145
224
|
|
|
146
225
|
## Stories
|
|
147
226
|
|
|
148
227
|
Основные истории: `stories/SuggestField/SuggestField.stories.tsx`
|
|
149
|
-
|
|
228
|
+
Файлы примеров: `stories/SuggestField/examples/`
|
|
229
|
+
|
|
230
|
+
| Story | Example file | Что демонстрирует |
|
|
231
|
+
|---|---|---|
|
|
232
|
+
| `Playground` | `PlaygroundExample.tsx` | Интерактивный контроль `size`, `status`, `label`, `placeholder`, `tooltipHint`, `loading`, `dropdownListLoading`, `clearInputOnFocus` и наличия префикса / постфикса / описания |
|
|
233
|
+
| `Default` | `DefaultExample.tsx` | Базовая связка: фильтрация `options` по вводу и `tooltipOpen` при пустом результате |
|
|
234
|
+
| `Sizes` | `SizesExample.tsx` | Размеры SM / MD / LG |
|
|
235
|
+
| `Statuses` | `StatusesExample.tsx` | Все значения `EFormFieldStatus`: default, disabled, error, warning |
|
|
236
|
+
| `Loading` | `LoadingExample.tsx` | Лоадер в постфиксе поля при `loading` |
|
|
237
|
+
| `Production` | `ProductionExample.tsx` | Боевая композиция: `HelpBox` в постфиксе, подсказка со ссылкой |
|
|
238
|
+
| `CustomOptions` | `CustomOptionsExample.tsx` | Опции с `content`: иконка, название и категория в строке списка |
|
|
239
|
+
| `Async` | `AsyncExample.tsx` | Асинхронный поиск: debounce, `loading`, постраничная догрузка по `onScrollEnd` и `dropdownListLoading` |
|
|
240
|
+
| `VisualTests` | `DefaultExample.tsx` | Скриншот-регрессия: рендерит `DefaultExample`, а `play` открывает выпадающий список кликом по полю |
|
|
150
241
|
|
|
151
242
|
---
|
|
152
243
|
|
|
@@ -154,4 +245,878 @@ Re-export `FormFieldInput` — для удобства доступа.
|
|
|
154
245
|
|
|
155
246
|
| Дата | Изменение |
|
|
156
247
|
|---|---|
|
|
157
|
-
| 2026-
|
|
248
|
+
| 2026-09-23 | Создан документ |
|
|
249
|
+
| 2026-09-29 | Аудит симметрии `related` (TRI-156): добавлены обратные ссылки (`MultiselectField`, `SelectExtendedField`) и контракт по рендеру `LoaderSmall`. Публичный API и поведение не затронуты. |
|
|
250
|
+
| 2026-10-02 | Сброс значения только кнопкой очистки на обеих платформах: при `clearInputOnFocus` значение терялось от фокуса без выбора. Blur на десктопе и закрытие дропдауна на мобильном больше не вызывают `onSelect(undefined)`; мобильная кнопка очистки сама зовёт `onSelect(undefined)` и `onFilter("")` (последний — при непустом `label`), как десктопная; на закрытии мобильного дропдауна его поле ввода возвращается к `label`. `SuggestField` отдаёт мобильному варианту все props, как десктопному: на мобильной ширине больше не теряются `className`, `id`, `data-test-id`, `active` и `renderInput`. Обновлены `onSelect`, поведение обеих платформ и «Инварианты» |
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Примеры использования
|
|
255
|
+
|
|
256
|
+
### Default
|
|
257
|
+
|
|
258
|
+
`stories/SuggestField/examples/DefaultExample.tsx`
|
|
259
|
+
|
|
260
|
+
```tsx
|
|
261
|
+
import React, { useState } from "react";
|
|
262
|
+
import {
|
|
263
|
+
SuggestField,
|
|
264
|
+
useMobileView,
|
|
265
|
+
ISuggestFieldOption,
|
|
266
|
+
EComponentSize,
|
|
267
|
+
EFormFieldStatus,
|
|
268
|
+
} from "@sberbusiness/triplex-next";
|
|
269
|
+
|
|
270
|
+
const FOOD_NAMES = [
|
|
271
|
+
"Mushroom",
|
|
272
|
+
"Tomato",
|
|
273
|
+
"Eggplant",
|
|
274
|
+
"Grapes",
|
|
275
|
+
"Melon",
|
|
276
|
+
"Watermelon",
|
|
277
|
+
"Tangerine",
|
|
278
|
+
"Lemon",
|
|
279
|
+
"Banana",
|
|
280
|
+
"Pineapple",
|
|
281
|
+
"Red Apple",
|
|
282
|
+
"Green Apple",
|
|
283
|
+
"Pear",
|
|
284
|
+
"Peach",
|
|
285
|
+
"Cherries",
|
|
286
|
+
"Strawberry",
|
|
287
|
+
] as const;
|
|
288
|
+
|
|
289
|
+
const FOOD_OPTIONS: ISuggestFieldOption[] = FOOD_NAMES.map((item, index) => ({
|
|
290
|
+
id: `suggest-field-option-${index}`,
|
|
291
|
+
label: item,
|
|
292
|
+
}));
|
|
293
|
+
|
|
294
|
+
const getFilteredOptions = (query: string): ISuggestFieldOption[] =>
|
|
295
|
+
FOOD_OPTIONS.filter(({ label }) => label.toLowerCase().includes(query.toLowerCase()));
|
|
296
|
+
|
|
297
|
+
export const DefaultExample = () => {
|
|
298
|
+
const [value, setValue] = useState<ISuggestFieldOption | undefined>(undefined);
|
|
299
|
+
const [options, setOptions] = useState<ISuggestFieldOption[]>([]);
|
|
300
|
+
const [inputPristine, setInputPristine] = useState(true);
|
|
301
|
+
const [tooltipOpen, setTooltipOpen] = useState<boolean>(false);
|
|
302
|
+
|
|
303
|
+
const adaptive = useMobileView();
|
|
304
|
+
|
|
305
|
+
const reset = (newOptions: typeof options) => {
|
|
306
|
+
setOptions(newOptions);
|
|
307
|
+
setInputPristine(true);
|
|
308
|
+
setTooltipOpen(false);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const handleFilter = (inputValue: string) => {
|
|
312
|
+
setInputPristine(false);
|
|
313
|
+
|
|
314
|
+
if (inputValue.length === 0) {
|
|
315
|
+
setOptions(FOOD_OPTIONS);
|
|
316
|
+
setTooltipOpen(false);
|
|
317
|
+
} else {
|
|
318
|
+
const filteredOptions = getFilteredOptions(inputValue);
|
|
319
|
+
|
|
320
|
+
setOptions(filteredOptions);
|
|
321
|
+
setTooltipOpen(filteredOptions.length === 0);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const handleSelect = (nextValue: typeof value) => {
|
|
326
|
+
setValue(nextValue);
|
|
327
|
+
reset([]);
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const handleInputBlur: React.FocusEventHandler<HTMLInputElement> = () => {
|
|
331
|
+
if (!adaptive) {
|
|
332
|
+
reset([]);
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const handleInputMouseDown: React.MouseEventHandler<HTMLInputElement> = (event) => {
|
|
337
|
+
if (options.length === 0) {
|
|
338
|
+
const query = event.currentTarget.value;
|
|
339
|
+
const filteredOptions = getFilteredOptions(inputPristine ? "" : query);
|
|
340
|
+
|
|
341
|
+
setOptions(filteredOptions);
|
|
342
|
+
setTooltipOpen(filteredOptions.length === 0 && query.length !== 0);
|
|
343
|
+
} else if (adaptive) {
|
|
344
|
+
reset(FOOD_OPTIONS);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<div style={{ maxWidth: 300 }}>
|
|
350
|
+
<SuggestField
|
|
351
|
+
value={value}
|
|
352
|
+
options={options}
|
|
353
|
+
size={EComponentSize.LG}
|
|
354
|
+
status={EFormFieldStatus.DEFAULT}
|
|
355
|
+
label="Label"
|
|
356
|
+
placeholder="Type to proceed"
|
|
357
|
+
tooltipHint="No matches found."
|
|
358
|
+
tooltipOpen={tooltipOpen}
|
|
359
|
+
onFilter={handleFilter}
|
|
360
|
+
onSelect={handleSelect}
|
|
361
|
+
inputProps={{
|
|
362
|
+
onBlur: handleInputBlur,
|
|
363
|
+
onMouseDown: handleInputMouseDown,
|
|
364
|
+
}}
|
|
365
|
+
/>
|
|
366
|
+
</div>
|
|
367
|
+
);
|
|
368
|
+
};
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Sizes
|
|
372
|
+
|
|
373
|
+
`stories/SuggestField/examples/SizesExample.tsx`
|
|
374
|
+
|
|
375
|
+
```tsx
|
|
376
|
+
import React, { useState } from "react";
|
|
377
|
+
import {
|
|
378
|
+
SuggestField,
|
|
379
|
+
useMobileView,
|
|
380
|
+
ISuggestFieldOption,
|
|
381
|
+
EComponentSize,
|
|
382
|
+
EFormFieldStatus,
|
|
383
|
+
} from "@sberbusiness/triplex-next";
|
|
384
|
+
|
|
385
|
+
interface ISizeItemProps {
|
|
386
|
+
size: EComponentSize;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const FOOD_NAMES = [
|
|
390
|
+
"Mushroom",
|
|
391
|
+
"Tomato",
|
|
392
|
+
"Eggplant",
|
|
393
|
+
"Grapes",
|
|
394
|
+
"Melon",
|
|
395
|
+
"Watermelon",
|
|
396
|
+
"Tangerine",
|
|
397
|
+
"Lemon",
|
|
398
|
+
"Banana",
|
|
399
|
+
"Pineapple",
|
|
400
|
+
"Red Apple",
|
|
401
|
+
"Green Apple",
|
|
402
|
+
"Pear",
|
|
403
|
+
"Peach",
|
|
404
|
+
"Cherries",
|
|
405
|
+
"Strawberry",
|
|
406
|
+
] as const;
|
|
407
|
+
|
|
408
|
+
const FOOD_OPTIONS: ISuggestFieldOption[] = FOOD_NAMES.map((item, index) => ({
|
|
409
|
+
id: `suggest-field-option-${index}`,
|
|
410
|
+
label: item,
|
|
411
|
+
}));
|
|
412
|
+
|
|
413
|
+
const getFilteredOptions = (query: string): ISuggestFieldOption[] =>
|
|
414
|
+
FOOD_OPTIONS.filter(({ label }) => label.toLowerCase().includes(query.toLowerCase()));
|
|
415
|
+
|
|
416
|
+
const SizeItem = ({ size }: ISizeItemProps) => {
|
|
417
|
+
const [value, setValue] = useState<ISuggestFieldOption | undefined>(undefined);
|
|
418
|
+
const [options, setOptions] = useState<ISuggestFieldOption[]>([]);
|
|
419
|
+
const [inputPristine, setInputPristine] = useState(true);
|
|
420
|
+
const [tooltipOpen, setTooltipOpen] = useState<boolean>(false);
|
|
421
|
+
|
|
422
|
+
const adaptive = useMobileView();
|
|
423
|
+
|
|
424
|
+
const reset = (newOptions: typeof options) => {
|
|
425
|
+
setOptions(newOptions);
|
|
426
|
+
setInputPristine(true);
|
|
427
|
+
setTooltipOpen(false);
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const handleFilter = (inputValue: string) => {
|
|
431
|
+
setInputPristine(false);
|
|
432
|
+
|
|
433
|
+
if (inputValue.length === 0) {
|
|
434
|
+
setOptions(FOOD_OPTIONS);
|
|
435
|
+
setTooltipOpen(false);
|
|
436
|
+
} else {
|
|
437
|
+
const filteredOptions = getFilteredOptions(inputValue);
|
|
438
|
+
|
|
439
|
+
setOptions(filteredOptions);
|
|
440
|
+
setTooltipOpen(filteredOptions.length === 0);
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
const handleSelect = (nextValue: typeof value) => {
|
|
445
|
+
setValue(nextValue);
|
|
446
|
+
reset([]);
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const handleInputBlur: React.FocusEventHandler<HTMLInputElement> = () => {
|
|
450
|
+
if (!adaptive) {
|
|
451
|
+
reset([]);
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const handleInputMouseDown: React.MouseEventHandler<HTMLInputElement> = (event) => {
|
|
456
|
+
if (options.length === 0) {
|
|
457
|
+
const query = event.currentTarget.value;
|
|
458
|
+
const filteredOptions = getFilteredOptions(inputPristine ? "" : query);
|
|
459
|
+
|
|
460
|
+
setOptions(filteredOptions);
|
|
461
|
+
setTooltipOpen(filteredOptions.length === 0 && query.length !== 0);
|
|
462
|
+
} else if (adaptive) {
|
|
463
|
+
reset(FOOD_OPTIONS);
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
return (
|
|
468
|
+
<div style={{ maxWidth: 300 }}>
|
|
469
|
+
<div style={{ marginBottom: 8, fontSize: 16, fontWeight: 700 }}>{size.toUpperCase()}</div>
|
|
470
|
+
<SuggestField
|
|
471
|
+
value={value}
|
|
472
|
+
options={options}
|
|
473
|
+
size={size}
|
|
474
|
+
status={EFormFieldStatus.DEFAULT}
|
|
475
|
+
label="Label"
|
|
476
|
+
placeholder="Type to proceed"
|
|
477
|
+
tooltipHint="No matches found."
|
|
478
|
+
tooltipOpen={tooltipOpen}
|
|
479
|
+
onFilter={handleFilter}
|
|
480
|
+
onSelect={handleSelect}
|
|
481
|
+
inputProps={{
|
|
482
|
+
onBlur: handleInputBlur,
|
|
483
|
+
onMouseDown: handleInputMouseDown,
|
|
484
|
+
}}
|
|
485
|
+
/>
|
|
486
|
+
</div>
|
|
487
|
+
);
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const SIZES = Object.values(EComponentSize);
|
|
491
|
+
|
|
492
|
+
export const SizesExample = () => (
|
|
493
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
|
494
|
+
{SIZES.map((size) => (
|
|
495
|
+
<SizeItem key={size} size={size} />
|
|
496
|
+
))}
|
|
497
|
+
</div>
|
|
498
|
+
);
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### Statuses
|
|
502
|
+
|
|
503
|
+
`stories/SuggestField/examples/StatusesExample.tsx`
|
|
504
|
+
|
|
505
|
+
```tsx
|
|
506
|
+
import React, { useState } from "react";
|
|
507
|
+
import {
|
|
508
|
+
SuggestField,
|
|
509
|
+
useMobileView,
|
|
510
|
+
ISuggestFieldOption,
|
|
511
|
+
EComponentSize,
|
|
512
|
+
EFormFieldStatus,
|
|
513
|
+
} from "@sberbusiness/triplex-next";
|
|
514
|
+
|
|
515
|
+
interface IStatusItemProps {
|
|
516
|
+
status: EFormFieldStatus;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const FOOD_NAMES = [
|
|
520
|
+
"Mushroom",
|
|
521
|
+
"Tomato",
|
|
522
|
+
"Eggplant",
|
|
523
|
+
"Grapes",
|
|
524
|
+
"Melon",
|
|
525
|
+
"Watermelon",
|
|
526
|
+
"Tangerine",
|
|
527
|
+
"Lemon",
|
|
528
|
+
"Banana",
|
|
529
|
+
"Pineapple",
|
|
530
|
+
"Red Apple",
|
|
531
|
+
"Green Apple",
|
|
532
|
+
"Pear",
|
|
533
|
+
"Peach",
|
|
534
|
+
"Cherries",
|
|
535
|
+
"Strawberry",
|
|
536
|
+
] as const;
|
|
537
|
+
|
|
538
|
+
const FOOD_OPTIONS: ISuggestFieldOption[] = FOOD_NAMES.map((item, index) => ({
|
|
539
|
+
id: `suggest-field-option-${index}`,
|
|
540
|
+
label: item,
|
|
541
|
+
}));
|
|
542
|
+
|
|
543
|
+
const getFilteredOptions = (query: string): ISuggestFieldOption[] =>
|
|
544
|
+
FOOD_OPTIONS.filter(({ label }) => label.toLowerCase().includes(query.toLowerCase()));
|
|
545
|
+
|
|
546
|
+
const StatusItem = ({ status }: IStatusItemProps) => {
|
|
547
|
+
const [value, setValue] = useState<ISuggestFieldOption | undefined>(undefined);
|
|
548
|
+
const [options, setOptions] = useState<ISuggestFieldOption[]>([]);
|
|
549
|
+
const [inputPristine, setInputPristine] = useState(true);
|
|
550
|
+
const [tooltipOpen, setTooltipOpen] = useState<boolean>(false);
|
|
551
|
+
|
|
552
|
+
const adaptive = useMobileView();
|
|
553
|
+
|
|
554
|
+
const reset = (newOptions: typeof options) => {
|
|
555
|
+
setOptions(newOptions);
|
|
556
|
+
setInputPristine(true);
|
|
557
|
+
setTooltipOpen(false);
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const handleFilter = (inputValue: string) => {
|
|
561
|
+
setInputPristine(false);
|
|
562
|
+
|
|
563
|
+
if (inputValue.length === 0) {
|
|
564
|
+
setOptions(FOOD_OPTIONS);
|
|
565
|
+
setTooltipOpen(false);
|
|
566
|
+
} else {
|
|
567
|
+
const filteredOptions = getFilteredOptions(inputValue);
|
|
568
|
+
|
|
569
|
+
setOptions(filteredOptions);
|
|
570
|
+
setTooltipOpen(filteredOptions.length === 0);
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
const handleSelect = (nextValue: typeof value) => {
|
|
575
|
+
setValue(nextValue);
|
|
576
|
+
reset([]);
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
const handleInputBlur: React.FocusEventHandler<HTMLInputElement> = () => {
|
|
580
|
+
if (!adaptive) {
|
|
581
|
+
reset([]);
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
const handleInputMouseDown: React.MouseEventHandler<HTMLInputElement> = (event) => {
|
|
586
|
+
if (options.length === 0) {
|
|
587
|
+
const query = event.currentTarget.value;
|
|
588
|
+
const filteredOptions = getFilteredOptions(inputPristine ? "" : query);
|
|
589
|
+
|
|
590
|
+
setOptions(filteredOptions);
|
|
591
|
+
setTooltipOpen(filteredOptions.length === 0 && query.length !== 0);
|
|
592
|
+
} else if (adaptive) {
|
|
593
|
+
reset(FOOD_OPTIONS);
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
return (
|
|
598
|
+
<div style={{ maxWidth: 300 }}>
|
|
599
|
+
<div style={{ marginBottom: 8, fontSize: 16, fontWeight: 700 }}>{status.toUpperCase()}</div>
|
|
600
|
+
<SuggestField
|
|
601
|
+
value={value}
|
|
602
|
+
options={options}
|
|
603
|
+
size={EComponentSize.LG}
|
|
604
|
+
status={status}
|
|
605
|
+
label="Label"
|
|
606
|
+
placeholder="Type to proceed"
|
|
607
|
+
tooltipHint="No matches found."
|
|
608
|
+
tooltipOpen={tooltipOpen}
|
|
609
|
+
onFilter={handleFilter}
|
|
610
|
+
onSelect={handleSelect}
|
|
611
|
+
inputProps={{
|
|
612
|
+
onBlur: handleInputBlur,
|
|
613
|
+
onMouseDown: handleInputMouseDown,
|
|
614
|
+
}}
|
|
615
|
+
/>
|
|
616
|
+
</div>
|
|
617
|
+
);
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
const STATUSES = Object.values(EFormFieldStatus);
|
|
621
|
+
|
|
622
|
+
export const StatusesExample = () => (
|
|
623
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
|
624
|
+
{STATUSES.map((status) => (
|
|
625
|
+
<StatusItem key={status} status={status} />
|
|
626
|
+
))}
|
|
627
|
+
</div>
|
|
628
|
+
);
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
### Loading
|
|
632
|
+
|
|
633
|
+
`stories/SuggestField/examples/LoadingExample.tsx`
|
|
634
|
+
|
|
635
|
+
```tsx
|
|
636
|
+
import React from "react";
|
|
637
|
+
import { SuggestField, EComponentSize, EFormFieldStatus } from "@sberbusiness/triplex-next";
|
|
638
|
+
|
|
639
|
+
export const LoadingExample = () => {
|
|
640
|
+
return (
|
|
641
|
+
<div style={{ maxWidth: 300 }}>
|
|
642
|
+
<SuggestField
|
|
643
|
+
value={undefined}
|
|
644
|
+
options={[]}
|
|
645
|
+
size={EComponentSize.LG}
|
|
646
|
+
status={EFormFieldStatus.DEFAULT}
|
|
647
|
+
label="Label"
|
|
648
|
+
placeholder="Type to proceed"
|
|
649
|
+
tooltipHint="No matches found."
|
|
650
|
+
tooltipOpen={false}
|
|
651
|
+
loading={true}
|
|
652
|
+
onFilter={() => {}}
|
|
653
|
+
onSelect={() => {}}
|
|
654
|
+
inputProps={{}}
|
|
655
|
+
/>
|
|
656
|
+
</div>
|
|
657
|
+
);
|
|
658
|
+
};
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
### Production
|
|
662
|
+
|
|
663
|
+
`stories/SuggestField/examples/ProductionExample.tsx`
|
|
664
|
+
|
|
665
|
+
```tsx
|
|
666
|
+
import React, { useState, useRef } from "react";
|
|
667
|
+
import {
|
|
668
|
+
SuggestField,
|
|
669
|
+
HelpBox,
|
|
670
|
+
Text,
|
|
671
|
+
Link,
|
|
672
|
+
useMobileView,
|
|
673
|
+
ISuggestFieldOption,
|
|
674
|
+
EComponentSize,
|
|
675
|
+
EFormFieldStatus,
|
|
676
|
+
ETooltipSize,
|
|
677
|
+
ETextSize,
|
|
678
|
+
EFontType,
|
|
679
|
+
} from "@sberbusiness/triplex-next";
|
|
680
|
+
import { DefaulticonStrokePrdIcon24 } from "@sberbusiness/icons-next";
|
|
681
|
+
|
|
682
|
+
const FOOD_NAMES = [
|
|
683
|
+
"Mushroom",
|
|
684
|
+
"Tomato",
|
|
685
|
+
"Eggplant",
|
|
686
|
+
"Grapes",
|
|
687
|
+
"Melon",
|
|
688
|
+
"Watermelon",
|
|
689
|
+
"Tangerine",
|
|
690
|
+
"Lemon",
|
|
691
|
+
"Banana",
|
|
692
|
+
"Pineapple",
|
|
693
|
+
"Red Apple",
|
|
694
|
+
"Green Apple",
|
|
695
|
+
"Pear",
|
|
696
|
+
"Peach",
|
|
697
|
+
"Cherries",
|
|
698
|
+
"Strawberry",
|
|
699
|
+
] as const;
|
|
700
|
+
|
|
701
|
+
const FOOD_OPTIONS: ISuggestFieldOption[] = FOOD_NAMES.map((item, index) => ({
|
|
702
|
+
id: `suggest-field-option-${index}`,
|
|
703
|
+
label: item,
|
|
704
|
+
}));
|
|
705
|
+
|
|
706
|
+
const getFilteredOptions = (query: string): ISuggestFieldOption[] =>
|
|
707
|
+
FOOD_OPTIONS.filter(({ label }) => label.toLowerCase().includes(query.toLowerCase()));
|
|
708
|
+
|
|
709
|
+
export const ProductionExample = () => {
|
|
710
|
+
const [value, setValue] = useState<ISuggestFieldOption | undefined>(undefined);
|
|
711
|
+
const [options, setOptions] = useState<ISuggestFieldOption[]>([]);
|
|
712
|
+
const [inputPristine, setInputPristine] = useState(true);
|
|
713
|
+
const [tooltipOpen, setTooltipOpen] = useState<boolean>(false);
|
|
714
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
715
|
+
|
|
716
|
+
const adaptive = useMobileView();
|
|
717
|
+
|
|
718
|
+
const reset = (newOptions: typeof options) => {
|
|
719
|
+
setOptions(newOptions);
|
|
720
|
+
setInputPristine(true);
|
|
721
|
+
setTooltipOpen(false);
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
const handleFilter = (inputValue: string) => {
|
|
725
|
+
setInputPristine(false);
|
|
726
|
+
|
|
727
|
+
if (inputValue.length === 0) {
|
|
728
|
+
setOptions(FOOD_OPTIONS);
|
|
729
|
+
setTooltipOpen(false);
|
|
730
|
+
} else {
|
|
731
|
+
const filteredOptions = getFilteredOptions(inputValue);
|
|
732
|
+
|
|
733
|
+
setOptions(filteredOptions);
|
|
734
|
+
setTooltipOpen(filteredOptions.length === 0);
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
const handleSelect = (nextValue: typeof value) => {
|
|
739
|
+
setValue(nextValue);
|
|
740
|
+
reset([]);
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
const handleInputBlur: React.FocusEventHandler<HTMLInputElement> = () => {
|
|
744
|
+
if (!adaptive) {
|
|
745
|
+
reset([]);
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
const handleInputMouseDown: React.MouseEventHandler<HTMLInputElement> = (event) => {
|
|
750
|
+
if (options.length === 0) {
|
|
751
|
+
const query = event.currentTarget.value;
|
|
752
|
+
const filteredOptions = getFilteredOptions(inputPristine ? "" : query);
|
|
753
|
+
|
|
754
|
+
setOptions(filteredOptions);
|
|
755
|
+
setTooltipOpen(filteredOptions.length === 0 && query.length !== 0);
|
|
756
|
+
} else if (adaptive) {
|
|
757
|
+
reset(FOOD_OPTIONS);
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
const handleClear: React.MouseEventHandler<HTMLButtonElement> = () => {
|
|
762
|
+
// На мобильном фокус открыл бы полноэкранный дропдаун сразу после очистки.
|
|
763
|
+
if (!adaptive) {
|
|
764
|
+
inputRef.current?.focus();
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
const handleLinkClick: React.MouseEventHandler<HTMLAnchorElement> = (event) => {
|
|
769
|
+
event.preventDefault();
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
return (
|
|
773
|
+
<div style={{ maxWidth: 300 }}>
|
|
774
|
+
<SuggestField
|
|
775
|
+
value={value}
|
|
776
|
+
options={options}
|
|
777
|
+
size={EComponentSize.LG}
|
|
778
|
+
status={EFormFieldStatus.DEFAULT}
|
|
779
|
+
label="Label"
|
|
780
|
+
placeholder="Type to proceed"
|
|
781
|
+
tooltipHint="No matches found."
|
|
782
|
+
tooltipOpen={tooltipOpen}
|
|
783
|
+
onFilter={handleFilter}
|
|
784
|
+
onSelect={handleSelect}
|
|
785
|
+
onClear={handleClear}
|
|
786
|
+
inputProps={{
|
|
787
|
+
onBlur: handleInputBlur,
|
|
788
|
+
onMouseDown: handleInputMouseDown,
|
|
789
|
+
ref: inputRef,
|
|
790
|
+
}}
|
|
791
|
+
prefix={<DefaulticonStrokePrdIcon24 paletteIndex={5} />}
|
|
792
|
+
postfix={
|
|
793
|
+
<React.Fragment>
|
|
794
|
+
<DefaulticonStrokePrdIcon24 paletteIndex={5} />
|
|
795
|
+
<HelpBox tooltipSize={ETooltipSize.SM}>Helpful details appear here</HelpBox>
|
|
796
|
+
</React.Fragment>
|
|
797
|
+
}
|
|
798
|
+
description={
|
|
799
|
+
<Text tag="div" size={ETextSize.B4} type={EFontType.SECONDARY}>
|
|
800
|
+
(21) Description{" "}
|
|
801
|
+
<Link href="#" onClick={handleLinkClick}>
|
|
802
|
+
Link text
|
|
803
|
+
</Link>
|
|
804
|
+
</Text>
|
|
805
|
+
}
|
|
806
|
+
/>
|
|
807
|
+
</div>
|
|
808
|
+
);
|
|
809
|
+
};
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
### CustomOptions
|
|
813
|
+
|
|
814
|
+
`stories/SuggestField/examples/CustomOptionsExample.tsx`
|
|
815
|
+
|
|
816
|
+
```tsx
|
|
817
|
+
import React, { useState } from "react";
|
|
818
|
+
import {
|
|
819
|
+
SuggestField,
|
|
820
|
+
Text,
|
|
821
|
+
useMobileView,
|
|
822
|
+
ISuggestFieldOption,
|
|
823
|
+
EComponentSize,
|
|
824
|
+
EFormFieldStatus,
|
|
825
|
+
ETextSize,
|
|
826
|
+
EFontType,
|
|
827
|
+
} from "@sberbusiness/triplex-next";
|
|
828
|
+
|
|
829
|
+
const FOOD_DATA = [
|
|
830
|
+
{ label: "Mushroom", icon: "🍄", category: "Vegetable" },
|
|
831
|
+
{ label: "Tomato", icon: "🍅", category: "Vegetable" },
|
|
832
|
+
{ label: "Eggplant", icon: "🍆", category: "Vegetable" },
|
|
833
|
+
{ label: "Grapes", icon: "🍇", category: "Fruit" },
|
|
834
|
+
{ label: "Melon", icon: "🍈", category: "Fruit" },
|
|
835
|
+
{ label: "Watermelon", icon: "🍉", category: "Fruit" },
|
|
836
|
+
{ label: "Tangerine", icon: "🍊", category: "Fruit" },
|
|
837
|
+
{ label: "Lemon", icon: "🍋", category: "Fruit" },
|
|
838
|
+
{ label: "Banana", icon: "🍌", category: "Fruit" },
|
|
839
|
+
{ label: "Pineapple", icon: "🍍", category: "Fruit" },
|
|
840
|
+
{ label: "Red apple", icon: "🍎", category: "Fruit" },
|
|
841
|
+
{ label: "Green apple", icon: "🍏", category: "Fruit" },
|
|
842
|
+
{ label: "Pear", icon: "🍐", category: "Fruit" },
|
|
843
|
+
{ label: "Peach", icon: "🍑", category: "Fruit" },
|
|
844
|
+
{ label: "Cherries", icon: "🍒", category: "Fruit" },
|
|
845
|
+
{ label: "Strawberry", icon: "🍓", category: "Fruit" },
|
|
846
|
+
] as const;
|
|
847
|
+
|
|
848
|
+
const FOOD_OPTIONS: ISuggestFieldOption[] = FOOD_DATA.map((item, index) => ({
|
|
849
|
+
id: `suggest-field-option-${index}`,
|
|
850
|
+
label: item.label,
|
|
851
|
+
content: (
|
|
852
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
853
|
+
<span>{item.icon}</span>
|
|
854
|
+
<Text size={ETextSize.B2} type={EFontType.PRIMARY}>
|
|
855
|
+
{item.label}
|
|
856
|
+
</Text>
|
|
857
|
+
<Text size={ETextSize.B3} type={EFontType.SECONDARY} style={{ marginLeft: "auto" }}>
|
|
858
|
+
{item.category}
|
|
859
|
+
</Text>
|
|
860
|
+
</div>
|
|
861
|
+
),
|
|
862
|
+
}));
|
|
863
|
+
|
|
864
|
+
const getFilteredOptions = (query: string): ISuggestFieldOption[] =>
|
|
865
|
+
FOOD_OPTIONS.filter(({ label }) => label.toLowerCase().includes(query.toLowerCase()));
|
|
866
|
+
|
|
867
|
+
export const CustomOptionsExample = () => {
|
|
868
|
+
const [value, setValue] = useState<ISuggestFieldOption | undefined>(undefined);
|
|
869
|
+
const [options, setOptions] = useState<ISuggestFieldOption[]>([]);
|
|
870
|
+
const [inputPristine, setInputPristine] = useState(true);
|
|
871
|
+
const [tooltipOpen, setTooltipOpen] = useState<boolean>(false);
|
|
872
|
+
|
|
873
|
+
const adaptive = useMobileView();
|
|
874
|
+
|
|
875
|
+
const reset = (newOptions: typeof options) => {
|
|
876
|
+
setOptions(newOptions);
|
|
877
|
+
setInputPristine(true);
|
|
878
|
+
setTooltipOpen(false);
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
const handleFilter = (inputValue: string) => {
|
|
882
|
+
setInputPristine(false);
|
|
883
|
+
|
|
884
|
+
if (inputValue.length === 0) {
|
|
885
|
+
setOptions(FOOD_OPTIONS);
|
|
886
|
+
setTooltipOpen(false);
|
|
887
|
+
} else {
|
|
888
|
+
const filteredOptions = getFilteredOptions(inputValue);
|
|
889
|
+
|
|
890
|
+
setOptions(filteredOptions);
|
|
891
|
+
setTooltipOpen(filteredOptions.length === 0);
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
const handleSelect = (nextValue: typeof value) => {
|
|
896
|
+
setValue(nextValue);
|
|
897
|
+
reset([]);
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
const handleInputBlur: React.FocusEventHandler<HTMLInputElement> = () => {
|
|
901
|
+
if (!adaptive) {
|
|
902
|
+
reset([]);
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
|
|
906
|
+
const handleInputMouseDown: React.MouseEventHandler<HTMLInputElement> = (event) => {
|
|
907
|
+
if (options.length === 0) {
|
|
908
|
+
const query = event.currentTarget.value;
|
|
909
|
+
const filteredOptions = getFilteredOptions(inputPristine ? "" : query);
|
|
910
|
+
|
|
911
|
+
setOptions(filteredOptions);
|
|
912
|
+
setTooltipOpen(filteredOptions.length === 0 && query.length !== 0);
|
|
913
|
+
} else if (adaptive) {
|
|
914
|
+
reset(FOOD_OPTIONS);
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
return (
|
|
919
|
+
<div style={{ maxWidth: 300 }}>
|
|
920
|
+
<SuggestField
|
|
921
|
+
value={value}
|
|
922
|
+
options={options}
|
|
923
|
+
size={EComponentSize.LG}
|
|
924
|
+
status={EFormFieldStatus.DEFAULT}
|
|
925
|
+
label="Label"
|
|
926
|
+
placeholder="Type to proceed"
|
|
927
|
+
tooltipHint="No matches found."
|
|
928
|
+
tooltipOpen={tooltipOpen}
|
|
929
|
+
onFilter={handleFilter}
|
|
930
|
+
onSelect={handleSelect}
|
|
931
|
+
inputProps={{
|
|
932
|
+
onBlur: handleInputBlur,
|
|
933
|
+
onMouseDown: handleInputMouseDown,
|
|
934
|
+
}}
|
|
935
|
+
/>
|
|
936
|
+
</div>
|
|
937
|
+
);
|
|
938
|
+
};
|
|
939
|
+
```
|
|
940
|
+
|
|
941
|
+
### Async
|
|
942
|
+
|
|
943
|
+
`stories/SuggestField/examples/AsyncExample.tsx`
|
|
944
|
+
|
|
945
|
+
```tsx
|
|
946
|
+
import React, { useState, useRef, useCallback, useEffect } from "react";
|
|
947
|
+
import {
|
|
948
|
+
SuggestField,
|
|
949
|
+
useMobileView,
|
|
950
|
+
ISuggestFieldOption,
|
|
951
|
+
EComponentSize,
|
|
952
|
+
EFormFieldStatus,
|
|
953
|
+
} from "@sberbusiness/triplex-next";
|
|
954
|
+
import { debounce } from "lodash-es";
|
|
955
|
+
|
|
956
|
+
const FOOD_NAMES = [
|
|
957
|
+
"Mushroom",
|
|
958
|
+
"Tomato",
|
|
959
|
+
"Eggplant",
|
|
960
|
+
"Grapes",
|
|
961
|
+
"Melon",
|
|
962
|
+
"Watermelon",
|
|
963
|
+
"Tangerine",
|
|
964
|
+
"Lemon",
|
|
965
|
+
"Banana",
|
|
966
|
+
"Pineapple",
|
|
967
|
+
"Red Apple",
|
|
968
|
+
"Green Apple",
|
|
969
|
+
"Pear",
|
|
970
|
+
"Peach",
|
|
971
|
+
"Cherries",
|
|
972
|
+
"Strawberry",
|
|
973
|
+
] as const;
|
|
974
|
+
|
|
975
|
+
const FOOD_OPTIONS: ISuggestFieldOption[] = FOOD_NAMES.map((item, index) => ({
|
|
976
|
+
id: `suggest-field-option-${index}`,
|
|
977
|
+
label: item,
|
|
978
|
+
}));
|
|
979
|
+
|
|
980
|
+
const getFilteredOptions = (query: string): ISuggestFieldOption[] =>
|
|
981
|
+
FOOD_OPTIONS.filter(({ label }) => label.toLowerCase().includes(query.toLowerCase()));
|
|
982
|
+
|
|
983
|
+
export const AsyncExample = () => {
|
|
984
|
+
const [value, setValue] = useState<ISuggestFieldOption | undefined>(undefined);
|
|
985
|
+
const [options, setOptions] = useState<ISuggestFieldOption[]>([]);
|
|
986
|
+
const [loading, setLoading] = useState<boolean>(false);
|
|
987
|
+
const [tooltipOpen, setTooltipOpen] = useState<boolean>(false);
|
|
988
|
+
const [dropdownListLoading, setDropdownListLoading] = useState<boolean>(false);
|
|
989
|
+
|
|
990
|
+
const filteredOptionsRef = useRef<typeof options>([]);
|
|
991
|
+
const lastSearchQueryRef = useRef<string>("");
|
|
992
|
+
const activeRequestRef = useRef<boolean>(false);
|
|
993
|
+
const scrollTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
|
994
|
+
|
|
995
|
+
const adaptive = useMobileView();
|
|
996
|
+
|
|
997
|
+
const debouncedFilter = useRef(
|
|
998
|
+
// eslint-disable-next-line react-hooks/refs
|
|
999
|
+
debounce((inputValue: string) => {
|
|
1000
|
+
if (!activeRequestRef.current) return;
|
|
1001
|
+
|
|
1002
|
+
const filteredOptions = getFilteredOptions(inputValue);
|
|
1003
|
+
|
|
1004
|
+
filteredOptionsRef.current = filteredOptions;
|
|
1005
|
+
lastSearchQueryRef.current = inputValue;
|
|
1006
|
+
|
|
1007
|
+
setOptions(filteredOptions.slice(0, 6));
|
|
1008
|
+
setLoading(false);
|
|
1009
|
+
setTooltipOpen(filteredOptions.length === 0);
|
|
1010
|
+
}, 1000),
|
|
1011
|
+
).current;
|
|
1012
|
+
|
|
1013
|
+
const stopTimers = useCallback(() => {
|
|
1014
|
+
debouncedFilter.cancel();
|
|
1015
|
+
clearTimeout(scrollTimeoutRef.current);
|
|
1016
|
+
}, [debouncedFilter]);
|
|
1017
|
+
|
|
1018
|
+
const stopLoading = () => {
|
|
1019
|
+
stopTimers();
|
|
1020
|
+
setLoading(false);
|
|
1021
|
+
setDropdownListLoading(false);
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
const reset = () => {
|
|
1025
|
+
filteredOptionsRef.current = [];
|
|
1026
|
+
lastSearchQueryRef.current = "";
|
|
1027
|
+
activeRequestRef.current = false;
|
|
1028
|
+
stopLoading();
|
|
1029
|
+
setOptions([]);
|
|
1030
|
+
setTooltipOpen(false);
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
const startSearch = (query: string) => {
|
|
1034
|
+
stopTimers();
|
|
1035
|
+
activeRequestRef.current = true;
|
|
1036
|
+
|
|
1037
|
+
setTooltipOpen(false);
|
|
1038
|
+
setLoading(true);
|
|
1039
|
+
setDropdownListLoading(false);
|
|
1040
|
+
debouncedFilter(query);
|
|
1041
|
+
};
|
|
1042
|
+
|
|
1043
|
+
const handleFilter = (inputValue: string) => {
|
|
1044
|
+
if (inputValue.length === 0) {
|
|
1045
|
+
return reset();
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
if (inputValue !== lastSearchQueryRef.current) {
|
|
1049
|
+
startSearch(inputValue);
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
|
|
1053
|
+
const handleSelect = (nextValue: typeof value) => {
|
|
1054
|
+
setValue(nextValue);
|
|
1055
|
+
reset();
|
|
1056
|
+
};
|
|
1057
|
+
|
|
1058
|
+
const handleScrollEnd = () => {
|
|
1059
|
+
const filteredOptions = filteredOptionsRef.current;
|
|
1060
|
+
|
|
1061
|
+
if (dropdownListLoading || options.length === 0 || options.length === filteredOptions.length) {
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
clearTimeout(scrollTimeoutRef.current);
|
|
1066
|
+
setDropdownListLoading(true);
|
|
1067
|
+
|
|
1068
|
+
scrollTimeoutRef.current = setTimeout(() => {
|
|
1069
|
+
setOptions((prevOptions) => {
|
|
1070
|
+
const nextChunk = filteredOptions.slice(prevOptions.length, prevOptions.length + 6);
|
|
1071
|
+
return [...prevOptions, ...nextChunk];
|
|
1072
|
+
});
|
|
1073
|
+
setDropdownListLoading(false);
|
|
1074
|
+
}, 1000);
|
|
1075
|
+
};
|
|
1076
|
+
|
|
1077
|
+
const handleInputBlur: React.FocusEventHandler<HTMLInputElement> = () => {
|
|
1078
|
+
if (adaptive === false) {
|
|
1079
|
+
reset();
|
|
1080
|
+
}
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
const handleInputMouseDown: React.MouseEventHandler<HTMLInputElement> = (event) => {
|
|
1084
|
+
const query = event.currentTarget.value;
|
|
1085
|
+
|
|
1086
|
+
if (query.length !== 0 && !loading && query !== lastSearchQueryRef.current) {
|
|
1087
|
+
startSearch(query);
|
|
1088
|
+
}
|
|
1089
|
+
};
|
|
1090
|
+
|
|
1091
|
+
useEffect(() => {
|
|
1092
|
+
return () => {
|
|
1093
|
+
activeRequestRef.current = false;
|
|
1094
|
+
stopTimers();
|
|
1095
|
+
};
|
|
1096
|
+
}, [stopTimers]);
|
|
1097
|
+
|
|
1098
|
+
return (
|
|
1099
|
+
<div style={{ maxWidth: 300 }}>
|
|
1100
|
+
<SuggestField
|
|
1101
|
+
value={value}
|
|
1102
|
+
options={options}
|
|
1103
|
+
size={EComponentSize.LG}
|
|
1104
|
+
status={EFormFieldStatus.DEFAULT}
|
|
1105
|
+
label="Label"
|
|
1106
|
+
placeholder="Type to proceed"
|
|
1107
|
+
tooltipHint="No matches found."
|
|
1108
|
+
loading={loading}
|
|
1109
|
+
tooltipOpen={tooltipOpen}
|
|
1110
|
+
onFilter={handleFilter}
|
|
1111
|
+
onSelect={handleSelect}
|
|
1112
|
+
onScrollEnd={handleScrollEnd}
|
|
1113
|
+
inputProps={{
|
|
1114
|
+
onBlur: handleInputBlur,
|
|
1115
|
+
onMouseDown: handleInputMouseDown,
|
|
1116
|
+
}}
|
|
1117
|
+
/>
|
|
1118
|
+
</div>
|
|
1119
|
+
);
|
|
1120
|
+
};
|
|
1121
|
+
```
|
|
1122
|
+
|