@sberbusiness/triplex-next-mcp-server 1.53.0 → 1.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/bundle/components/ChipSelect.md +7 -5
- package/bundle/components/ChipSelectTarget.md +388 -0
- package/bundle/components/ChipSuggest.md +8 -15
- package/bundle/components/ChipSuggestTarget.md +506 -0
- package/bundle/components/Dropdown.md +2 -1
- package/bundle/components/MultiselectField.md +2 -57
- package/bundle/components/Portal.md +5 -2
- package/bundle/components/Puller.md +85 -90
- package/bundle/components/SelectExtendedField.md +6 -4
- package/bundle/components/SelectField.md +4 -2
- package/bundle/manifest.json +19 -2
- package/bundle/release-notes/1.53.0.mdx +1 -1
- package/bundle/release-notes/1.54.0.mdx +38 -0
- package/bundle/release-notes/1.55.0.mdx +7 -0
- package/bundle/tokens.json +1 -1
- package/dist/index.js +39 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
---
|
|
2
|
+
component: ChipSuggestTarget
|
|
3
|
+
category: Chips
|
|
4
|
+
related: [ChipSuggest, ChipSelectTarget, Chip, Suggest]
|
|
5
|
+
tokens: []
|
|
6
|
+
stories: stories/Chips/ChipSuggest.stories.tsx
|
|
7
|
+
version: "1.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# ChipSuggestTarget
|
|
11
|
+
|
|
12
|
+
## Назначение
|
|
13
|
+
|
|
14
|
+
Публичный target для `ChipSuggest`: рендерит `Chip` с подписью и стрелкой либо
|
|
15
|
+
очисткой, открывает и закрывает список через `SuggestContext`.
|
|
16
|
+
Выбранное значение и открытость берёт из ближайшего `Suggest`.
|
|
17
|
+
|
|
18
|
+
Используй когда: собираешь свою композицию `Suggest` с чипсом вместо поля.
|
|
19
|
+
Для готового поиска со списком используй `ChipSuggest`; для target с открытостью
|
|
20
|
+
через props вместо контекста — `ChipSelectTarget`.
|
|
21
|
+
|
|
22
|
+
Публичные импорты:
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { ChipSuggestTarget, type IChipSuggestTargetProps, type ISuggestOption } from "@sberbusiness/triplex-next";
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Компонент и интерфейс сохраняют generic-параметр `<T extends ISuggestOption>`.
|
|
29
|
+
Рендери target внутри `Suggest<T>`: вне provider он получает пустое значение,
|
|
30
|
+
закрытый список и setter-заглушку; самостоятельной работы со списком нет.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Варианты и props
|
|
35
|
+
|
|
36
|
+
`IChipSuggestTargetProps<T>` наследует `IChipProps` без `prefix` / `postfix`
|
|
37
|
+
и добавляет опциональный `clearSelected`. Обязательных custom-props нет.
|
|
38
|
+
|
|
39
|
+
| Prop | По умолчанию | Назначение и ограничения |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `children` | — | Подпись. Target не подставляет `value.label`: это делает `ChipSuggest` либо потребитель своей композиции |
|
|
42
|
+
| `clearSelected` | — | Вызывается без аргументов встроенной очисткой. Значение `Suggest.value` обновляет потребитель; без callback кнопка остаётся без действия |
|
|
43
|
+
| `onClick` | — | Получает React mouse event после `setDropdownOpen(!dropdownOpen)` |
|
|
44
|
+
| `onKeyDown` | — | Получает все дошедшие до target события клавиатуры, кроме Enter / Space на встроенной очистке; Enter / Space — после переключения |
|
|
45
|
+
| `size` | `EComponentSize.MD` | SM / MD / LG; передаётся в `Chip`, стрелку и очистку |
|
|
46
|
+
| `type` | `EChipType.TYPE_1` | Визуальный тип `Chip`; при использовании через `ChipSuggest` задаётся на родителе |
|
|
47
|
+
| `disabled` | `false` | Отключает target и встроенную очистку; ограничения — в Accessibility |
|
|
48
|
+
| `showNotificationIcon` | `false` | Значок `Badge.Dot` через `Chip` |
|
|
49
|
+
| `className`, `style`, `id`, `data-*`, `aria-*`, прочие span-атрибуты | — | Передаются в корневой `<span>`; `className` объединяется с классами `Chip` |
|
|
50
|
+
| `ref` | — | Указывает на `HTMLSpanElement` чипса, а не на корень `Suggest` или dropdown |
|
|
51
|
+
|
|
52
|
+
### Состояние из контекста
|
|
53
|
+
|
|
54
|
+
- При `value === undefined` отображается стрелка с `rotated={dropdownOpen}`.
|
|
55
|
+
Любое выбранное значение включает очистку и исходное `selected`-оформление.
|
|
56
|
+
- Клик, Enter и Space переключают открытость в обе стороны. В отличие от
|
|
57
|
+
`ChipSelectTarget`, уже открытый список по Enter / Space закрывается.
|
|
58
|
+
- Переключение target вызывает именно `setDropdownOpen`, а не `closeDropdown`:
|
|
59
|
+
дополнительный сброс строки фильтрации принадлежит другим частям `Suggest`.
|
|
60
|
+
|
|
61
|
+
Spread унаследованных props находится после внутренних `selected` и
|
|
62
|
+
`aria-expanded`: они могут быть переопределены потребителем. Выбор автоматического
|
|
63
|
+
postfix при этом зависит только от `value` в контексте.
|
|
64
|
+
`prefix` / `postfix` исключены из TypeScript API target.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Дизайн-токены
|
|
69
|
+
|
|
70
|
+
Собственных стилей и токенов у target нет, поэтому `tokens: []`.
|
|
71
|
+
Внешний вид наследуется от `Chip`: см. [Chip-ai.md](Chip-ai.md#дизайн-токены).
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Инварианты
|
|
76
|
+
|
|
77
|
+
- Сохраняются именованные экспорты `ChipSuggestTarget` и `IChipSuggestTargetProps`
|
|
78
|
+
через `Chip/ChipSuggest/index.ts` и `Chip/index.ts`, generic-сигнатура и тип ref.
|
|
79
|
+
- `forwardRef` обязателен. Ref-target и корневой элемент — `<span>` `Chip`.
|
|
80
|
+
`displayName` — `"ChipSuggestTarget"`.
|
|
81
|
+
- `ChipSuggestTargetWithRef` сохраняет React forwardRef-объект, а assertion
|
|
82
|
+
восстанавливает generic-вызов: не убирать параметр `T` из публичной сигнатуры.
|
|
83
|
+
- `value`, `dropdownOpen` и setter читаются из `useSuggestContext<T>`;
|
|
84
|
+
не добавлять параллельный локальный state.
|
|
85
|
+
- Сначала переключается открытость, затем вызывается пользовательский callback.
|
|
86
|
+
`preventDefault()` в нём не отменяет уже вызванный setter.
|
|
87
|
+
- Клик и Enter / Space встроенной очистки не всплывают в target;
|
|
88
|
+
`clearSelected` вызывается без DOM-события.
|
|
89
|
+
- Сохраняются порядок spread, внешний вид, размеры, типы, имена токенов и story IDs.
|
|
90
|
+
Код остаётся совместимым с React 17.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Accessibility
|
|
95
|
+
|
|
96
|
+
- По умолчанию корень — `<span role="button" tabIndex={0}>` с `aria-expanded`,
|
|
97
|
+
не combobox. Имя задают `children` или ARIA-атрибуты потребителя.
|
|
98
|
+
- Enter / Space переключают список; Space не прокручивает страницу благодаря
|
|
99
|
+
`preventDefault()` в `Chip`. Синтетический click на самом span не создаётся.
|
|
100
|
+
- Escape, клик вне компонента и состояние фильтра обрабатывает `Suggest`.
|
|
101
|
+
Сам target не переносит фокус: поле ввода, `FocusTrap` и возврат фокуса
|
|
102
|
+
принадлежат dropdown-композиции `ChipSuggest`.
|
|
103
|
+
- Встроенная очистка — нативная кнопка: Enter / Space активируют её нативный
|
|
104
|
+
click и вызывают `clearSelected`.
|
|
105
|
+
- У встроенной очистки нет доступного имени и отдельного prop для него
|
|
106
|
+
(TRI-139). ARIA-имя самого target на вложенную кнопку не передаётся.
|
|
107
|
+
- `disabled` означает CSS `pointer-events: none` и `tabIndex=-1` у `Chip`
|
|
108
|
+
и нативный `disabled` у встроенной очистки. В обработчиках target флаг
|
|
109
|
+
не проверяется: программно отправленные события не защищены.
|
|
110
|
+
Не передавай `tabIndex`: он перекрывает `-1`, target снова получит фокус,
|
|
111
|
+
и Enter / Space переключат список.
|
|
112
|
+
- `aria-controls`, `aria-haspopup` и `aria-pressed` автоматически не создаются.
|
|
113
|
+
Фокус оформляется через `Chip.Shadow_Focus`.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Связанные компоненты
|
|
118
|
+
|
|
119
|
+
- [ChipSuggest](ChipSuggest-ai.md) — готовый родитель, формирует подпись,
|
|
120
|
+
передаёт `targetProps` и рендерит target внутри `Suggest`.
|
|
121
|
+
- [ChipSelectTarget](ChipSelectTarget-ai.md) — альтернатива с `opened` /
|
|
122
|
+
`setOpened` через props и другими правилами Enter / Space / Tab.
|
|
123
|
+
- [Chip](Chip-ai.md) — непосредственно рендерится внутри; источник унаследованных
|
|
124
|
+
props, span-семантики и визуальных состояний.
|
|
125
|
+
- [Suggest](../Suggest/Suggest-ai.md) — источник значения, открытости и setter
|
|
126
|
+
через контекст; его provider обязателен для рабочей композиции.
|
|
127
|
+
- `ChipClearButton` и `ChipDropdownArrow` — автоматические варианты postfix;
|
|
128
|
+
собственного AI.md для этих обёрток нет.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Stories
|
|
133
|
+
|
|
134
|
+
Основные истории: `stories/Chips/ChipSuggest.stories.tsx`
|
|
135
|
+
Файлы примеров: `stories/Chips/examples/ChipSuggest/`
|
|
136
|
+
|
|
137
|
+
Используются истории родителя: отдельный stories-файл атому не нужен.
|
|
138
|
+
|
|
139
|
+
| Story | Example file | Что демонстрирует |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `Playground` | `PlaygroundExample.tsx` | Тип, размер, подпись и targetProps |
|
|
142
|
+
| `Default` | `DefaultExample.tsx` | Target в готовой композиции поиска и очистки |
|
|
143
|
+
| `Types` | `TypesExample.tsx` | TYPE_1 / TYPE_2 target |
|
|
144
|
+
| `Sizes` | `SizesExample.tsx` | SM / MD / LG |
|
|
145
|
+
| `States` | `StatesExample.tsx` | Выбор с очисткой и disabled target без выбора |
|
|
146
|
+
| `WithNotificationIcon` | `WithNotificationIconExample.tsx` | Значок у опции списка, не у target; скриншот-прогон пропущен |
|
|
147
|
+
| `VisualTests` | `VisualTestsExample.tsx` | Три размера и открытые списки; раскрытие выполняет useLayoutEffect примера |
|
|
148
|
+
|
|
149
|
+
`Default`, `Types`, `Sizes`, `States` и `VisualTests` имеют Linux/CI baseline
|
|
150
|
+
на `xs` / `xl` в `__screenshots__/`. Фокус базового чипса проверяет
|
|
151
|
+
`VisualTests` в stories `Chip`.
|
|
152
|
+
|
|
153
|
+
Контракт target покрывает `src/components/Chip/__tests__/ChipSuggestTarget.test.tsx`,
|
|
154
|
+
интеграцию со списком — `ChipSuggest.test.tsx`.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## История изменений
|
|
159
|
+
|
|
160
|
+
| Дата | Изменение |
|
|
161
|
+
|---|---|
|
|
162
|
+
| 2026-10-29 | Создан документ. |
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Примеры использования
|
|
167
|
+
|
|
168
|
+
### Default
|
|
169
|
+
|
|
170
|
+
`stories/Chips/examples/ChipSuggest/DefaultExample.tsx`
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
import React, { useRef, useState } from "react";
|
|
174
|
+
import { ChipSuggest, EComponentSize, type ISuggestFieldOption } from "@sberbusiness/triplex-next";
|
|
175
|
+
import { mapFruitsToSuggestOptions, SUGGEST_STORY_FRUITS } from "./storyConstants";
|
|
176
|
+
|
|
177
|
+
export const DefaultExample = () => {
|
|
178
|
+
const initialOptions = mapFruitsToSuggestOptions([...SUGGEST_STORY_FRUITS]);
|
|
179
|
+
|
|
180
|
+
const [value, setValue] = useState<ISuggestFieldOption>();
|
|
181
|
+
const [options, setOptions] = useState<ISuggestFieldOption[]>([]);
|
|
182
|
+
const [tooltipOpen, setTooltipOpen] = useState(false);
|
|
183
|
+
const initialOptionsRef = useRef<ISuggestFieldOption[]>(initialOptions);
|
|
184
|
+
|
|
185
|
+
const handleDropdownOpen = () => {
|
|
186
|
+
setOptions(initialOptionsRef.current);
|
|
187
|
+
setTooltipOpen(false);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const handleFilter = (inputValue: string) => {
|
|
191
|
+
if (inputValue.length === 0) {
|
|
192
|
+
setOptions(initialOptionsRef.current);
|
|
193
|
+
setTooltipOpen(false);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const filteredOptions = initialOptionsRef.current.filter(({ label }) =>
|
|
198
|
+
String(label).toLowerCase().includes(inputValue.toLowerCase()),
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
setOptions(filteredOptions);
|
|
202
|
+
setTooltipOpen(filteredOptions.length === 0);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<ChipSuggest
|
|
207
|
+
value={value}
|
|
208
|
+
options={options}
|
|
209
|
+
tooltipOpen={tooltipOpen}
|
|
210
|
+
onSelect={setValue}
|
|
211
|
+
onFilter={handleFilter}
|
|
212
|
+
targetProps={{ clearSelected: () => setValue(undefined) }}
|
|
213
|
+
dropdownProps={{ onOpen: handleDropdownOpen }}
|
|
214
|
+
label="Suggest label"
|
|
215
|
+
placeholder="Type to proceed"
|
|
216
|
+
noOptionsText="No matches found."
|
|
217
|
+
size={EComponentSize.MD}
|
|
218
|
+
/>
|
|
219
|
+
);
|
|
220
|
+
};
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Types
|
|
224
|
+
|
|
225
|
+
`stories/Chips/examples/ChipSuggest/TypesExample.tsx`
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
import React, { useRef, useState } from "react";
|
|
229
|
+
import { ChipSuggest, EChipType, EComponentSize, type ISuggestFieldOption } from "@sberbusiness/triplex-next";
|
|
230
|
+
import { mapFruitsToSuggestOptions, SUGGEST_STORY_FRUITS } from "./storyConstants";
|
|
231
|
+
|
|
232
|
+
type TypeItemProps = {
|
|
233
|
+
type: EChipType;
|
|
234
|
+
label: string;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const initialOptions = mapFruitsToSuggestOptions([...SUGGEST_STORY_FRUITS]);
|
|
238
|
+
|
|
239
|
+
const TypeItem = ({ type, label }: TypeItemProps) => {
|
|
240
|
+
const [value, setValue] = useState<ISuggestFieldOption>();
|
|
241
|
+
const [options, setOptions] = useState<ISuggestFieldOption[]>([]);
|
|
242
|
+
const [tooltipOpen, setTooltipOpen] = useState(false);
|
|
243
|
+
const initialOptionsRef = useRef<ISuggestFieldOption[]>(initialOptions);
|
|
244
|
+
|
|
245
|
+
const handleDropdownOpen = () => {
|
|
246
|
+
setOptions(initialOptionsRef.current);
|
|
247
|
+
setTooltipOpen(false);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const handleFilter = (inputValue: string) => {
|
|
251
|
+
if (inputValue.length === 0) {
|
|
252
|
+
setOptions(initialOptionsRef.current);
|
|
253
|
+
setTooltipOpen(false);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const filteredOptions = initialOptionsRef.current.filter(({ label }) =>
|
|
258
|
+
String(label).toLowerCase().includes(inputValue.toLowerCase()),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
setOptions(filteredOptions);
|
|
262
|
+
setTooltipOpen(filteredOptions.length === 0);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const handleClearSelected = () => setValue(undefined);
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<div>
|
|
269
|
+
<div style={{ marginBottom: 8, fontSize: 16, fontWeight: 700 }}>{label}</div>
|
|
270
|
+
<ChipSuggest
|
|
271
|
+
type={type}
|
|
272
|
+
size={EComponentSize.MD}
|
|
273
|
+
label="Suggest label"
|
|
274
|
+
placeholder="Type to proceed"
|
|
275
|
+
noOptionsText="No matches found."
|
|
276
|
+
value={value}
|
|
277
|
+
options={options}
|
|
278
|
+
tooltipOpen={tooltipOpen}
|
|
279
|
+
onSelect={setValue}
|
|
280
|
+
onFilter={handleFilter}
|
|
281
|
+
targetProps={{ clearSelected: handleClearSelected }}
|
|
282
|
+
dropdownProps={{ onOpen: handleDropdownOpen }}
|
|
283
|
+
/>
|
|
284
|
+
</div>
|
|
285
|
+
);
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const typeOptions = Object.values(EChipType).map((type) => ({
|
|
289
|
+
value: type,
|
|
290
|
+
label: type.toUpperCase(),
|
|
291
|
+
}));
|
|
292
|
+
|
|
293
|
+
export const TypesExample = () => (
|
|
294
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
295
|
+
{typeOptions.map(({ value, label }) => (
|
|
296
|
+
<TypeItem key={value} type={value} label={label} />
|
|
297
|
+
))}
|
|
298
|
+
</div>
|
|
299
|
+
);
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Sizes
|
|
303
|
+
|
|
304
|
+
`stories/Chips/examples/ChipSuggest/SizesExample.tsx`
|
|
305
|
+
|
|
306
|
+
```tsx
|
|
307
|
+
import React, { useRef, useState } from "react";
|
|
308
|
+
import { ChipSuggest, EComponentSize, type ISuggestFieldOption } from "@sberbusiness/triplex-next";
|
|
309
|
+
import { mapFruitsToSuggestOptions, SUGGEST_STORY_FRUITS } from "./storyConstants";
|
|
310
|
+
|
|
311
|
+
type SizeItemProps = {
|
|
312
|
+
size: EComponentSize;
|
|
313
|
+
label: string;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const initialOptions = mapFruitsToSuggestOptions([...SUGGEST_STORY_FRUITS]);
|
|
317
|
+
|
|
318
|
+
const SizeItem = ({ size, label }: SizeItemProps) => {
|
|
319
|
+
const [value, setValue] = useState<ISuggestFieldOption>();
|
|
320
|
+
const [options, setOptions] = useState<ISuggestFieldOption[]>([]);
|
|
321
|
+
const [tooltipOpen, setTooltipOpen] = useState(false);
|
|
322
|
+
const initialOptionsRef = useRef<ISuggestFieldOption[]>(initialOptions);
|
|
323
|
+
|
|
324
|
+
const handleDropdownOpen = () => {
|
|
325
|
+
setOptions(initialOptionsRef.current);
|
|
326
|
+
setTooltipOpen(false);
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const handleFilter = (inputValue: string) => {
|
|
330
|
+
if (inputValue.length === 0) {
|
|
331
|
+
setOptions(initialOptionsRef.current);
|
|
332
|
+
setTooltipOpen(false);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const filteredOptions = initialOptionsRef.current.filter(({ label }) =>
|
|
337
|
+
String(label).toLowerCase().includes(inputValue.toLowerCase()),
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
setOptions(filteredOptions);
|
|
341
|
+
setTooltipOpen(filteredOptions.length === 0);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const handleClearSelected = () => setValue(undefined);
|
|
345
|
+
|
|
346
|
+
return (
|
|
347
|
+
<div>
|
|
348
|
+
<div style={{ marginBottom: 8, fontSize: 16, fontWeight: 700 }}>{label}</div>
|
|
349
|
+
<ChipSuggest
|
|
350
|
+
size={size}
|
|
351
|
+
label="Suggest label"
|
|
352
|
+
placeholder="Type to proceed"
|
|
353
|
+
noOptionsText="No matches found."
|
|
354
|
+
value={value}
|
|
355
|
+
options={options}
|
|
356
|
+
tooltipOpen={tooltipOpen}
|
|
357
|
+
onSelect={setValue}
|
|
358
|
+
onFilter={handleFilter}
|
|
359
|
+
targetProps={{ clearSelected: handleClearSelected }}
|
|
360
|
+
dropdownProps={{ onOpen: handleDropdownOpen }}
|
|
361
|
+
/>
|
|
362
|
+
</div>
|
|
363
|
+
);
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const sizeOptions = Object.values(EComponentSize).map((size) => ({
|
|
367
|
+
value: size,
|
|
368
|
+
label: size.toUpperCase(),
|
|
369
|
+
}));
|
|
370
|
+
|
|
371
|
+
export const SizesExample = () => (
|
|
372
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
373
|
+
{sizeOptions.map(({ value, label }) => (
|
|
374
|
+
<SizeItem key={value} size={value} label={label} />
|
|
375
|
+
))}
|
|
376
|
+
</div>
|
|
377
|
+
);
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### States
|
|
381
|
+
|
|
382
|
+
`stories/Chips/examples/ChipSuggest/StatesExample.tsx`
|
|
383
|
+
|
|
384
|
+
```tsx
|
|
385
|
+
import React, { useRef, useState } from "react";
|
|
386
|
+
import { ChipSuggest, EComponentSize, type ISuggestFieldOption } from "@sberbusiness/triplex-next";
|
|
387
|
+
import { mapFruitsToSuggestOptions, SUGGEST_STORY_FRUITS } from "./storyConstants";
|
|
388
|
+
|
|
389
|
+
const states = [
|
|
390
|
+
{ id: "selected", label: "SELECTED" },
|
|
391
|
+
{ id: "disabled", label: "DISABLED", targetProps: { disabled: true } },
|
|
392
|
+
];
|
|
393
|
+
|
|
394
|
+
export const StatesExample = () => {
|
|
395
|
+
const initialOptions = mapFruitsToSuggestOptions([...SUGGEST_STORY_FRUITS]);
|
|
396
|
+
|
|
397
|
+
const [valueSelected, setValueSelected] = useState<ISuggestFieldOption | undefined>(initialOptions[0]);
|
|
398
|
+
const [valueDisabled, setValueDisabled] = useState<ISuggestFieldOption>();
|
|
399
|
+
const [options, setOptions] = useState<ISuggestFieldOption[]>([]);
|
|
400
|
+
const [tooltipOpen, setTooltipOpen] = useState(false);
|
|
401
|
+
const initialOptionsRef = useRef<ISuggestFieldOption[]>(initialOptions);
|
|
402
|
+
|
|
403
|
+
const handleDropdownOpen = () => {
|
|
404
|
+
setOptions(initialOptionsRef.current);
|
|
405
|
+
setTooltipOpen(false);
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const handleFilter = (inputValue: string) => {
|
|
409
|
+
if (inputValue.length === 0) {
|
|
410
|
+
setOptions(initialOptionsRef.current);
|
|
411
|
+
setTooltipOpen(false);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const filteredOptions = initialOptionsRef.current.filter(({ label }) =>
|
|
416
|
+
String(label).toLowerCase().includes(inputValue.toLowerCase()),
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
setOptions(filteredOptions);
|
|
420
|
+
setTooltipOpen(filteredOptions.length === 0);
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
return (
|
|
424
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
425
|
+
{states.map((state) => (
|
|
426
|
+
<div key={state.id}>
|
|
427
|
+
<div style={{ marginBottom: 8, fontSize: 16, fontWeight: 700 }}>{state.label}</div>
|
|
428
|
+
<ChipSuggest
|
|
429
|
+
label="Suggest label"
|
|
430
|
+
size={EComponentSize.MD}
|
|
431
|
+
placeholder="Type to proceed"
|
|
432
|
+
noOptionsText="No matches found."
|
|
433
|
+
value={state.id === "selected" ? valueSelected : valueDisabled}
|
|
434
|
+
options={options}
|
|
435
|
+
tooltipOpen={tooltipOpen}
|
|
436
|
+
onSelect={state.id === "selected" ? setValueSelected : setValueDisabled}
|
|
437
|
+
onFilter={handleFilter}
|
|
438
|
+
targetProps={{
|
|
439
|
+
clearSelected: () =>
|
|
440
|
+
state.id === "selected" ? setValueSelected(undefined) : setValueDisabled(undefined),
|
|
441
|
+
...state.targetProps,
|
|
442
|
+
}}
|
|
443
|
+
dropdownProps={{ onOpen: handleDropdownOpen }}
|
|
444
|
+
/>
|
|
445
|
+
</div>
|
|
446
|
+
))}
|
|
447
|
+
</div>
|
|
448
|
+
);
|
|
449
|
+
};
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### WithNotificationIcon
|
|
453
|
+
|
|
454
|
+
`stories/Chips/examples/ChipSuggest/WithNotificationIconExample.tsx`
|
|
455
|
+
|
|
456
|
+
```tsx
|
|
457
|
+
import React, { useRef, useState } from "react";
|
|
458
|
+
import { ChipSuggest, EComponentSize, type ISuggestFieldOption } from "@sberbusiness/triplex-next";
|
|
459
|
+
import { mapFruitsToOptionsWithNotification, SUGGEST_STORY_FRUITS_SHORT } from "./storyConstants";
|
|
460
|
+
|
|
461
|
+
export const WithNotificationIconExample = () => {
|
|
462
|
+
const initialOptions = mapFruitsToOptionsWithNotification([...SUGGEST_STORY_FRUITS_SHORT], 2);
|
|
463
|
+
|
|
464
|
+
const [value, setValue] = useState<ISuggestFieldOption>();
|
|
465
|
+
const [options, setOptions] = useState<ISuggestFieldOption[]>([]);
|
|
466
|
+
const [tooltipOpen, setTooltipOpen] = useState(false);
|
|
467
|
+
const initialOptionsRef = useRef<ISuggestFieldOption[]>(initialOptions);
|
|
468
|
+
|
|
469
|
+
const handleDropdownOpen = () => {
|
|
470
|
+
setOptions(initialOptionsRef.current);
|
|
471
|
+
setTooltipOpen(false);
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const handleFilter = (inputValue: string) => {
|
|
475
|
+
if (inputValue.length === 0) {
|
|
476
|
+
setOptions(initialOptionsRef.current);
|
|
477
|
+
setTooltipOpen(false);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const filteredOptions = initialOptionsRef.current.filter(({ label }) =>
|
|
482
|
+
String(label).toLowerCase().includes(inputValue.toLowerCase()),
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
setOptions(filteredOptions);
|
|
486
|
+
setTooltipOpen(filteredOptions.length === 0);
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
return (
|
|
490
|
+
<ChipSuggest
|
|
491
|
+
value={value}
|
|
492
|
+
options={options}
|
|
493
|
+
tooltipOpen={tooltipOpen}
|
|
494
|
+
onSelect={setValue}
|
|
495
|
+
onFilter={handleFilter}
|
|
496
|
+
targetProps={{ clearSelected: () => setValue(undefined) }}
|
|
497
|
+
dropdownProps={{ onOpen: handleDropdownOpen }}
|
|
498
|
+
label="Suggest label"
|
|
499
|
+
placeholder="Type to proceed"
|
|
500
|
+
noOptionsText="No matches found."
|
|
501
|
+
size={EComponentSize.MD}
|
|
502
|
+
/>
|
|
503
|
+
);
|
|
504
|
+
};
|
|
505
|
+
```
|
|
506
|
+
|
|
@@ -103,7 +103,7 @@ DropdownList.Background_Selected
|
|
|
103
103
|
- `forwardRef` на `Dropdown`, `DropdownDesktop`, `DropdownListItem` — не убирать. Ref `Dropdown` указывает на корневой `<div>` десктопного меню, а в мобильном режиме — на контентный `<div>` `Puller` (`.puller`).
|
|
104
104
|
- Меню в закрытом состоянии не рендерит ничего (`DropdownDesktop` возвращает `null`) — на это опираются потребители, монтирующие `Dropdown` безусловно.
|
|
105
105
|
- Компонент управляемый: не добавлять внутренний state открытости, иначе сломается контракт `opened` / `setOpened`.
|
|
106
|
-
- `Dropdown` рендерится в `document.body` через `Portal`. Смена контейнера портала — breaking change для потребителей, которые ищут меню в DOM.
|
|
106
|
+
- `Dropdown` рендерится в `document.body` через `Portal`. Смена контейнера портала — breaking change для потребителей, которые ищут меню в DOM. Мобильная версия рендерится туда же, если не задан `pullerProps.renderContainer`.
|
|
107
107
|
- Публичные barrel-экспорты `src/components/Dropdown/index.ts`, `desktop/index.ts` должны сохраняться: компонент используется внутри `ButtonDropdown`, `SelectField`, `SelectExtendedField`, `SuggestField`, `MultiselectField`, `TabsLine`, `DatePickerExtended`, `Chip`, `List`.
|
|
108
108
|
- `DropdownList` проставляет дочерним элементам `active`, `size`, `ref`, `onMouseOver` и `onMouseOut` через `cloneElement` — эти props у `DropdownList.Item` нельзя считать «пользовательскими».
|
|
109
109
|
- Пока десктопное меню открыто, на `document.body` висит класс блокировки скролла, а на документе — обработчики `wheel` и `keydown`. Снятие обработчиков в cleanup эффекта обязательно. Класс вешается только через `lockBodyScroll` (`src/utils/scroll/bodyScrollLock.ts`): прямой `classList.remove` снял бы блокировку при другом открытом меню.
|
|
@@ -194,6 +194,7 @@ DropdownList.Background_Selected
|
|
|
194
194
|
| 2026-09-29 | Аудит симметрии `related` (TRI-156): из `related` убраны имена без строки в `docs/ai/ROADMAP.md` (`OverlayBase`) и потребители `SelectField` / `SelectExtendedField` / `SuggestField` / `MultiselectField` / `TabsLine` — по `docs/ai/CONTEXT.md` ссылка стоит с их стороны; добавлен `LoaderSmall` — контракт по рендеру в верную сторону. Пояснения к убранным именам сохранены прозой в «Связанные компоненты»; публичный API и поведение не затронуты. |
|
|
195
195
|
| 2026-10-21 | **Breaking** (TRIPLEX-961): мобильная версия выделена в компонент `Puller`, `DropdownMobile*` удалены без алиасов. Prop `mobileViewProps` переименован в `pullerProps`, группы токенов `DropdownMobile` / `DropdownMobileList` — в `Puller` / `PullerList` (ключ `Content_Background` → `Body_Background`). |
|
|
196
196
|
| 2026-10-23 | Исправлено: закрытие одного из открытых `DropdownDesktop` снимало блокировку скролла страницы при другом открытом меню. **Breaking:** класс блокировки на `body` переименован `.dropdownDesktopOverflowHidden` → `.dropdownDesktopScrollLock`. |
|
|
197
|
+
| 2026-10-31 | Мобильная версия рендерится в `pullerProps.renderContainer`, если он задан, иначе в `document.body` (TRIPLEX-961). |
|
|
197
198
|
|
|
198
199
|
---
|
|
199
200
|
|