@sberbusiness/triplex-next-mcp-server 1.41.0-beta → 1.43.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 (41) hide show
  1. package/bundle/components/AlertProcess.md +460 -0
  2. package/bundle/components/Amount.md +145 -169
  3. package/bundle/components/Badge.md +155 -219
  4. package/bundle/components/Body.md +108 -116
  5. package/bundle/components/Button.md +1 -0
  6. package/bundle/components/CarouselExtended.md +355 -194
  7. package/bundle/components/Checkbox.md +7 -1
  8. package/bundle/components/CheckboxTreeExtended.md +149 -248
  9. package/bundle/components/CheckboxXGroup.md +2 -0
  10. package/bundle/components/CheckboxYGroup.md +7 -2
  11. package/bundle/components/ChipGroup.md +195 -126
  12. package/bundle/components/ComposedKeyDownListener.md +323 -0
  13. package/bundle/components/FooterPage.md +2 -1
  14. package/bundle/components/HeaderPage.md +2 -1
  15. package/bundle/components/Island.md +100 -12
  16. package/bundle/components/KeyDownListener.md +11 -72
  17. package/bundle/components/LightBox.md +6 -0
  18. package/bundle/components/LoaderSmall.md +230 -0
  19. package/bundle/components/MaskedField.md +481 -0
  20. package/bundle/components/MediaWidth.md +252 -108
  21. package/bundle/components/MobileView.md +192 -68
  22. package/bundle/components/ModalWindow.md +3 -1
  23. package/bundle/components/Notification.md +21 -3
  24. package/bundle/components/Page.md +3 -1
  25. package/bundle/components/Pagination.md +20 -0
  26. package/bundle/components/SelectExtendedField.md +691 -236
  27. package/bundle/components/SliderExtended.md +358 -222
  28. package/bundle/components/TabsExtended.md +380 -194
  29. package/bundle/components/UnorderedList.md +254 -0
  30. package/bundle/components/UnorderedListExtended.md +255 -0
  31. package/bundle/components/WindowResizeListener.md +249 -0
  32. package/bundle/guides/commits.md +66 -3
  33. package/bundle/guides/context.md +26 -0
  34. package/bundle/guides/stories.md +3 -1
  35. package/bundle/guides/template.md +1 -1
  36. package/bundle/manifest.json +64 -21
  37. package/bundle/release-notes/1.41.1.mdx +18 -0
  38. package/bundle/release-notes/1.42.0.mdx +81 -0
  39. package/bundle/release-notes/1.43.0.mdx +94 -0
  40. package/bundle/release-notes/1.44.0.mdx +7 -0
  41. package/package.json +1 -1
@@ -1,9 +1,7 @@
1
1
  ---
2
2
  component: SelectExtendedField
3
- category: Form Controls
4
- figma-node: ""
5
- figma-file: ""
6
- related: [SelectField, MultiselectField, Dropdown, FormField, KeyDownListener]
3
+ category: SelectExtendedField
4
+ related: [SelectField, MultiselectField, SuggestField, Chip, Dropdown, FormField, KeyDownListener, IconWrapper, LoaderSmall]
7
5
  tokens: []
8
6
  stories: stories/SelectExtendedField/SelectExtendedField.stories.tsx
9
7
  version: "1.0"
@@ -13,310 +11,767 @@ version: "1.0"
13
11
 
14
12
  ## Назначение
15
13
 
16
- **Базовый компонент** для построения custom-Select'ов: оркестрирует
17
- открытие/закрытие dropdown'а, click-outside, ESC и (опционально)
18
- TAB-закрытие, прокидывает `opened/setOpened/targetRef/dropdownRef` в
19
- **render-prop'ы** для target и dropdown. Сам не диктует ни форму
20
- target'а (icon vs text vs input), ни содержимое dropdown'а — потребитель
21
- рендерит их через render-функции.
14
+ Базовый компонент Select. Владеет **только** состоянием открытости выпадающего блока и
15
+ правилами его закрытия. Разметку поля выбора компонент отдаёт в рендер-функцию
16
+ `renderTarget`, разметку выпадающего блока — в рендер-функцию `children`. Собственной
17
+ разметки, кроме позиционирующего `<div>`-обёртки, у него нет.
22
18
 
23
- `SelectExtendedField.Target` (`SelectExtendedFieldTarget`) и
24
- `SelectExtendedField.Dropdown` — типичные реализации; их можно заменить
25
- своими.
19
+ Используй когда: нужно поле выбора с **нестандартным** содержимым выпадающего блока
20
+ (форма фильтра, чекбоксы с подтверждением, дерево, произвольный контент) или
21
+ нестандартным полем выбора — и при этом нужна готовая логика открытия/закрытия.
26
22
 
27
- Используй когда: строишь нестандартный Select (например, multi-select с
28
- поиском, color-picker как select, и т.п.). Для типичных — `SelectField`,
29
- `MultiselectField`.
23
+ Не используй когда:
30
24
 
31
- ---
32
-
33
- ## Файловая структура
34
-
35
- ```text
36
- src/components/SelectExtendedField/
37
- ├── SelectExtendedField.tsx # forwardRef + Object.assign({Target, Dropdown})
38
- ├── components/
39
- │ ├── SelectExtendedFieldTarget.tsx # forwardRef; FormField + caret + clear + loader
40
- │ ├── SelectExtendedFieldDropdown.tsx # FC; <Dropdown> wrapper + .List
41
- │ └── SelectExtendedFieldDropdownDefault.tsx # FC; готовая раскладка опций (desktop + mobile)
42
- ├── styles/
43
- ├── index.ts
44
- └── __tests__/
45
- ```
25
+ - нужен обычный select со списком опций — используй `SelectField`;
26
+ - нужен множественный выбор с тегами — используй `MultiselectField`;
27
+ - нужен поиск по вводу — используй `SuggestField`;
28
+ - нужен фильтр-чип со списком — используй `Chip.Select` / `Chip.Sort`;
29
+ - выпадающий блок не привязан к полю ввода — используй `Dropdown` или `ButtonDropdownExtended`.
46
30
 
47
31
  ---
48
32
 
49
- ## Compound-структура (через render-prop'ы)
33
+ ## Варианты и props
50
34
 
51
- ```tsx
52
- <SelectExtendedField
53
- closeOnTab
54
- onOpen={...} onClose={...}
55
- renderTarget={({ opened, setOpened }) => (
56
- <SelectExtendedField.Target
57
- opened={opened}
58
- setOpened={setOpened}
59
- label={selected?.label}
60
- placeholder="Выбрать..."
61
- fieldLabel="Label"
62
- size={EComponentSize.MD}
63
- onClear={() => onChange(undefined)}
64
- />
65
- )}
66
- >
67
- {({ opened, setOpened, targetRef, dropdownRef }) => (
68
- <SelectExtendedField.Dropdown
69
- opened={opened}
70
- setOpened={setOpened}
71
- targetRef={targetRef}
72
- forwardedRef={dropdownRef}
73
- >
74
- <SelectExtendedField.Dropdown.List dropdownOpened={opened}>
75
- {options.map(...)}
76
- </SelectExtendedField.Dropdown.List>
77
- </SelectExtendedField.Dropdown>
78
- )}
79
- </SelectExtendedField>
80
- ```
35
+ ### `SelectExtendedField`
81
36
 
82
- ---
37
+ `ISelectExtendedFieldProps` расширяет `React.HTMLAttributes<HTMLDivElement>` c
38
+ переопределённым `children`. Неизвестные компоненту атрибуты попадают на корневой `<div>`.
83
39
 
84
- ## Варианты и props
40
+ #### Обязательные props
41
+
42
+ | Prop | Тип | Описание |
43
+ |---|---|---|
44
+ | `renderTarget` | `(props: ISelectExtendedFieldTargetProvideProps) => React.ReactNode` | Рендер-функция поля выбора. Получает `opened` и `setOpened` |
45
+ | `children` | `(props: ISelectExtendedFieldDropdownProvideProps) => React.ReactNode` | Рендер-функция выпадающего блока. Получает `opened`, `setOpened`, `targetRef`, `dropdownRef` |
85
46
 
86
- ### SelectExtendedField — `ISelectExtendedFieldProps extends Omit<HTMLAttributes<HTMLDivElement>, "children">`
47
+ #### Опциональные props
87
48
 
88
- | Prop | Тип | Default | Описание |
49
+ | Prop | Тип | По умолчанию | Описание |
89
50
  |---|---|---|---|
90
- | `renderTarget` | `(props: ISelectExtendedFieldTargetProvideProps) => ReactNode` | — | Render-prop для target. Получает `{ opened, setOpened }`. |
91
- | `children` | `(props: ISelectExtendedFieldDropdownProvideProps) => ReactNode` | — | Render-prop для dropdown. Получает `{ opened, setOpened, targetRef, dropdownRef }`. |
92
- | `onOpen?` | `() => void` | — | Только при реальном переходе `false → true` (skip mount). |
93
- | `onClose?` | `() => void` | — | Только при `true → false`. |
94
- | `closeOnTab?` | `boolean` | `false` | Tab закрывает dropdown. |
95
- | `...HTMLAttributes` | — | — | На корневой `<div>`. |
51
+ | `closeOnTab` | `boolean` | `false` | Закрывать выпадающий блок по нажатию Tab внутри компонента |
52
+ | `onOpen` | `() => void` | — | Вызывается при переходе `opened` в `true`. На первом рендере не вызывается |
53
+ | `onClose` | `() => void` | — | Вызывается при переходе `opened` в `false`. На первом рендере не вызывается |
54
+ | `className` | `string` | — | Дополнительный класс корневого `<div>` |
55
+
56
+ #### Что получают рендер-функции
57
+
58
+ | Поле | Кому | Описание |
59
+ |---|---|---|
60
+ | `opened` | обеим | Текущее состояние открытости |
61
+ | `setOpened` | обеим | Сеттер состояния открытости. Стабилен между рендерами (сеттер `useState`) |
62
+ | `targetRef` | `children` | Ссылка на **корневой `<div>` компонента**, а не на поле ввода. По ней `Dropdown` считает своё положение |
63
+ | `dropdownRef` | `children` | Ссылка, которую нужно повесить на контейнер выпадающего блока. По ней компонент отличает нажатие внутри блока от нажатия вне его |
64
+
65
+ ### `SelectExtendedField.Target` (`SelectExtendedFieldTarget`)
96
66
 
97
- ### SelectExtendedField.Target — `ISelectExtendedFieldTargetProps extends Omit<IFormFieldProps, "prefix" \| "postfix">`
67
+ Готовое поле выбора поверх `FormField`. `ISelectExtendedFieldTargetProps` расширяет
68
+ `IFormFieldProps` без `prefix` / `postfix` (они переопределены своими типами).
98
69
 
99
- | Prop | Тип | Default | Описание |
70
+ | Prop | Тип | По умолчанию | Описание |
100
71
  |---|---|---|---|
101
- | `opened` | `boolean` | — | Из render-prop'а. |
102
- | `setOpened` | `(opened: boolean) => void` | — | Из render-prop'а. |
103
- | `fieldLabel` | `ReactNode` | — | Label, рендерится через `FormFieldLabel`. |
104
- | `label?` | `ReactNode` | — | Текущий выбранный (отображается в `FormFieldTarget`). |
105
- | `placeholder?` | `ReactNode` | — | Когда `label` пуст. |
106
- | `loading?` | `boolean` | — | Заменяет caret на `LoaderSmall` (BRAND). Блокирует клики и keys. |
107
- | `prefix?` / `postfix?` | `ReactNode` | — | До/после target. Caret/Loader всегда рендерятся в Postfix. |
108
- | `onClear?` | `() => void` | — | Если передан — рендерится `FormFieldClear`. |
109
- | `size` | `EComponentSize` | `MD` | Размер. Маппится на `CaretdownStrokeSrvIcon{16/20/24}` и `LoaderSmall size`. |
110
- | `status` | `EFormFieldStatus` | — | `DISABLED` блокирует клики/keys. |
111
- | `tabIndex` | `number` | — | Стандартно. |
112
-
113
- - `aria-expanded={opened}`, `aria-haspopup="listbox"` — на корне FormField.
114
- - Открытие по: **click**, **Enter**, **Space**, **ArrowDown**, **ArrowUp** (только когда `!opened`); тогда `preventDefault + stopPropagation`.
115
-
116
- ### SelectExtendedField.Dropdown — `ISelectExtendedFieldDropdownProps extends Omit<IDropdownProps, "forwardedRef">`
72
+ | `fieldLabel` | `React.ReactNode` | — | Обязательный. Заголовок поля (плавающий label) |
73
+ | `opened` | `boolean` | — | Обязательный. Состояние открытости: поворот каретки, `aria-expanded`, активное состояние `FormField` |
74
+ | `setOpened` | `(opened: boolean) => void` | — | Обязательный. Вызывается по клику и по клавишам открытия |
75
+ | `label` | `React.ReactNode` | — | Выбранное значение |
76
+ | `placeholder` | `React.ReactNode` | — | Отображается, пока нет `label` |
77
+ | `loading` | `boolean` | `false` | Вместо каретки — лоадер; клик и клавиатура не открывают блок |
78
+ | `onClear` | `() => void` | — | Если передан, в постфиксе появляется кнопка очистки |
79
+ | `prefix` / `postfix` | `React.ReactNode` | — | Слоты `FormFieldPrefix` / `FormFieldPostfix`. Постфикс идёт **после** каретки |
80
+ | `size` | `EComponentSize` | `EComponentSize.MD` | Размер поля. Определяет размер каретки и лоадера |
81
+ | `status` | `EFormFieldStatus` | `EFormFieldStatus.DEFAULT` | Визуальное состояние. `DISABLED` блокирует открытие |
82
+
83
+ ### `SelectExtendedField.Dropdown` (`SelectExtendedFieldDropdown`)
84
+
85
+ Обёртка над `Dropdown`. Отличается только тем, что ссылку принимает не через `ref`,
86
+ а через prop `forwardedRef` — рендер-функция `children` отдаёт готовый `dropdownRef`,
87
+ и передать его через `ref` в JSX было бы невозможно без промежуточной обёртки.
88
+ Остальные props — как у `Dropdown` (`size`, `width`, `direction`, `alignment`,
89
+ `mobileViewProps`, ...). Статическое свойство `List` — это `DropdownList`.
90
+
91
+ ### Ограничения
92
+
93
+ - Компонент **не управляемый снаружи**: состояние открытости внутреннее, prop'а `opened` у него нет. Открыть блок программно можно только через `setOpened`, полученный в рендер-функции.
94
+ - `SelectExtendedFieldDropdown` — не `forwardRef`-компонент. Ссылка передаётся через `forwardedRef`.
95
+ - Выпадающий блок компонент не рендерит сам и не знает, открыт ли он визуально: `opened` нужно передать в `SelectExtendedFieldDropdown` (или в свою разметку) самостоятельно.
96
+ - Компонент не хранит выбранное значение и не знает про опции. Значение — целиком забота потребителя.
97
+ - Клик по кнопке очистки (`onClear`) не раскрывает выпадающий блок: `SelectExtendedFieldTarget` останавливает всплытие в обработчике `FormFieldClear`. Без этого событие дошло бы до `onClick` на `FormField` и переключило `opened` — так же гасят всплытие `Chip.Select` и `Chip.DatePicker`. Не убирать: затрагивает `SelectField` и `MultiselectField.Target`, которые рендерят тот же `Target`.
117
98
 
118
- | Prop | Тип | Описание |
119
- |---|---|---|
120
- | `forwardedRef` | `RefObject<HTMLDivElement>` | Из render-prop'а (dropdownRef). |
121
- | `targetRef` | `RefObject<HTMLElement>` | Из render-prop'а. |
122
- | `opened` | `boolean` | — |
123
- | `children?` | `ReactNode` | — |
124
- | `...IDropdownProps` | — | width, mobileViewProps, size и т.п. |
99
+ ---
125
100
 
126
- Slots: **`SelectExtendedField.Dropdown.List`** = `DropdownList` (re-export).
101
+ ## Дизайн-токены
127
102
 
128
- ### Provide-types
103
+ Собственных токенов у компонента нет: `SelectExtendedField.module.less` задаёт только
104
+ `position: relative`, а `SelectExtendedFieldTarget.module.less` — раскладку, поворот
105
+ каретки и блокировку указателя в `disabled`. Цвета, фон, границы и тени приходят из
106
+ компонентов, на которых построено поле:
129
107
 
130
- ```ts
131
- ISelectExtendedFieldTargetProvideProps: { opened, setOpened }
132
- ISelectExtendedFieldDropdownProvideProps: { opened, setOpened, targetRef, dropdownRef }
133
- ISelectExtendedFieldDefaultOption: { value: string; label: ReactNode; ...IDropdownListItemProps без active/onSelect/selected/key/className/keyCodesForSelection }
134
- ```
108
+ - `FormField` и его части (`FormFieldLabel`, `FormFieldTarget`, `FormFieldPrefix`, `FormFieldPostfix`, `FormFieldClear`) — `--triplex-next-FormField-*`;
109
+ - `Dropdown` и `DropdownList` — `--triplex-next-Dropdown-*`, `--triplex-next-DropdownList-*`;
110
+ - `IconWrapper` вокруг каретки — `--triplex-next-IconWrapper-*`;
111
+ - `LoaderSmall` в состоянии загрузки — токены лоадера.
135
112
 
136
- ### SelectExtendedFieldDropdownDefault (отдельная утилита)
113
+ Если понадобится собственное визуальное состояние поля выбора — токен заводится
114
+ в `SelectExtendedFieldTarget`, а не в `FormField`.
137
115
 
138
- Готовая раскладка опций для desktop (`Dropdown.List`) **и** mobile (`DropdownMobileHeader/Body/List`). Не часть Object.assign — импортируется отдельно:
116
+ ---
139
117
 
140
- ```ts
141
- import { SelectExtendedFieldDropdownDefault } from "@sberbusiness/triplex-next/components/SelectExtendedField";
142
- ```
118
+ ## Инварианты
143
119
 
144
- (используется в `SelectField`).
120
+ - `forwardRef` на `SelectExtendedField` и `SelectExtendedFieldTarget` — не убирать. Ref `SelectExtendedField` указывает на корневой `<div>` (тот же элемент, что и `targetRef`), ref `SelectExtendedFieldTarget` — на `FormFieldTarget`, то есть на элемент со значением, а не на корень поля.
121
+ - Barrel `src/components/SelectExtendedField/index.ts` должен сохраняться: компонент используется внутри `SelectField`, `MultiselectField`, `Chip.Select`, `Chip.Sort`, `Chip.Multiselect`.
122
+ - `SelectExtendedFieldDropdownDefault` — **внутренний** компонент, из barrel не экспортируется. Его импортируют по прямому пути `components/SelectExtendedFieldDropdownDefault` внутри библиотеки. Не выносить в публичный API без отдельного решения.
123
+ - Статические свойства `SelectExtendedField.Target` и `SelectExtendedField.Dropdown`, а также `SelectExtendedFieldDropdown.List` — часть публичного API.
124
+ - `setOpened`, отдаваемый рендер-функциям, — это сеттер `useState`. Он обязан оставаться стабильным между рендерами: построенные на нём компоненты кладут его в зависимости колбэков — например `Chip.Select` через `ChipSelectTarget` (`useCallback(..., [opened, setOpened, onKeyDown])`).
125
+ - `onOpen` / `onClose` вызываются **только на смену состояния** и не срабатывают на первом рендере и на повторном маунте в React 18 StrictMode. Реализовано сравнением предыдущего значения `opened` (`prevOpenedRef`), а не флагом «смонтирован» — флаг ломается на двойном маунте.
126
+ - Актуальные `onOpen` / `onClose` хранятся в ref: эффект зависит только от `opened`, поэтому смена идентичности колбэков между рендерами их не вызывает.
127
+ - Закрытие по нажатию мышью вне блока опирается на `dropdownRef`, а не на DOM-вложенность: `Dropdown` рендерится через `Portal` в `document.body`. Если потребитель не повесит `dropdownRef` на свой контейнер, любое нажатие внутри выпадающего блока закроет его.
128
+ - Слушатель `mousedown` на `document` навешивается только пока блок открыт и снимается в cleanup эффекта.
129
+ - Компонент React 17-совместим (ветка `release-0`): не использовать `useId`, `useSyncExternalStore`, `useInsertionEffect` и поведение, зависящее от automatic batching.
130
+ - В `SelectExtendedFieldTarget.module.less` нет классов `loading`, `placeholder`, `label`. Не добавлять ссылки на них в `clsx`: у CSS-модуля такого свойства нет, и в атрибут `class` попадёт литерал `undefined` (баг был исправлен в 1.41.0). Если состоянию нужен стиль — сначала заведи класс в LESS.
145
131
 
146
132
  ---
147
133
 
148
- ## Ключевые особенности реализации
134
+ ## Accessibility
149
135
 
150
- ### Stable `onOpen`/`onClose` через ref
136
+ - `SelectExtendedFieldTarget` выставляет на корневом элементе `aria-haspopup="listbox"` и `aria-expanded`, отражающий `opened`.
137
+ - Роли `listbox` / `option` и клавиатурную навигацию по опциям (`ArrowUp` / `ArrowDown`, выбор по `Enter` / `Space`) даёт `DropdownList` — то есть `SelectExtendedField.Dropdown.List`. Если выпадающий блок не список, эти роли не появляются и навигацию организует потребитель.
138
+ - Клавиши открытия на поле выбора: `Space`, `Enter`, `ArrowDown`, `ArrowUp`. Обработчик вызывает `preventDefault` (чтобы страница не прокручивалась) и `stopPropagation` — иначе то же нажатие поймает документный обработчик `DropdownListItem` и список сразу выберет опцию.
139
+ - В состояниях `loading` и `status={EFormFieldStatus.DISABLED}` поле не реагирует ни на клик, ни на клавиатуру; `onClick` / `onKeyDown` потребителя в этих состояниях тоже не вызываются.
140
+ - `Escape` закрывает блок из любого места страницы — слушатель `KeyDownListener` висит на `window`.
141
+ - `Tab` закрывает блок только при `closeOnTab`. Фокус компонент не перехватывает и не возвращает: focus trap, если нужен, добавляет потребитель.
142
+ - Связь поля со списком (`aria-controls`, `aria-activedescendant`) компонент **не выставляет** — это делает потребитель. `SelectField` передаёт `listId` в `SelectExtendedFieldDropdownDefault` именно для этого.
143
+ - Компонент не хардкодит текстовые строки: `fieldLabel`, `placeholder`, `aria-label` для кнопки очистки и заголовок мобильной версии передаёт потребитель — библиотека мультиязычная.
151
144
 
152
- ```ts
153
- const onOpenRef = useRef(onOpen);
154
- const onCloseRef = useRef(onClose);
145
+ ---
155
146
 
156
- useLayoutEffect(() => {
157
- onOpenRef.current = onOpen;
158
- onCloseRef.current = onClose;
159
- });
160
- ```
147
+ ## Связанные компоненты
161
148
 
162
- - Колбэки хранятся в ref'ах и обновляются после каждого коммита через `useLayoutEffect` без deps.
163
- - `useEffect([opened])` читает `onOpenRef.current` — поэтому **эффект не пересоздаётся** при смене ссылок на колбэки.
149
+ Отдельного AI.md пока нет ни у `SelectExtendedFieldTarget`, ни у
150
+ `SelectExtendedFieldDropdown` — они описаны здесь. Это область задачи TRI-74
151
+ («Scope: только SelectExtendedField; соседние экспортируемые компоненты из
152
+ `src/components/SelectExtendedField/` — отдельные задачи»), а не вывод по критериям
153
+ `docs/ai/CONTEXT.md` → «Когда создавать `{ComponentName}-ai.md`». По этим критериям
154
+ оба заслуживают собственных AI.md и отдельных строк в `docs/ai/ROADMAP.md`.
155
+ Решение о том, заводить ли их, за владельцем репозитория.
164
156
 
165
- ### Skip mount-вызов `onOpen` / `onClose`
157
+ **Внутри папки компонента:**
166
158
 
167
- ```ts
168
- if (!isMountedRef.current) {
169
- isMountedRef.current = true;
170
- return;
171
- }
172
- if (opened) onOpenRef.current?.();
173
- else onCloseRef.current?.();
174
- ```
159
+ - `SelectExtendedFieldTarget` (`SelectExtendedField.Target`) — готовое поле выбора поверх `FormField`.
160
+ - `SelectExtendedFieldDropdown` (`SelectExtendedField.Dropdown`) — обёртка над `Dropdown` со ссылкой через `forwardedRef`. Статическое свойство `List` — `DropdownList`.
161
+ - `SelectExtendedFieldDropdownDefault` — внутренний компонент: готовый выпадающий блок со списком опций и мобильной версией. Не экспортируется из barrel, используется в `Chip.Select`, `Chip.Sort` и `SelectField`.
175
162
 
176
- - Default `opened = false` → без skip'а `onClose` вызвался бы при mount.
163
+ **Построены на SelectExtendedField:**
177
164
 
178
- ### Click-outside через `document.mousedown`
165
+ - `SelectField` — обычный select со списком опций.
166
+ - `MultiselectField` — множественный выбор.
167
+ - `Chip.Select`, `Chip.Sort`, `Chip.Multiselect` — фильтр-чипы.
179
168
 
180
- ```ts
181
- useEffect(() => {
182
- document.addEventListener("mousedown", listenMouseDown);
183
- return () => document.removeEventListener("mousedown", listenMouseDown);
184
- }, [listenMouseDown]);
185
- ```
169
+ `SuggestField` на `SelectExtendedField` **не** построен: он собран на `Dropdown`
170
+ напрямую. Правки в этом компоненте его не затрагивают.
186
171
 
187
- - Закрывает, если клик вне `targetRef` и `dropdownRef`.
188
- - **`mousedown`**, не `click` — закрывает раньше, чем сработает `click` на dropdown items.
172
+ **Инфраструктура:**
189
173
 
190
- ### ESC — через KeyDownListener (window)
174
+ - `Dropdown` — выпадающее меню через `Portal`, позиционирование, мобильная версия.
175
+ - `FormField` — визуальный контейнер поля, контекст размера и статуса.
176
+ - `KeyDownListener` — закрытие по `Escape`.
177
+ - `IconWrapper`, `LoaderSmall` — каретка и лоадер в постфиксе поля.
191
178
 
192
- ```tsx
193
- <KeyDownListener onMatch={closeDropdown} eventKeyCode={EVENT_KEY_CODES.ESCAPE}>
194
- ...
195
- </KeyDownListener>
196
- ```
179
+ ---
197
180
 
198
- - Глобальный listener, см. `KeyDownListener-AI.md`.
181
+ ## Stories
199
182
 
200
- ### TAB — через onKeyDown в самом контейнере
183
+ Основные истории: `stories/SelectExtendedField/SelectExtendedField.stories.tsx`
184
+ Файлы примеров: `stories/SelectExtendedField/examples/`
201
185
 
202
- ```ts
203
- if (closeOnTab && isKey(key, "TAB")) closeDropdown();
204
- ```
186
+ | Story | Example file | Что демонстрирует |
187
+ |---|---|---|
188
+ | `Playground` | `Playground.tsx` | Интерактивный контроль `closeOnTab` и настроек поля выбора: размер, статус, загрузка, кнопка очистки |
189
+ | `Default` | `Default.tsx` | Базовый сценарий: поле выбора и список опций |
190
+ | `Sizes` | `Sizes.tsx` | Размеры SM / MD / LG |
191
+ | `Statuses` | `Statuses.tsx` | Состояния поля: DEFAULT / DISABLED / ERROR / WARNING |
192
+ | `Loading` | `Loading.tsx` | Состояние загрузки: лоадер вместо каретки, поле не открывается |
193
+ | `WithClearButton` | `WithClearButton.tsx` | Кнопка очистки значения через `onClear`; видна пока поле под курсором или в фокусе |
194
+ | `WithPrefixAndPostfix` | `WithPrefixAndPostfix.tsx` | Слоты `prefix` и `postfix` поля выбора |
195
+ | `Example` | `Example.tsx` | Произвольный выпадающий блок: чекбоксы и кнопки вместо списка, значение подтверждается явно |
196
+ | `VisualTests` | `VisualTests.tsx` | Раскрытый список и видимая кнопка очистки (через `play`), длинное значение, загрузка в размере SM |
205
197
 
206
- - Опционально. Контракт: `closeOnTab` — потребитель решает (по UX).
198
+ ---
207
199
 
208
- ### Target: открытие по клавишам
200
+ ## История изменений
209
201
 
210
- ```ts
211
- if (!opened && [SPACE, ENTER, ARROW_DOWN, ARROW_UP].includes(event.keyCode)) {
212
- event.preventDefault();
213
- event.stopPropagation(); // чтобы dropdown сразу не среагировал тем же keydown
214
- setOpened(!opened);
215
- }
216
- ```
202
+ | Дата | Изменение |
203
+ |---|---|
204
+ | 2026-08-07 | Создан документ AI-ready для `SelectExtendedField` (TRI-74). |
205
+ | 2026-08-07 | AI-рефакторинг (TRI-74): `onOpen` / `onClose` сравнивают предыдущее значение `opened` вместо флага «смонтирован» — колбэки больше не срабатывают ложно при двойном маунте в React 18 StrictMode. Удалены ссылки на несуществующие CSS-классы в `SelectExtendedFieldTarget` (в `class` попадал литерал `undefined`). Клик по кнопке очистки больше не раскрывает выпадающий блок — всплытие останавливается, как в `Chip.Select`. Слушатель `mousedown` навешивается только пока блок открыт, callback-ref стабилизирован через `useCallback`, убран мёртвый внутренний `targetRef` в `SelectExtendedFieldTarget`, дедуплицирован список опций десктопной и мобильной версий в `SelectExtendedFieldDropdownDefault`. JSDoc на всех props, unit-тесты разбиты по субкомпонентам и расширены с 42 до 69 кейсов. Stories переведены на modern pattern. Публичный API не изменён. |
217
206
 
218
- - `event.keyCode` — **deprecated** API (тех-долг).
219
- - `stopPropagation` — комментарий в коде: предотвращает срабатывание `keydown` в `DropdownListItem` через `document.addEventListener("keydown")`.
207
+ ---
220
208
 
221
- ### Target: caret + loader через size-map
209
+ ## Примеры использования
222
210
 
223
- - `CaretdownStrokeSrvIcon{16/20/24}` для `SM/MD/LG`.
224
- - `LoaderSmall theme=BRAND` тех же размеров.
225
- - `IconWrapper active={opened} disabled={status===DISABLED}` оборачивает caret — управляет цветом через `@sberbusiness/icons-next` глобальные классы.
211
+ ### Default
226
212
 
227
- ### `setRef` мерж forwardRef + targetRef
213
+ `stories/SelectExtendedField/examples/Default.tsx`
228
214
 
229
- ```ts
230
- const setRef = (instance) => {
231
- targetRef.current = instance;
232
- if (typeof ref === "function") ref(instance); else if (ref) ref.current = instance;
215
+ ```tsx
216
+ import React, { useState } from "react";
217
+ import {
218
+ DropdownListItem,
219
+ EDropdownWidth,
220
+ SelectExtendedField,
221
+ SelectExtendedFieldDropdown,
222
+ SelectExtendedFieldTarget,
223
+ } from "@sberbusiness/triplex-next";
224
+
225
+ const OPTIONS = [
226
+ { id: "option1", label: "Первая опция" },
227
+ { id: "option2", label: "Вторая опция" },
228
+ { id: "option3", label: "Третья опция" },
229
+ ];
230
+
231
+ export const Default = () => {
232
+ const [selectedId, setSelectedId] = useState<string>();
233
+
234
+ const selectedOption = OPTIONS.find((option) => option.id === selectedId);
235
+
236
+ return (
237
+ <div style={{ maxWidth: "300px" }}>
238
+ <SelectExtendedField
239
+ closeOnTab
240
+ renderTarget={({ opened, setOpened }) => (
241
+ <SelectExtendedFieldTarget
242
+ opened={opened}
243
+ setOpened={setOpened}
244
+ fieldLabel="Выберите опцию"
245
+ label={selectedOption?.label}
246
+ placeholder="Не выбрано"
247
+ />
248
+ )}
249
+ >
250
+ {({ opened, setOpened, targetRef, dropdownRef }) => (
251
+ <SelectExtendedFieldDropdown
252
+ opened={opened}
253
+ setOpened={setOpened}
254
+ targetRef={targetRef}
255
+ forwardedRef={dropdownRef}
256
+ width={EDropdownWidth.TARGET}
257
+ >
258
+ <SelectExtendedFieldDropdown.List dropdownOpened={opened}>
259
+ {OPTIONS.map((option) => (
260
+ <DropdownListItem
261
+ key={option.id}
262
+ id={option.id}
263
+ selected={option.id === selectedId}
264
+ onSelect={() => {
265
+ setSelectedId(option.id);
266
+ setOpened(false);
267
+ }}
268
+ >
269
+ {option.label}
270
+ </DropdownListItem>
271
+ ))}
272
+ </SelectExtendedFieldDropdown.List>
273
+ </SelectExtendedFieldDropdown>
274
+ )}
275
+ </SelectExtendedField>
276
+ </div>
277
+ );
233
278
  };
234
279
  ```
235
280
 
236
- ### Target слоты Postfix
281
+ ### Sizes
237
282
 
238
- ```tsx
239
- <FormFieldPostfix>
240
- {onClear && <FormFieldClear onClick={onClear} />}
241
- {loading ? <LoaderSmall .../> : <IconWrapper .../>}
242
- {postfix}
243
- </FormFieldPostfix>
244
- ```
283
+ `stories/SelectExtendedField/examples/Sizes.tsx`
245
284
 
246
- - Порядок: clear → loader/caret → custom postfix.
285
+ ```tsx
286
+ import React, { useState } from "react";
287
+ import {
288
+ DropdownListItem,
289
+ EComponentSize,
290
+ EDropdownWidth,
291
+ SelectExtendedField,
292
+ SelectExtendedFieldDropdown,
293
+ SelectExtendedFieldTarget,
294
+ } from "@sberbusiness/triplex-next";
295
+
296
+ const OPTIONS = [
297
+ { id: "option1", label: "Первая опция" },
298
+ { id: "option2", label: "Вторая опция" },
299
+ { id: "option3", label: "Третья опция" },
300
+ ];
301
+
302
+ const SIZES = Object.values(EComponentSize);
303
+
304
+ interface ISizeItemProps {
305
+ size: EComponentSize;
306
+ }
247
307
 
248
- ### Loading блокирует клики и keydown
308
+ const SizeItem = ({ size }: ISizeItemProps) => {
309
+ const [selectedId, setSelectedId] = useState<string>("option1");
310
+
311
+ const selectedOption = OPTIONS.find((option) => option.id === selectedId);
312
+
313
+ return (
314
+ <div>
315
+ <div style={{ marginBottom: "8px", fontSize: "16px", fontWeight: "700" }}>{size.toUpperCase()}</div>
316
+ <SelectExtendedField
317
+ closeOnTab
318
+ renderTarget={({ opened, setOpened }) => (
319
+ <SelectExtendedFieldTarget
320
+ opened={opened}
321
+ setOpened={setOpened}
322
+ size={size}
323
+ fieldLabel="Выберите опцию"
324
+ label={selectedOption?.label}
325
+ placeholder="Не выбрано"
326
+ />
327
+ )}
328
+ >
329
+ {({ opened, setOpened, targetRef, dropdownRef }) => (
330
+ <SelectExtendedFieldDropdown
331
+ opened={opened}
332
+ setOpened={setOpened}
333
+ targetRef={targetRef}
334
+ forwardedRef={dropdownRef}
335
+ size={size}
336
+ width={EDropdownWidth.TARGET}
337
+ >
338
+ <SelectExtendedFieldDropdown.List dropdownOpened={opened} size={size}>
339
+ {OPTIONS.map((option) => (
340
+ <DropdownListItem
341
+ key={option.id}
342
+ id={option.id}
343
+ selected={option.id === selectedId}
344
+ onSelect={() => {
345
+ setSelectedId(option.id);
346
+ setOpened(false);
347
+ }}
348
+ >
349
+ {option.label}
350
+ </DropdownListItem>
351
+ ))}
352
+ </SelectExtendedFieldDropdown.List>
353
+ </SelectExtendedFieldDropdown>
354
+ )}
355
+ </SelectExtendedField>
356
+ </div>
357
+ );
358
+ };
249
359
 
250
- ```ts
251
- if (loading || status === DISABLED) return; // в handleClick
252
- if (loading && status === DISABLED) return; // в handleKeyDown — bug? && вместо ||
360
+ export const Sizes = () => (
361
+ <div style={{ maxWidth: "300px", display: "flex", flexDirection: "column", gap: "16px" }}>
362
+ {SIZES.map((size) => (
363
+ <SizeItem key={size} size={size} />
364
+ ))}
365
+ </div>
366
+ );
253
367
  ```
254
368
 
255
- - В `handleKeyDown` используется `&&` — **похоже на баг**: keys блокируются только если ОБА условия true. Тех-долг, не трогать без задачи.
256
-
257
- ---
369
+ ### Statuses
258
370
 
259
- ## Инварианты
371
+ `stories/SelectExtendedField/examples/Statuses.tsx`
260
372
 
261
- - **`renderTarget` и `children` — render-prop'ы** (функции). Изменение API — breaking.
262
- - **`onOpen` / `onClose` через ref'ы** — stable callback pattern. Не пересоздавать эффект на смене ссылок.
263
- - **Skip mount-вызов** колбэков через `isMountedRef`.
264
- - **Click-outside на `mousedown`** (не `click`) — public behavior contract.
265
- - **ESC закрывает через `KeyDownListener` (window)** — глобальный listener.
266
- - **`closeOnTab` default `false`** — UX-выбор.
267
- - **Target открывает по `Enter / Space / ArrowDown / ArrowUp`** + `preventDefault + stopPropagation`.
268
- - **Target использует `event.keyCode`** — deprecated, тех-долг.
269
- - **Target loading: `&&` вместо `||`** в handleKeyDown — известный потенциальный баг.
270
- - **`aria-haspopup="listbox"`** — публичная семантика.
271
- - **`SelectExtendedField.Dropdown.List = DropdownList`** — реэкспорт.
272
- - **`SelectExtendedFieldDropdownDefault`** — не slot, отдельный экспорт.
273
- - **`Target.size` default `MD`**.
274
- - **Target caret-icon фиксирован** (`CaretdownStrokeSrvIcon`) — нельзя заменить.
275
- - **`forwardRef`** на SelectExtendedField и Target; **`FC`** на Dropdown.
373
+ ```tsx
374
+ import React, { useState } from "react";
375
+ import {
376
+ DropdownListItem,
377
+ EDropdownWidth,
378
+ EFormFieldStatus,
379
+ SelectExtendedField,
380
+ SelectExtendedFieldDropdown,
381
+ SelectExtendedFieldTarget,
382
+ } from "@sberbusiness/triplex-next";
383
+
384
+ const OPTIONS = [
385
+ { id: "option1", label: "Первая опция" },
386
+ { id: "option2", label: "Вторая опция" },
387
+ { id: "option3", label: "Третья опция" },
388
+ ];
389
+
390
+ const STATUSES = Object.values(EFormFieldStatus);
391
+
392
+ interface IStatusItemProps {
393
+ status: EFormFieldStatus;
394
+ }
276
395
 
277
- ---
396
+ const StatusItem = ({ status }: IStatusItemProps) => {
397
+ const [selectedId, setSelectedId] = useState<string>("option1");
398
+
399
+ const selectedOption = OPTIONS.find((option) => option.id === selectedId);
400
+
401
+ return (
402
+ <div>
403
+ <div style={{ marginBottom: "8px", fontSize: "16px", fontWeight: "700" }}>{status.toUpperCase()}</div>
404
+ <SelectExtendedField
405
+ closeOnTab
406
+ renderTarget={({ opened, setOpened }) => (
407
+ <SelectExtendedFieldTarget
408
+ opened={opened}
409
+ setOpened={setOpened}
410
+ status={status}
411
+ fieldLabel="Выберите опцию"
412
+ label={selectedOption?.label}
413
+ placeholder="Не выбрано"
414
+ />
415
+ )}
416
+ >
417
+ {({ opened, setOpened, targetRef, dropdownRef }) => (
418
+ <SelectExtendedFieldDropdown
419
+ opened={opened}
420
+ setOpened={setOpened}
421
+ targetRef={targetRef}
422
+ forwardedRef={dropdownRef}
423
+ width={EDropdownWidth.TARGET}
424
+ >
425
+ <SelectExtendedFieldDropdown.List dropdownOpened={opened}>
426
+ {OPTIONS.map((option) => (
427
+ <DropdownListItem
428
+ key={option.id}
429
+ id={option.id}
430
+ selected={option.id === selectedId}
431
+ onSelect={() => {
432
+ setSelectedId(option.id);
433
+ setOpened(false);
434
+ }}
435
+ >
436
+ {option.label}
437
+ </DropdownListItem>
438
+ ))}
439
+ </SelectExtendedFieldDropdown.List>
440
+ </SelectExtendedFieldDropdown>
441
+ )}
442
+ </SelectExtendedField>
443
+ </div>
444
+ );
445
+ };
278
446
 
279
- ## Accessibility
447
+ export const Statuses = () => (
448
+ <div style={{ maxWidth: "300px", display: "flex", flexDirection: "column", gap: "16px" }}>
449
+ {STATUSES.map((status) => (
450
+ <StatusItem key={status} status={status} />
451
+ ))}
452
+ </div>
453
+ );
454
+ ```
280
455
 
281
- - `aria-expanded={opened}` + `aria-haspopup="listbox"` на target — корректная семантика combobox-like.
282
- - ESC закрывает (стандарт).
283
- - Open by `Enter`/`Space`/`Arrow*` — стандартное keyboard поведение для select.
284
- - **Target — `<div>` (от FormField)**, не `<button>` — для AT может звучать как regular область. Если нужен strict role — потребитель добавляет через `...rest` (`role="combobox"` и т.п.).
285
- - `aria-label` / `aria-labelledby` для target — потребитель.
286
- - `loading` блокирует клики и (по идее) keys — но по факту `keydown` блокировка имеет потенциальный баг (см. выше).
287
- - `tabIndex` — наследуется от FormField/FormFieldProps.
456
+ ### Loading
288
457
 
289
- ---
458
+ `stories/SelectExtendedField/examples/Loading.tsx`
290
459
 
291
- ## Дизайн-токены
460
+ ```tsx
461
+ import React, { useState } from "react";
462
+ import {
463
+ DropdownListItem,
464
+ EDropdownWidth,
465
+ SelectExtendedField,
466
+ SelectExtendedFieldDropdown,
467
+ SelectExtendedFieldTarget,
468
+ } from "@sberbusiness/triplex-next";
469
+
470
+ const OPTIONS = [
471
+ { id: "option1", label: "Первая опция" },
472
+ { id: "option2", label: "Вторая опция" },
473
+ { id: "option3", label: "Третья опция" },
474
+ ];
475
+
476
+ export const Loading = () => {
477
+ const [selectedId, setSelectedId] = useState<string>();
478
+
479
+ const selectedOption = OPTIONS.find((option) => option.id === selectedId);
480
+
481
+ return (
482
+ <div style={{ maxWidth: "300px" }}>
483
+ <SelectExtendedField
484
+ closeOnTab
485
+ renderTarget={({ opened, setOpened }) => (
486
+ <SelectExtendedFieldTarget
487
+ opened={opened}
488
+ setOpened={setOpened}
489
+ loading
490
+ fieldLabel="Выберите опцию"
491
+ label={selectedOption?.label}
492
+ placeholder="Не выбрано"
493
+ />
494
+ )}
495
+ >
496
+ {({ opened, setOpened, targetRef, dropdownRef }) => (
497
+ <SelectExtendedFieldDropdown
498
+ opened={opened}
499
+ setOpened={setOpened}
500
+ targetRef={targetRef}
501
+ forwardedRef={dropdownRef}
502
+ width={EDropdownWidth.TARGET}
503
+ >
504
+ <SelectExtendedFieldDropdown.List dropdownOpened={opened}>
505
+ {OPTIONS.map((option) => (
506
+ <DropdownListItem
507
+ key={option.id}
508
+ id={option.id}
509
+ selected={option.id === selectedId}
510
+ onSelect={() => {
511
+ setSelectedId(option.id);
512
+ setOpened(false);
513
+ }}
514
+ >
515
+ {option.label}
516
+ </DropdownListItem>
517
+ ))}
518
+ </SelectExtendedFieldDropdown.List>
519
+ </SelectExtendedFieldDropdown>
520
+ )}
521
+ </SelectExtendedField>
522
+ </div>
523
+ );
524
+ };
525
+ ```
292
526
 
293
- Нет собственных. Цвета/spacing — от `FormField`, `Dropdown`, `Loader`.
527
+ ### WithClearButton
294
528
 
295
- ---
529
+ `stories/SelectExtendedField/examples/WithClearButton.tsx`
296
530
 
297
- ## Связанные компоненты
531
+ ```tsx
532
+ import React, { useState } from "react";
533
+ import {
534
+ DropdownListItem,
535
+ EDropdownWidth,
536
+ SelectExtendedField,
537
+ SelectExtendedFieldDropdown,
538
+ SelectExtendedFieldTarget,
539
+ } from "@sberbusiness/triplex-next";
540
+
541
+ const OPTIONS = [
542
+ { id: "option1", label: "Первая опция" },
543
+ { id: "option2", label: "Вторая опция" },
544
+ { id: "option3", label: "Третья опция" },
545
+ ];
546
+
547
+ export const WithClearButton = () => {
548
+ const [selectedId, setSelectedId] = useState<string | undefined>("option2");
549
+
550
+ const selectedOption = OPTIONS.find((option) => option.id === selectedId);
551
+
552
+ return (
553
+ <div style={{ maxWidth: "300px" }}>
554
+ <SelectExtendedField
555
+ closeOnTab
556
+ renderTarget={({ opened, setOpened }) => (
557
+ <SelectExtendedFieldTarget
558
+ opened={opened}
559
+ setOpened={setOpened}
560
+ fieldLabel="Выберите опцию"
561
+ label={selectedOption?.label}
562
+ placeholder="Не выбрано"
563
+ onClear={selectedOption ? () => setSelectedId(undefined) : undefined}
564
+ />
565
+ )}
566
+ >
567
+ {({ opened, setOpened, targetRef, dropdownRef }) => (
568
+ <SelectExtendedFieldDropdown
569
+ opened={opened}
570
+ setOpened={setOpened}
571
+ targetRef={targetRef}
572
+ forwardedRef={dropdownRef}
573
+ width={EDropdownWidth.TARGET}
574
+ >
575
+ <SelectExtendedFieldDropdown.List dropdownOpened={opened}>
576
+ {OPTIONS.map((option) => (
577
+ <DropdownListItem
578
+ key={option.id}
579
+ id={option.id}
580
+ selected={option.id === selectedId}
581
+ onSelect={() => {
582
+ setSelectedId(option.id);
583
+ setOpened(false);
584
+ }}
585
+ >
586
+ {option.label}
587
+ </DropdownListItem>
588
+ ))}
589
+ </SelectExtendedFieldDropdown.List>
590
+ </SelectExtendedFieldDropdown>
591
+ )}
592
+ </SelectExtendedField>
593
+ </div>
594
+ );
595
+ };
596
+ ```
298
597
 
299
- - `SelectField` — высокоуровневая обёртка для типичного Select.
300
- - `MultiselectField` — multi-выбор.
301
- - `Dropdown` — popover.
302
- - `FormField` (Target/Label/Prefix/Postfix/Clear) — основа target.
303
- - `KeyDownListener` — ESC.
304
- - `LoaderSmall` — индикатор загрузки в target.
305
- - `IconWrapper` — управление цветом caret.
306
- - `CaretdownStrokeSrvIcon{16/20/24}` — иконка раскрытия.
307
- - `EComponentSize`, `EFormFieldStatus`.
598
+ ### WithPrefixAndPostfix
308
599
 
309
- ---
600
+ `stories/SelectExtendedField/examples/WithPrefixAndPostfix.tsx`
310
601
 
311
- ## Stories
602
+ ```tsx
603
+ import React, { useState } from "react";
604
+ import { DefaulticonStrokePrdIcon20, QuestioncircleFilledSrvIcon16 } from "@sberbusiness/icons-next";
605
+ import {
606
+ DropdownListItem,
607
+ EDropdownWidth,
608
+ SelectExtendedField,
609
+ SelectExtendedFieldDropdown,
610
+ SelectExtendedFieldTarget,
611
+ } from "@sberbusiness/triplex-next";
612
+
613
+ const OPTIONS = [
614
+ { id: "option1", label: "Первая опция" },
615
+ { id: "option2", label: "Вторая опция" },
616
+ { id: "option3", label: "Третья опция" },
617
+ ];
618
+
619
+ export const WithPrefixAndPostfix = () => {
620
+ const [selectedId, setSelectedId] = useState<string | undefined>("option1");
621
+
622
+ const selectedOption = OPTIONS.find((option) => option.id === selectedId);
623
+
624
+ return (
625
+ <div style={{ maxWidth: "300px" }}>
626
+ <SelectExtendedField
627
+ closeOnTab
628
+ renderTarget={({ opened, setOpened }) => (
629
+ <SelectExtendedFieldTarget
630
+ opened={opened}
631
+ setOpened={setOpened}
632
+ fieldLabel="Выберите опцию"
633
+ label={selectedOption?.label}
634
+ placeholder="Не выбрано"
635
+ prefix={<DefaulticonStrokePrdIcon20 paletteIndex={5} />}
636
+ postfix={<QuestioncircleFilledSrvIcon16 paletteIndex={5} />}
637
+ />
638
+ )}
639
+ >
640
+ {({ opened, setOpened, targetRef, dropdownRef }) => (
641
+ <SelectExtendedFieldDropdown
642
+ opened={opened}
643
+ setOpened={setOpened}
644
+ targetRef={targetRef}
645
+ forwardedRef={dropdownRef}
646
+ width={EDropdownWidth.TARGET}
647
+ >
648
+ <SelectExtendedFieldDropdown.List dropdownOpened={opened}>
649
+ {OPTIONS.map((option) => (
650
+ <DropdownListItem
651
+ key={option.id}
652
+ id={option.id}
653
+ selected={option.id === selectedId}
654
+ onSelect={() => {
655
+ setSelectedId(option.id);
656
+ setOpened(false);
657
+ }}
658
+ >
659
+ {option.label}
660
+ </DropdownListItem>
661
+ ))}
662
+ </SelectExtendedFieldDropdown.List>
663
+ </SelectExtendedFieldDropdown>
664
+ )}
665
+ </SelectExtendedField>
666
+ </div>
667
+ );
668
+ };
669
+ ```
312
670
 
313
- `stories/SelectExtendedField/SelectExtendedField.stories.tsx`
314
- Примеры: `stories/SelectExtendedField/examples/`
671
+ ### Example
315
672
 
316
- ---
673
+ `stories/SelectExtendedField/examples/Example.tsx`
317
674
 
318
- ## История изменений
675
+ ```tsx
676
+ import React, { useState } from "react";
677
+ import {
678
+ Button,
679
+ Checkbox,
680
+ EButtonTheme,
681
+ EComponentSize,
682
+ EDropdownWidth,
683
+ ETextSize,
684
+ SelectExtendedField,
685
+ SelectExtendedFieldDropdown,
686
+ SelectExtendedFieldTarget,
687
+ Text,
688
+ } from "@sberbusiness/triplex-next";
689
+
690
+ const CATEGORIES = [
691
+ { id: "income", label: "Поступления" },
692
+ { id: "outcome", label: "Списания" },
693
+ { id: "commission", label: "Комиссии" },
694
+ ];
695
+
696
+ /**
697
+ * Произвольное содержимое выпадающего блока: чекбоксы и кнопки вместо списка опций.
698
+ * Значение подтверждается кнопкой «Применить», поэтому выбор хранится в двух состояниях.
699
+ */
700
+ export const Example = () => {
701
+ const [appliedIds, setAppliedIds] = useState<string[]>(["income"]);
702
+ const [draftIds, setDraftIds] = useState<string[]>(["income"]);
703
+
704
+ const appliedLabel = CATEGORIES.filter((category) => appliedIds.includes(category.id))
705
+ .map((category) => category.label)
706
+ .join(", ");
707
+
708
+ const toggleDraftId = (id: string) => {
709
+ setDraftIds((ids) => (ids.includes(id) ? ids.filter((draftId) => draftId !== id) : [...ids, id]));
710
+ };
711
+
712
+ return (
713
+ <div style={{ maxWidth: "320px" }}>
714
+ <SelectExtendedField
715
+ closeOnTab
716
+ onOpen={() => setDraftIds(appliedIds)}
717
+ renderTarget={({ opened, setOpened }) => (
718
+ <SelectExtendedFieldTarget
719
+ opened={opened}
720
+ setOpened={setOpened}
721
+ size={EComponentSize.LG}
722
+ fieldLabel="Категории операций"
723
+ label={appliedLabel || undefined}
724
+ placeholder="Все категории"
725
+ />
726
+ )}
727
+ >
728
+ {({ opened, setOpened, targetRef, dropdownRef }) => (
729
+ <SelectExtendedFieldDropdown
730
+ opened={opened}
731
+ setOpened={setOpened}
732
+ targetRef={targetRef}
733
+ forwardedRef={dropdownRef}
734
+ size={EComponentSize.LG}
735
+ width={EDropdownWidth.MIN_TARGET}
736
+ >
737
+ <div style={{ display: "flex", flexDirection: "column", gap: "12px", padding: "16px" }}>
738
+ <Text tag="div" size={ETextSize.B3}>
739
+ Показывать операции
740
+ </Text>
741
+ {CATEGORIES.map((category) => (
742
+ <Checkbox
743
+ key={category.id}
744
+ checked={draftIds.includes(category.id)}
745
+ onChange={() => toggleDraftId(category.id)}
746
+ >
747
+ {category.label}
748
+ </Checkbox>
749
+ ))}
750
+ <div style={{ display: "flex", gap: "8px" }}>
751
+ <Button
752
+ theme={EButtonTheme.GENERAL}
753
+ size={EComponentSize.MD}
754
+ onClick={() => {
755
+ setAppliedIds(draftIds);
756
+ setOpened(false);
757
+ }}
758
+ >
759
+ Применить
760
+ </Button>
761
+ <Button
762
+ theme={EButtonTheme.SECONDARY}
763
+ size={EComponentSize.MD}
764
+ onClick={() => setDraftIds([])}
765
+ >
766
+ Сбросить
767
+ </Button>
768
+ </div>
769
+ </div>
770
+ </SelectExtendedFieldDropdown>
771
+ )}
772
+ </SelectExtendedField>
773
+ </div>
774
+ );
775
+ };
776
+ ```
319
777
 
320
- | Дата | Изменение |
321
- |---|---|
322
- | 2026-04-18 | Создан документ |