@sberbusiness/triplex-next-mcp-server 1.44.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/README.md +7 -6
- package/bundle/components/AlertContext.md +11 -8
- package/bundle/components/AlertProcess.md +13 -10
- package/bundle/components/Badge.md +6 -3
- package/bundle/components/Button.md +46 -43
- package/bundle/components/ButtonDropdownExtended.md +3 -2
- package/bundle/components/ButtonIcon.md +7 -4
- package/bundle/components/Calendar.md +62 -59
- package/bundle/components/Caption.md +26 -23
- package/bundle/components/CardAction.md +615 -0
- package/bundle/components/Checkbox.md +32 -29
- package/bundle/components/CheckboxTreeExtended.md +9 -5
- package/bundle/components/Chip.md +51 -47
- 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/CodeText.md +26 -23
- package/bundle/components/Col.md +1 -2
- package/bundle/components/CollapsibleTree.md +8 -5
- package/bundle/components/DateField.md +222 -303
- package/bundle/components/DatePickerExtended.md +3 -1
- package/bundle/components/Dropdown.md +33 -28
- package/bundle/components/EmptyView.md +12 -5
- package/bundle/components/FooterPage.md +6 -2
- package/bundle/components/FormField.md +47 -42
- package/bundle/components/FormGroup.md +192 -52
- package/bundle/components/Header.md +491 -211
- package/bundle/components/HeaderPage.md +6 -2
- package/bundle/components/HelpBox.md +7 -1
- package/bundle/components/ImageGallery.md +33 -34
- package/bundle/components/ImageGalleryExtended.md +33 -34
- package/bundle/components/Island.md +16 -13
- package/bundle/components/IslandAccordion.md +443 -152
- package/bundle/components/IslandWidget.md +622 -157
- package/bundle/components/LightBox.md +14 -12
- package/bundle/components/Link.md +211 -96
- package/bundle/components/ListActionItem.md +7 -4
- package/bundle/components/ListItem.md +11 -8
- package/bundle/components/ListItemControlsButton.md +16 -13
- package/bundle/components/ListItemControlsButtonDropdown.md +9 -6
- package/bundle/components/ListItemSelectable.md +7 -4
- package/bundle/components/ListMasterFooter.md +8 -5
- package/bundle/components/ListMasterHeader.md +8 -5
- package/bundle/components/ListSortableItem.md +11 -8
- package/bundle/components/ListTableItem.md +7 -4
- package/bundle/components/LoaderMiddle.md +7 -4
- package/bundle/components/LoaderScreen.md +11 -8
- package/bundle/components/LoaderSmall.md +7 -4
- package/bundle/components/Marker.md +184 -69
- package/bundle/components/MarkerStatus.md +206 -88
- package/bundle/components/MaskedField.md +4 -5
- package/bundle/components/MasterTable.md +602 -0
- package/bundle/components/ModalWindow.md +7 -4
- package/bundle/components/MonthYearField.md +3 -1
- package/bundle/components/MultiselectField.md +2183 -132
- package/bundle/components/Notification.md +12 -9
- package/bundle/components/Overlay.md +9 -4
- package/bundle/components/Page.md +2 -2
- package/bundle/components/Pagination.md +15 -8
- package/bundle/components/Row.md +1 -1
- package/bundle/components/SelectExtendedField.md +3 -3
- package/bundle/components/SelectField.md +466 -148
- package/bundle/components/SliderExtended.md +39 -36
- package/bundle/components/SmallInput.md +194 -51
- package/bundle/components/Suggest.md +535 -99
- package/bundle/components/SwipeableArea.md +428 -79
- package/bundle/components/TableBasic.md +19 -11
- package/bundle/components/TabsExtended.md +24 -21
- package/bundle/components/Tag.md +6 -2
- package/bundle/components/Text.md +28 -25
- package/bundle/components/TextField.md +3 -4
- package/bundle/components/ThemeProvider.md +50 -1
- package/bundle/components/Title.md +26 -23
- package/bundle/components/Tooltip.md +24 -21
- package/bundle/components/TopOverlay.md +7 -4
- package/bundle/guides/codestyle.md +3 -0
- package/bundle/guides/context.md +84 -12
- package/bundle/guides/guidelines.md +321 -0
- package/bundle/guides/template.md +22 -12
- package/bundle/manifest.json +75 -17
- package/bundle/release-notes/1.45.0.mdx +117 -0
- package/bundle/release-notes/1.46.0.mdx +66 -0
- package/bundle/release-notes/1.47.0.mdx +7 -0
- package/bundle/tokens.json +6362 -0
- package/dist/index.js +177 -28
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1251 @@
|
|
|
1
|
+
---
|
|
2
|
+
component: ChipMultiselect
|
|
3
|
+
category: Chips
|
|
4
|
+
related: [Chip, ChipGroup, ChipSelect, MultiselectField, SelectExtendedField]
|
|
5
|
+
tokens: []
|
|
6
|
+
stories: stories/Chips/ChipMultiselect.stories.tsx
|
|
7
|
+
version: "1.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# ChipMultiselect
|
|
11
|
+
|
|
12
|
+
## Назначение
|
|
13
|
+
|
|
14
|
+
Фильтр-чипс с множественным выбором: поле выбора выглядит как `Chip`, а по нажатию
|
|
15
|
+
раскрывается произвольный выпадающий блок — список чекбоксов, дерево категорий,
|
|
16
|
+
поиск с кнопками подтверждения.
|
|
17
|
+
|
|
18
|
+
Собственной разметки у компонента нет. Это связка двух готовых компонентов:
|
|
19
|
+
`MultiselectField` владеет состоянием открытости и выпадающим блоком, а в его
|
|
20
|
+
`renderTarget` подставляется `Chip` со счётчиком выбранного, стрелкой раскрытия
|
|
21
|
+
и кнопкой сброса.
|
|
22
|
+
|
|
23
|
+
Компонент **не владеет выбором**. Он не знает, что выбрано и сколько: `selected`,
|
|
24
|
+
текст в `label` / `displayedValue` и реакция на `clearSelected` — целиком на
|
|
25
|
+
потребителе. Компонент только отражает переданное состояние и сообщает о намерении
|
|
26
|
+
сбросить его.
|
|
27
|
+
|
|
28
|
+
Используй когда: над таблицей или списком нужен компактный фильтр с множественным
|
|
29
|
+
выбором, а выбранное сворачивается в счётчик («Выбрано 3») вместо тегов.
|
|
30
|
+
|
|
31
|
+
Не используй когда:
|
|
32
|
+
|
|
33
|
+
- нужно полноразмерное поле формы с тегами выбранного — используй `MultiselectField` напрямую;
|
|
34
|
+
- нужен выбор одного значения в виде чипса — используй `ChipSelect`;
|
|
35
|
+
- нужен чипс-переключатель без выпадающего блока — используй `Chip`;
|
|
36
|
+
- нужен выпадающий блок без поля выбора — используй `Dropdown`.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Варианты и props
|
|
41
|
+
|
|
42
|
+
`IChipMultiselectProps` собирается из трёх источников:
|
|
43
|
+
|
|
44
|
+
1. `Omit<IMultiselectFieldProps, "renderTarget">` — всё поведение выпадающего
|
|
45
|
+
блока: обязательный render-prop `children`, `size`, `onOpen` / `onClose`,
|
|
46
|
+
`closeOnTab` и атрибуты корневого `<div>`. `renderTarget` исключён:
|
|
47
|
+
поле выбора компонент рисует сам и переопределить его нельзя.
|
|
48
|
+
2. `Pick<IChipProps, "type">` — визуальный тип чипса (`EChipType`).
|
|
49
|
+
3. Собственные props — ниже.
|
|
50
|
+
|
|
51
|
+
### Обязательные props
|
|
52
|
+
|
|
53
|
+
| Prop | Тип | Описание |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `label` | `React.ReactNode` | Название фильтра. Показывается, пока ничего не выбрано, и как запасной вариант при `selected` без `displayedValue` |
|
|
56
|
+
| `clearSelected` | `() => void` | Вызывается по нажатию на кнопку сброса. Компонент сам ничего не сбрасывает — снять выбор должен потребитель |
|
|
57
|
+
| `children` | render-prop | Выпадающий блок. Приходит из `MultiselectField`: `({ opened, setOpened, targetRef, dropdownRef }) => ReactNode` |
|
|
58
|
+
|
|
59
|
+
### Опциональные props
|
|
60
|
+
|
|
61
|
+
| Prop | Тип | По умолчанию | Описание |
|
|
62
|
+
|---|---|---|---|
|
|
63
|
+
| `selected` | `boolean` | `undefined` | Признак «выбран хотя бы один вариант». Переключает и внешний вид чипса, и содержимое postfix: стрелка → кнопка сброса |
|
|
64
|
+
| `displayedValue` | `React.ReactNode` | `undefined` | Текст вместо `label` при `selected`. Обычно счётчик выбранного |
|
|
65
|
+
| `disabled` | `boolean` | `undefined` | Уходит только на target-`Chip`: приглушает его и убирает из таба. Открытие списка по клику **не** блокирует — см. «Ограничения» |
|
|
66
|
+
| `size` | `EComponentSize` | `EComponentSize.MD` | Раздаётся одновременно target-чипсу (и его иконкам) и частям выпадающего блока через `MultiselectFieldContext` |
|
|
67
|
+
| `type` | `EChipType` | `EChipType.TYPE_1` | Визуальный тип target-чипса |
|
|
68
|
+
|
|
69
|
+
### Как считается содержимое чипса
|
|
70
|
+
|
|
71
|
+
Одна строка, о которой стоит помнить при правках:
|
|
72
|
+
`selected ? (displayedValue ?? label) : label`.
|
|
73
|
+
|
|
74
|
+
- ничего не выбрано → всегда `label`, `displayedValue` игнорируется;
|
|
75
|
+
- выбрано и `displayedValue` задан → `displayedValue`;
|
|
76
|
+
- выбрано, а `displayedValue` не задан (или `null`) → снова `label`.
|
|
77
|
+
|
|
78
|
+
Postfix переключается тем же флагом `selected`: `ChipDropdownArrow` (со стрелкой,
|
|
79
|
+
повёрнутой при `opened`) → `ChipClearButton`.
|
|
80
|
+
|
|
81
|
+
### Ограничения
|
|
82
|
+
|
|
83
|
+
Известные особенности текущей реализации — учитывай при использовании, но не
|
|
84
|
+
«чини» их попутно: это заметные изменения поведения, они решаются отдельными
|
|
85
|
+
задачами.
|
|
86
|
+
|
|
87
|
+
- **`disabled` приглушает чипс, но ничего не запрещает.** Уходит он только на
|
|
88
|
+
target-`Chip`, и тот при `disabled` лишь добавляет класс и ставит
|
|
89
|
+
`tabIndex={-1}` — обработчик `onClick` остаётся на элементе. Отсюда два
|
|
90
|
+
следствия: клик мышью по отключённому чипсу раскроет выпадающий блок
|
|
91
|
+
(с клавиатуры — нет, элемент не в табе), а кнопка сброса при
|
|
92
|
+
`disabled` + `selected` вообще не получает `disabled`: она остаётся в табе
|
|
93
|
+
и по клику по-прежнему вызывает `clearSelected`. То же верно для `ChipSelect`.
|
|
94
|
+
Если продукту нужен настоящий запрет — не рендери компонент или перехвати
|
|
95
|
+
события выше.
|
|
96
|
+
- **У кнопки сброса нет доступного имени.** `ChipClearButton` — `ButtonIcon`
|
|
97
|
+
с `aria-hidden` иконкой и без `aria-label`, а прокинуть его снаружи нельзя:
|
|
98
|
+
в `IChipMultiselectProps` нет пути к props кнопки сброса. Скринридер прочитает
|
|
99
|
+
её как безымянную кнопку.
|
|
100
|
+
- **Нет связки `aria-controls`** между полем выбора и выпадающим блоком,
|
|
101
|
+
а роль поля — `listbox` вместо `combobox` (см. «Accessibility»).
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Дизайн-токены
|
|
106
|
+
|
|
107
|
+
Собственных токенов нет. Единственный класс, который компонент добавляет сам —
|
|
108
|
+
`chipGroupItem` из `Chip.module.less`, и в нём только `display: inline-block`
|
|
109
|
+
(чтобы чипс корректно вставал в ряд `ChipGroup`).
|
|
110
|
+
|
|
111
|
+
Внешний вид приходит целиком из вложенных компонентов: цвета и состояния — из
|
|
112
|
+
`Chip` (группа токенов `Chip.*`, см. `Chip-ai.md`), фокус кнопки сброса — из
|
|
113
|
+
`Chip.Shadow_Focus`, разделитель выпадающего блока — из `MultiselectField.Divider_Color`.
|
|
114
|
+
Переопределять их нужно там, а не здесь.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Инварианты
|
|
119
|
+
|
|
120
|
+
- `forwardRef` на компоненте — не убирать.
|
|
121
|
+
- **Ref уходит на target-`Chip`, а не на корневой элемент `MultiselectField`.**
|
|
122
|
+
Это осознанный контракт (потребителю нужен именно чипс — для позиционирования
|
|
123
|
+
и измерений), он зафиксирован unit-тестом и используется в stories.
|
|
124
|
+
Известное расхождение: тип объявлен как `HTMLDivElement`, а фактический
|
|
125
|
+
элемент — `<span>` (`HTMLSpanElement`), потому что `Chip` рендерит `<span>`.
|
|
126
|
+
Исправление типа — breaking change для потребителей с `useRef<HTMLDivElement>`,
|
|
127
|
+
поэтому тип оставлен как есть; не «чини» его попутно.
|
|
128
|
+
- `renderTarget` исключён из публичных props через `Omit` — поле выбора всегда
|
|
129
|
+
рисует сам компонент. Не открывать его наружу без обсуждения.
|
|
130
|
+
- `role="listbox"` на target-`Chip` перебивает `role="button"`, который `Chip`
|
|
131
|
+
ставит по умолчанию: в `Chip` `...restProps` разложены после `role`.
|
|
132
|
+
Порядок спреда в `Chip.tsx` менять нельзя, иначе роль здесь молча вернётся к `button`.
|
|
133
|
+
- Публичные имена `ChipMultiselect`, `IChipMultiselectProps` экспортируются из
|
|
134
|
+
`src/components/Chip/index.ts` — сохранять.
|
|
135
|
+
- Класс `chipGroupItem` на корневом элементе — по нему `ChipGroup` раскладывает
|
|
136
|
+
дочерние элементы; не убирать и не переименовывать.
|
|
137
|
+
- `clearSelected` обязателен и вызывается без аргументов — сигнатуру не расширять
|
|
138
|
+
без обсуждения.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Accessibility
|
|
143
|
+
|
|
144
|
+
- Поле выбора — `<span role="listbox">` с `tabIndex=0` (`-1` при `disabled`) и
|
|
145
|
+
`aria-expanded`, отражающим состояние выпадающего блока.
|
|
146
|
+
- Клавиатура на поле выбора: `Enter` и `Space` переключают открытость (открывают
|
|
147
|
+
и закрывают). Прокрутку страницы по `Space` гасит сам `Chip`. `Escape` и клик
|
|
148
|
+
вне поля закрывают блок — это делает `SelectExtendedField` уровнем ниже.
|
|
149
|
+
- Кнопка сброса живёт внутри поля выбора, поэтому её события специально
|
|
150
|
+
**не** доходят до чипса: `click`, `Enter` и `Space` останавливают всплытие.
|
|
151
|
+
Иначе сброс выбора попутно открывал бы список. Другие клавиши всплывают штатно.
|
|
152
|
+
- Кнопка сброса — `ButtonIcon` с `aria-hidden` иконкой внутри. Доступного имени
|
|
153
|
+
у неё сейчас нет, и задать его снаружи нечем: props кнопки сброса не
|
|
154
|
+
прокидываются через `IChipMultiselectProps`. Скринридер прочитает её как
|
|
155
|
+
безымянную кнопку — известный пробел, см. «Ограничения».
|
|
156
|
+
- `role="listbox"` описывает не раскрывающий элемент, а список: эталонная роль
|
|
157
|
+
для такого триггера — `combobox` (как в `ChipSort`). Связки `aria-controls`
|
|
158
|
+
между полем выбора и выпадающим блоком тоже нет. Менять это здесь — заметное
|
|
159
|
+
изменение поведения, поэтому решается отдельной задачей.
|
|
160
|
+
- Перехват фокуса внутри открытого блока и возврат фокуса на поле после закрытия
|
|
161
|
+
с клавиатуры обеспечивает `MultiselectField` — см. `MultiselectField-ai.md`.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Связанные компоненты
|
|
166
|
+
|
|
167
|
+
- `Chip` — база поля выбора. Компонент рендерит его в `renderTarget` и наследует от
|
|
168
|
+
него prop `type`; все визуальные состояния и токены живут там.
|
|
169
|
+
- `ChipGroup` — контейнер для ряда чипсов; корневой класс `chipGroupItem`
|
|
170
|
+
проставляется именно ради него.
|
|
171
|
+
- `ChipSelect` — тот же чипс-фильтр, но для выбора одного значения.
|
|
172
|
+
- `MultiselectField` — родитель по типам и по рендеру: даёт состояние открытости,
|
|
173
|
+
выпадающий блок (`MultiselectField.Dropdown`), focus trap и раздачу размера.
|
|
174
|
+
- `SelectExtendedField` — уровнем ниже `MultiselectField`; из него приходит тип
|
|
175
|
+
`ISelectExtendedFieldTargetProvideProps` (`opened` / `setOpened`) и закрытие по
|
|
176
|
+
`Escape` и клику вне поля.
|
|
177
|
+
- `ChipClearButton` и `ChipDropdownArrow` — части postfix внутри той же папки.
|
|
178
|
+
Отдельных AI.md у них нет: это тривиальные обёртки над `ButtonIcon` и `<span>`
|
|
179
|
+
с иконкой, выбирающие иконку по `size`.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Stories
|
|
184
|
+
|
|
185
|
+
Основные истории: `stories/Chips/ChipMultiselect.stories.tsx`
|
|
186
|
+
Файлы примеров: `stories/Chips/examples/ChipMultiselect/`
|
|
187
|
+
|
|
188
|
+
| Story | Example file | Что демонстрирует |
|
|
189
|
+
|---|---|---|
|
|
190
|
+
| `Playground` | `PlaygroundExample.tsx` | Интерактивный контроль `type`, `size`, `label`, `displayedValue`, `disabled` |
|
|
191
|
+
| `Default` | `DefaultExample.tsx` | Базовое использование: чекбоксы в выпадающем блоке, фильтр по вводу, сброс через `clearSelected`, мобильный вид |
|
|
192
|
+
| `Types` | `TypesExample.tsx` | `TYPE_1` и `TYPE_2` |
|
|
193
|
+
| `Sizes` | `SizesExample.tsx` | Размеры SM / MD / LG — размер раздаётся и чипсу, и выпадающему блоку |
|
|
194
|
+
| `Loading` | `LoadingExample.tsx` | Загрузка содержимого выпадающего блока (`MultiselectField.Dropdown.Content` с `loading`) |
|
|
195
|
+
| `VisualTests` | `VisualTestsExample.tsx` | Скриншот-регрессия: все размеры с раскрытыми списками — пример сам открывает их в `useLayoutEffect`, `play`-функции у story нет |
|
|
196
|
+
| `Example: with CheckboxTree` | `WithCheckboxTreeExample.tsx` | Дерево категорий вместо плоского списка чекбоксов |
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## История изменений
|
|
201
|
+
|
|
202
|
+
| Дата | Изменение |
|
|
203
|
+
|---|---|
|
|
204
|
+
| 2026-09-07 | Создан документ (TRI-135). AI-рефакторинг: JSDoc на props и компоненте, мемоизация обработчиков кнопки сброса, порядок импортов; поведение и публичный API не менялись |
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Примеры использования
|
|
209
|
+
|
|
210
|
+
### Default
|
|
211
|
+
|
|
212
|
+
`stories/Chips/examples/ChipMultiselect/DefaultExample.tsx`
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
import React, { useMemo, useState } from "react";
|
|
216
|
+
import {
|
|
217
|
+
MultiselectField,
|
|
218
|
+
DropdownMobileHeader,
|
|
219
|
+
DropdownMobileInput,
|
|
220
|
+
DropdownMobileClose,
|
|
221
|
+
DropdownMobileBody,
|
|
222
|
+
DropdownMobileFooter,
|
|
223
|
+
EDropdownAlignment,
|
|
224
|
+
Button,
|
|
225
|
+
EButtonTheme,
|
|
226
|
+
EComponentSize,
|
|
227
|
+
FormField,
|
|
228
|
+
FormFieldLabel,
|
|
229
|
+
FormFieldInput,
|
|
230
|
+
FormFieldPostfix,
|
|
231
|
+
FormFieldClear,
|
|
232
|
+
Checkbox,
|
|
233
|
+
CheckboxYGroup,
|
|
234
|
+
ChipMultiselect,
|
|
235
|
+
ETextSize,
|
|
236
|
+
Text,
|
|
237
|
+
} from "@sberbusiness/triplex-next";
|
|
238
|
+
import { CHIP_MULTISELECT_OPTIONS } from "./storyConstants";
|
|
239
|
+
|
|
240
|
+
export const DefaultExample = () => {
|
|
241
|
+
const chipSize = EComponentSize.MD;
|
|
242
|
+
|
|
243
|
+
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
|
244
|
+
const [filter, setFilter] = useState("");
|
|
245
|
+
const selectedSet = useMemo(() => new Set(selectedIds), [selectedIds]);
|
|
246
|
+
const visibleOptions = useMemo(() => {
|
|
247
|
+
const lower = filter.trim().toLowerCase();
|
|
248
|
+
if (!lower.length) {
|
|
249
|
+
return CHIP_MULTISELECT_OPTIONS;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return CHIP_MULTISELECT_OPTIONS.filter((opt) => opt.label.toLowerCase().includes(lower));
|
|
253
|
+
}, [filter]);
|
|
254
|
+
|
|
255
|
+
const handleToggle = (optionId: string, checked: boolean) => {
|
|
256
|
+
setSelectedIds((prev) => {
|
|
257
|
+
if (checked) {
|
|
258
|
+
if (prev.includes(optionId)) {
|
|
259
|
+
return prev;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return [...prev, optionId];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return prev.filter((id) => id !== optionId);
|
|
266
|
+
});
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const renderCheckboxList = () => (
|
|
270
|
+
<CheckboxYGroup aria-label="Options">
|
|
271
|
+
{visibleOptions.map((opt) => (
|
|
272
|
+
<Checkbox
|
|
273
|
+
key={opt.id}
|
|
274
|
+
id={opt.id}
|
|
275
|
+
size={chipSize}
|
|
276
|
+
checked={selectedSet.has(opt.id)}
|
|
277
|
+
onChange={(event) => handleToggle(opt.id, event.target.checked)}
|
|
278
|
+
>
|
|
279
|
+
{opt.label}
|
|
280
|
+
</Checkbox>
|
|
281
|
+
))}
|
|
282
|
+
</CheckboxYGroup>
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const renderDropdownContent = () => {
|
|
286
|
+
const renderCheckboxes = !filter.trim() || visibleOptions.length > 0;
|
|
287
|
+
return renderCheckboxes ? (
|
|
288
|
+
renderCheckboxList()
|
|
289
|
+
) : (
|
|
290
|
+
<div className={`not-found ${chipSize}`}>
|
|
291
|
+
<Text size={ETextSize.B3}>Nothing was found</Text>
|
|
292
|
+
</div>
|
|
293
|
+
);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const unselectAll = () => {
|
|
297
|
+
setSelectedIds([]);
|
|
298
|
+
setFilter("");
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const handleClickClearFilter = () => {
|
|
302
|
+
unselectAll();
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const handleClearInput = () => {
|
|
306
|
+
setFilter("");
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
310
|
+
setFilter(event.target.value);
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<ChipMultiselect
|
|
315
|
+
size={chipSize}
|
|
316
|
+
clearSelected={unselectAll}
|
|
317
|
+
selected={selectedIds.length > 0}
|
|
318
|
+
label="Multiselect label"
|
|
319
|
+
displayedValue="Multiselect value"
|
|
320
|
+
>
|
|
321
|
+
{({ opened, setOpened, targetRef, dropdownRef }) => (
|
|
322
|
+
<MultiselectField.Dropdown
|
|
323
|
+
opened={opened}
|
|
324
|
+
setOpened={setOpened}
|
|
325
|
+
targetRef={targetRef}
|
|
326
|
+
ref={dropdownRef}
|
|
327
|
+
alignment={EDropdownAlignment.LEFT}
|
|
328
|
+
focusTrapProps={{ focusTrapOptions: { initialFocus: false } }}
|
|
329
|
+
mobileViewProps={{
|
|
330
|
+
children: (
|
|
331
|
+
<>
|
|
332
|
+
<DropdownMobileHeader
|
|
333
|
+
controlButtons={<DropdownMobileClose onClick={() => setOpened(false)} />}
|
|
334
|
+
>
|
|
335
|
+
<DropdownMobileInput
|
|
336
|
+
autoFocus={true}
|
|
337
|
+
placeholder="Type to proceed"
|
|
338
|
+
value={filter}
|
|
339
|
+
onChange={handleFilterChange}
|
|
340
|
+
/>
|
|
341
|
+
</DropdownMobileHeader>
|
|
342
|
+
<DropdownMobileBody>{renderDropdownContent()}</DropdownMobileBody>
|
|
343
|
+
<DropdownMobileFooter>
|
|
344
|
+
<Button
|
|
345
|
+
theme={EButtonTheme.GENERAL}
|
|
346
|
+
size={EComponentSize.SM}
|
|
347
|
+
onClick={() => setOpened(false)}
|
|
348
|
+
>
|
|
349
|
+
Button text
|
|
350
|
+
</Button>
|
|
351
|
+
<Button
|
|
352
|
+
theme={EButtonTheme.LINK}
|
|
353
|
+
size={EComponentSize.SM}
|
|
354
|
+
onClick={handleClickClearFilter}
|
|
355
|
+
>
|
|
356
|
+
Button link text
|
|
357
|
+
</Button>
|
|
358
|
+
</DropdownMobileFooter>
|
|
359
|
+
</>
|
|
360
|
+
),
|
|
361
|
+
}}
|
|
362
|
+
>
|
|
363
|
+
<MultiselectField.Dropdown.Header>
|
|
364
|
+
<FormField size={EComponentSize.SM}>
|
|
365
|
+
<FormFieldLabel>Type to proceed</FormFieldLabel>
|
|
366
|
+
<FormFieldInput value={filter} onChange={handleFilterChange} />
|
|
367
|
+
<FormFieldPostfix>
|
|
368
|
+
<FormFieldClear onClick={handleClearInput} />
|
|
369
|
+
</FormFieldPostfix>
|
|
370
|
+
</FormField>
|
|
371
|
+
</MultiselectField.Dropdown.Header>
|
|
372
|
+
<MultiselectField.Dropdown.Content>{renderDropdownContent()}</MultiselectField.Dropdown.Content>
|
|
373
|
+
<MultiselectField.Dropdown.Footer>
|
|
374
|
+
<Button
|
|
375
|
+
theme={EButtonTheme.SECONDARY}
|
|
376
|
+
size={EComponentSize.SM}
|
|
377
|
+
onClick={() => setOpened(false)}
|
|
378
|
+
>
|
|
379
|
+
Button text
|
|
380
|
+
</Button>
|
|
381
|
+
<Button theme={EButtonTheme.LINK} size={EComponentSize.SM} onClick={handleClickClearFilter}>
|
|
382
|
+
Button link text
|
|
383
|
+
</Button>
|
|
384
|
+
</MultiselectField.Dropdown.Footer>
|
|
385
|
+
</MultiselectField.Dropdown>
|
|
386
|
+
)}
|
|
387
|
+
</ChipMultiselect>
|
|
388
|
+
);
|
|
389
|
+
};
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Types
|
|
393
|
+
|
|
394
|
+
`stories/Chips/examples/ChipMultiselect/TypesExample.tsx`
|
|
395
|
+
|
|
396
|
+
```tsx
|
|
397
|
+
import React, { useMemo, useState } from "react";
|
|
398
|
+
import clsx from "clsx";
|
|
399
|
+
import {
|
|
400
|
+
MultiselectField,
|
|
401
|
+
EComponentSize,
|
|
402
|
+
DropdownMobileHeader,
|
|
403
|
+
DropdownMobileInput,
|
|
404
|
+
DropdownMobileClose,
|
|
405
|
+
DropdownMobileBody,
|
|
406
|
+
DropdownMobileFooter,
|
|
407
|
+
EDropdownAlignment,
|
|
408
|
+
Button,
|
|
409
|
+
EButtonTheme,
|
|
410
|
+
FormField,
|
|
411
|
+
FormFieldLabel,
|
|
412
|
+
FormFieldInput,
|
|
413
|
+
FormFieldPostfix,
|
|
414
|
+
FormFieldClear,
|
|
415
|
+
Checkbox,
|
|
416
|
+
CheckboxYGroup,
|
|
417
|
+
ChipMultiselect,
|
|
418
|
+
EChipType,
|
|
419
|
+
ETextSize,
|
|
420
|
+
Text,
|
|
421
|
+
} from "@sberbusiness/triplex-next";
|
|
422
|
+
import { CHIP_MULTISELECT_OPTIONS } from "./storyConstants";
|
|
423
|
+
|
|
424
|
+
type TypeItemProps = {
|
|
425
|
+
type: EChipType;
|
|
426
|
+
label: string;
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const TypeItem = ({ type, label }: TypeItemProps) => {
|
|
430
|
+
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
|
431
|
+
const [filter, setFilter] = useState("");
|
|
432
|
+
const selectedSet = useMemo(() => new Set(selectedIds), [selectedIds]);
|
|
433
|
+
const visibleOptions = useMemo(() => {
|
|
434
|
+
const lower = filter.trim().toLowerCase();
|
|
435
|
+
if (!lower.length) {
|
|
436
|
+
return CHIP_MULTISELECT_OPTIONS;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return CHIP_MULTISELECT_OPTIONS.filter((opt) => opt.label.toLowerCase().includes(lower));
|
|
440
|
+
}, [filter]);
|
|
441
|
+
|
|
442
|
+
const handleToggle = (optionId: string, checked: boolean) => {
|
|
443
|
+
setSelectedIds((prev) => {
|
|
444
|
+
if (checked) {
|
|
445
|
+
if (prev.includes(optionId)) {
|
|
446
|
+
return prev;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return [...prev, optionId];
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return prev.filter((id) => id !== optionId);
|
|
453
|
+
});
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const renderCheckboxList = () => (
|
|
457
|
+
<CheckboxYGroup aria-label="Options">
|
|
458
|
+
{visibleOptions.map((opt) => (
|
|
459
|
+
<Checkbox
|
|
460
|
+
key={opt.id}
|
|
461
|
+
id={opt.id}
|
|
462
|
+
checked={selectedSet.has(opt.id)}
|
|
463
|
+
onChange={(event) => handleToggle(opt.id, event.target.checked)}
|
|
464
|
+
>
|
|
465
|
+
{opt.label}
|
|
466
|
+
</Checkbox>
|
|
467
|
+
))}
|
|
468
|
+
</CheckboxYGroup>
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
const renderDropdownContent = () => {
|
|
472
|
+
const renderCheckboxes = !filter.trim() || visibleOptions.length > 0;
|
|
473
|
+
return renderCheckboxes ? (
|
|
474
|
+
renderCheckboxList()
|
|
475
|
+
) : (
|
|
476
|
+
<div className={clsx("not-found", "md")}>
|
|
477
|
+
<Text size={ETextSize.B3}>Nothing was found</Text>
|
|
478
|
+
</div>
|
|
479
|
+
);
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const unselectAll = () => {
|
|
483
|
+
setSelectedIds([]);
|
|
484
|
+
setFilter("");
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
const handleClickClearFilter = () => {
|
|
488
|
+
unselectAll();
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const handleClearInput = () => {
|
|
492
|
+
setFilter("");
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
496
|
+
setFilter(event.target.value);
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
return (
|
|
500
|
+
<div>
|
|
501
|
+
<div style={{ marginBottom: 8, fontSize: 16, fontWeight: 700 }}>{label}</div>
|
|
502
|
+
<ChipMultiselect
|
|
503
|
+
type={type}
|
|
504
|
+
clearSelected={unselectAll}
|
|
505
|
+
selected={selectedIds.length > 0}
|
|
506
|
+
label="Multiselect label"
|
|
507
|
+
displayedValue="Multiselect value"
|
|
508
|
+
>
|
|
509
|
+
{({ opened, setOpened, targetRef, dropdownRef }) => (
|
|
510
|
+
<MultiselectField.Dropdown
|
|
511
|
+
opened={opened}
|
|
512
|
+
setOpened={setOpened}
|
|
513
|
+
targetRef={targetRef}
|
|
514
|
+
ref={dropdownRef}
|
|
515
|
+
alignment={EDropdownAlignment.LEFT}
|
|
516
|
+
focusTrapProps={{ focusTrapOptions: { initialFocus: false } }}
|
|
517
|
+
mobileViewProps={{
|
|
518
|
+
children: (
|
|
519
|
+
<>
|
|
520
|
+
<DropdownMobileHeader
|
|
521
|
+
controlButtons={<DropdownMobileClose onClick={() => setOpened(false)} />}
|
|
522
|
+
>
|
|
523
|
+
<DropdownMobileInput
|
|
524
|
+
autoFocus={true}
|
|
525
|
+
placeholder="Type to proceed"
|
|
526
|
+
value={filter}
|
|
527
|
+
onChange={handleFilterChange}
|
|
528
|
+
/>
|
|
529
|
+
</DropdownMobileHeader>
|
|
530
|
+
<DropdownMobileBody>{renderDropdownContent()}</DropdownMobileBody>
|
|
531
|
+
<DropdownMobileFooter>
|
|
532
|
+
<Button
|
|
533
|
+
theme={EButtonTheme.GENERAL}
|
|
534
|
+
size={EComponentSize.SM}
|
|
535
|
+
onClick={() => setOpened(false)}
|
|
536
|
+
>
|
|
537
|
+
Button text
|
|
538
|
+
</Button>
|
|
539
|
+
<Button
|
|
540
|
+
theme={EButtonTheme.LINK}
|
|
541
|
+
size={EComponentSize.SM}
|
|
542
|
+
onClick={handleClickClearFilter}
|
|
543
|
+
>
|
|
544
|
+
Button link text
|
|
545
|
+
</Button>
|
|
546
|
+
</DropdownMobileFooter>
|
|
547
|
+
</>
|
|
548
|
+
),
|
|
549
|
+
}}
|
|
550
|
+
>
|
|
551
|
+
<MultiselectField.Dropdown.Header>
|
|
552
|
+
<FormField size={EComponentSize.SM}>
|
|
553
|
+
<FormFieldLabel>Type to proceed</FormFieldLabel>
|
|
554
|
+
<FormFieldInput value={filter} onChange={handleFilterChange} />
|
|
555
|
+
<FormFieldPostfix>
|
|
556
|
+
<FormFieldClear onClick={handleClearInput} />
|
|
557
|
+
</FormFieldPostfix>
|
|
558
|
+
</FormField>
|
|
559
|
+
</MultiselectField.Dropdown.Header>
|
|
560
|
+
<MultiselectField.Dropdown.Content>{renderDropdownContent()}</MultiselectField.Dropdown.Content>
|
|
561
|
+
<MultiselectField.Dropdown.Footer>
|
|
562
|
+
<Button
|
|
563
|
+
theme={EButtonTheme.SECONDARY}
|
|
564
|
+
size={EComponentSize.SM}
|
|
565
|
+
onClick={() => setOpened(false)}
|
|
566
|
+
>
|
|
567
|
+
Button text
|
|
568
|
+
</Button>
|
|
569
|
+
<Button theme={EButtonTheme.LINK} size={EComponentSize.SM} onClick={handleClickClearFilter}>
|
|
570
|
+
Button link text
|
|
571
|
+
</Button>
|
|
572
|
+
</MultiselectField.Dropdown.Footer>
|
|
573
|
+
</MultiselectField.Dropdown>
|
|
574
|
+
)}
|
|
575
|
+
</ChipMultiselect>
|
|
576
|
+
</div>
|
|
577
|
+
);
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const typeOptions = Object.values(EChipType).map((type) => ({
|
|
581
|
+
value: type,
|
|
582
|
+
label: type.toUpperCase(),
|
|
583
|
+
}));
|
|
584
|
+
|
|
585
|
+
export const TypesExample = () => (
|
|
586
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
587
|
+
{typeOptions.map(({ value, label }) => (
|
|
588
|
+
<TypeItem key={value} type={value} label={label} />
|
|
589
|
+
))}
|
|
590
|
+
</div>
|
|
591
|
+
);
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
### Sizes
|
|
595
|
+
|
|
596
|
+
`stories/Chips/examples/ChipMultiselect/SizesExample.tsx`
|
|
597
|
+
|
|
598
|
+
```tsx
|
|
599
|
+
import React, { useMemo, useState } from "react";
|
|
600
|
+
import clsx from "clsx";
|
|
601
|
+
import {
|
|
602
|
+
EComponentSize,
|
|
603
|
+
MultiselectField,
|
|
604
|
+
DropdownMobileHeader,
|
|
605
|
+
DropdownMobileInput,
|
|
606
|
+
DropdownMobileClose,
|
|
607
|
+
DropdownMobileBody,
|
|
608
|
+
DropdownMobileFooter,
|
|
609
|
+
EDropdownAlignment,
|
|
610
|
+
Button,
|
|
611
|
+
EButtonTheme,
|
|
612
|
+
FormField,
|
|
613
|
+
FormFieldLabel,
|
|
614
|
+
FormFieldInput,
|
|
615
|
+
FormFieldPostfix,
|
|
616
|
+
FormFieldClear,
|
|
617
|
+
Checkbox,
|
|
618
|
+
CheckboxYGroup,
|
|
619
|
+
ChipMultiselect,
|
|
620
|
+
ETextSize,
|
|
621
|
+
Text,
|
|
622
|
+
} from "@sberbusiness/triplex-next";
|
|
623
|
+
import { CHIP_MULTISELECT_OPTIONS } from "./storyConstants";
|
|
624
|
+
|
|
625
|
+
type SizeItemProps = {
|
|
626
|
+
size: EComponentSize;
|
|
627
|
+
label: string;
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
const SizeItem = ({ size, label }: SizeItemProps) => {
|
|
631
|
+
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
|
632
|
+
const [filter, setFilter] = useState("");
|
|
633
|
+
const selectedSet = useMemo(() => new Set(selectedIds), [selectedIds]);
|
|
634
|
+
const visibleOptions = useMemo(() => {
|
|
635
|
+
const lower = filter.trim().toLowerCase();
|
|
636
|
+
if (!lower.length) {
|
|
637
|
+
return CHIP_MULTISELECT_OPTIONS;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return CHIP_MULTISELECT_OPTIONS.filter((opt) => opt.label.toLowerCase().includes(lower));
|
|
641
|
+
}, [filter]);
|
|
642
|
+
|
|
643
|
+
const handleToggle = (optionId: string, checked: boolean) => {
|
|
644
|
+
setSelectedIds((prev) => {
|
|
645
|
+
if (checked) {
|
|
646
|
+
if (prev.includes(optionId)) {
|
|
647
|
+
return prev;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return [...prev, optionId];
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return prev.filter((id) => id !== optionId);
|
|
654
|
+
});
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
const renderCheckboxList = () => (
|
|
658
|
+
<CheckboxYGroup aria-label="Options">
|
|
659
|
+
{visibleOptions.map((opt) => (
|
|
660
|
+
<Checkbox
|
|
661
|
+
key={opt.id}
|
|
662
|
+
id={opt.id}
|
|
663
|
+
size={size}
|
|
664
|
+
checked={selectedSet.has(opt.id)}
|
|
665
|
+
onChange={(event) => handleToggle(opt.id, event.target.checked)}
|
|
666
|
+
>
|
|
667
|
+
{opt.label}
|
|
668
|
+
</Checkbox>
|
|
669
|
+
))}
|
|
670
|
+
</CheckboxYGroup>
|
|
671
|
+
);
|
|
672
|
+
|
|
673
|
+
const renderDropdownContent = () => {
|
|
674
|
+
const renderCheckboxes = !filter.trim() || visibleOptions.length > 0;
|
|
675
|
+
return renderCheckboxes ? (
|
|
676
|
+
renderCheckboxList()
|
|
677
|
+
) : (
|
|
678
|
+
<div className={clsx("not-found", size)}>
|
|
679
|
+
<Text size={ETextSize.B3}>Nothing was found</Text>
|
|
680
|
+
</div>
|
|
681
|
+
);
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
const unselectAll = () => {
|
|
685
|
+
setSelectedIds([]);
|
|
686
|
+
setFilter("");
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
const handleClickClearFilter = () => {
|
|
690
|
+
unselectAll();
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
const handleClearInput = () => {
|
|
694
|
+
setFilter("");
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
698
|
+
setFilter(event.target.value);
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
return (
|
|
702
|
+
<div>
|
|
703
|
+
<div style={{ marginBottom: 8, fontSize: 16, fontWeight: 700 }}>{label}</div>
|
|
704
|
+
<ChipMultiselect
|
|
705
|
+
size={size}
|
|
706
|
+
clearSelected={unselectAll}
|
|
707
|
+
selected={selectedIds.length > 0}
|
|
708
|
+
label="Multiselect label"
|
|
709
|
+
displayedValue="Multiselect value"
|
|
710
|
+
>
|
|
711
|
+
{({ opened, setOpened, targetRef, dropdownRef }) => (
|
|
712
|
+
<MultiselectField.Dropdown
|
|
713
|
+
opened={opened}
|
|
714
|
+
setOpened={setOpened}
|
|
715
|
+
targetRef={targetRef}
|
|
716
|
+
ref={dropdownRef}
|
|
717
|
+
alignment={EDropdownAlignment.LEFT}
|
|
718
|
+
focusTrapProps={{ focusTrapOptions: { initialFocus: false } }}
|
|
719
|
+
mobileViewProps={{
|
|
720
|
+
children: (
|
|
721
|
+
<>
|
|
722
|
+
<DropdownMobileHeader
|
|
723
|
+
controlButtons={<DropdownMobileClose onClick={() => setOpened(false)} />}
|
|
724
|
+
>
|
|
725
|
+
<DropdownMobileInput
|
|
726
|
+
autoFocus={true}
|
|
727
|
+
placeholder="Type to proceed"
|
|
728
|
+
value={filter}
|
|
729
|
+
onChange={handleFilterChange}
|
|
730
|
+
/>
|
|
731
|
+
</DropdownMobileHeader>
|
|
732
|
+
<DropdownMobileBody>{renderDropdownContent()}</DropdownMobileBody>
|
|
733
|
+
<DropdownMobileFooter>
|
|
734
|
+
<Button
|
|
735
|
+
theme={EButtonTheme.GENERAL}
|
|
736
|
+
size={EComponentSize.SM}
|
|
737
|
+
onClick={() => setOpened(false)}
|
|
738
|
+
>
|
|
739
|
+
Button text
|
|
740
|
+
</Button>
|
|
741
|
+
<Button
|
|
742
|
+
theme={EButtonTheme.LINK}
|
|
743
|
+
size={EComponentSize.SM}
|
|
744
|
+
onClick={handleClickClearFilter}
|
|
745
|
+
>
|
|
746
|
+
Button link text
|
|
747
|
+
</Button>
|
|
748
|
+
</DropdownMobileFooter>
|
|
749
|
+
</>
|
|
750
|
+
),
|
|
751
|
+
}}
|
|
752
|
+
>
|
|
753
|
+
<MultiselectField.Dropdown.Header>
|
|
754
|
+
<FormField size={size === EComponentSize.LG ? EComponentSize.MD : EComponentSize.SM}>
|
|
755
|
+
<FormFieldLabel>Type to proceed</FormFieldLabel>
|
|
756
|
+
<FormFieldInput value={filter} onChange={handleFilterChange} />
|
|
757
|
+
<FormFieldPostfix>
|
|
758
|
+
<FormFieldClear onClick={handleClearInput} />
|
|
759
|
+
</FormFieldPostfix>
|
|
760
|
+
</FormField>
|
|
761
|
+
</MultiselectField.Dropdown.Header>
|
|
762
|
+
<MultiselectField.Dropdown.Content>{renderDropdownContent()}</MultiselectField.Dropdown.Content>
|
|
763
|
+
<MultiselectField.Dropdown.Footer>
|
|
764
|
+
<Button
|
|
765
|
+
theme={EButtonTheme.SECONDARY}
|
|
766
|
+
size={size === EComponentSize.LG ? EComponentSize.MD : EComponentSize.SM}
|
|
767
|
+
onClick={() => setOpened(false)}
|
|
768
|
+
>
|
|
769
|
+
Button text
|
|
770
|
+
</Button>
|
|
771
|
+
<Button
|
|
772
|
+
theme={EButtonTheme.LINK}
|
|
773
|
+
size={size === EComponentSize.LG ? EComponentSize.MD : EComponentSize.SM}
|
|
774
|
+
onClick={handleClickClearFilter}
|
|
775
|
+
>
|
|
776
|
+
Button link text
|
|
777
|
+
</Button>
|
|
778
|
+
</MultiselectField.Dropdown.Footer>
|
|
779
|
+
</MultiselectField.Dropdown>
|
|
780
|
+
)}
|
|
781
|
+
</ChipMultiselect>
|
|
782
|
+
</div>
|
|
783
|
+
);
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
const sizeOptions = Object.values(EComponentSize).map((size) => ({
|
|
787
|
+
value: size,
|
|
788
|
+
label: size.toUpperCase(),
|
|
789
|
+
}));
|
|
790
|
+
|
|
791
|
+
export const SizesExample = () => (
|
|
792
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
793
|
+
{sizeOptions.map(({ value, label }) => (
|
|
794
|
+
<SizeItem key={value} size={value} label={label} />
|
|
795
|
+
))}
|
|
796
|
+
</div>
|
|
797
|
+
);
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
### Loading
|
|
801
|
+
|
|
802
|
+
`stories/Chips/examples/ChipMultiselect/LoadingExample.tsx`
|
|
803
|
+
|
|
804
|
+
```tsx
|
|
805
|
+
import React, { useMemo, useState } from "react";
|
|
806
|
+
import {
|
|
807
|
+
EComponentSize,
|
|
808
|
+
MultiselectField,
|
|
809
|
+
FormField,
|
|
810
|
+
FormFieldLabel,
|
|
811
|
+
FormFieldInput,
|
|
812
|
+
FormFieldPostfix,
|
|
813
|
+
FormFieldClear,
|
|
814
|
+
Checkbox,
|
|
815
|
+
CheckboxYGroup,
|
|
816
|
+
ChipMultiselect,
|
|
817
|
+
ETextSize,
|
|
818
|
+
Text,
|
|
819
|
+
DropdownMobileHeader,
|
|
820
|
+
DropdownMobileInput,
|
|
821
|
+
DropdownMobileClose,
|
|
822
|
+
DropdownMobileBody,
|
|
823
|
+
DropdownMobileFooter,
|
|
824
|
+
EDropdownAlignment,
|
|
825
|
+
Button,
|
|
826
|
+
EButtonTheme,
|
|
827
|
+
DropdownMobileLoader,
|
|
828
|
+
} from "@sberbusiness/triplex-next";
|
|
829
|
+
import { CHIP_MULTISELECT_OPTIONS } from "./storyConstants";
|
|
830
|
+
|
|
831
|
+
export const LoadingExample = () => {
|
|
832
|
+
const chipSize = EComponentSize.MD;
|
|
833
|
+
|
|
834
|
+
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
|
835
|
+
const [filter, setFilter] = useState("");
|
|
836
|
+
const selectedSet = useMemo(() => new Set(selectedIds), [selectedIds]);
|
|
837
|
+
const visibleOptions = useMemo(() => {
|
|
838
|
+
const lower = filter.trim().toLowerCase();
|
|
839
|
+
if (!lower.length) {
|
|
840
|
+
return CHIP_MULTISELECT_OPTIONS;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
return CHIP_MULTISELECT_OPTIONS.filter((opt) => opt.label.toLowerCase().includes(lower));
|
|
844
|
+
}, [filter]);
|
|
845
|
+
|
|
846
|
+
const handleToggle = (optionId: string, checked: boolean) => {
|
|
847
|
+
setSelectedIds((prev) => {
|
|
848
|
+
if (checked) {
|
|
849
|
+
if (prev.includes(optionId)) {
|
|
850
|
+
return prev;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
return [...prev, optionId];
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
return prev.filter((id) => id !== optionId);
|
|
857
|
+
});
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
const renderCheckboxList = () => (
|
|
861
|
+
<CheckboxYGroup aria-label="Options">
|
|
862
|
+
{visibleOptions.map((opt) => (
|
|
863
|
+
<Checkbox
|
|
864
|
+
key={opt.id}
|
|
865
|
+
id={opt.id}
|
|
866
|
+
size={chipSize}
|
|
867
|
+
checked={selectedSet.has(opt.id)}
|
|
868
|
+
onChange={(event) => handleToggle(opt.id, event.target.checked)}
|
|
869
|
+
>
|
|
870
|
+
{opt.label}
|
|
871
|
+
</Checkbox>
|
|
872
|
+
))}
|
|
873
|
+
</CheckboxYGroup>
|
|
874
|
+
);
|
|
875
|
+
|
|
876
|
+
const renderDropdownContent = () => {
|
|
877
|
+
const renderCheckboxes = !filter.trim() || visibleOptions.length > 0;
|
|
878
|
+
return renderCheckboxes ? (
|
|
879
|
+
renderCheckboxList()
|
|
880
|
+
) : (
|
|
881
|
+
<div className={`not-found ${chipSize}`}>
|
|
882
|
+
<Text size={ETextSize.B3}>Nothing was found</Text>
|
|
883
|
+
</div>
|
|
884
|
+
);
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
const unselectAll = () => {
|
|
888
|
+
setSelectedIds([]);
|
|
889
|
+
setFilter("");
|
|
890
|
+
};
|
|
891
|
+
|
|
892
|
+
const handleClickClearFilter = () => {
|
|
893
|
+
unselectAll();
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
const handleClearInput = () => {
|
|
897
|
+
setFilter("");
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
901
|
+
setFilter(event.target.value);
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
return (
|
|
905
|
+
<ChipMultiselect
|
|
906
|
+
size={chipSize}
|
|
907
|
+
clearSelected={unselectAll}
|
|
908
|
+
selected={selectedIds.length > 0}
|
|
909
|
+
label="Multiselect label"
|
|
910
|
+
>
|
|
911
|
+
{({ opened, setOpened, targetRef, dropdownRef }) => (
|
|
912
|
+
<MultiselectField.Dropdown
|
|
913
|
+
opened={opened}
|
|
914
|
+
setOpened={setOpened}
|
|
915
|
+
targetRef={targetRef}
|
|
916
|
+
ref={dropdownRef}
|
|
917
|
+
alignment={EDropdownAlignment.LEFT}
|
|
918
|
+
focusTrapProps={{ focusTrapOptions: { initialFocus: false } }}
|
|
919
|
+
mobileViewProps={{
|
|
920
|
+
children: (
|
|
921
|
+
<>
|
|
922
|
+
<DropdownMobileHeader
|
|
923
|
+
controlButtons={
|
|
924
|
+
<>
|
|
925
|
+
<DropdownMobileLoader />
|
|
926
|
+
<DropdownMobileClose onClick={() => setOpened(false)} />
|
|
927
|
+
</>
|
|
928
|
+
}
|
|
929
|
+
>
|
|
930
|
+
<DropdownMobileInput
|
|
931
|
+
autoFocus={true}
|
|
932
|
+
placeholder="Type to proceed"
|
|
933
|
+
value={filter}
|
|
934
|
+
onChange={handleFilterChange}
|
|
935
|
+
/>
|
|
936
|
+
</DropdownMobileHeader>
|
|
937
|
+
<DropdownMobileBody>{renderDropdownContent()}</DropdownMobileBody>
|
|
938
|
+
<DropdownMobileFooter>
|
|
939
|
+
<Button
|
|
940
|
+
theme={EButtonTheme.GENERAL}
|
|
941
|
+
size={EComponentSize.SM}
|
|
942
|
+
onClick={() => setOpened(false)}
|
|
943
|
+
>
|
|
944
|
+
Button text
|
|
945
|
+
</Button>
|
|
946
|
+
<Button
|
|
947
|
+
theme={EButtonTheme.LINK}
|
|
948
|
+
size={EComponentSize.SM}
|
|
949
|
+
onClick={handleClickClearFilter}
|
|
950
|
+
>
|
|
951
|
+
Button link text
|
|
952
|
+
</Button>
|
|
953
|
+
</DropdownMobileFooter>
|
|
954
|
+
</>
|
|
955
|
+
),
|
|
956
|
+
}}
|
|
957
|
+
>
|
|
958
|
+
<MultiselectField.Dropdown.Header>
|
|
959
|
+
<FormField size={EComponentSize.SM}>
|
|
960
|
+
<FormFieldLabel>Type to proceed</FormFieldLabel>
|
|
961
|
+
<FormFieldInput value={filter} onChange={handleFilterChange} />
|
|
962
|
+
<FormFieldPostfix>
|
|
963
|
+
<FormFieldClear onClick={handleClearInput} />
|
|
964
|
+
</FormFieldPostfix>
|
|
965
|
+
</FormField>
|
|
966
|
+
</MultiselectField.Dropdown.Header>
|
|
967
|
+
<MultiselectField.Dropdown.Content loading>
|
|
968
|
+
{renderDropdownContent()}
|
|
969
|
+
</MultiselectField.Dropdown.Content>
|
|
970
|
+
<MultiselectField.Dropdown.Footer>
|
|
971
|
+
<Button
|
|
972
|
+
theme={EButtonTheme.SECONDARY}
|
|
973
|
+
size={EComponentSize.SM}
|
|
974
|
+
onClick={() => setOpened(false)}
|
|
975
|
+
>
|
|
976
|
+
Button text
|
|
977
|
+
</Button>
|
|
978
|
+
<Button theme={EButtonTheme.LINK} size={EComponentSize.SM} onClick={handleClickClearFilter}>
|
|
979
|
+
Button link text
|
|
980
|
+
</Button>
|
|
981
|
+
</MultiselectField.Dropdown.Footer>
|
|
982
|
+
</MultiselectField.Dropdown>
|
|
983
|
+
)}
|
|
984
|
+
</ChipMultiselect>
|
|
985
|
+
);
|
|
986
|
+
};
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
### Example: with CheckboxTree
|
|
990
|
+
|
|
991
|
+
`stories/Chips/examples/ChipMultiselect/WithCheckboxTreeExample.tsx`
|
|
992
|
+
|
|
993
|
+
```tsx
|
|
994
|
+
import React, { useState } from "react";
|
|
995
|
+
import {
|
|
996
|
+
MultiselectField,
|
|
997
|
+
DropdownMobileHeader,
|
|
998
|
+
DropdownMobileInput,
|
|
999
|
+
DropdownMobileClose,
|
|
1000
|
+
DropdownMobileBody,
|
|
1001
|
+
DropdownMobileFooter,
|
|
1002
|
+
Button,
|
|
1003
|
+
EButtonTheme,
|
|
1004
|
+
EComponentSize,
|
|
1005
|
+
FormField,
|
|
1006
|
+
FormFieldLabel,
|
|
1007
|
+
FormFieldInput,
|
|
1008
|
+
FormFieldPostfix,
|
|
1009
|
+
FormFieldClear,
|
|
1010
|
+
ChipMultiselect,
|
|
1011
|
+
ETextSize,
|
|
1012
|
+
Text,
|
|
1013
|
+
CheckboxTreeExtended,
|
|
1014
|
+
checkChildrenCheckboxes,
|
|
1015
|
+
traverseCheckboxes,
|
|
1016
|
+
checkParentCheckboxes,
|
|
1017
|
+
ICheckboxTreeCheckboxData,
|
|
1018
|
+
} from "@sberbusiness/triplex-next";
|
|
1019
|
+
|
|
1020
|
+
export const WithCheckboxTreeExample = () => {
|
|
1021
|
+
const checkboxesInitial = [
|
|
1022
|
+
{
|
|
1023
|
+
bulk: false,
|
|
1024
|
+
checked: false,
|
|
1025
|
+
children: [
|
|
1026
|
+
{
|
|
1027
|
+
bulk: false,
|
|
1028
|
+
checked: false,
|
|
1029
|
+
children: [
|
|
1030
|
+
{
|
|
1031
|
+
checked: false,
|
|
1032
|
+
id: "multiselect-option-1-1",
|
|
1033
|
+
label: "Значение 1-1",
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
checked: false,
|
|
1037
|
+
id: "multiselect-option-1-2",
|
|
1038
|
+
label: "Значение 1-2",
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
checked: false,
|
|
1042
|
+
id: "multiselect-option-1-3",
|
|
1043
|
+
label: "Значение 1-3",
|
|
1044
|
+
},
|
|
1045
|
+
],
|
|
1046
|
+
id: "multiselect-option-1",
|
|
1047
|
+
label: "Группа 1",
|
|
1048
|
+
},
|
|
1049
|
+
{
|
|
1050
|
+
bulk: false,
|
|
1051
|
+
checked: false,
|
|
1052
|
+
children: [
|
|
1053
|
+
{
|
|
1054
|
+
checked: false,
|
|
1055
|
+
id: "multiselect-option-2-1",
|
|
1056
|
+
label: "Значение 2-1",
|
|
1057
|
+
},
|
|
1058
|
+
{
|
|
1059
|
+
checked: false,
|
|
1060
|
+
id: "multiselect-option-2-2",
|
|
1061
|
+
label: "Значение 2-2",
|
|
1062
|
+
},
|
|
1063
|
+
],
|
|
1064
|
+
id: "multiselect-option-2",
|
|
1065
|
+
label: "Группа 2",
|
|
1066
|
+
},
|
|
1067
|
+
{
|
|
1068
|
+
checked: false,
|
|
1069
|
+
id: "multiselect-option-3",
|
|
1070
|
+
label: "Значение 3",
|
|
1071
|
+
},
|
|
1072
|
+
],
|
|
1073
|
+
id: "multiselect-option-0",
|
|
1074
|
+
label: "Все",
|
|
1075
|
+
},
|
|
1076
|
+
];
|
|
1077
|
+
|
|
1078
|
+
const [checkboxes, setCheckboxes] = useState(() => structuredClone(checkboxesInitial));
|
|
1079
|
+
const [filter, setFilter] = useState("");
|
|
1080
|
+
const [filteredCheckboxesId, setFilteredCheckboxesId] = useState<string[]>([]);
|
|
1081
|
+
|
|
1082
|
+
const handleChange = (checkbox: ICheckboxTreeCheckboxData) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
1083
|
+
checkbox.checked = checkbox.bulk ? true : event.target.checked;
|
|
1084
|
+
checkChildrenCheckboxes(checkbox);
|
|
1085
|
+
traverseCheckboxes(checkboxes, checkParentCheckboxes);
|
|
1086
|
+
setCheckboxes([...checkboxes]);
|
|
1087
|
+
};
|
|
1088
|
+
|
|
1089
|
+
const renderCheckboxNode = (checkbox: ICheckboxTreeCheckboxData) => {
|
|
1090
|
+
if (filter && !filteredCheckboxesId.includes(checkbox.id)) return null;
|
|
1091
|
+
return (
|
|
1092
|
+
<CheckboxTreeExtended.Node
|
|
1093
|
+
key={checkbox.id}
|
|
1094
|
+
id={checkbox.id}
|
|
1095
|
+
checkbox={(props) => (
|
|
1096
|
+
<CheckboxTreeExtended.Checkbox
|
|
1097
|
+
{...props}
|
|
1098
|
+
bulk={checkbox.bulk}
|
|
1099
|
+
checked={checkbox.checked}
|
|
1100
|
+
onChange={handleChange(checkbox)}
|
|
1101
|
+
>
|
|
1102
|
+
{checkbox.label}
|
|
1103
|
+
</CheckboxTreeExtended.Checkbox>
|
|
1104
|
+
)}
|
|
1105
|
+
>
|
|
1106
|
+
{checkbox.children && checkbox.children.map((child) => renderCheckboxNode(child))}
|
|
1107
|
+
</CheckboxTreeExtended.Node>
|
|
1108
|
+
);
|
|
1109
|
+
};
|
|
1110
|
+
|
|
1111
|
+
const renderDropdownContent = () => {
|
|
1112
|
+
const renderCheckboxes = !filter || (filteredCheckboxesId.length && filter);
|
|
1113
|
+
return renderCheckboxes ? (
|
|
1114
|
+
<CheckboxTreeExtended>{checkboxes.map((checkbox) => renderCheckboxNode(checkbox))}</CheckboxTreeExtended>
|
|
1115
|
+
) : (
|
|
1116
|
+
<div className="not-found md">
|
|
1117
|
+
<Text size={ETextSize.B3}>Nothing was found</Text>
|
|
1118
|
+
</div>
|
|
1119
|
+
);
|
|
1120
|
+
};
|
|
1121
|
+
|
|
1122
|
+
const unselectAll = () => {
|
|
1123
|
+
const nextCheckboxes = [...checkboxes];
|
|
1124
|
+
traverseCheckboxes(nextCheckboxes, (checkbox) => {
|
|
1125
|
+
checkbox.checked = false;
|
|
1126
|
+
checkbox.bulk = false;
|
|
1127
|
+
});
|
|
1128
|
+
setCheckboxes(nextCheckboxes);
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
const handleClickClearFilter = () => {
|
|
1132
|
+
setFilter("");
|
|
1133
|
+
setFilteredCheckboxesId([]);
|
|
1134
|
+
unselectAll();
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
const handleClearInput = () => {
|
|
1138
|
+
setFilter("");
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1141
|
+
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
1142
|
+
const { value } = event.target;
|
|
1143
|
+
const filteredCheckboxes = [...checkboxes];
|
|
1144
|
+
const filteredCheckboxesId = new Set<string>();
|
|
1145
|
+
|
|
1146
|
+
const addMatchedSubtree = (node: ICheckboxTreeCheckboxData) => {
|
|
1147
|
+
filteredCheckboxesId.add(node.id);
|
|
1148
|
+
node.children?.forEach(addMatchedSubtree);
|
|
1149
|
+
};
|
|
1150
|
+
|
|
1151
|
+
const setFilteredValue = (checkbox: ICheckboxTreeCheckboxData) => {
|
|
1152
|
+
if (
|
|
1153
|
+
checkbox.label &&
|
|
1154
|
+
typeof checkbox.label === "string" &&
|
|
1155
|
+
checkbox.label.toLowerCase().includes(value.toLowerCase())
|
|
1156
|
+
) {
|
|
1157
|
+
addMatchedSubtree(checkbox);
|
|
1158
|
+
} else if (checkbox.children) {
|
|
1159
|
+
const intersection = checkbox.children
|
|
1160
|
+
.map((item: ICheckboxTreeCheckboxData) => item.id)
|
|
1161
|
+
.filter((id) => Array.from(filteredCheckboxesId).includes(id));
|
|
1162
|
+
|
|
1163
|
+
if (intersection.length) filteredCheckboxesId.add(checkbox.id);
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
|
|
1167
|
+
traverseCheckboxes(filteredCheckboxes, setFilteredValue);
|
|
1168
|
+
|
|
1169
|
+
setFilter(value);
|
|
1170
|
+
setFilteredCheckboxesId(Array.from(filteredCheckboxesId));
|
|
1171
|
+
};
|
|
1172
|
+
|
|
1173
|
+
const renderDropdown = ({
|
|
1174
|
+
opened,
|
|
1175
|
+
setOpened,
|
|
1176
|
+
targetRef,
|
|
1177
|
+
dropdownRef,
|
|
1178
|
+
}: {
|
|
1179
|
+
opened: boolean;
|
|
1180
|
+
setOpened: (opened: boolean) => void;
|
|
1181
|
+
targetRef: React.RefObject<HTMLDivElement>;
|
|
1182
|
+
dropdownRef: React.RefObject<HTMLDivElement>;
|
|
1183
|
+
}) => (
|
|
1184
|
+
<MultiselectField.Dropdown
|
|
1185
|
+
opened={opened}
|
|
1186
|
+
setOpened={setOpened}
|
|
1187
|
+
targetRef={targetRef}
|
|
1188
|
+
ref={dropdownRef}
|
|
1189
|
+
focusTrapProps={{
|
|
1190
|
+
focusTrapOptions: { initialFocus: false },
|
|
1191
|
+
}}
|
|
1192
|
+
mobileViewProps={{
|
|
1193
|
+
children: (
|
|
1194
|
+
<>
|
|
1195
|
+
<DropdownMobileHeader controlButtons={<DropdownMobileClose onClick={() => setOpened(false)} />}>
|
|
1196
|
+
<DropdownMobileInput
|
|
1197
|
+
placeholder="Type to proceed"
|
|
1198
|
+
value={filter}
|
|
1199
|
+
onChange={handleFilterChange}
|
|
1200
|
+
/>
|
|
1201
|
+
</DropdownMobileHeader>
|
|
1202
|
+
<DropdownMobileBody>{renderDropdownContent()}</DropdownMobileBody>
|
|
1203
|
+
<DropdownMobileFooter>
|
|
1204
|
+
<Button
|
|
1205
|
+
theme={EButtonTheme.GENERAL}
|
|
1206
|
+
size={EComponentSize.SM}
|
|
1207
|
+
onClick={() => setOpened(false)}
|
|
1208
|
+
>
|
|
1209
|
+
Button text
|
|
1210
|
+
</Button>
|
|
1211
|
+
<Button theme={EButtonTheme.LINK} size={EComponentSize.SM} onClick={handleClickClearFilter}>
|
|
1212
|
+
Button link text
|
|
1213
|
+
</Button>
|
|
1214
|
+
</DropdownMobileFooter>
|
|
1215
|
+
</>
|
|
1216
|
+
),
|
|
1217
|
+
}}
|
|
1218
|
+
>
|
|
1219
|
+
<MultiselectField.Dropdown.Header>
|
|
1220
|
+
<FormField size={EComponentSize.SM}>
|
|
1221
|
+
<FormFieldLabel>Type to proceed</FormFieldLabel>
|
|
1222
|
+
<FormFieldInput value={filter} onChange={handleFilterChange} />
|
|
1223
|
+
<FormFieldPostfix>
|
|
1224
|
+
<FormFieldClear onClick={handleClearInput} />
|
|
1225
|
+
</FormFieldPostfix>
|
|
1226
|
+
</FormField>
|
|
1227
|
+
</MultiselectField.Dropdown.Header>
|
|
1228
|
+
<MultiselectField.Dropdown.Content>{renderDropdownContent()}</MultiselectField.Dropdown.Content>
|
|
1229
|
+
<MultiselectField.Dropdown.Footer>
|
|
1230
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.SM} onClick={() => setOpened(false)}>
|
|
1231
|
+
Button text
|
|
1232
|
+
</Button>
|
|
1233
|
+
<Button theme={EButtonTheme.LINK} size={EComponentSize.SM} onClick={handleClickClearFilter}>
|
|
1234
|
+
Button link text
|
|
1235
|
+
</Button>
|
|
1236
|
+
</MultiselectField.Dropdown.Footer>
|
|
1237
|
+
</MultiselectField.Dropdown>
|
|
1238
|
+
);
|
|
1239
|
+
|
|
1240
|
+
return (
|
|
1241
|
+
<ChipMultiselect
|
|
1242
|
+
clearSelected={unselectAll}
|
|
1243
|
+
selected={Boolean(checkboxes.filter((checkbox) => checkbox.checked).length)}
|
|
1244
|
+
label="Multiselect label"
|
|
1245
|
+
>
|
|
1246
|
+
{(dropdownProps) => renderDropdown(dropdownProps)}
|
|
1247
|
+
</ChipMultiselect>
|
|
1248
|
+
);
|
|
1249
|
+
};
|
|
1250
|
+
```
|
|
1251
|
+
|