@sberbusiness/triplex-next-mcp-server 1.43.0-beta → 1.45.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/README.md +7 -6
- package/bundle/components/AlertContext.md +11 -8
- package/bundle/components/AlertProcess.md +16 -12
- package/bundle/components/Badge.md +6 -3
- package/bundle/components/BodyPage.md +72 -4
- package/bundle/components/Button.md +46 -43
- package/bundle/components/ButtonDropdownExtended.md +3 -2
- package/bundle/components/ButtonIcon.md +7 -4
- package/bundle/components/Calendar.md +62 -59
- package/bundle/components/Caption.md +26 -23
- package/bundle/components/Checkbox.md +32 -29
- package/bundle/components/CheckboxTreeExtended.md +9 -5
- package/bundle/components/Chip.md +44 -41
- package/bundle/components/CodeText.md +26 -23
- package/bundle/components/Col.md +1 -2
- package/bundle/components/CollapsibleTree.md +8 -5
- package/bundle/components/CollapsibleTreeExtended.md +6 -1
- package/bundle/components/DatePickerExtended.md +409 -262
- package/bundle/components/Dropdown.md +33 -28
- package/bundle/components/EmptyView.md +12 -5
- package/bundle/components/ExpandAnimation.md +263 -218
- package/bundle/components/Footer.md +235 -278
- package/bundle/components/FooterPage.md +6 -2
- package/bundle/components/FormField.md +44 -41
- package/bundle/components/FormGroup.md +192 -52
- package/bundle/components/Header.md +491 -211
- package/bundle/components/HeaderPage.md +6 -2
- package/bundle/components/ImageGallery.md +33 -34
- package/bundle/components/ImageGalleryExtended.md +33 -34
- package/bundle/components/Island.md +52 -14
- package/bundle/components/LightBox.md +25 -18
- package/bundle/components/Link.md +211 -96
- package/bundle/components/ListActionItem.md +7 -4
- package/bundle/components/ListItem.md +11 -8
- package/bundle/components/ListItemControlsButton.md +16 -13
- package/bundle/components/ListItemControlsButtonDropdown.md +9 -6
- package/bundle/components/ListItemSelectable.md +7 -4
- package/bundle/components/ListMasterFooter.md +8 -5
- package/bundle/components/ListMasterHeader.md +8 -5
- package/bundle/components/ListSortableItem.md +11 -8
- package/bundle/components/ListTableItem.md +7 -4
- package/bundle/components/LoaderMiddle.md +7 -4
- package/bundle/components/LoaderScreen.md +11 -8
- package/bundle/components/LoaderSmall.md +7 -4
- package/bundle/components/Marker.md +184 -69
- package/bundle/components/MarkerStatus.md +206 -88
- package/bundle/components/MaskedField.md +2 -4
- package/bundle/components/MasterTable.md +602 -0
- package/bundle/components/ModalWindow.md +7 -4
- package/bundle/components/MultiselectField.md +2183 -132
- package/bundle/components/Notification.md +12 -9
- package/bundle/components/Overlay.md +33 -5
- package/bundle/components/Page.md +2 -2
- package/bundle/components/Pagination.md +27 -21
- package/bundle/components/Row.md +1 -1
- package/bundle/components/SelectExtendedField.md +3 -3
- package/bundle/components/SelectField.md +466 -148
- package/bundle/components/SliderExtended.md +39 -36
- package/bundle/components/SmallInput.md +194 -51
- package/bundle/components/Suggest.md +535 -99
- package/bundle/components/SwipeableArea.md +428 -79
- package/bundle/components/TableBasic.md +2302 -0
- package/bundle/components/TabsExtended.md +24 -21
- package/bundle/components/Tag.md +263 -78
- package/bundle/components/Text.md +28 -25
- package/bundle/components/TextField.md +3 -4
- package/bundle/components/ThemeProvider.md +50 -1
- package/bundle/components/Title.md +26 -23
- package/bundle/components/Tooltip.md +24 -21
- package/bundle/components/TopOverlay.md +368 -89
- package/bundle/components/TreeView.md +343 -166
- package/bundle/components/TreeViewNode.md +436 -0
- package/bundle/guides/codestyle.md +3 -0
- package/bundle/guides/commits.md +106 -26
- package/bundle/guides/context.md +84 -12
- package/bundle/guides/guidelines.md +321 -0
- package/bundle/guides/template.md +22 -12
- package/bundle/manifest.json +54 -20
- package/bundle/release-notes/1.44.0.mdx +113 -0
- package/bundle/release-notes/1.45.0.mdx +124 -0
- package/bundle/release-notes/1.46.0.mdx +7 -0
- package/bundle/tokens.json +6362 -0
- package/dist/index.js +177 -28
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
component: SelectField
|
|
3
|
-
category:
|
|
4
|
-
|
|
5
|
-
figma-file: ""
|
|
6
|
-
related: [SelectExtendedField, MultiselectField, Dropdown, FormField]
|
|
3
|
+
category: SelectField
|
|
4
|
+
related: [SelectExtendedField, MultiselectField, SuggestField, Chip, Dropdown, Pagination]
|
|
7
5
|
tokens: []
|
|
8
6
|
stories: stories/SelectField/SelectField.stories.tsx
|
|
9
7
|
version: "1.0"
|
|
@@ -13,201 +11,521 @@ version: "1.0"
|
|
|
13
11
|
|
|
14
12
|
## Назначение
|
|
15
13
|
|
|
16
|
-
Готовый Select
|
|
17
|
-
`SelectExtendedField`
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
mobile bottom-sheet) c шириной `EDropdownWidth.TARGET`.
|
|
14
|
+
Готовый Select со списком опций: поле выбора и выпадающий блок собраны заранее. Тонкий слой над
|
|
15
|
+
`SelectExtendedField` — состоянием открытости владеет он, а `SelectField` отвечает за подстановку
|
|
16
|
+
готового поля выбора (`SelectExtendedField.Target`), готового списка
|
|
17
|
+
(`SelectExtendedFieldDropdownDefault`) и за aria-связку между ними.
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
- `instanceId` через `lodash uniqueId()` — связывает Target ↔ List через `aria-controls`.
|
|
25
|
-
- **`closeOnTab = true`** захардкожен.
|
|
19
|
+
Используй когда: нужен обычный выбор одного значения из плоского списка опций с текстовыми
|
|
20
|
+
подписями — вместе с мобильной версией списка и клавиатурной навигацией «из коробки».
|
|
26
21
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
`
|
|
22
|
+
Не используй когда:
|
|
23
|
+
|
|
24
|
+
- нужен множественный выбор с тегами — `MultiselectField`;
|
|
25
|
+
- нужен поиск/фильтрация по вводу — `SuggestField`;
|
|
26
|
+
- содержимое выпадающего блока нестандартное (форма, чекбоксы с подтверждением, дерево)
|
|
27
|
+
или нужно нестандартное поле выбора — `SelectExtendedField`, он для этого и существует;
|
|
28
|
+
- выбор оформляется фильтр-чипом, а не полем формы — `Chip.Select` / `Chip.Sort`;
|
|
29
|
+
- выпадающий блок не привязан к полю ввода — `Dropdown` или `ButtonDropdownExtended`.
|
|
30
30
|
|
|
31
31
|
---
|
|
32
32
|
|
|
33
|
-
##
|
|
33
|
+
## Варианты и props
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
src/components/SelectField/
|
|
37
|
-
├── SelectField.tsx # forwardRef; склейка SelectExtendedField + Default-Dropdown
|
|
38
|
-
├── index.ts
|
|
39
|
-
└── __tests__/
|
|
40
|
-
```
|
|
35
|
+
`ISelectFieldProps` собирается из трёх источников:
|
|
41
36
|
|
|
42
|
-
|
|
37
|
+
1. `Omit<ISelectExtendedFieldProps, "children" | "onChange" | "renderTarget">` — то есть
|
|
38
|
+
`onOpen`, `onClose`, `closeOnTab` и все атрибуты `React.HTMLAttributes<HTMLDivElement>`,
|
|
39
|
+
попадающие на корневой `<div>`;
|
|
40
|
+
2. `Pick<ISelectExtendedFieldTargetProps, "loading" | "status" | "placeholder">` — состояния
|
|
41
|
+
поля выбора;
|
|
42
|
+
3. собственные props списка.
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
### Обязательные props
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
| Prop | Тип | Описание |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `size` | `EComponentSize` | Размер. Задаётся один раз и применяется и к полю выбора, и к выпадающему блоку со списком |
|
|
49
|
+
| `options` | `ISelectFieldOption[]` | Список опций. `ISelectFieldOption` — это `ISelectExtendedFieldDefaultOption`: `id`, `value`, `label` плюс props `DropdownListItem` |
|
|
50
|
+
| `onChange` | `(option: ISelectFieldOption) => void` | Вызывается с **объектом опции** (не с `id` и не с `value`). Список после выбора закрывается сам |
|
|
47
51
|
|
|
48
|
-
|
|
49
|
-
extends Omit<ISelectExtendedFieldProps, "children" | "onChange" | "renderTarget">
|
|
50
|
-
extends Pick<ISelectExtendedFieldTargetProps, "loading" | "status" | "placeholder">
|
|
51
|
-
```
|
|
52
|
+
### Опциональные props
|
|
52
53
|
|
|
53
|
-
| Prop | Тип |
|
|
54
|
+
| Prop | Тип | По умолчанию | Описание |
|
|
54
55
|
|---|---|---|---|
|
|
55
|
-
| `
|
|
56
|
-
| `
|
|
57
|
-
| `
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
56
|
+
| `value` | `ISelectFieldOption` | — | Выбранное значение. С опциями списка сопоставляется по полю `id` — подсветка выбранной опции считается как `option.id === value.id` |
|
|
57
|
+
| `placeholder` | `React.ReactNode` | — | Отображается, пока `value` не задан |
|
|
58
|
+
| `status` | `EFormFieldStatus` | `EFormFieldStatus.DEFAULT` | Визуальное состояние поля. `DISABLED` блокирует открытие списка |
|
|
59
|
+
| `loading` | `boolean` | `false` | Вместо каретки — лоадер; список не раскрывается ни мышью, ни с клавиатуры |
|
|
60
|
+
| `targetProps` | `Omit<ISelectExtendedFieldTargetProps, "opened" \| "setOpened" \| "size">` | — | Уходит целиком в `SelectExtendedField.Target`. Здесь задаётся **обязательный** заголовок поля `fieldLabel`, а также `prefix`, `postfix`, `onClear` |
|
|
61
|
+
| `dropdownProps` | `ISelectExtendedFieldDropdownDefaultProps["dropdownProps"]` | — | Настройки `Dropdown` (`direction`, `alignment`, `className`, ...). `width`, `size`, `opened` и `targetRef` компонент задаёт сам |
|
|
62
|
+
| `dropdownListItemClassName` | `string` | — | Класс каждого элемента списка — и десктопного `DropdownListItem`, и мобильного `DropdownMobileListItem` |
|
|
63
|
+
| `mobileTitle` | `React.ReactNode` | — | Заголовок выпадающего блока в мобильном режиме. Отдельный prop: на мобильном список раскрывается на весь экран и `fieldLabel` в нём не виден |
|
|
64
|
+
| `closeOnTab` | `boolean` | `true` | Компонент передаёт `closeOnTab={true}`, но `{...rest}` идёт после, поэтому переданное потребителем значение выигрывает |
|
|
65
|
+
| `children` | `never` | — | Не принимаются: выпадающий блок компонент рендерит сам |
|
|
66
|
+
|
|
67
|
+
### Что компонент задаёт сам и переопределить нельзя
|
|
68
|
+
|
|
69
|
+
- **Ширина выпадающего блока** — всегда `EDropdownWidth.TARGET` (по ширине поля).
|
|
70
|
+
- **Содержимое выпадающего блока** — `SelectExtendedFieldDropdownDefault` со списком `options`
|
|
71
|
+
и мобильной версией. Другого содержимого не бывает; если нужно другое — это `SelectExtendedField`.
|
|
72
|
+
- **`opened` / `setOpened`** поля выбора — приходят от `SelectExtendedField` и спредятся
|
|
73
|
+
**после** `targetProps`, поэтому подменить их через `targetProps` нельзя.
|
|
74
|
+
|
|
75
|
+
### Ограничения
|
|
76
|
+
|
|
77
|
+
- Компонент **не управляемый снаружи** по открытости: prop'а `opened` нет, открыть список
|
|
78
|
+
программно невозможно.
|
|
79
|
+
- Значение не хранится внутри: `value` + `onChange` — контролируемый компонент, состояние ведёт
|
|
80
|
+
потребитель.
|
|
81
|
+
- Заголовок поля живёт в `targetProps.fieldLabel`, а не в собственном prop'е. `SelectField`
|
|
82
|
+
подставляет `fieldLabel={undefined}` до спреда `targetProps` — иначе обязательное поле
|
|
83
|
+
`ISelectExtendedFieldTargetProps` осталось бы незаполненным. Потребители, которым заголовок
|
|
84
|
+
не нужен, передают пустую строку (так делает `PaginationSelect`).
|
|
85
|
+
- `role="combobox"`, `aria-controls` и прочие атрибуты поля стоят **до** `{...targetProps}`,
|
|
86
|
+
поэтому потребитель может их перекрыть. Делать это без причины не стоит — сломается
|
|
87
|
+
aria-связка со списком.
|
|
76
88
|
|
|
77
89
|
---
|
|
78
90
|
|
|
79
|
-
##
|
|
91
|
+
## Дизайн-токены
|
|
80
92
|
|
|
81
|
-
|
|
93
|
+
Собственных токенов и собственных стилей у компонента нет — в папке нет `.module.less`.
|
|
94
|
+
Внешний вид целиком приходит из компонентов, на которых он собран:
|
|
82
95
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
```
|
|
96
|
+
- `FormField` и его части (`FormFieldLabel`, `FormFieldTarget`, `FormFieldPrefix`,
|
|
97
|
+
`FormFieldPostfix`, `FormFieldClear`) — группа `FormField`;
|
|
98
|
+
- `Dropdown` и `DropdownList` — группы `Dropdown` и `DropdownList`;
|
|
99
|
+
- `IconWrapper` вокруг каретки и `LoaderSmall` в состоянии загрузки — их собственные группы.
|
|
88
100
|
|
|
89
|
-
|
|
90
|
-
|
|
101
|
+
Переопределяются они через `ThemeProvider` (prop `tokens`) — см. `ThemeProvider-ai.md` →
|
|
102
|
+
«Как переопределять токены». Если понадобится собственное визуальное состояние поля выбора,
|
|
103
|
+
токен заводится в `SelectExtendedFieldTarget`, а не здесь.
|
|
91
104
|
|
|
92
|
-
|
|
105
|
+
---
|
|
93
106
|
|
|
94
|
-
|
|
95
|
-
<DropdownListContext.Provider value={{ activeDescendant, setActiveDescendant }}>
|
|
96
|
-
<SelectExtendedFieldDropdownDefault ... />
|
|
97
|
-
</DropdownListContext.Provider>
|
|
98
|
-
```
|
|
107
|
+
## Инварианты
|
|
99
108
|
|
|
100
|
-
-
|
|
101
|
-
|
|
109
|
+
- **`forwardRef`** — не убирать. `ref` уходит в `SelectExtendedField.Target`, то есть указывает
|
|
110
|
+
на `FormFieldTarget` — элемент со значением, а **не** на корневой `<div>` компонента.
|
|
111
|
+
Корневой `<div>` — это `SelectExtendedField`, и своей ссылки на него `SelectField` наружу не отдаёт.
|
|
112
|
+
- **Имена `SelectField`, `ISelectFieldProps`, `ISelectFieldOption`** и `displayName` — публичный API:
|
|
113
|
+
экспортируются из barrel `src/components/SelectField/index.ts` и из `src/components/index.ts`.
|
|
114
|
+
От `ISelectFieldProps` строятся `IChipSelectProps` (`Chip.Select`) и `IPaginationSelectProps`
|
|
115
|
+
(`Pagination`), `ISelectFieldOption` используется в тестах и stories `Chip` — переименование
|
|
116
|
+
ломает их.
|
|
117
|
+
- **`ISelectFieldOption` — пустой `extends ISelectExtendedFieldDefaultOption`.** Это осознанный
|
|
118
|
+
публичный алиас: потребитель не должен импортировать тип из чужой папки. Не схлопывать в
|
|
119
|
+
`type ... = ...` и не удалять.
|
|
120
|
+
- **Accessibility-контракт поля выбора** — `role="combobox"`, `aria-controls`,
|
|
121
|
+
`aria-activedescendant`, проброс `aria-labelledby`. Менять только вместе с `DropdownList`.
|
|
122
|
+
- **`aria-labelledby` не уходит в `...rest`**: он адресован полю выбора, а не корневому `<div>`.
|
|
123
|
+
Если вернуть его в `rest`, `PaginationSelect` потеряет связь с подписью пагинации.
|
|
124
|
+
- **Идентификатор списка стабилен на всё время жизни компонента** (`useState(() => uniqueId())`).
|
|
125
|
+
На него ссылается `aria-controls`; пересоздание на каждый рендер рвёт связку.
|
|
126
|
+
- **`EDropdownWidth.TARGET`** — часть дизайна компонента, а не деталь реализации: список всегда
|
|
127
|
+
по ширине поля.
|
|
128
|
+
- **React 17-совместимость** (ветка `release-0`): не использовать `useId`,
|
|
129
|
+
`useSyncExternalStore`, `useInsertionEffect` и поведение, зависящее от automatic batching.
|
|
130
|
+
Уникальный идентификатор берётся из `uniqueId` (`lodash-es`).
|
|
131
|
+
- **`SelectExtendedFieldDropdownDefault` импортируется по прямому пути**
|
|
132
|
+
(`components/SelectExtendedFieldDropdownDefault`) — он не экспортируется из barrel
|
|
133
|
+
`SelectExtendedField`. Не выносить его в публичный API «заодно».
|
|
102
134
|
|
|
103
|
-
|
|
135
|
+
---
|
|
104
136
|
|
|
105
|
-
|
|
106
|
-
const instanceId = useRef(uniqueId());
|
|
107
|
-
```
|
|
137
|
+
## Accessibility
|
|
108
138
|
|
|
109
|
-
-
|
|
110
|
-
|
|
139
|
+
- Поле выбора получает `role="combobox"` и `aria-controls` с идентификатором списка опций.
|
|
140
|
+
Идентификатор генерируется компонентом и передаётся в `SelectExtendedFieldDropdownDefault`
|
|
141
|
+
как `listId` — то есть в `id` реального `DropdownList`. Пока список закрыт (а также при
|
|
142
|
+
`loading`), выпадающего блока в DOM нет, поэтому `aria-controls` указывает на несуществующий
|
|
143
|
+
`id` — так же было и до появления этого документа. Скринридеры такую ссылку игнорируют,
|
|
144
|
+
а `aria-expanded` при этом корректно сообщает, что список свёрнут.
|
|
145
|
+
- `aria-activedescendant` отражает опцию, подсвеченную с клавиатуры. Значение поднимается снизу:
|
|
146
|
+
`DropdownList` кладёт `id` активного элемента в `DropdownListContext`, провайдер которого
|
|
147
|
+
`SelectField` рендерит вокруг выпадающего блока. Без этого провайдера связка не работает.
|
|
148
|
+
- `aria-expanded` и `aria-haspopup="listbox"` выставляет `SelectExtendedFieldTarget`;
|
|
149
|
+
роли `listbox` / `option` и навигацию `ArrowUp` / `ArrowDown` — `DropdownList`.
|
|
150
|
+
- Клавиши открытия списка: `Space`, `Enter`, `ArrowDown`, `ArrowUp`. Закрытие — `Escape`
|
|
151
|
+
(из любого места страницы), `Tab` (`closeOnTab`) и нажатие мышью вне поля и списка.
|
|
152
|
+
- В состояниях `loading` и `status={EFormFieldStatus.DISABLED}` поле не реагирует ни на клик,
|
|
153
|
+
ни на клавиатуру.
|
|
154
|
+
- **Доступное имя задаёт потребитель.** Компонент не хардкодит текст (библиотека мультиязычная):
|
|
155
|
+
имя приходит либо из `targetProps.fieldLabel`, либо из `aria-labelledby` — так поступает
|
|
156
|
+
`PaginationSelect`, связывая поле с подписью пагинации и передавая пустой `fieldLabel`.
|
|
157
|
+
- Фокус компонент не перехватывает и не возвращает: focus trap, если он нужен, добавляет потребитель.
|
|
111
158
|
|
|
112
|
-
|
|
159
|
+
---
|
|
113
160
|
|
|
114
|
-
|
|
115
|
-
<SelectExtendedField.Target
|
|
116
|
-
label={value?.label} placeholder={...} loading={...} status={...}
|
|
117
|
-
role="combobox"
|
|
118
|
-
aria-controls={instanceId.current}
|
|
119
|
-
aria-activedescendant={activeDescendant}
|
|
120
|
-
aria-labelledby={ariaLabelledby}
|
|
121
|
-
ref={setRef}
|
|
122
|
-
size={size}
|
|
123
|
-
fieldLabel={undefined}
|
|
124
|
-
{...targetProps} // потребительские targetProps — переопределяют дефолты
|
|
125
|
-
{...props} // provide-props (opened, setOpened) — переопределяют всё
|
|
126
|
-
/>
|
|
127
|
-
```
|
|
161
|
+
## Связанные компоненты
|
|
128
162
|
|
|
129
|
-
-
|
|
130
|
-
|
|
163
|
+
- `SelectExtendedField` — база. `ISelectFieldProps` строится из его props, а сам компонент
|
|
164
|
+
рендерит `SelectExtendedField` с `SelectExtendedField.Target` в `renderTarget` и
|
|
165
|
+
`SelectExtendedFieldDropdownDefault` в `children`. Любая правка в нём отражается на `SelectField`.
|
|
166
|
+
- `MultiselectField` — множественный выбор с тегами, тоже на `SelectExtendedField`.
|
|
167
|
+
Альтернатива, когда значений может быть несколько.
|
|
168
|
+
- `SuggestField` — поле выбора с поиском по вводу. Собран на `Dropdown` напрямую, не на
|
|
169
|
+
`SelectExtendedField`, поэтому правки в `SelectField` его не затрагивают.
|
|
170
|
+
- `Chip` — `Chip.Select` и `Chip.Sort` решают ту же задачу в виде фильтр-чипа;
|
|
171
|
+
`IChipSelectProps` собирается из `ISelectFieldProps`.
|
|
172
|
+
- `Dropdown` — выпадающий блок, `DropdownList` / `DropdownListItem` и `DropdownListContext`,
|
|
173
|
+
через который поднимается активная опция для `aria-activedescendant`.
|
|
174
|
+
- `Pagination` — потребитель внутри библиотеки: `PaginationSelect` оборачивает `SelectField`
|
|
175
|
+
для выбора количества элементов на странице и опирается на проброс `aria-labelledby`
|
|
176
|
+
и на пустой `targetProps.fieldLabel`.
|
|
131
177
|
|
|
132
|
-
|
|
178
|
+
---
|
|
133
179
|
|
|
134
|
-
|
|
135
|
-
- **`width: EDropdownWidth.TARGET`** — dropdown ровно ширины target'а.
|
|
136
|
-
- **`role="combobox"`** на target.
|
|
137
|
-
- **Default Dropdown layout** (desktop List + mobile bottom-sheet) — нельзя заменить (для нестандартного — использовать `SelectExtendedField`).
|
|
180
|
+
## Stories
|
|
138
181
|
|
|
139
|
-
|
|
182
|
+
Основные истории: `stories/SelectField/SelectField.stories.tsx`
|
|
183
|
+
Файлы примеров: `stories/SelectField/examples/`
|
|
140
184
|
|
|
141
|
-
|
|
185
|
+
| Story | Example file | Что демонстрирует |
|
|
186
|
+
|---|---|---|
|
|
187
|
+
| `Playground` | `Playground.tsx` | Интерактивный контроль `size`, `status`, `loading`, `placeholder`, заголовка поля и описания под ним |
|
|
188
|
+
| `Default` | `Default.tsx` | Минимальный сценарий: список опций и выбранное значение в состоянии потребителя |
|
|
189
|
+
| `Sizes` | `Sizes.tsx` | Размеры SM / MD / LG |
|
|
190
|
+
| `Statuses` | `Statuses.tsx` | Состояния поля: DEFAULT / DISABLED / ERROR / WARNING |
|
|
191
|
+
| `Loading` | `Loading.tsx` | Состояние загрузки: лоадер вместо каретки, список не раскрывается |
|
|
192
|
+
| `WithDescription` | `WithDescription.tsx` | Композиция с `FormGroup` и `FormFieldDescription`: описание и текст ошибки под полем |
|
|
193
|
+
| `Example` | `Example.tsx` | Production-like композиция: зависимые списки «Организация» → «Счёт списания» |
|
|
194
|
+
| `VisualTests` | `VisualTests.tsx` | Скриншот-регрессия: раскрытый список с подсвеченной опцией (через `play`), длинное значение, загрузка в размере SM, пустое заблокированное поле |
|
|
142
195
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
```
|
|
196
|
+
`Playground` исключён из скриншот-тестов, `WithDescription` — тоже: поле в нём повторяет
|
|
197
|
+
состояния из `Default` и `Statuses`.
|
|
146
198
|
|
|
147
|
-
|
|
199
|
+
---
|
|
148
200
|
|
|
149
|
-
|
|
201
|
+
## История изменений
|
|
150
202
|
|
|
151
|
-
|
|
203
|
+
| Дата | Изменение |
|
|
204
|
+
|---|---|
|
|
205
|
+
| 2026-09-03 | Создан документ (TRI-75). AI-рефакторинг: удалён мёртвый внутренний `targetRef`, внешний `ref` пробрасывается напрямую и больше не пересоздаётся на каждый рендер; идентификатор списка переведён с `useRef(uniqueId())` на `useState(() => uniqueId())`, чтобы счётчик не крутился на каждом рендере; параметры рендер-функций перестали затенять `props` компонента; импорты приведены к алиасу пакета; JSDoc на всех props и на компоненте. Unit-тесты расширены с 15 до 24 кейсов (`onChange` с аргументом, опции и выбранное значение, `aria-activedescendant` через `DropdownListContext`, стабильность `listId`, ref-target). Stories переведены на modern pattern, `States` разделена на `Statuses` и `Loading`. Публичный API не изменён |
|
|
152
206
|
|
|
153
207
|
---
|
|
154
208
|
|
|
155
|
-
##
|
|
209
|
+
## Примеры использования
|
|
156
210
|
|
|
157
|
-
|
|
158
|
-
- **`closeOnTab=true`** захардкожен.
|
|
159
|
-
- **`width=TARGET`** захардкожен.
|
|
160
|
-
- **`role="combobox"`** на target — публичный a11y-контракт.
|
|
161
|
-
- **`aria-controls`** + **`aria-activedescendant`** связывают target и list через `instanceId` и `DropdownListContext`.
|
|
162
|
-
- **`uniqueId()` через `useRef`** — SSR-небезопасно (тех-долг).
|
|
163
|
-
- **`children: never`** — запрещены.
|
|
164
|
-
- **`option.id`** — обязательный (наследуется от `IDropdownListItemProps`); используется как `key` и `selected`.
|
|
165
|
-
- **`targetProps` мержатся между defaults и provide-props** — порядок важен.
|
|
166
|
-
- **`fieldLabel` по умолчанию `undefined`** — потребитель добавляет через `targetProps.fieldLabel`.
|
|
167
|
-
- **`loading` блокирует Dropdown** (`opened && !loading`).
|
|
168
|
-
- **Default Dropdown layout фиксирован** — для кастомного использовать `SelectExtendedField`.
|
|
169
|
-
- **`forwardRef<HTMLDivElement>`** — на target ref.
|
|
211
|
+
### Default
|
|
170
212
|
|
|
171
|
-
|
|
213
|
+
`stories/SelectField/examples/Default.tsx`
|
|
172
214
|
|
|
173
|
-
|
|
215
|
+
```tsx
|
|
216
|
+
import React, { useState } from "react";
|
|
217
|
+
import { EComponentSize, ISelectFieldOption, SelectField } from "@sberbusiness/triplex-next";
|
|
218
|
+
|
|
219
|
+
const OPTIONS: ISelectFieldOption[] = [
|
|
220
|
+
{ id: "option1", value: "option1", label: "Первая опция" },
|
|
221
|
+
{ id: "option2", value: "option2", label: "Вторая опция" },
|
|
222
|
+
{ id: "option3", value: "option3", label: "Третья опция" },
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
export const Default = () => {
|
|
226
|
+
const [value, setValue] = useState<ISelectFieldOption>();
|
|
227
|
+
|
|
228
|
+
return (
|
|
229
|
+
<div style={{ maxWidth: "300px" }}>
|
|
230
|
+
<SelectField
|
|
231
|
+
size={EComponentSize.MD}
|
|
232
|
+
value={value}
|
|
233
|
+
options={OPTIONS}
|
|
234
|
+
onChange={setValue}
|
|
235
|
+
placeholder="Не выбрано"
|
|
236
|
+
targetProps={{ fieldLabel: "Выберите опцию" }}
|
|
237
|
+
mobileTitle="Выберите опцию"
|
|
238
|
+
/>
|
|
239
|
+
</div>
|
|
240
|
+
);
|
|
241
|
+
};
|
|
242
|
+
```
|
|
174
243
|
|
|
175
|
-
|
|
176
|
-
- `aria-labelledby` — потребитель.
|
|
177
|
-
- ESC закрывает (от SelectExtendedField).
|
|
178
|
-
- `closeOnTab=true` — Tab закрывает (UX-выбор).
|
|
179
|
-
- `loading` — клики/keys блокированы (с known bug в keydown — см. SelectExtendedField-AI).
|
|
180
|
-
- **`uniqueId()` SSR-небезопасно** — может привести к несовпадению `id` между сервером и клиентом.
|
|
244
|
+
### Sizes
|
|
181
245
|
|
|
182
|
-
|
|
246
|
+
`stories/SelectField/examples/Sizes.tsx`
|
|
183
247
|
|
|
184
|
-
|
|
248
|
+
```tsx
|
|
249
|
+
import React, { useState } from "react";
|
|
250
|
+
import { EComponentSize, ISelectFieldOption, SelectField } from "@sberbusiness/triplex-next";
|
|
251
|
+
|
|
252
|
+
const OPTIONS: ISelectFieldOption[] = [
|
|
253
|
+
{ id: "option1", value: "option1", label: "Первая опция" },
|
|
254
|
+
{ id: "option2", value: "option2", label: "Вторая опция" },
|
|
255
|
+
{ id: "option3", value: "option3", label: "Третья опция" },
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
const SIZES = Object.values(EComponentSize);
|
|
259
|
+
|
|
260
|
+
interface ISizeItemProps {
|
|
261
|
+
size: EComponentSize;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const SizeItem = ({ size }: ISizeItemProps) => {
|
|
265
|
+
const [value, setValue] = useState<ISelectFieldOption | undefined>(OPTIONS[0]);
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<div>
|
|
269
|
+
<div style={{ marginBottom: "8px", fontSize: "16px", fontWeight: "700" }}>{size.toUpperCase()}</div>
|
|
270
|
+
<SelectField
|
|
271
|
+
size={size}
|
|
272
|
+
value={value}
|
|
273
|
+
options={OPTIONS}
|
|
274
|
+
onChange={setValue}
|
|
275
|
+
placeholder="Не выбрано"
|
|
276
|
+
targetProps={{ fieldLabel: "Выберите опцию" }}
|
|
277
|
+
mobileTitle="Выберите опцию"
|
|
278
|
+
/>
|
|
279
|
+
</div>
|
|
280
|
+
);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
export const Sizes = () => (
|
|
284
|
+
<div style={{ maxWidth: "300px", display: "flex", flexDirection: "column", gap: "16px" }}>
|
|
285
|
+
{SIZES.map((size) => (
|
|
286
|
+
<SizeItem key={size} size={size} />
|
|
287
|
+
))}
|
|
288
|
+
</div>
|
|
289
|
+
);
|
|
290
|
+
```
|
|
185
291
|
|
|
186
|
-
|
|
292
|
+
### Statuses
|
|
187
293
|
|
|
188
|
-
|
|
294
|
+
`stories/SelectField/examples/Statuses.tsx`
|
|
189
295
|
|
|
190
|
-
|
|
296
|
+
```tsx
|
|
297
|
+
import React, { useState } from "react";
|
|
298
|
+
import { EComponentSize, EFormFieldStatus, ISelectFieldOption, SelectField } from "@sberbusiness/triplex-next";
|
|
299
|
+
|
|
300
|
+
const OPTIONS: ISelectFieldOption[] = [
|
|
301
|
+
{ id: "option1", value: "option1", label: "Первая опция" },
|
|
302
|
+
{ id: "option2", value: "option2", label: "Вторая опция" },
|
|
303
|
+
{ id: "option3", value: "option3", label: "Третья опция" },
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
const STATUSES = Object.values(EFormFieldStatus);
|
|
307
|
+
|
|
308
|
+
interface IStatusItemProps {
|
|
309
|
+
status: EFormFieldStatus;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const StatusItem = ({ status }: IStatusItemProps) => {
|
|
313
|
+
const [value, setValue] = useState<ISelectFieldOption | undefined>(OPTIONS[0]);
|
|
314
|
+
|
|
315
|
+
return (
|
|
316
|
+
<div>
|
|
317
|
+
<div style={{ marginBottom: "8px", fontSize: "16px", fontWeight: "700" }}>{status.toUpperCase()}</div>
|
|
318
|
+
<SelectField
|
|
319
|
+
size={EComponentSize.LG}
|
|
320
|
+
status={status}
|
|
321
|
+
value={value}
|
|
322
|
+
options={OPTIONS}
|
|
323
|
+
onChange={setValue}
|
|
324
|
+
placeholder="Не выбрано"
|
|
325
|
+
targetProps={{ fieldLabel: "Выберите опцию" }}
|
|
326
|
+
mobileTitle="Выберите опцию"
|
|
327
|
+
/>
|
|
328
|
+
</div>
|
|
329
|
+
);
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
export const Statuses = () => (
|
|
333
|
+
<div style={{ maxWidth: "300px", display: "flex", flexDirection: "column", gap: "16px" }}>
|
|
334
|
+
{STATUSES.map((status) => (
|
|
335
|
+
<StatusItem key={status} status={status} />
|
|
336
|
+
))}
|
|
337
|
+
</div>
|
|
338
|
+
);
|
|
339
|
+
```
|
|
191
340
|
|
|
192
|
-
|
|
193
|
-
- `MultiselectField` — multi-выбор.
|
|
194
|
-
- `SelectExtendedFieldDropdownDefault` — переиспользуется для default-раскладки.
|
|
195
|
-
- `Dropdown` (+`DropdownList`+`DropdownMobile*`) — popover.
|
|
196
|
-
- `DropdownListContext` — для `activeDescendant`.
|
|
197
|
-
- `EDropdownWidth.TARGET`, `EComponentSize`.
|
|
198
|
-
- `lodash uniqueId`.
|
|
341
|
+
### Loading
|
|
199
342
|
|
|
200
|
-
|
|
343
|
+
`stories/SelectField/examples/Loading.tsx`
|
|
201
344
|
|
|
202
|
-
|
|
345
|
+
```tsx
|
|
346
|
+
import React, { useState } from "react";
|
|
347
|
+
import { EComponentSize, ISelectFieldOption, SelectField } from "@sberbusiness/triplex-next";
|
|
348
|
+
|
|
349
|
+
const OPTIONS: ISelectFieldOption[] = [
|
|
350
|
+
{ id: "option1", value: "option1", label: "Первая опция" },
|
|
351
|
+
{ id: "option2", value: "option2", label: "Вторая опция" },
|
|
352
|
+
{ id: "option3", value: "option3", label: "Третья опция" },
|
|
353
|
+
];
|
|
354
|
+
|
|
355
|
+
export const Loading = () => {
|
|
356
|
+
const [value, setValue] = useState<ISelectFieldOption>();
|
|
357
|
+
|
|
358
|
+
return (
|
|
359
|
+
<div style={{ maxWidth: "300px" }}>
|
|
360
|
+
<SelectField
|
|
361
|
+
size={EComponentSize.LG}
|
|
362
|
+
loading
|
|
363
|
+
value={value}
|
|
364
|
+
options={OPTIONS}
|
|
365
|
+
onChange={setValue}
|
|
366
|
+
placeholder="Не выбрано"
|
|
367
|
+
targetProps={{ fieldLabel: "Загрузка опций" }}
|
|
368
|
+
mobileTitle="Загрузка опций"
|
|
369
|
+
/>
|
|
370
|
+
</div>
|
|
371
|
+
);
|
|
372
|
+
};
|
|
373
|
+
```
|
|
203
374
|
|
|
204
|
-
|
|
205
|
-
Примеры: `stories/SelectField/examples/`
|
|
375
|
+
### WithDescription
|
|
206
376
|
|
|
207
|
-
|
|
377
|
+
`stories/SelectField/examples/WithDescription.tsx`
|
|
208
378
|
|
|
209
|
-
|
|
379
|
+
```tsx
|
|
380
|
+
import React, { useState } from "react";
|
|
381
|
+
import {
|
|
382
|
+
EComponentSize,
|
|
383
|
+
EFontType,
|
|
384
|
+
EFormFieldStatus,
|
|
385
|
+
ETextSize,
|
|
386
|
+
FormFieldDescription,
|
|
387
|
+
FormGroup,
|
|
388
|
+
ISelectFieldOption,
|
|
389
|
+
SelectField,
|
|
390
|
+
Text,
|
|
391
|
+
} from "@sberbusiness/triplex-next";
|
|
392
|
+
|
|
393
|
+
const OPTIONS: ISelectFieldOption[] = [
|
|
394
|
+
{ id: "option1", value: "option1", label: "Первая опция" },
|
|
395
|
+
{ id: "option2", value: "option2", label: "Вторая опция" },
|
|
396
|
+
{ id: "option3", value: "option3", label: "Третья опция" },
|
|
397
|
+
];
|
|
398
|
+
|
|
399
|
+
export const WithDescription = () => {
|
|
400
|
+
const [value, setValue] = useState<ISelectFieldOption>();
|
|
401
|
+
// Поле считается тронутым после первого закрытия списка: до взаимодействия ошибку не показываем.
|
|
402
|
+
const [touched, setTouched] = useState(false);
|
|
403
|
+
|
|
404
|
+
const status = touched && !value ? EFormFieldStatus.ERROR : EFormFieldStatus.DEFAULT;
|
|
405
|
+
|
|
406
|
+
return (
|
|
407
|
+
<div style={{ maxWidth: "300px" }}>
|
|
408
|
+
<FormGroup>
|
|
409
|
+
<SelectField
|
|
410
|
+
size={EComponentSize.LG}
|
|
411
|
+
status={status}
|
|
412
|
+
value={value}
|
|
413
|
+
options={OPTIONS}
|
|
414
|
+
onChange={setValue}
|
|
415
|
+
onClose={() => setTouched(true)}
|
|
416
|
+
placeholder="Не выбрано"
|
|
417
|
+
targetProps={{ fieldLabel: "Выберите опцию" }}
|
|
418
|
+
mobileTitle="Выберите опцию"
|
|
419
|
+
/>
|
|
420
|
+
<FormFieldDescription>
|
|
421
|
+
<Text
|
|
422
|
+
tag="div"
|
|
423
|
+
size={ETextSize.B4}
|
|
424
|
+
type={status === EFormFieldStatus.ERROR ? EFontType.ERROR : EFontType.SECONDARY}
|
|
425
|
+
>
|
|
426
|
+
{status === EFormFieldStatus.ERROR ? "Обязательное поле" : "Значение можно изменить позже"}
|
|
427
|
+
</Text>
|
|
428
|
+
</FormFieldDescription>
|
|
429
|
+
</FormGroup>
|
|
430
|
+
</div>
|
|
431
|
+
);
|
|
432
|
+
};
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Example
|
|
436
|
+
|
|
437
|
+
`stories/SelectField/examples/Example.tsx`
|
|
438
|
+
|
|
439
|
+
```tsx
|
|
440
|
+
import React, { useState } from "react";
|
|
441
|
+
import {
|
|
442
|
+
EComponentSize,
|
|
443
|
+
EFontType,
|
|
444
|
+
EFormFieldStatus,
|
|
445
|
+
ETextSize,
|
|
446
|
+
ETooltipSize,
|
|
447
|
+
FormFieldDescription,
|
|
448
|
+
FormGroup,
|
|
449
|
+
HelpBox,
|
|
450
|
+
ISelectFieldOption,
|
|
451
|
+
SelectField,
|
|
452
|
+
Text,
|
|
453
|
+
} from "@sberbusiness/triplex-next";
|
|
454
|
+
|
|
455
|
+
/** Организации, доступные пользователю. */
|
|
456
|
+
const ORGANIZATIONS: ISelectFieldOption[] = [
|
|
457
|
+
{ id: "alpha", value: "alpha", label: "ООО «Альфа»" },
|
|
458
|
+
{ id: "beta", value: "beta", label: "ООО «Бета»" },
|
|
459
|
+
];
|
|
460
|
+
|
|
461
|
+
/** Счета в разрезе организации: список второго поля зависит от выбора в первом. */
|
|
462
|
+
const ACCOUNTS_BY_ORGANIZATION: Record<string, ISelectFieldOption[]> = {
|
|
463
|
+
alpha: [
|
|
464
|
+
{ id: "alpha-rub", value: "alpha-rub", label: "40702810 ··· 4501 — рубли" },
|
|
465
|
+
{ id: "alpha-usd", value: "alpha-usd", label: "40702840 ··· 7712 — доллары" },
|
|
466
|
+
],
|
|
467
|
+
beta: [
|
|
468
|
+
{ id: "beta-rub", value: "beta-rub", label: "40702810 ··· 9034 — рубли" },
|
|
469
|
+
{ id: "beta-eur", value: "beta-eur", label: "40702978 ··· 2288 — евро" },
|
|
470
|
+
],
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
export const Example = () => {
|
|
474
|
+
const [organization, setOrganization] = useState<ISelectFieldOption>();
|
|
475
|
+
const [account, setAccount] = useState<ISelectFieldOption>();
|
|
476
|
+
|
|
477
|
+
const accounts = organization ? ACCOUNTS_BY_ORGANIZATION[organization.id] : [];
|
|
478
|
+
|
|
479
|
+
/** Смена организации сбрасывает счёт: выбранный счёт относился к другой организации. */
|
|
480
|
+
const handleOrganizationChange = (option: ISelectFieldOption) => {
|
|
481
|
+
setOrganization(option);
|
|
482
|
+
setAccount(undefined);
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const handleOrganizationClear = () => {
|
|
486
|
+
setOrganization(undefined);
|
|
487
|
+
setAccount(undefined);
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
return (
|
|
491
|
+
<div style={{ maxWidth: "320px", display: "flex", flexDirection: "column", gap: "16px" }}>
|
|
492
|
+
<FormGroup>
|
|
493
|
+
<SelectField
|
|
494
|
+
size={EComponentSize.LG}
|
|
495
|
+
value={organization}
|
|
496
|
+
options={ORGANIZATIONS}
|
|
497
|
+
onChange={handleOrganizationChange}
|
|
498
|
+
placeholder="Не выбрана"
|
|
499
|
+
targetProps={{
|
|
500
|
+
fieldLabel: "Организация",
|
|
501
|
+
onClear: organization ? handleOrganizationClear : undefined,
|
|
502
|
+
postfix: (
|
|
503
|
+
<HelpBox tooltipSize={ETooltipSize.SM}>Организации, к которым у вас есть доступ.</HelpBox>
|
|
504
|
+
),
|
|
505
|
+
}}
|
|
506
|
+
mobileTitle="Организация"
|
|
507
|
+
/>
|
|
508
|
+
</FormGroup>
|
|
509
|
+
|
|
510
|
+
<FormGroup>
|
|
511
|
+
<SelectField
|
|
512
|
+
size={EComponentSize.LG}
|
|
513
|
+
status={organization ? EFormFieldStatus.DEFAULT : EFormFieldStatus.DISABLED}
|
|
514
|
+
value={account}
|
|
515
|
+
options={accounts}
|
|
516
|
+
onChange={setAccount}
|
|
517
|
+
placeholder="Не выбран"
|
|
518
|
+
targetProps={{ fieldLabel: "Счёт списания" }}
|
|
519
|
+
mobileTitle="Счёт списания"
|
|
520
|
+
/>
|
|
521
|
+
<FormFieldDescription>
|
|
522
|
+
<Text tag="div" size={ETextSize.B4} type={EFontType.SECONDARY}>
|
|
523
|
+
{organization ? "Доступны счета выбранной организации" : "Сначала выберите организацию"}
|
|
524
|
+
</Text>
|
|
525
|
+
</FormFieldDescription>
|
|
526
|
+
</FormGroup>
|
|
527
|
+
</div>
|
|
528
|
+
);
|
|
529
|
+
};
|
|
530
|
+
```
|
|
210
531
|
|
|
211
|
-
| Дата | Изменение |
|
|
212
|
-
|---|---|
|
|
213
|
-
| 2026-04-18 | Создан документ |
|