@sberbusiness/triplex-next-mcp-server 0.24.0-beta1
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 +124 -0
- package/bundle/.gitkeep +0 -0
- package/bundle/components/AbstractTree.md +198 -0
- package/bundle/components/AccordionBase.md +227 -0
- package/bundle/components/Alert.md +317 -0
- package/bundle/components/Amount.md +225 -0
- package/bundle/components/AmountField.md +289 -0
- package/bundle/components/Avatar.md +209 -0
- package/bundle/components/Badge.md +277 -0
- package/bundle/components/Body.md +167 -0
- package/bundle/components/Button.md +230 -0
- package/bundle/components/ButtonDropdown.md +159 -0
- package/bundle/components/ButtonIcon.md +122 -0
- package/bundle/components/Calendar.md +445 -0
- package/bundle/components/Card.md +374 -0
- package/bundle/components/CarouselExtended.md +250 -0
- package/bundle/components/Checkbox.md +126 -0
- package/bundle/components/CheckboxTree.md +280 -0
- package/bundle/components/CheckboxTreeExtended.md +321 -0
- package/bundle/components/CheckboxXGroup.md +68 -0
- package/bundle/components/CheckboxYGroup.md +67 -0
- package/bundle/components/Chip.md +404 -0
- package/bundle/components/ChipGroup.md +179 -0
- package/bundle/components/Col.md +221 -0
- package/bundle/components/CollapsableTree.md +255 -0
- package/bundle/components/Confirm.md +343 -0
- package/bundle/components/DateField.md +371 -0
- package/bundle/components/DatePickerExtended.md +342 -0
- package/bundle/components/DateRange.md +275 -0
- package/bundle/components/DesignTokens.md +385 -0
- package/bundle/components/Divider.md +196 -0
- package/bundle/components/DocumentNumberEdit.md +280 -0
- package/bundle/components/Dropdown.md +433 -0
- package/bundle/components/Ellipsis.md +208 -0
- package/bundle/components/EmptyView.md +99 -0
- package/bundle/components/ExpandAnimation.md +281 -0
- package/bundle/components/Footer.md +339 -0
- package/bundle/components/FormField.md +326 -0
- package/bundle/components/FormGroup.md +99 -0
- package/bundle/components/Gap.md +113 -0
- package/bundle/components/Header.md +280 -0
- package/bundle/components/HelpBox.md +154 -0
- package/bundle/components/IconWrapper.md +145 -0
- package/bundle/components/Island.md +197 -0
- package/bundle/components/IslandAccordion.md +230 -0
- package/bundle/components/IslandWidget.md +233 -0
- package/bundle/components/KeyDownListener.md +157 -0
- package/bundle/components/LightBox.md +322 -0
- package/bundle/components/Link.md +161 -0
- package/bundle/components/List.md +247 -0
- package/bundle/components/ListMaster.md +204 -0
- package/bundle/components/Loader.md +161 -0
- package/bundle/components/LoaderScreen.md +134 -0
- package/bundle/components/Marker.md +127 -0
- package/bundle/components/MarkerStatus.md +148 -0
- package/bundle/components/MediaWidth.md +169 -0
- package/bundle/components/MobileView.md +120 -0
- package/bundle/components/ModalWindow.md +308 -0
- package/bundle/components/MonthYearField.md +168 -0
- package/bundle/components/MultiselectField.md +213 -0
- package/bundle/components/Notification.md +233 -0
- package/bundle/components/NumberField.md +178 -0
- package/bundle/components/OrderedList.md +169 -0
- package/bundle/components/Overlay.md +237 -0
- package/bundle/components/Page.md +246 -0
- package/bundle/components/Pagination.md +298 -0
- package/bundle/components/Portal.md +119 -0
- package/bundle/components/Radio.md +174 -0
- package/bundle/components/Row.md +125 -0
- package/bundle/components/SMSField.md +160 -0
- package/bundle/components/SegmentedControl.md +235 -0
- package/bundle/components/SelectExtendedField.md +322 -0
- package/bundle/components/SelectField.md +213 -0
- package/bundle/components/Skeleton.md +129 -0
- package/bundle/components/Slider.md +157 -0
- package/bundle/components/SliderExtended.md +210 -0
- package/bundle/components/SliderRange.md +160 -0
- package/bundle/components/SmallInput.md +101 -0
- package/bundle/components/Spoiler.md +121 -0
- package/bundle/components/StatusTracker.md +151 -0
- package/bundle/components/Step.md +121 -0
- package/bundle/components/Stepper.md +163 -0
- package/bundle/components/Suggest.md +160 -0
- package/bundle/components/SuggestField.md +157 -0
- package/bundle/components/SwipeableArea.md +144 -0
- package/bundle/components/Table.md +199 -0
- package/bundle/components/Tabs.md +152 -0
- package/bundle/components/TabsExtended.md +273 -0
- package/bundle/components/TabsLine.md +189 -0
- package/bundle/components/Tag.md +135 -0
- package/bundle/components/TagColor.md +121 -0
- package/bundle/components/TagGroup.md +100 -0
- package/bundle/components/TextField.md +152 -0
- package/bundle/components/TextareaField.md +124 -0
- package/bundle/components/ThemeProvider.md +181 -0
- package/bundle/components/Tooltip.md +254 -0
- package/bundle/components/TopOverlay.md +145 -0
- package/bundle/components/TreeView.md +224 -0
- package/bundle/components/Triggers.md +121 -0
- package/bundle/guides/codestyle.md +205 -0
- package/bundle/guides/commits.md +166 -0
- package/bundle/guides/context.md +277 -0
- package/bundle/guides/stories.md +618 -0
- package/bundle/guides/template.md +154 -0
- package/bundle/guides/tests.md +301 -0
- package/bundle/icons.json +4156 -0
- package/bundle/manifest.json +796 -0
- package/bundle/release-notes/1.1.0.mdx +26 -0
- package/bundle/release-notes/1.1.1.mdx +14 -0
- package/bundle/release-notes/1.10.0.mdx +43 -0
- package/bundle/release-notes/1.11.0.mdx +19 -0
- package/bundle/release-notes/1.12.0.mdx +21 -0
- package/bundle/release-notes/1.13.0.mdx +43 -0
- package/bundle/release-notes/1.14.0.mdx +29 -0
- package/bundle/release-notes/1.15.0.mdx +56 -0
- package/bundle/release-notes/1.16.0.mdx +55 -0
- package/bundle/release-notes/1.17.0.mdx +15 -0
- package/bundle/release-notes/1.18.0.mdx +24 -0
- package/bundle/release-notes/1.19.0.mdx +48 -0
- package/bundle/release-notes/1.19.1.mdx +9 -0
- package/bundle/release-notes/1.2.0.mdx +80 -0
- package/bundle/release-notes/1.20.0.mdx +48 -0
- package/bundle/release-notes/1.21.0.mdx +66 -0
- package/bundle/release-notes/1.22.0.mdx +75 -0
- package/bundle/release-notes/1.23.0.mdx +50 -0
- package/bundle/release-notes/1.24.0.mdx +59 -0
- package/bundle/release-notes/1.25.0.mdx +52 -0
- package/bundle/release-notes/1.26.0.mdx +11 -0
- package/bundle/release-notes/1.3.0.mdx +21 -0
- package/bundle/release-notes/1.4.0.mdx +37 -0
- package/bundle/release-notes/1.5.0.mdx +24 -0
- package/bundle/release-notes/1.6.0.mdx +37 -0
- package/bundle/release-notes/1.6.1.mdx +11 -0
- package/bundle/release-notes/1.7.0.mdx +29 -0
- package/bundle/release-notes/1.8.0.mdx +38 -0
- package/bundle/release-notes/1.9.0.mdx +28 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +324 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
---
|
|
2
|
+
component: SelectExtendedField
|
|
3
|
+
category: Form Controls
|
|
4
|
+
figma-node: ""
|
|
5
|
+
figma-file: ""
|
|
6
|
+
related: [SelectField, MultiselectField, Dropdown, FormField, KeyDownListener]
|
|
7
|
+
tokens: []
|
|
8
|
+
stories: stories/SelectExtendedField/SelectExtendedField.stories.tsx
|
|
9
|
+
version: "1.0"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# SelectExtendedField
|
|
13
|
+
|
|
14
|
+
## Назначение
|
|
15
|
+
|
|
16
|
+
**Базовый компонент** для построения custom-Select'ов: оркестрирует
|
|
17
|
+
открытие/закрытие dropdown'а, click-outside, ESC и (опционально)
|
|
18
|
+
TAB-закрытие, прокидывает `opened/setOpened/targetRef/dropdownRef` в
|
|
19
|
+
**render-prop'ы** для target и dropdown. Сам не диктует ни форму
|
|
20
|
+
target'а (icon vs text vs input), ни содержимое dropdown'а — потребитель
|
|
21
|
+
рендерит их через render-функции.
|
|
22
|
+
|
|
23
|
+
`SelectExtendedField.Target` (`SelectExtendedFieldTarget`) и
|
|
24
|
+
`SelectExtendedField.Dropdown` — типичные реализации; их можно заменить
|
|
25
|
+
своими.
|
|
26
|
+
|
|
27
|
+
Используй когда: строишь нестандартный Select (например, multi-select с
|
|
28
|
+
поиском, color-picker как select, и т.п.). Для типичных — `SelectField`,
|
|
29
|
+
`MultiselectField`.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Файловая структура
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
src/components/SelectExtendedField/
|
|
37
|
+
├── SelectExtendedField.tsx # forwardRef + Object.assign({Target, Dropdown})
|
|
38
|
+
├── components/
|
|
39
|
+
│ ├── SelectExtendedFieldTarget.tsx # forwardRef; FormField + caret + clear + loader
|
|
40
|
+
│ ├── SelectExtendedFieldDropdown.tsx # FC; <Dropdown> wrapper + .List
|
|
41
|
+
│ └── SelectExtendedFieldDropdownDefault.tsx # FC; готовая раскладка опций (desktop + mobile)
|
|
42
|
+
├── styles/
|
|
43
|
+
├── index.ts
|
|
44
|
+
└── __tests__/
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Compound-структура (через render-prop'ы)
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
<SelectExtendedField
|
|
53
|
+
closeOnTab
|
|
54
|
+
onOpen={...} onClose={...}
|
|
55
|
+
renderTarget={({ opened, setOpened }) => (
|
|
56
|
+
<SelectExtendedField.Target
|
|
57
|
+
opened={opened}
|
|
58
|
+
setOpened={setOpened}
|
|
59
|
+
label={selected?.label}
|
|
60
|
+
placeholder="Выбрать..."
|
|
61
|
+
fieldLabel="Label"
|
|
62
|
+
size={EComponentSize.MD}
|
|
63
|
+
onClear={() => onChange(undefined)}
|
|
64
|
+
/>
|
|
65
|
+
)}
|
|
66
|
+
>
|
|
67
|
+
{({ opened, setOpened, targetRef, dropdownRef }) => (
|
|
68
|
+
<SelectExtendedField.Dropdown
|
|
69
|
+
opened={opened}
|
|
70
|
+
setOpened={setOpened}
|
|
71
|
+
targetRef={targetRef}
|
|
72
|
+
forwardedRef={dropdownRef}
|
|
73
|
+
>
|
|
74
|
+
<SelectExtendedField.Dropdown.List dropdownOpened={opened}>
|
|
75
|
+
{options.map(...)}
|
|
76
|
+
</SelectExtendedField.Dropdown.List>
|
|
77
|
+
</SelectExtendedField.Dropdown>
|
|
78
|
+
)}
|
|
79
|
+
</SelectExtendedField>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Варианты и props
|
|
85
|
+
|
|
86
|
+
### SelectExtendedField — `ISelectExtendedFieldProps extends Omit<HTMLAttributes<HTMLDivElement>, "children">`
|
|
87
|
+
|
|
88
|
+
| Prop | Тип | Default | Описание |
|
|
89
|
+
|---|---|---|---|
|
|
90
|
+
| `renderTarget` | `(props: ISelectExtendedFieldTargetProvideProps) => ReactNode` | — | Render-prop для target. Получает `{ opened, setOpened }`. |
|
|
91
|
+
| `children` | `(props: ISelectExtendedFieldDropdownProvideProps) => ReactNode` | — | Render-prop для dropdown. Получает `{ opened, setOpened, targetRef, dropdownRef }`. |
|
|
92
|
+
| `onOpen?` | `() => void` | — | Только при реальном переходе `false → true` (skip mount). |
|
|
93
|
+
| `onClose?` | `() => void` | — | Только при `true → false`. |
|
|
94
|
+
| `closeOnTab?` | `boolean` | `false` | Tab закрывает dropdown. |
|
|
95
|
+
| `...HTMLAttributes` | — | — | На корневой `<div>`. |
|
|
96
|
+
|
|
97
|
+
### SelectExtendedField.Target — `ISelectExtendedFieldTargetProps extends Omit<IFormFieldProps, "prefix" \| "postfix">`
|
|
98
|
+
|
|
99
|
+
| Prop | Тип | Default | Описание |
|
|
100
|
+
|---|---|---|---|
|
|
101
|
+
| `opened` | `boolean` | — | Из render-prop'а. |
|
|
102
|
+
| `setOpened` | `(opened: boolean) => void` | — | Из render-prop'а. |
|
|
103
|
+
| `fieldLabel` | `ReactNode` | — | Label, рендерится через `FormFieldLabel`. |
|
|
104
|
+
| `label?` | `ReactNode` | — | Текущий выбранный (отображается в `FormFieldTarget`). |
|
|
105
|
+
| `placeholder?` | `ReactNode` | — | Когда `label` пуст. |
|
|
106
|
+
| `loading?` | `boolean` | — | Заменяет caret на `LoaderSmall` (BRAND). Блокирует клики и keys. |
|
|
107
|
+
| `prefix?` / `postfix?` | `ReactNode` | — | До/после target. Caret/Loader всегда рендерятся в Postfix. |
|
|
108
|
+
| `onClear?` | `() => void` | — | Если передан — рендерится `FormFieldClear`. |
|
|
109
|
+
| `size` | `EComponentSize` | `MD` | Размер. Маппится на `CaretdownStrokeSrvIcon{16/20/24}` и `LoaderSmall size`. |
|
|
110
|
+
| `status` | `EFormFieldStatus` | — | `DISABLED` блокирует клики/keys. |
|
|
111
|
+
| `tabIndex` | `number` | — | Стандартно. |
|
|
112
|
+
|
|
113
|
+
- `aria-expanded={opened}`, `aria-haspopup="listbox"` — на корне FormField.
|
|
114
|
+
- Открытие по: **click**, **Enter**, **Space**, **ArrowDown**, **ArrowUp** (только когда `!opened`); тогда `preventDefault + stopPropagation`.
|
|
115
|
+
|
|
116
|
+
### SelectExtendedField.Dropdown — `ISelectExtendedFieldDropdownProps extends Omit<IDropdownProps, "forwardedRef">`
|
|
117
|
+
|
|
118
|
+
| Prop | Тип | Описание |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| `forwardedRef` | `RefObject<HTMLDivElement>` | Из render-prop'а (dropdownRef). |
|
|
121
|
+
| `targetRef` | `RefObject<HTMLElement>` | Из render-prop'а. |
|
|
122
|
+
| `opened` | `boolean` | — |
|
|
123
|
+
| `children?` | `ReactNode` | — |
|
|
124
|
+
| `...IDropdownProps` | — | width, mobileViewProps, size и т.п. |
|
|
125
|
+
|
|
126
|
+
Slots: **`SelectExtendedField.Dropdown.List`** = `DropdownList` (re-export).
|
|
127
|
+
|
|
128
|
+
### Provide-types
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
ISelectExtendedFieldTargetProvideProps: { opened, setOpened }
|
|
132
|
+
ISelectExtendedFieldDropdownProvideProps: { opened, setOpened, targetRef, dropdownRef }
|
|
133
|
+
ISelectExtendedFieldDefaultOption: { value: string; label: ReactNode; ...IDropdownListItemProps без active/onSelect/selected/key/className/keyCodesForSelection }
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### SelectExtendedFieldDropdownDefault (отдельная утилита)
|
|
137
|
+
|
|
138
|
+
Готовая раскладка опций для desktop (`Dropdown.List`) **и** mobile (`DropdownMobileHeader/Body/List`). Не часть Object.assign — импортируется отдельно:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { SelectExtendedFieldDropdownDefault } from "@sberbusiness/triplex-next/components/SelectExtendedField";
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
(используется в `SelectField`).
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Ключевые особенности реализации
|
|
149
|
+
|
|
150
|
+
### Stable `onOpen`/`onClose` через ref
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const onOpenRef = useRef(onOpen);
|
|
154
|
+
const onCloseRef = useRef(onClose);
|
|
155
|
+
|
|
156
|
+
useLayoutEffect(() => {
|
|
157
|
+
onOpenRef.current = onOpen;
|
|
158
|
+
onCloseRef.current = onClose;
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
- Колбэки хранятся в ref'ах и обновляются после каждого коммита через `useLayoutEffect` без deps.
|
|
163
|
+
- `useEffect([opened])` читает `onOpenRef.current` — поэтому **эффект не пересоздаётся** при смене ссылок на колбэки.
|
|
164
|
+
|
|
165
|
+
### Skip mount-вызов `onOpen` / `onClose`
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
if (!isMountedRef.current) {
|
|
169
|
+
isMountedRef.current = true;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (opened) onOpenRef.current?.();
|
|
173
|
+
else onCloseRef.current?.();
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
- Default `opened = false` → без skip'а `onClose` вызвался бы при mount.
|
|
177
|
+
|
|
178
|
+
### Click-outside через `document.mousedown`
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
document.addEventListener("mousedown", listenMouseDown);
|
|
183
|
+
return () => document.removeEventListener("mousedown", listenMouseDown);
|
|
184
|
+
}, [listenMouseDown]);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
- Закрывает, если клик вне `targetRef` и `dropdownRef`.
|
|
188
|
+
- **`mousedown`**, не `click` — закрывает раньше, чем сработает `click` на dropdown items.
|
|
189
|
+
|
|
190
|
+
### ESC — через KeyDownListener (window)
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
<KeyDownListener onMatch={closeDropdown} eventKeyCode={EVENT_KEY_CODES.ESCAPE}>
|
|
194
|
+
...
|
|
195
|
+
</KeyDownListener>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
- Глобальный listener, см. `KeyDownListener-AI.md`.
|
|
199
|
+
|
|
200
|
+
### TAB — через onKeyDown в самом контейнере
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
if (closeOnTab && isKey(key, "TAB")) closeDropdown();
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
- Опционально. Контракт: `closeOnTab` — потребитель решает (по UX).
|
|
207
|
+
|
|
208
|
+
### Target: открытие по клавишам
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
if (!opened && [SPACE, ENTER, ARROW_DOWN, ARROW_UP].includes(event.keyCode)) {
|
|
212
|
+
event.preventDefault();
|
|
213
|
+
event.stopPropagation(); // чтобы dropdown сразу не среагировал тем же keydown
|
|
214
|
+
setOpened(!opened);
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
- `event.keyCode` — **deprecated** API (тех-долг).
|
|
219
|
+
- `stopPropagation` — комментарий в коде: предотвращает срабатывание `keydown` в `DropdownListItem` через `document.addEventListener("keydown")`.
|
|
220
|
+
|
|
221
|
+
### Target: caret + loader через size-map
|
|
222
|
+
|
|
223
|
+
- `CaretdownStrokeSrvIcon{16/20/24}` для `SM/MD/LG`.
|
|
224
|
+
- `LoaderSmall theme=BRAND` тех же размеров.
|
|
225
|
+
- `IconWrapper active={opened} disabled={status===DISABLED}` оборачивает caret — управляет цветом через `@sberbusiness/icons-next` глобальные классы.
|
|
226
|
+
|
|
227
|
+
### `setRef` мерж forwardRef + targetRef
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
const setRef = (instance) => {
|
|
231
|
+
targetRef.current = instance;
|
|
232
|
+
if (typeof ref === "function") ref(instance); else if (ref) ref.current = instance;
|
|
233
|
+
};
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Target слоты Postfix
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
<FormFieldPostfix>
|
|
240
|
+
{onClear && <FormFieldClear onClick={onClear} />}
|
|
241
|
+
{loading ? <LoaderSmall .../> : <IconWrapper .../>}
|
|
242
|
+
{postfix}
|
|
243
|
+
</FormFieldPostfix>
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
- Порядок: clear → loader/caret → custom postfix.
|
|
247
|
+
|
|
248
|
+
### Loading блокирует клики и keydown
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
if (loading || status === DISABLED) return; // в handleClick
|
|
252
|
+
if (loading && status === DISABLED) return; // в handleKeyDown — bug? && вместо ||
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
- В `handleKeyDown` используется `&&` — **похоже на баг**: keys блокируются только если ОБА условия true. Тех-долг, не трогать без задачи.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Инварианты
|
|
260
|
+
|
|
261
|
+
- **`renderTarget` и `children` — render-prop'ы** (функции). Изменение API — breaking.
|
|
262
|
+
- **`onOpen` / `onClose` через ref'ы** — stable callback pattern. Не пересоздавать эффект на смене ссылок.
|
|
263
|
+
- **Skip mount-вызов** колбэков через `isMountedRef`.
|
|
264
|
+
- **Click-outside на `mousedown`** (не `click`) — public behavior contract.
|
|
265
|
+
- **ESC закрывает через `KeyDownListener` (window)** — глобальный listener.
|
|
266
|
+
- **`closeOnTab` default `false`** — UX-выбор.
|
|
267
|
+
- **Target открывает по `Enter / Space / ArrowDown / ArrowUp`** + `preventDefault + stopPropagation`.
|
|
268
|
+
- **Target использует `event.keyCode`** — deprecated, тех-долг.
|
|
269
|
+
- **Target loading: `&&` вместо `||`** в handleKeyDown — известный потенциальный баг.
|
|
270
|
+
- **`aria-haspopup="listbox"`** — публичная семантика.
|
|
271
|
+
- **`SelectExtendedField.Dropdown.List = DropdownList`** — реэкспорт.
|
|
272
|
+
- **`SelectExtendedFieldDropdownDefault`** — не slot, отдельный экспорт.
|
|
273
|
+
- **`Target.size` default `MD`**.
|
|
274
|
+
- **Target caret-icon фиксирован** (`CaretdownStrokeSrvIcon`) — нельзя заменить.
|
|
275
|
+
- **`forwardRef`** на SelectExtendedField и Target; **`FC`** на Dropdown.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Accessibility
|
|
280
|
+
|
|
281
|
+
- `aria-expanded={opened}` + `aria-haspopup="listbox"` на target — корректная семантика combobox-like.
|
|
282
|
+
- ESC закрывает (стандарт).
|
|
283
|
+
- Open by `Enter`/`Space`/`Arrow*` — стандартное keyboard поведение для select.
|
|
284
|
+
- **Target — `<div>` (от FormField)**, не `<button>` — для AT может звучать как regular область. Если нужен strict role — потребитель добавляет через `...rest` (`role="combobox"` и т.п.).
|
|
285
|
+
- `aria-label` / `aria-labelledby` для target — потребитель.
|
|
286
|
+
- `loading` блокирует клики и (по идее) keys — но по факту `keydown` блокировка имеет потенциальный баг (см. выше).
|
|
287
|
+
- `tabIndex` — наследуется от FormField/FormFieldProps.
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Дизайн-токены
|
|
292
|
+
|
|
293
|
+
Нет собственных. Цвета/spacing — от `FormField`, `Dropdown`, `Loader`.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Связанные компоненты
|
|
298
|
+
|
|
299
|
+
- `SelectField` — высокоуровневая обёртка для типичного Select.
|
|
300
|
+
- `MultiselectField` — multi-выбор.
|
|
301
|
+
- `Dropdown` — popover.
|
|
302
|
+
- `FormField` (Target/Label/Prefix/Postfix/Clear) — основа target.
|
|
303
|
+
- `KeyDownListener` — ESC.
|
|
304
|
+
- `LoaderSmall` — индикатор загрузки в target.
|
|
305
|
+
- `IconWrapper` — управление цветом caret.
|
|
306
|
+
- `CaretdownStrokeSrvIcon{16/20/24}` — иконка раскрытия.
|
|
307
|
+
- `EComponentSize`, `EFormFieldStatus`.
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## Stories
|
|
312
|
+
|
|
313
|
+
`stories/SelectExtendedField/SelectExtendedField.stories.tsx`
|
|
314
|
+
Примеры: `stories/SelectExtendedField/examples/`
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## История изменений
|
|
319
|
+
|
|
320
|
+
| Дата | Изменение |
|
|
321
|
+
|---|---|
|
|
322
|
+
| 2026-04-18 | Создан документ |
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
---
|
|
2
|
+
component: SelectField
|
|
3
|
+
category: Form Controls
|
|
4
|
+
figma-node: ""
|
|
5
|
+
figma-file: ""
|
|
6
|
+
related: [SelectExtendedField, MultiselectField, Dropdown, FormField]
|
|
7
|
+
tokens: []
|
|
8
|
+
stories: stories/SelectField/SelectField.stories.tsx
|
|
9
|
+
version: "1.0"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# SelectField
|
|
13
|
+
|
|
14
|
+
## Назначение
|
|
15
|
+
|
|
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`.
|
|
21
|
+
|
|
22
|
+
Управляет:
|
|
23
|
+
- `activeDescendant` через `DropdownListContext` — для a11y `aria-activedescendant`.
|
|
24
|
+
- `instanceId` через `lodash uniqueId()` — связывает Target ↔ List через `aria-controls`.
|
|
25
|
+
- **`closeOnTab = true`** захардкожен.
|
|
26
|
+
|
|
27
|
+
Используй когда: типичный select из списка опций. Для multi-выбора —
|
|
28
|
+
`MultiselectField`. Для нестандартного target/dropdown —
|
|
29
|
+
`SelectExtendedField` напрямую.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Файловая структура
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
src/components/SelectField/
|
|
37
|
+
├── SelectField.tsx # forwardRef; склейка SelectExtendedField + Default-Dropdown
|
|
38
|
+
├── index.ts
|
|
39
|
+
└── __tests__/
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Варианты и props
|
|
45
|
+
|
|
46
|
+
### `ISelectFieldProps`
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
extends Omit<ISelectExtendedFieldProps, "children" | "onChange" | "renderTarget">
|
|
50
|
+
extends Pick<ISelectExtendedFieldTargetProps, "loading" | "status" | "placeholder">
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
| Prop | Тип | Default | Описание |
|
|
54
|
+
|---|---|---|---|
|
|
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}`.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Ключевые особенности реализации
|
|
80
|
+
|
|
81
|
+
### Layered render-prop'ы
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
<SelectExtendedField renderTarget={renderTarget} closeOnTab={true}>
|
|
85
|
+
{renderDropdown}
|
|
86
|
+
</SelectExtendedField>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- `renderTarget` рендерит `SelectExtendedField.Target` с `value?.label`.
|
|
90
|
+
- `renderDropdown` рендерит `SelectExtendedFieldDropdownDefault` с `width=TARGET`.
|
|
91
|
+
|
|
92
|
+
### `DropdownListContext` — `activeDescendant` для a11y
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
<DropdownListContext.Provider value={{ activeDescendant, setActiveDescendant }}>
|
|
96
|
+
<SelectExtendedFieldDropdownDefault ... />
|
|
97
|
+
</DropdownListContext.Provider>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
- `DropdownList` items пишут свой `id` в `activeDescendant` при keyboard-навигации.
|
|
101
|
+
- Target читает `activeDescendant` и ставит `aria-activedescendant` — корректная семантика combobox.
|
|
102
|
+
|
|
103
|
+
### `instanceId = uniqueId()` через `useRef`
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const instanceId = useRef(uniqueId());
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- **SSR-небезопасно** (`uniqueId` глобальный счётчик).
|
|
110
|
+
- Используется как `aria-controls` на target и `id` на List — связывает их.
|
|
111
|
+
|
|
112
|
+
### Target props order
|
|
113
|
+
|
|
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
|
+
```
|
|
128
|
+
|
|
129
|
+
- Порядок: defaults → `targetProps` (потребитель) → `provide-props` (system).
|
|
130
|
+
- **`fieldLabel: undefined`** по умолчанию — потребитель должен передать через `targetProps.fieldLabel`.
|
|
131
|
+
|
|
132
|
+
### Жёстко зашито
|
|
133
|
+
|
|
134
|
+
- **`closeOnTab: true`**.
|
|
135
|
+
- **`width: EDropdownWidth.TARGET`** — dropdown ровно ширины target'а.
|
|
136
|
+
- **`role="combobox"`** на target.
|
|
137
|
+
- **Default Dropdown layout** (desktop List + mobile bottom-sheet) — нельзя заменить (для нестандартного — использовать `SelectExtendedField`).
|
|
138
|
+
|
|
139
|
+
### `loading` блокирует открытие dropdown'а
|
|
140
|
+
|
|
141
|
+
В `SelectExtendedFieldDropdownDefault`:
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
opened={opened && !loading}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
- При `loading=true` dropdown не показывается, в target — спиннер.
|
|
148
|
+
|
|
149
|
+
### `setRef` мерж forwardRef + targetRef
|
|
150
|
+
|
|
151
|
+
Стандартный паттерн.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Инварианты
|
|
156
|
+
|
|
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.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Accessibility
|
|
174
|
+
|
|
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` между сервером и клиентом.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Дизайн-токены
|
|
185
|
+
|
|
186
|
+
Нет собственных — наследуются от `SelectExtendedField` / `Dropdown` / `FormField`.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Связанные компоненты
|
|
191
|
+
|
|
192
|
+
- `SelectExtendedField` — базовый, для кастомных Select'ов.
|
|
193
|
+
- `MultiselectField` — multi-выбор.
|
|
194
|
+
- `SelectExtendedFieldDropdownDefault` — переиспользуется для default-раскладки.
|
|
195
|
+
- `Dropdown` (+`DropdownList`+`DropdownMobile*`) — popover.
|
|
196
|
+
- `DropdownListContext` — для `activeDescendant`.
|
|
197
|
+
- `EDropdownWidth.TARGET`, `EComponentSize`.
|
|
198
|
+
- `lodash uniqueId`.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Stories
|
|
203
|
+
|
|
204
|
+
`stories/SelectField/SelectField.stories.tsx`
|
|
205
|
+
Примеры: `stories/SelectField/examples/`
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## История изменений
|
|
210
|
+
|
|
211
|
+
| Дата | Изменение |
|
|
212
|
+
|---|---|
|
|
213
|
+
| 2026-04-18 | Создан документ |
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
component: Skeleton
|
|
3
|
+
category: Feedback
|
|
4
|
+
figma-node: ""
|
|
5
|
+
figma-file: ""
|
|
6
|
+
related: [Loader, LoaderScreen]
|
|
7
|
+
tokens: [triplex-next-Skeleton-BackgroundColor_Light_Start, triplex-next-Skeleton-BackgroundColor_Light_End, triplex-next-Skeleton-BackgroundColor_Dark_Start, triplex-next-Skeleton-BackgroundColor_Dark_End]
|
|
8
|
+
stories: stories/Skeleton/Skeleton.stories.tsx
|
|
9
|
+
version: "1.0"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Skeleton
|
|
13
|
+
|
|
14
|
+
## Назначение
|
|
15
|
+
|
|
16
|
+
Плейсхолдер для загружающегося контента — пульсирующий `<div>` с
|
|
17
|
+
`border-radius: 16px` и `flex-grow: 1`. Поддерживает две темы (`DARK` /
|
|
18
|
+
`LIGHT`) — отличаются фоновыми переменными в @keyframes.
|
|
19
|
+
|
|
20
|
+
Используй когда: показываешь shape будущего контента, чтобы избежать
|
|
21
|
+
layout shift и психологически сократить ощущаемое время загрузки. Для
|
|
22
|
+
точечного индикатора — `Loader`/`LoaderScreen`.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Файловая структура
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
src/components/Skeleton/
|
|
30
|
+
├── Skeleton.tsx # FC; <div> + класс по type
|
|
31
|
+
├── enums.ts # ESkeletonType (DARK | LIGHT)
|
|
32
|
+
├── styles/Skeleton.module.less # 2 @keyframes пульсации
|
|
33
|
+
├── index.ts
|
|
34
|
+
└── __tests__/
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Варианты и props
|
|
40
|
+
|
|
41
|
+
### `ISkeletonProps extends HTMLAttributes<HTMLDivElement>`
|
|
42
|
+
|
|
43
|
+
| Prop | Тип | Default | Описание |
|
|
44
|
+
|---|---|---|---|
|
|
45
|
+
| `type` | `ESkeletonType` | `DARK` | Тема (DARK/LIGHT). Влияет на анимационные переменные. |
|
|
46
|
+
| `...HTMLAttributes` | — | — | На `<div>`. Размер задаётся через `style`/`className`. |
|
|
47
|
+
|
|
48
|
+
### Enum
|
|
49
|
+
|
|
50
|
+
- `ESkeletonType.DARK | LIGHT`.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Ключевые особенности реализации
|
|
55
|
+
|
|
56
|
+
### Анимация — 2s ease-in-out infinite через background-color keyframes
|
|
57
|
+
|
|
58
|
+
```less
|
|
59
|
+
.skeleton.dark { animation: skeleton-dark-pulse 2s ease-in-out infinite; }
|
|
60
|
+
@keyframes skeleton-dark-pulse {
|
|
61
|
+
0% { background-color: var(--triplex-next-Skeleton-BackgroundColor_Dark_Start); }
|
|
62
|
+
50% { background-color: var(--triplex-next-Skeleton-BackgroundColor_Dark_End); }
|
|
63
|
+
100% { background-color: var(--triplex-next-Skeleton-BackgroundColor_Dark_Start); }
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- Пульсация Start ↔ End ↔ Start.
|
|
68
|
+
- **`prefers-reduced-motion`** не учитывается — потенциальный a11y-долг.
|
|
69
|
+
|
|
70
|
+
### `border-radius: 16px`, `flex-grow: 1`
|
|
71
|
+
|
|
72
|
+
- Скругление **захардкожено** (16px). Для других форм (круги аватаров, узкие линии текста) — потребитель override через className/style.
|
|
73
|
+
- `flex-grow: 1` — растягивается во flex-контейнере. **В блочном родителе** Skeleton сам по себе не имеет ширины — нужна `style.width` или `className` с шириной.
|
|
74
|
+
|
|
75
|
+
### **`FC, не forwardRef`**
|
|
76
|
+
|
|
77
|
+
- Ref на `<div>` не пробрасывается.
|
|
78
|
+
|
|
79
|
+
### **Нет `aria-busy`/`role="status"`**
|
|
80
|
+
|
|
81
|
+
- Skeleton — чисто визуальный. AT не озвучивает «загрузка». Если важно — потребитель добавляет `role="status"` / `aria-busy="true"` через `...rest`.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Инварианты
|
|
86
|
+
|
|
87
|
+
- **`type` default `DARK`**.
|
|
88
|
+
- **`border-radius: 16px`** захардкожено — изменение требует override.
|
|
89
|
+
- **`flex-grow: 1`** захардкожено — поведение во flex-контейнере.
|
|
90
|
+
- **Анимация 2s ease-in-out infinite** — захардкожено, не настраивается.
|
|
91
|
+
- **CSS-переменные `--triplex-next-Skeleton-BackgroundColor_{Light|Dark}_{Start|End}`** — публичные токены.
|
|
92
|
+
- **`FC, не forwardRef`**.
|
|
93
|
+
- **`prefers-reduced-motion` не учитывается** — тех-долг.
|
|
94
|
+
- **`data-tx`** — версия пакета.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Accessibility
|
|
99
|
+
|
|
100
|
+
- **Нет `role` / `aria-busy`** по умолчанию — потребитель должен добавить, если нужно.
|
|
101
|
+
- **Нет `prefers-reduced-motion` media-query** — анимация играет всегда. Пользователи с предпочтением reduced motion увидят пульсацию (тех-долг).
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Дизайн-токены
|
|
106
|
+
|
|
107
|
+
- `--triplex-next-Skeleton-BackgroundColor_Light_Start` / `_End`.
|
|
108
|
+
- `--triplex-next-Skeleton-BackgroundColor_Dark_Start` / `_End`.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Связанные компоненты
|
|
113
|
+
|
|
114
|
+
- `Loader` / `LoaderScreen` — для активной точечной загрузки.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Stories
|
|
119
|
+
|
|
120
|
+
`stories/Skeleton/Skeleton.stories.tsx`
|
|
121
|
+
Примеры: `stories/Skeleton/examples/`
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## История изменений
|
|
126
|
+
|
|
127
|
+
| Дата | Изменение |
|
|
128
|
+
|---|---|
|
|
129
|
+
| 2026-04-18 | Создан документ |
|