@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.
@@ -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
+