@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.
- package/bundle/components/AlertProcess.md +460 -0
- package/bundle/components/Amount.md +145 -169
- package/bundle/components/Badge.md +155 -219
- package/bundle/components/Body.md +108 -116
- package/bundle/components/Button.md +1 -0
- package/bundle/components/CarouselExtended.md +355 -194
- package/bundle/components/Checkbox.md +7 -1
- package/bundle/components/CheckboxTreeExtended.md +149 -248
- package/bundle/components/CheckboxXGroup.md +2 -0
- package/bundle/components/CheckboxYGroup.md +7 -2
- package/bundle/components/ChipGroup.md +195 -126
- package/bundle/components/ComposedKeyDownListener.md +323 -0
- package/bundle/components/FooterPage.md +2 -1
- package/bundle/components/HeaderPage.md +2 -1
- package/bundle/components/Island.md +100 -12
- package/bundle/components/KeyDownListener.md +11 -72
- package/bundle/components/LightBox.md +6 -0
- package/bundle/components/LoaderSmall.md +230 -0
- package/bundle/components/MaskedField.md +481 -0
- package/bundle/components/MediaWidth.md +252 -108
- package/bundle/components/MobileView.md +192 -68
- package/bundle/components/ModalWindow.md +3 -1
- package/bundle/components/Notification.md +21 -3
- package/bundle/components/Page.md +3 -1
- package/bundle/components/Pagination.md +20 -0
- package/bundle/components/SelectExtendedField.md +691 -236
- package/bundle/components/SliderExtended.md +358 -222
- package/bundle/components/TabsExtended.md +380 -194
- package/bundle/components/UnorderedList.md +254 -0
- package/bundle/components/UnorderedListExtended.md +255 -0
- package/bundle/components/WindowResizeListener.md +249 -0
- package/bundle/guides/commits.md +66 -3
- package/bundle/guides/context.md +26 -0
- package/bundle/guides/stories.md +3 -1
- package/bundle/guides/template.md +1 -1
- package/bundle/manifest.json +64 -21
- package/bundle/release-notes/1.41.1.mdx +18 -0
- package/bundle/release-notes/1.42.0.mdx +81 -0
- package/bundle/release-notes/1.43.0.mdx +94 -0
- package/bundle/release-notes/1.44.0.mdx +7 -0
- package/package.json +1 -1
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
component: SelectExtendedField
|
|
3
|
-
category:
|
|
4
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
target'а (icon vs text vs input), ни содержимое dropdown'а — потребитель
|
|
21
|
-
рендерит их через render-функции.
|
|
14
|
+
Базовый компонент Select. Владеет **только** состоянием открытости выпадающего блока и
|
|
15
|
+
правилами его закрытия. Разметку поля выбора компонент отдаёт в рендер-функцию
|
|
16
|
+
`renderTarget`, разметку выпадающего блока — в рендер-функцию `children`. Собственной
|
|
17
|
+
разметки, кроме позиционирующего `<div>`-обёртки, у него нет.
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
Используй когда: нужно поле выбора с **нестандартным** содержимым выпадающего блока
|
|
20
|
+
(форма фильтра, чекбоксы с подтверждением, дерево, произвольный контент) или
|
|
21
|
+
нестандартным полем выбора — и при этом нужна готовая логика открытия/закрытия.
|
|
26
22
|
|
|
27
|
-
|
|
28
|
-
поиском, color-picker как select, и т.п.). Для типичных — `SelectField`,
|
|
29
|
-
`MultiselectField`.
|
|
23
|
+
Не используй когда:
|
|
30
24
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
##
|
|
33
|
+
## Варианты и props
|
|
50
34
|
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
+
#### Опциональные props
|
|
87
48
|
|
|
88
|
-
| Prop | Тип |
|
|
49
|
+
| Prop | Тип | По умолчанию | Описание |
|
|
89
50
|
|---|---|---|---|
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
67
|
+
Готовое поле выбора поверх `FormField`. `ISelectExtendedFieldTargetProps` расширяет
|
|
68
|
+
`IFormFieldProps` без `prefix` / `postfix` (они переопределены своими типами).
|
|
98
69
|
|
|
99
|
-
| Prop | Тип |
|
|
70
|
+
| Prop | Тип | По умолчанию | Описание |
|
|
100
71
|
|---|---|---|---|
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `label
|
|
105
|
-
| `placeholder
|
|
106
|
-
| `loading
|
|
107
|
-
| `
|
|
108
|
-
| `
|
|
109
|
-
| `size` | `EComponentSize` | `MD` |
|
|
110
|
-
| `status` | `EFormFieldStatus` |
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
+
## Дизайн-токены
|
|
127
102
|
|
|
128
|
-
|
|
103
|
+
Собственных токенов у компонента нет: `SelectExtendedField.module.less` задаёт только
|
|
104
|
+
`position: relative`, а `SelectExtendedFieldTarget.module.less` — раскладку, поворот
|
|
105
|
+
каретки и блокировку указателя в `disabled`. Цвета, фон, границы и тени приходят из
|
|
106
|
+
компонентов, на которых построено поле:
|
|
129
107
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
113
|
+
Если понадобится собственное визуальное состояние поля выбора — токен заводится
|
|
114
|
+
в `SelectExtendedFieldTarget`, а не в `FormField`.
|
|
137
115
|
|
|
138
|
-
|
|
116
|
+
---
|
|
139
117
|
|
|
140
|
-
|
|
141
|
-
import { SelectExtendedFieldDropdownDefault } from "@sberbusiness/triplex-next/components/SelectExtendedField";
|
|
142
|
-
```
|
|
118
|
+
## Инварианты
|
|
143
119
|
|
|
144
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
const onOpenRef = useRef(onOpen);
|
|
154
|
-
const onCloseRef = useRef(onClose);
|
|
145
|
+
---
|
|
155
146
|
|
|
156
|
-
|
|
157
|
-
onOpenRef.current = onOpen;
|
|
158
|
-
onCloseRef.current = onClose;
|
|
159
|
-
});
|
|
160
|
-
```
|
|
147
|
+
## Связанные компоненты
|
|
161
148
|
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
157
|
+
**Внутри папки компонента:**
|
|
166
158
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
163
|
+
**Построены на SelectExtendedField:**
|
|
177
164
|
|
|
178
|
-
|
|
165
|
+
- `SelectField` — обычный select со списком опций.
|
|
166
|
+
- `MultiselectField` — множественный выбор.
|
|
167
|
+
- `Chip.Select`, `Chip.Sort`, `Chip.Multiselect` — фильтр-чипы.
|
|
179
168
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
document.addEventListener("mousedown", listenMouseDown);
|
|
183
|
-
return () => document.removeEventListener("mousedown", listenMouseDown);
|
|
184
|
-
}, [listenMouseDown]);
|
|
185
|
-
```
|
|
169
|
+
`SuggestField` на `SelectExtendedField` **не** построен: он собран на `Dropdown`
|
|
170
|
+
напрямую. Правки в этом компоненте его не затрагивают.
|
|
186
171
|
|
|
187
|
-
|
|
188
|
-
- **`mousedown`**, не `click` — закрывает раньше, чем сработает `click` на dropdown items.
|
|
172
|
+
**Инфраструктура:**
|
|
189
173
|
|
|
190
|
-
|
|
174
|
+
- `Dropdown` — выпадающее меню через `Portal`, позиционирование, мобильная версия.
|
|
175
|
+
- `FormField` — визуальный контейнер поля, контекст размера и статуса.
|
|
176
|
+
- `KeyDownListener` — закрытие по `Escape`.
|
|
177
|
+
- `IconWrapper`, `LoaderSmall` — каретка и лоадер в постфиксе поля.
|
|
191
178
|
|
|
192
|
-
|
|
193
|
-
<KeyDownListener onMatch={closeDropdown} eventKeyCode={EVENT_KEY_CODES.ESCAPE}>
|
|
194
|
-
...
|
|
195
|
-
</KeyDownListener>
|
|
196
|
-
```
|
|
179
|
+
---
|
|
197
180
|
|
|
198
|
-
|
|
181
|
+
## Stories
|
|
199
182
|
|
|
200
|
-
|
|
183
|
+
Основные истории: `stories/SelectExtendedField/SelectExtendedField.stories.tsx`
|
|
184
|
+
Файлы примеров: `stories/SelectExtendedField/examples/`
|
|
201
185
|
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
198
|
+
---
|
|
207
199
|
|
|
208
|
-
|
|
200
|
+
## История изменений
|
|
209
201
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
219
|
-
- `stopPropagation` — комментарий в коде: предотвращает срабатывание `keydown` в `DropdownListItem` через `document.addEventListener("keydown")`.
|
|
207
|
+
---
|
|
220
208
|
|
|
221
|
-
|
|
209
|
+
## Примеры использования
|
|
222
210
|
|
|
223
|
-
|
|
224
|
-
- `LoaderSmall theme=BRAND` тех же размеров.
|
|
225
|
-
- `IconWrapper active={opened} disabled={status===DISABLED}` оборачивает caret — управляет цветом через `@sberbusiness/icons-next` глобальные классы.
|
|
211
|
+
### Default
|
|
226
212
|
|
|
227
|
-
|
|
213
|
+
`stories/SelectExtendedField/examples/Default.tsx`
|
|
228
214
|
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
###
|
|
281
|
+
### Sizes
|
|
237
282
|
|
|
238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
---
|
|
369
|
+
### Statuses
|
|
258
370
|
|
|
259
|
-
|
|
371
|
+
`stories/SelectExtendedField/examples/Statuses.tsx`
|
|
260
372
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 | Создан документ |
|