@sberbusiness/triplex-next-mcp-server 1.45.0-beta → 1.46.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/bundle/components/CardAction.md +615 -0
- package/bundle/components/Chip.md +7 -6
- package/bundle/components/ChipMonthYearPicker.md +344 -0
- package/bundle/components/ChipMultiselect.md +1251 -0
- package/bundle/components/ChipOptions.md +329 -0
- package/bundle/components/ChipSort.md +333 -0
- package/bundle/components/ChipSuggest.md +587 -0
- package/bundle/components/DateField.md +222 -303
- package/bundle/components/DatePickerExtended.md +3 -1
- package/bundle/components/FormField.md +3 -1
- package/bundle/components/HelpBox.md +7 -1
- package/bundle/components/IslandAccordion.md +443 -152
- package/bundle/components/IslandWidget.md +622 -157
- package/bundle/components/MaskedField.md +2 -1
- package/bundle/components/MonthYearField.md +3 -1
- package/bundle/manifest.json +46 -5
- package/bundle/release-notes/1.46.0.mdx +59 -0
- package/bundle/release-notes/1.47.0.mdx +7 -0
- package/bundle/tokens.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
---
|
|
2
|
+
component: ChipSuggest
|
|
3
|
+
category: Chips
|
|
4
|
+
related: [Chip, ChipSelect, Suggest, Dropdown, FormField]
|
|
5
|
+
tokens: []
|
|
6
|
+
stories: stories/Chips/ChipSuggest.stories.tsx
|
|
7
|
+
version: "1.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# ChipSuggest
|
|
11
|
+
|
|
12
|
+
## Назначение
|
|
13
|
+
|
|
14
|
+
Чипс-фильтр с выпадающим списком и фильтрацией по вводу: собранная композиция
|
|
15
|
+
`Suggest` (состояние и контекст) + `Chip` (target-элемент) + `Dropdown` (список).
|
|
16
|
+
Пока значение не выбрано, чипс показывает `label` и стрелку списка; после выбора —
|
|
17
|
+
`label` выбранной опции (или `displayedValue`) и кнопку очистки.
|
|
18
|
+
|
|
19
|
+
Сам список не фильтрует: `options` формирует потребитель в ответ на `onFilter`, как и в
|
|
20
|
+
`Suggest`. Выбранное значение контролируемое — компонент его не хранит.
|
|
21
|
+
|
|
22
|
+
Используй когда: нужен фильтр в ряду чипсов (панель фильтров `MasterTable`, `ChipGroup`),
|
|
23
|
+
и список опций достаточно длинный, чтобы искать по нему вводом.
|
|
24
|
+
|
|
25
|
+
Не используй когда:
|
|
26
|
+
- Список короткий и фильтрация не нужна — возьми `ChipSelect`.
|
|
27
|
+
- Нужно выбрать несколько значений — возьми `ChipMultiselect`.
|
|
28
|
+
- Нужен suggest не чипсом, а обычным полем формы с лейблом и статусами — возьми
|
|
29
|
+
`SuggestField`.
|
|
30
|
+
- Нужна нестандартная разметка управляющего элемента или списка — собирай композицию
|
|
31
|
+
сам на headless-`Suggest`.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Варианты и props
|
|
36
|
+
|
|
37
|
+
`IChipSuggestProps<T extends ISuggestOption = ISuggestOption>` расширяет `ISuggestProps<T>`
|
|
38
|
+
(все props `Suggest`, включая `value`, `options`, `size`, `onSelect`, `onFilter`,
|
|
39
|
+
`placeholder`, `noOptionsText`, `loading`, `dropdownListLoading`, `tooltipOpen`,
|
|
40
|
+
`clearInputOnFocus`, `onScrollEnd`) и `Pick<IChipProps, "type">`. Ниже — только то,
|
|
41
|
+
что добавляет или переопределяет `ChipSuggest`.
|
|
42
|
+
|
|
43
|
+
### Обязательные props
|
|
44
|
+
|
|
45
|
+
| Prop | Тип | Описание |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `label` | `React.ReactNode` | Название поля. Показывается на чипсе, пока значение не выбрано, и лейблом над полем фильтрации внутри списка — то есть отображается всегда, даже при выбранном значении |
|
|
48
|
+
| `value` | `T \| undefined` | Выбранная опция (из `ISuggestProps`). Контролируемое значение: сброс делает потребитель в `targetProps.clearSelected` |
|
|
49
|
+
| `options` | `T[]` | Список опций (из `ISuggestProps`). Фильтрует потребитель в ответ на `onFilter` |
|
|
50
|
+
| `size` | `EComponentSize` | Размер (из `ISuggestProps`). Раздаётся target-элементу и выпадающему списку |
|
|
51
|
+
| `onSelect` | `(value: T \| undefined) => void` | Выбор опции (из `ISuggestProps`) |
|
|
52
|
+
| `onFilter` | `(value: string) => void` | Изменение поля фильтрации (из `ISuggestProps`) |
|
|
53
|
+
|
|
54
|
+
### Опциональные props
|
|
55
|
+
|
|
56
|
+
| Prop | Тип | По умолчанию | Описание |
|
|
57
|
+
|---|---|---|---|
|
|
58
|
+
| `type` | `EChipType` | `EChipType.TYPE_1` (дефолт `Chip`) | Тип чипса. Задавать только здесь: `targetProps.type` не работает — см. «Инварианты» |
|
|
59
|
+
| `displayedValue` | `React.ReactNode` | — | Что показать на чипсе вместо `value.label`. Учитывается только при выбранном `value` |
|
|
60
|
+
| `targetProps` | `IChipSuggestTargetProps<T>` | — | Props target-элемента. Через него передаётся `clearSelected`, а также props `Chip` (`disabled`, `className`, `data-*`, `onClick`, `size`). Два исключения — см. «Инварианты»: `targetProps.type` не применяется никогда, `targetProps.onKeyDown` вызывается только на `Enter`/`Space` |
|
|
61
|
+
| `dropdownProps` | `Omit<IChipSuggestDropdownProps<T>, "targetRef">` | — | Props выпадающего списка: props `Dropdown` (`onOpen`, `onClose`, `mobileViewProps`, …) плюс `focusTrapProps`. `targetRef` проставляет сам `ChipSuggest` |
|
|
62
|
+
|
|
63
|
+
`className` попадает на корневой `<div>` (рядом с внутренним классом `chipGroupItem`),
|
|
64
|
+
остальные `React.HTMLAttributes<HTMLDivElement>` уходят в `Suggest`.
|
|
65
|
+
|
|
66
|
+
### Сброс значения
|
|
67
|
+
|
|
68
|
+
Кнопка очистки появляется на чипсе автоматически, как только `value !== undefined`, но
|
|
69
|
+
сам сброс компонент не делает — он вызывает `targetProps.clearSelected`. Без этого колбэка
|
|
70
|
+
кнопка отрисуется и ничего не сделает.
|
|
71
|
+
|
|
72
|
+
### Тип опции
|
|
73
|
+
|
|
74
|
+
`ISuggestOption`: `id`, `label`, `content` (кастомная разметка опции в списке вместо
|
|
75
|
+
`label`), `showNotificationIcon` (значок новых уведомлений на опции). Расширяется
|
|
76
|
+
дженериком: `<ChipSuggest<IMyOption> …>` протаскивает тип в `value`, `options`, `onSelect`
|
|
77
|
+
и `targetProps`.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Дизайн-токены
|
|
82
|
+
|
|
83
|
+
Собственных токенов нет: `styles/ChipSuggest.module.less` задаёт только отступ поля
|
|
84
|
+
фильтрации внутри списка. Внешний вид складывается из токенов компонентов, из которых
|
|
85
|
+
собран `ChipSuggest`, — переопределяй их через `ThemeProvider` (prop `tokens`), см.
|
|
86
|
+
`ThemeProvider-ai.md` → «Как переопределять токены»:
|
|
87
|
+
|
|
88
|
+
- чипс — группа `Chip` (`Chip-ai.md`);
|
|
89
|
+
- выпадающий список — группы `Dropdown` и `DropdownList` (`Dropdown-ai.md`);
|
|
90
|
+
- поле фильтрации внутри списка — группы `FormField` и `Tooltip`.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Инварианты
|
|
95
|
+
|
|
96
|
+
- `forwardRef` на `ChipSuggest` — не убирать. Ref указывает на корневой `<div>` компонента
|
|
97
|
+
`Suggest` (не на сам чипс), тот же элемент служит `targetRef` для `Dropdown`.
|
|
98
|
+
- Generic-сигнатура `<T extends ISuggestOption = ISuggestOption>` — часть публичного API
|
|
99
|
+
у `ChipSuggest`, `ChipSuggestTarget` и у всех трёх интерфейсов props. Параметр `T` в
|
|
100
|
+
`IChipSuggestTargetProps` и `IChipSuggestDropdownProps` в теле интерфейса не используется
|
|
101
|
+
(стоит точечный `eslint-disable`), но убрать его нельзя — сломается код потребителей,
|
|
102
|
+
указывающих тип опции явно.
|
|
103
|
+
- Barrel `ChipSuggest/index.ts` экспортирует `ChipSuggest`, `ChipSuggestTarget` и типы.
|
|
104
|
+
`ChipSuggestDropdown` и `ChipSuggestDesktopDropdownField` — внутренние, наружу уходит
|
|
105
|
+
только тип `IChipSuggestDropdownProps`.
|
|
106
|
+
- **Известное ограничение, а не проектное решение:** `type` передаётся target-элементу
|
|
107
|
+
**после** спреда `targetProps`, поэтому `targetProps.type` не применяется никогда — даже
|
|
108
|
+
когда сам `type` не задан (тогда `Chip` уходит в свой дефолт). Тип чипса задавай prop'ом
|
|
109
|
+
`type` на самом `ChipSuggest`. Починка меняет наблюдаемое поведение, поэтому делается
|
|
110
|
+
отдельной задачей и с записью в release notes; тестом это поведение намеренно не
|
|
111
|
+
зафиксировано, чтобы падающий тест не выглядел поломкой контракта при починке.
|
|
112
|
+
Обрати внимание на асимметрию: `size` передаётся **до** спреда `targetProps`,
|
|
113
|
+
поэтому `targetProps.size` как раз работает.
|
|
114
|
+
- **Известное ограничение, а не проектное решение:** `ChipSuggestTarget.handleKeyDown`
|
|
115
|
+
вызывает `targetProps.onKeyDown` только внутри ветки `Enter`/`Space`, поэтому на всех
|
|
116
|
+
остальных клавишах (стрелки, `Tab`, печатные символы) обработчик потребителя не
|
|
117
|
+
вызывается. Не рассчитывай на `targetProps.onKeyDown` для навигации по стрелкам.
|
|
118
|
+
Починка меняет наблюдаемое поведение → отдельная задача с записью в release notes.
|
|
119
|
+
- `prefix` намеренно вырезается из props и не доходит до корневого `<div>`: он приходит
|
|
120
|
+
из `React.HTMLAttributes` как RDFa-атрибут (то есть на `<div>` был бы валиден), но
|
|
121
|
+
`ChipSuggest` его не поддерживает — `ChipSuggestTarget` задаёт только `postfix`.
|
|
122
|
+
- `ChipSuggestDesktopDropdownField` — единственный компонент папки, оставленный
|
|
123
|
+
`React.FC` без `forwardRef`: он не экспортируется из barrel, а очевидного ref-таргета
|
|
124
|
+
у него нет (корень — `FormField`). Осознанное исключение из общего правила
|
|
125
|
+
`docs/ai/codestyle.md`, а не недосмотр; менять — отдельной задачей.
|
|
126
|
+
- `dropdownProps.targetRef` исключён из типа: ссылку на корневой `<div>` подставляет сам
|
|
127
|
+
`ChipSuggest`, иначе список потеряет привязку к чипсу.
|
|
128
|
+
- `ChipSuggestDropdown` обязан класть ссылку на себя в `dropdownRef` из `SuggestContext` —
|
|
129
|
+
по ней `Suggest` отличает клик внутри списка от клика вне компонента.
|
|
130
|
+
- `ChipSuggestTarget` и `ChipSuggestDesktopDropdownField` читают `SuggestContext` и вне
|
|
131
|
+
`Suggest` не работают: значения возьмутся из дефолта контекста и компонент «залипнет».
|
|
132
|
+
- `ChipSuggest` импортирует внутренний `SuggestMobileDropdownContent` по прямому пути —
|
|
133
|
+
переименование файла в папке `Suggest` ломает мобильный режим `ChipSuggest`.
|
|
134
|
+
- Корневой `<div>` всегда получает класс `chipGroupItem` — на нём держатся отступы внутри
|
|
135
|
+
`ChipGroup`.
|
|
136
|
+
- `setForwardedRef` из `ChipSuggest/utils.ts` — внутренний хелпер, из barrel не
|
|
137
|
+
экспортируется и в публичные `src/utils` не переносится. Это **не** конечное
|
|
138
|
+
состояние: точно такая же функция уже лежит в `src/components/List/utils.ts`, и та же
|
|
139
|
+
пятистрочка инлайнится ещё в полутора десятках компонентов (включая `Suggest.tsx`).
|
|
140
|
+
Консолидация во внутренний shared-модуль — отдельная задача; не плоди третью копию,
|
|
141
|
+
а при случае сошлись на неё.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Accessibility
|
|
146
|
+
|
|
147
|
+
Роль и фокус:
|
|
148
|
+
|
|
149
|
+
- Target — `Chip`, то есть `<span role="button">` с `tabIndex` `0` (или `-1` при
|
|
150
|
+
`disabled`), а не нативная кнопка. `aria-expanded` на нём отражает видимость списка.
|
|
151
|
+
- Кнопка очистки — вложенная нативная `<button>` (`ChipClearButton`), поэтому внутри
|
|
152
|
+
чипса два элемента с ролью `button`. В тестах ищи target по `data-testid`, а не по роли;
|
|
153
|
+
саму кнопку очистки, наоборот, можно брать семантически — `within(target).getByRole("button")`
|
|
154
|
+
матчит только её, потому что сам чипс служит контейнером поиска.
|
|
155
|
+
- **Известное ограничение, а не проектное решение:** у кнопки очистки нет доступного
|
|
156
|
+
имени. `ChipClearButton` рендерит `ButtonIcon` без `aria-label`, а иконка внутри —
|
|
157
|
+
`aria-hidden`, поэтому скринридер объявит её просто «кнопка». Пробросить имя снаружи
|
|
158
|
+
сейчас нельзя: `targetProps` доходит только до `Chip`, до `ChipClearButton` — нет.
|
|
159
|
+
Это расходится с TPX-G-18 (`docs/mcp/guidelines.md`: у `ButtonIcon` `aria-label`
|
|
160
|
+
обязателен). Баг общий для семейства — `ChipSelect`, `ChipMultiselect`, `ChipOptions`
|
|
161
|
+
и `ChipDatePicker` зовут `ChipClearButton` так же. В отличие от двух ограничений в
|
|
162
|
+
«Инвариантах», починка здесь **не** breaking: достаточно добавить `aria-label`
|
|
163
|
+
(и при необходимости prop для его переопределения). Отдельная задача — TRI-139.
|
|
164
|
+
- При открытии списка фокус уходит в поле фильтрации (`autoFocus`), содержимое обёрнуто в
|
|
165
|
+
`FocusTrap` с `clickOutsideDeactivates` и `returnFocusOnDeactivate` — при закрытии фокус
|
|
166
|
+
возвращается на чипс. Опции `FocusTrap` дополняются через `dropdownProps.focusTrapProps`
|
|
167
|
+
(переданный `focusTrapOptions` мерджится поверх умолчаний).
|
|
168
|
+
|
|
169
|
+
Клавиатура:
|
|
170
|
+
|
|
171
|
+
| Клавиша | Где | Что делает |
|
|
172
|
+
|---|---|---|
|
|
173
|
+
| `Enter`, `Space` | на чипсе | Открывает и закрывает список. `Space` не прокручивает страницу (`preventDefault` в `Chip`) |
|
|
174
|
+
| `Enter`, `Space` | на кнопке очистки | Сбрасывает значение нативным click'ом кнопки. Всплытие останавливается, чтобы список не переключился заодно |
|
|
175
|
+
| `Enter` | на опции списка | Выбор опции (`keyCodesForSelection` у `DropdownListItem`) |
|
|
176
|
+
| `Escape` | внутри компонента | Закрывает список и возвращает в поле фильтрации `label` текущего значения (обработка в `Suggest`). Закрытым списком событие всплывает выше — так `ChipSuggest` уживается внутри модальных окон |
|
|
177
|
+
|
|
178
|
+
Связь списка и поля: `id` списка (`dropdownListId` из `SuggestContext`) проставляется на
|
|
179
|
+
`DropdownList` и в `aria-controls` поля фильтрации. `aria-activedescendant` поле принимает,
|
|
180
|
+
но библиотека `activeDescendant` не выставляет — клавиатурная навигация по опциям остаётся
|
|
181
|
+
на `DropdownList`.
|
|
182
|
+
|
|
183
|
+
Текстов компонент не хардкодит: `label`, `placeholder` и `noOptionsText` задаёт потребитель.
|
|
184
|
+
`noOptionsText` показывается в `Tooltip` у поля фильтрации, пока поле в фокусе и потребитель
|
|
185
|
+
держит `tooltipOpen`.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Связанные компоненты
|
|
190
|
+
|
|
191
|
+
- `Chip` — target-элемент `ChipSuggest`; отсюда `type`, `size`, `disabled`, `selected`,
|
|
192
|
+
фокус и клавиатура чипса. `IChipSuggestTargetProps` наследует `IChipProps` без `prefix`
|
|
193
|
+
и `postfix`: `postfix` занят внутренней разметкой (стрелка или кнопка очистки),
|
|
194
|
+
`prefix` в этой композиции просто не используется.
|
|
195
|
+
- `ChipSelect` — тот же выбор одного значения чипсом, но без фильтрации ввода (построен на
|
|
196
|
+
`SelectExtendedField`). Основная альтернатива при коротком списке.
|
|
197
|
+
- `ChipMultiselect` — выбор нескольких значений чипсом.
|
|
198
|
+
- `Suggest` — headless-основа: состояние поля ввода, видимость списка, `SuggestContext`,
|
|
199
|
+
обработка `Escape` и закрытие по клику вне. `IChipSuggestProps` наследует `ISuggestProps`.
|
|
200
|
+
- `Dropdown` — выпадающий список; `IChipSuggestDropdownProps` наследует `IDropdownProps`
|
|
201
|
+
без `opened` / `setOpened` (ими управляет `SuggestContext`). Списком опций служат
|
|
202
|
+
`DropdownList` и `DropdownListItem`.
|
|
203
|
+
- `FormField` — из его частей (`FormFieldLabel`, `FormFieldInput`, `FormFieldPostfix`,
|
|
204
|
+
`FormFieldClear`) собрано поле фильтрации внутри десктопного списка.
|
|
205
|
+
- `ChipSuggestTarget` — сам чипс-триггер. Экспортируется из barrel, но самостоятельного
|
|
206
|
+
применения не имеет: `value` и `dropdownOpen` берёт из `SuggestContext`, поэтому работает
|
|
207
|
+
только внутри `Suggest`. Отдельного AI.md не имеет.
|
|
208
|
+
- `ChipSuggestDropdown` — выпадающий список компонента: `FocusTrap` + поле фильтрации +
|
|
209
|
+
`DropdownList` на десктопе, `SuggestMobileDropdownContent` на мобильной ширине. Из barrel
|
|
210
|
+
не экспортируется (наружу уходит только тип props), отдельного AI.md не имеет.
|
|
211
|
+
- `ChipSuggestDesktopDropdownField` — поле фильтрации внутри десктопного списка: лейбл,
|
|
212
|
+
инпут, кнопка очистки ввода, лоадер и `Tooltip` с `noOptionsText`. Всё берёт из
|
|
213
|
+
`SuggestContext`; из barrel не экспортируется, отдельного AI.md не имеет.
|
|
214
|
+
- `ChipGroup` — контейнер, в котором чипсы выстраиваются в ряд; корневой элемент
|
|
215
|
+
`ChipSuggest` заранее помечен классом `chipGroupItem`.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Stories
|
|
220
|
+
|
|
221
|
+
Основные истории: `stories/Chips/ChipSuggest.stories.tsx`
|
|
222
|
+
Файлы примеров: `stories/Chips/examples/ChipSuggest/`
|
|
223
|
+
|
|
224
|
+
| Story | Example file | Что демонстрирует |
|
|
225
|
+
|---|---|---|
|
|
226
|
+
| `Playground` | `PlaygroundExample.tsx` | Интерактивный контроль `type`, `size`, `label`, `displayedValue`, `placeholder`, `noOptionsText`, `loading`, `clearInputOnFocus`, `targetProps` |
|
|
227
|
+
| `Default` | `DefaultExample.tsx` | Базовая связка: локальный `value`, фильтрация опций в `onFilter`, `clearSelected`, подсказка при пустом списке |
|
|
228
|
+
| `Types` | `TypesExample.tsx` | Типы чипса `EChipType.TYPE_1` / `TYPE_2` |
|
|
229
|
+
| `Sizes` | `SizesExample.tsx` | Размеры SM / MD / LG |
|
|
230
|
+
| `States` | `StatesExample.tsx` | Состояния `selected` и `disabled` |
|
|
231
|
+
| `WithNotificationIcon` | `WithNotificationIconExample.tsx` | Опции с `showNotificationIcon`. Исключена из скриншотов (`testRunner: { skip: true }`) |
|
|
232
|
+
| `VisualTests` | `VisualTestsExample.tsx` | Скриншот-регрессия: три размера с открытыми списками (списки раскрывает `useLayoutEffect` в примере) |
|
|
233
|
+
|
|
234
|
+
Скриншотами покрыты `Default`, `Types`, `Sizes`, `States` и `VisualTests`;
|
|
235
|
+
`Playground` и `WithNotificationIcon` из прогона исключены.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## История изменений
|
|
240
|
+
|
|
241
|
+
| Дата | Изменение |
|
|
242
|
+
|---|---|
|
|
243
|
+
| 2026-09-07 | Создан документ (AI-рефакторинг ChipSuggest, TRI-134) |
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Примеры использования
|
|
248
|
+
|
|
249
|
+
### Default
|
|
250
|
+
|
|
251
|
+
`stories/Chips/examples/ChipSuggest/DefaultExample.tsx`
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
import React, { useRef, useState } from "react";
|
|
255
|
+
import { ChipSuggest, EComponentSize, type ISuggestFieldOption } from "@sberbusiness/triplex-next";
|
|
256
|
+
import { mapFruitsToSuggestOptions, SUGGEST_STORY_FRUITS } from "./storyConstants";
|
|
257
|
+
|
|
258
|
+
export const DefaultExample = () => {
|
|
259
|
+
const initialOptions = mapFruitsToSuggestOptions([...SUGGEST_STORY_FRUITS]);
|
|
260
|
+
|
|
261
|
+
const [value, setValue] = useState<ISuggestFieldOption>();
|
|
262
|
+
const [options, setOptions] = useState<ISuggestFieldOption[]>([]);
|
|
263
|
+
const [tooltipOpen, setTooltipOpen] = useState(false);
|
|
264
|
+
const initialOptionsRef = useRef<ISuggestFieldOption[]>(initialOptions);
|
|
265
|
+
|
|
266
|
+
const handleDropdownOpen = () => {
|
|
267
|
+
setOptions(initialOptionsRef.current);
|
|
268
|
+
setTooltipOpen(false);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const handleFilter = (inputValue: string) => {
|
|
272
|
+
if (inputValue.length === 0) {
|
|
273
|
+
setOptions(initialOptionsRef.current);
|
|
274
|
+
setTooltipOpen(false);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const filteredOptions = initialOptionsRef.current.filter(({ label }) =>
|
|
279
|
+
String(label).toLowerCase().includes(inputValue.toLowerCase()),
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
setOptions(filteredOptions);
|
|
283
|
+
setTooltipOpen(filteredOptions.length === 0);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<ChipSuggest
|
|
288
|
+
value={value}
|
|
289
|
+
options={options}
|
|
290
|
+
tooltipOpen={tooltipOpen}
|
|
291
|
+
onSelect={setValue}
|
|
292
|
+
onFilter={handleFilter}
|
|
293
|
+
targetProps={{ clearSelected: () => setValue(undefined) }}
|
|
294
|
+
dropdownProps={{ onOpen: handleDropdownOpen }}
|
|
295
|
+
label="Suggest label"
|
|
296
|
+
placeholder="Type to proceed"
|
|
297
|
+
noOptionsText="No matches found."
|
|
298
|
+
size={EComponentSize.MD}
|
|
299
|
+
/>
|
|
300
|
+
);
|
|
301
|
+
};
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Types
|
|
305
|
+
|
|
306
|
+
`stories/Chips/examples/ChipSuggest/TypesExample.tsx`
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
import React, { useRef, useState } from "react";
|
|
310
|
+
import { ChipSuggest, EChipType, EComponentSize, type ISuggestFieldOption } from "@sberbusiness/triplex-next";
|
|
311
|
+
import { mapFruitsToSuggestOptions, SUGGEST_STORY_FRUITS } from "./storyConstants";
|
|
312
|
+
|
|
313
|
+
type TypeItemProps = {
|
|
314
|
+
type: EChipType;
|
|
315
|
+
label: string;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const initialOptions = mapFruitsToSuggestOptions([...SUGGEST_STORY_FRUITS]);
|
|
319
|
+
|
|
320
|
+
const TypeItem = ({ type, label }: TypeItemProps) => {
|
|
321
|
+
const [value, setValue] = useState<ISuggestFieldOption>();
|
|
322
|
+
const [options, setOptions] = useState<ISuggestFieldOption[]>([]);
|
|
323
|
+
const [tooltipOpen, setTooltipOpen] = useState(false);
|
|
324
|
+
const initialOptionsRef = useRef<ISuggestFieldOption[]>(initialOptions);
|
|
325
|
+
|
|
326
|
+
const handleDropdownOpen = () => {
|
|
327
|
+
setOptions(initialOptionsRef.current);
|
|
328
|
+
setTooltipOpen(false);
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const handleFilter = (inputValue: string) => {
|
|
332
|
+
if (inputValue.length === 0) {
|
|
333
|
+
setOptions(initialOptionsRef.current);
|
|
334
|
+
setTooltipOpen(false);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const filteredOptions = initialOptionsRef.current.filter(({ label }) =>
|
|
339
|
+
String(label).toLowerCase().includes(inputValue.toLowerCase()),
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
setOptions(filteredOptions);
|
|
343
|
+
setTooltipOpen(filteredOptions.length === 0);
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const handleClearSelected = () => setValue(undefined);
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<div>
|
|
350
|
+
<div style={{ marginBottom: 8, fontSize: 16, fontWeight: 700 }}>{label}</div>
|
|
351
|
+
<ChipSuggest
|
|
352
|
+
type={type}
|
|
353
|
+
size={EComponentSize.MD}
|
|
354
|
+
label="Suggest label"
|
|
355
|
+
placeholder="Type to proceed"
|
|
356
|
+
noOptionsText="No matches found."
|
|
357
|
+
value={value}
|
|
358
|
+
options={options}
|
|
359
|
+
tooltipOpen={tooltipOpen}
|
|
360
|
+
onSelect={setValue}
|
|
361
|
+
onFilter={handleFilter}
|
|
362
|
+
targetProps={{ clearSelected: handleClearSelected }}
|
|
363
|
+
dropdownProps={{ onOpen: handleDropdownOpen }}
|
|
364
|
+
/>
|
|
365
|
+
</div>
|
|
366
|
+
);
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const typeOptions = Object.values(EChipType).map((type) => ({
|
|
370
|
+
value: type,
|
|
371
|
+
label: type.toUpperCase(),
|
|
372
|
+
}));
|
|
373
|
+
|
|
374
|
+
export const TypesExample = () => (
|
|
375
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
376
|
+
{typeOptions.map(({ value, label }) => (
|
|
377
|
+
<TypeItem key={value} type={value} label={label} />
|
|
378
|
+
))}
|
|
379
|
+
</div>
|
|
380
|
+
);
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Sizes
|
|
384
|
+
|
|
385
|
+
`stories/Chips/examples/ChipSuggest/SizesExample.tsx`
|
|
386
|
+
|
|
387
|
+
```tsx
|
|
388
|
+
import React, { useRef, useState } from "react";
|
|
389
|
+
import { ChipSuggest, EComponentSize, type ISuggestFieldOption } from "@sberbusiness/triplex-next";
|
|
390
|
+
import { mapFruitsToSuggestOptions, SUGGEST_STORY_FRUITS } from "./storyConstants";
|
|
391
|
+
|
|
392
|
+
type SizeItemProps = {
|
|
393
|
+
size: EComponentSize;
|
|
394
|
+
label: string;
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const initialOptions = mapFruitsToSuggestOptions([...SUGGEST_STORY_FRUITS]);
|
|
398
|
+
|
|
399
|
+
const SizeItem = ({ size, label }: SizeItemProps) => {
|
|
400
|
+
const [value, setValue] = useState<ISuggestFieldOption>();
|
|
401
|
+
const [options, setOptions] = useState<ISuggestFieldOption[]>([]);
|
|
402
|
+
const [tooltipOpen, setTooltipOpen] = useState(false);
|
|
403
|
+
const initialOptionsRef = useRef<ISuggestFieldOption[]>(initialOptions);
|
|
404
|
+
|
|
405
|
+
const handleDropdownOpen = () => {
|
|
406
|
+
setOptions(initialOptionsRef.current);
|
|
407
|
+
setTooltipOpen(false);
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const handleFilter = (inputValue: string) => {
|
|
411
|
+
if (inputValue.length === 0) {
|
|
412
|
+
setOptions(initialOptionsRef.current);
|
|
413
|
+
setTooltipOpen(false);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const filteredOptions = initialOptionsRef.current.filter(({ label }) =>
|
|
418
|
+
String(label).toLowerCase().includes(inputValue.toLowerCase()),
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
setOptions(filteredOptions);
|
|
422
|
+
setTooltipOpen(filteredOptions.length === 0);
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const handleClearSelected = () => setValue(undefined);
|
|
426
|
+
|
|
427
|
+
return (
|
|
428
|
+
<div>
|
|
429
|
+
<div style={{ marginBottom: 8, fontSize: 16, fontWeight: 700 }}>{label}</div>
|
|
430
|
+
<ChipSuggest
|
|
431
|
+
size={size}
|
|
432
|
+
label="Suggest label"
|
|
433
|
+
placeholder="Type to proceed"
|
|
434
|
+
noOptionsText="No matches found."
|
|
435
|
+
value={value}
|
|
436
|
+
options={options}
|
|
437
|
+
tooltipOpen={tooltipOpen}
|
|
438
|
+
onSelect={setValue}
|
|
439
|
+
onFilter={handleFilter}
|
|
440
|
+
targetProps={{ clearSelected: handleClearSelected }}
|
|
441
|
+
dropdownProps={{ onOpen: handleDropdownOpen }}
|
|
442
|
+
/>
|
|
443
|
+
</div>
|
|
444
|
+
);
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const sizeOptions = Object.values(EComponentSize).map((size) => ({
|
|
448
|
+
value: size,
|
|
449
|
+
label: size.toUpperCase(),
|
|
450
|
+
}));
|
|
451
|
+
|
|
452
|
+
export const SizesExample = () => (
|
|
453
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
454
|
+
{sizeOptions.map(({ value, label }) => (
|
|
455
|
+
<SizeItem key={value} size={value} label={label} />
|
|
456
|
+
))}
|
|
457
|
+
</div>
|
|
458
|
+
);
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### States
|
|
462
|
+
|
|
463
|
+
`stories/Chips/examples/ChipSuggest/StatesExample.tsx`
|
|
464
|
+
|
|
465
|
+
```tsx
|
|
466
|
+
import React, { useRef, useState } from "react";
|
|
467
|
+
import { ChipSuggest, EComponentSize, type ISuggestFieldOption } from "@sberbusiness/triplex-next";
|
|
468
|
+
import { mapFruitsToSuggestOptions, SUGGEST_STORY_FRUITS } from "./storyConstants";
|
|
469
|
+
|
|
470
|
+
const states = [
|
|
471
|
+
{ id: "selected", label: "SELECTED" },
|
|
472
|
+
{ id: "disabled", label: "DISABLED", targetProps: { disabled: true } },
|
|
473
|
+
];
|
|
474
|
+
|
|
475
|
+
export const StatesExample = () => {
|
|
476
|
+
const initialOptions = mapFruitsToSuggestOptions([...SUGGEST_STORY_FRUITS]);
|
|
477
|
+
|
|
478
|
+
const [valueSelected, setValueSelected] = useState<ISuggestFieldOption | undefined>(initialOptions[0]);
|
|
479
|
+
const [valueDisabled, setValueDisabled] = useState<ISuggestFieldOption>();
|
|
480
|
+
const [options, setOptions] = useState<ISuggestFieldOption[]>([]);
|
|
481
|
+
const [tooltipOpen, setTooltipOpen] = useState(false);
|
|
482
|
+
const initialOptionsRef = useRef<ISuggestFieldOption[]>(initialOptions);
|
|
483
|
+
|
|
484
|
+
const handleDropdownOpen = () => {
|
|
485
|
+
setOptions(initialOptionsRef.current);
|
|
486
|
+
setTooltipOpen(false);
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const handleFilter = (inputValue: string) => {
|
|
490
|
+
if (inputValue.length === 0) {
|
|
491
|
+
setOptions(initialOptionsRef.current);
|
|
492
|
+
setTooltipOpen(false);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const filteredOptions = initialOptionsRef.current.filter(({ label }) =>
|
|
497
|
+
String(label).toLowerCase().includes(inputValue.toLowerCase()),
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
setOptions(filteredOptions);
|
|
501
|
+
setTooltipOpen(filteredOptions.length === 0);
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
return (
|
|
505
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
506
|
+
{states.map((state) => (
|
|
507
|
+
<div key={state.id}>
|
|
508
|
+
<div style={{ marginBottom: 8, fontSize: 16, fontWeight: 700 }}>{state.label}</div>
|
|
509
|
+
<ChipSuggest
|
|
510
|
+
label="Suggest label"
|
|
511
|
+
size={EComponentSize.MD}
|
|
512
|
+
placeholder="Type to proceed"
|
|
513
|
+
noOptionsText="No matches found."
|
|
514
|
+
value={state.id === "selected" ? valueSelected : valueDisabled}
|
|
515
|
+
options={options}
|
|
516
|
+
tooltipOpen={tooltipOpen}
|
|
517
|
+
onSelect={state.id === "selected" ? setValueSelected : setValueDisabled}
|
|
518
|
+
onFilter={handleFilter}
|
|
519
|
+
targetProps={{
|
|
520
|
+
clearSelected: () =>
|
|
521
|
+
state.id === "selected" ? setValueSelected(undefined) : setValueDisabled(undefined),
|
|
522
|
+
...state.targetProps,
|
|
523
|
+
}}
|
|
524
|
+
dropdownProps={{ onOpen: handleDropdownOpen }}
|
|
525
|
+
/>
|
|
526
|
+
</div>
|
|
527
|
+
))}
|
|
528
|
+
</div>
|
|
529
|
+
);
|
|
530
|
+
};
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### WithNotificationIcon
|
|
534
|
+
|
|
535
|
+
`stories/Chips/examples/ChipSuggest/WithNotificationIconExample.tsx`
|
|
536
|
+
|
|
537
|
+
```tsx
|
|
538
|
+
import React, { useRef, useState } from "react";
|
|
539
|
+
import { ChipSuggest, EComponentSize, type ISuggestFieldOption } from "@sberbusiness/triplex-next";
|
|
540
|
+
import { mapFruitsToOptionsWithNotification, SUGGEST_STORY_FRUITS_SHORT } from "./storyConstants";
|
|
541
|
+
|
|
542
|
+
export const WithNotificationIconExample = () => {
|
|
543
|
+
const initialOptions = mapFruitsToOptionsWithNotification([...SUGGEST_STORY_FRUITS_SHORT], 2);
|
|
544
|
+
|
|
545
|
+
const [value, setValue] = useState<ISuggestFieldOption>();
|
|
546
|
+
const [options, setOptions] = useState<ISuggestFieldOption[]>([]);
|
|
547
|
+
const [tooltipOpen, setTooltipOpen] = useState(false);
|
|
548
|
+
const initialOptionsRef = useRef<ISuggestFieldOption[]>(initialOptions);
|
|
549
|
+
|
|
550
|
+
const handleDropdownOpen = () => {
|
|
551
|
+
setOptions(initialOptionsRef.current);
|
|
552
|
+
setTooltipOpen(false);
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
const handleFilter = (inputValue: string) => {
|
|
556
|
+
if (inputValue.length === 0) {
|
|
557
|
+
setOptions(initialOptionsRef.current);
|
|
558
|
+
setTooltipOpen(false);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const filteredOptions = initialOptionsRef.current.filter(({ label }) =>
|
|
563
|
+
String(label).toLowerCase().includes(inputValue.toLowerCase()),
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
setOptions(filteredOptions);
|
|
567
|
+
setTooltipOpen(filteredOptions.length === 0);
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
return (
|
|
571
|
+
<ChipSuggest
|
|
572
|
+
value={value}
|
|
573
|
+
options={options}
|
|
574
|
+
tooltipOpen={tooltipOpen}
|
|
575
|
+
onSelect={setValue}
|
|
576
|
+
onFilter={handleFilter}
|
|
577
|
+
targetProps={{ clearSelected: () => setValue(undefined) }}
|
|
578
|
+
dropdownProps={{ onOpen: handleDropdownOpen }}
|
|
579
|
+
label="Suggest label"
|
|
580
|
+
placeholder="Type to proceed"
|
|
581
|
+
noOptionsText="No matches found."
|
|
582
|
+
size={EComponentSize.MD}
|
|
583
|
+
/>
|
|
584
|
+
);
|
|
585
|
+
};
|
|
586
|
+
```
|
|
587
|
+
|