@sberbusiness/triplex-next-mcp-server 1.44.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.
Files changed (76) hide show
  1. package/README.md +7 -6
  2. package/bundle/components/AlertContext.md +11 -8
  3. package/bundle/components/AlertProcess.md +13 -10
  4. package/bundle/components/Badge.md +6 -3
  5. package/bundle/components/Button.md +46 -43
  6. package/bundle/components/ButtonDropdownExtended.md +3 -2
  7. package/bundle/components/ButtonIcon.md +7 -4
  8. package/bundle/components/Calendar.md +62 -59
  9. package/bundle/components/Caption.md +26 -23
  10. package/bundle/components/Checkbox.md +32 -29
  11. package/bundle/components/CheckboxTreeExtended.md +9 -5
  12. package/bundle/components/Chip.md +44 -41
  13. package/bundle/components/CodeText.md +26 -23
  14. package/bundle/components/Col.md +1 -2
  15. package/bundle/components/CollapsibleTree.md +8 -5
  16. package/bundle/components/Dropdown.md +33 -28
  17. package/bundle/components/EmptyView.md +12 -5
  18. package/bundle/components/FooterPage.md +6 -2
  19. package/bundle/components/FormField.md +44 -41
  20. package/bundle/components/FormGroup.md +192 -52
  21. package/bundle/components/Header.md +491 -211
  22. package/bundle/components/HeaderPage.md +6 -2
  23. package/bundle/components/ImageGallery.md +33 -34
  24. package/bundle/components/ImageGalleryExtended.md +33 -34
  25. package/bundle/components/Island.md +16 -13
  26. package/bundle/components/LightBox.md +14 -12
  27. package/bundle/components/Link.md +211 -96
  28. package/bundle/components/ListActionItem.md +7 -4
  29. package/bundle/components/ListItem.md +11 -8
  30. package/bundle/components/ListItemControlsButton.md +16 -13
  31. package/bundle/components/ListItemControlsButtonDropdown.md +9 -6
  32. package/bundle/components/ListItemSelectable.md +7 -4
  33. package/bundle/components/ListMasterFooter.md +8 -5
  34. package/bundle/components/ListMasterHeader.md +8 -5
  35. package/bundle/components/ListSortableItem.md +11 -8
  36. package/bundle/components/ListTableItem.md +7 -4
  37. package/bundle/components/LoaderMiddle.md +7 -4
  38. package/bundle/components/LoaderScreen.md +11 -8
  39. package/bundle/components/LoaderSmall.md +7 -4
  40. package/bundle/components/Marker.md +184 -69
  41. package/bundle/components/MarkerStatus.md +206 -88
  42. package/bundle/components/MaskedField.md +2 -4
  43. package/bundle/components/MasterTable.md +602 -0
  44. package/bundle/components/ModalWindow.md +7 -4
  45. package/bundle/components/MultiselectField.md +2183 -132
  46. package/bundle/components/Notification.md +12 -9
  47. package/bundle/components/Overlay.md +9 -4
  48. package/bundle/components/Page.md +2 -2
  49. package/bundle/components/Pagination.md +15 -8
  50. package/bundle/components/Row.md +1 -1
  51. package/bundle/components/SelectExtendedField.md +3 -3
  52. package/bundle/components/SelectField.md +466 -148
  53. package/bundle/components/SliderExtended.md +39 -36
  54. package/bundle/components/SmallInput.md +194 -51
  55. package/bundle/components/Suggest.md +535 -99
  56. package/bundle/components/SwipeableArea.md +428 -79
  57. package/bundle/components/TableBasic.md +19 -11
  58. package/bundle/components/TabsExtended.md +24 -21
  59. package/bundle/components/Tag.md +6 -2
  60. package/bundle/components/Text.md +28 -25
  61. package/bundle/components/TextField.md +3 -4
  62. package/bundle/components/ThemeProvider.md +50 -1
  63. package/bundle/components/Title.md +26 -23
  64. package/bundle/components/Tooltip.md +24 -21
  65. package/bundle/components/TopOverlay.md +7 -4
  66. package/bundle/guides/codestyle.md +3 -0
  67. package/bundle/guides/context.md +84 -12
  68. package/bundle/guides/guidelines.md +321 -0
  69. package/bundle/guides/template.md +22 -12
  70. package/bundle/manifest.json +31 -14
  71. package/bundle/release-notes/1.45.0.mdx +117 -0
  72. package/bundle/release-notes/1.46.0.mdx +7 -0
  73. package/bundle/tokens.json +6362 -0
  74. package/dist/index.js +177 -28
  75. package/dist/index.js.map +1 -1
  76. package/package.json +1 -1
@@ -1,9 +1,7 @@
1
1
  ---
2
2
  component: SelectField
3
- category: Form Controls
4
- figma-node: ""
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` с фиксированной раскладкой target'а и dropdown'а:
18
- - Target = `SelectExtendedField.Target` (FormField + caret + clear).
19
- - Dropdown = `SelectExtendedFieldDropdownDefault` (desktop list +
20
- mobile bottom-sheet) c шириной `EDropdownWidth.TARGET`.
14
+ Готовый Select со списком опций: поле выбора и выпадающий блок собраны заранее. Тонкий слой над
15
+ `SelectExtendedField` — состоянием открытости владеет он, а `SelectField` отвечает за подстановку
16
+ готового поля выбора (`SelectExtendedField.Target`), готового списка
17
+ (`SelectExtendedFieldDropdownDefault`) и за aria-связку между ними.
21
18
 
22
- Управляет:
23
- - `activeDescendant` через `DropdownListContext` — для a11y `aria-activedescendant`.
24
- - `instanceId` через `lodash uniqueId()` — связывает Target ↔ List через `aria-controls`.
25
- - **`closeOnTab = true`** захардкожен.
19
+ Используй когда: нужен обычный выбор одного значения из плоского списка опций с текстовыми
20
+ подписями — вместе с мобильной версией списка и клавиатурной навигацией «из коробки».
26
21
 
27
- Используй когда: типичный select из списка опций. Для multi-выбора —
28
- `MultiselectField`. Для нестандартного target/dropdown —
29
- `SelectExtendedField` напрямую.
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
- ```text
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
- ## Варианты и props
44
+ ### Обязательные props
45
45
 
46
- ### `ISelectFieldProps`
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
- ```ts
49
- extends Omit<ISelectExtendedFieldProps, "children" | "onChange" | "renderTarget">
50
- extends Pick<ISelectExtendedFieldTargetProps, "loading" | "status" | "placeholder">
51
- ```
52
+ ### Опциональные props
52
53
 
53
- | Prop | Тип | Default | Описание |
54
+ | Prop | Тип | По умолчанию | Описание |
54
55
  |---|---|---|---|
55
- | `size` | `EComponentSize` | — (обязателен) | Размер. |
56
- | `value?` | `ISelectFieldOption` | — | Текущая выбранная опция. |
57
- | `options` | `ISelectFieldOption[]` | — | Список опций. |
58
- | `onChange` | `(option: ISelectFieldOption) => void` | — | Колбэк выбора. |
59
- | `placeholder?` | `ReactNode` | — | На target. |
60
- | `loading?` | `boolean` | — | Спиннер в target, dropdown НЕ открывается. |
61
- | `status?` | `EFormFieldStatus` | — | DISABLED блокирует target. |
62
- | `targetProps?` | `Omit<ISelectExtendedFieldTargetProps, "opened" \| "setOpened" \| "size">` | — | Доп. props для target (spread'ятся **после** дефолтных, но **до** provide-props). |
63
- | `dropdownListItemClassName?` | `string` | — | className для каждого `DropdownListItem` / mobile-item. |
64
- | `mobileTitle?` | `ReactNode` | — | Заголовок mobile bottom-sheet. |
65
- | `aria-labelledby?` | `string` | — | На target. |
66
- | `children?` | `never` | — | Запрещено. |
67
- | `...ISelectExtendedFieldProps` | `onOpen`, `onClose`, ... | — | Прокидывается. **`closeOnTab` зашит в `true`**, передача игнорируется. |
68
-
69
- ### `ISelectFieldOption extends ISelectExtendedFieldDefaultOption`
70
-
71
- ```ts
72
- { value: string; label: ReactNode; ...IDropdownListItemProps без active/onSelect/selected/key/className/keyCodesForSelection }
73
- ```
74
-
75
- - **`id`** обязателен (часть `IDropdownListItemProps`) — используется как `key` и `selected={option.id === value?.id}`.
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
- ### Layered render-prop'ы
93
+ Собственных токенов и собственных стилей у компонента нет — в папке нет `.module.less`.
94
+ Внешний вид целиком приходит из компонентов, на которых он собран:
82
95
 
83
- ```tsx
84
- <SelectExtendedField renderTarget={renderTarget} closeOnTab={true}>
85
- {renderDropdown}
86
- </SelectExtendedField>
87
- ```
96
+ - `FormField` и его части (`FormFieldLabel`, `FormFieldTarget`, `FormFieldPrefix`,
97
+ `FormFieldPostfix`, `FormFieldClear`) — группа `FormField`;
98
+ - `Dropdown` и `DropdownList` — группы `Dropdown` и `DropdownList`;
99
+ - `IconWrapper` вокруг каретки и `LoaderSmall` в состоянии загрузки — их собственные группы.
88
100
 
89
- - `renderTarget` рендерит `SelectExtendedField.Target` с `value?.label`.
90
- - `renderDropdown` рендерит `SelectExtendedFieldDropdownDefault` с `width=TARGET`.
101
+ Переопределяются они через `ThemeProvider` (prop `tokens`) — см. `ThemeProvider-ai.md` →
102
+ «Как переопределять токены». Если понадобится собственное визуальное состояние поля выбора,
103
+ токен заводится в `SelectExtendedFieldTarget`, а не здесь.
91
104
 
92
- ### `DropdownListContext` — `activeDescendant` для a11y
105
+ ---
93
106
 
94
- ```tsx
95
- <DropdownListContext.Provider value={{ activeDescendant, setActiveDescendant }}>
96
- <SelectExtendedFieldDropdownDefault ... />
97
- </DropdownListContext.Provider>
98
- ```
107
+ ## Инварианты
99
108
 
100
- - `DropdownList` items пишут свой `id` в `activeDescendant` при keyboard-навигации.
101
- - Target читает `activeDescendant` и ставит `aria-activedescendant` — корректная семантика combobox.
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
- ### `instanceId = uniqueId()` через `useRef`
135
+ ---
104
136
 
105
- ```ts
106
- const instanceId = useRef(uniqueId());
107
- ```
137
+ ## Accessibility
108
138
 
109
- - **SSR-небезопасно** (`uniqueId` глобальный счётчик).
110
- - Используется как `aria-controls` на target и `id` на List — связывает их.
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
- ### Target props order
159
+ ---
113
160
 
114
- ```tsx
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
- - Порядок: defaults → `targetProps` (потребитель) → `provide-props` (system).
130
- - **`fieldLabel: undefined`** по умолчанию — потребитель должен передать через `targetProps.fieldLabel`.
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
- - **`closeOnTab: true`**.
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
- ### `loading` блокирует открытие dropdown'а
182
+ Основные истории: `stories/SelectField/SelectField.stories.tsx`
183
+ Файлы примеров: `stories/SelectField/examples/`
140
184
 
141
- В `SelectExtendedFieldDropdownDefault`:
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
- ```tsx
144
- opened={opened && !loading}
145
- ```
196
+ `Playground` исключён из скриншот-тестов, `WithDescription` — тоже: поле в нём повторяет
197
+ состояния из `Default` и `Statuses`.
146
198
 
147
- - При `loading=true` dropdown не показывается, в target — спиннер.
199
+ ---
148
200
 
149
- ### `setRef` мерж forwardRef + targetRef
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
- - **`size` обязателен** (нет default).
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
- ## Accessibility
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
- - `role="combobox"` + `aria-haspopup="listbox"` (от Target) + `aria-expanded` + `aria-controls` + `aria-activedescendant` — каноническая combobox-семантика.
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
- Нет собственных — наследуются от `SelectExtendedField` / `Dropdown` / `FormField`.
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
- - `SelectExtendedField` — базовый, для кастомных Select'ов.
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
- ## Stories
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
- `stories/SelectField/SelectField.stories.tsx`
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 | Создан документ |