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