@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,433 @@
|
|
|
1
|
+
---
|
|
2
|
+
component: Dropdown
|
|
3
|
+
category: Overlays
|
|
4
|
+
figma-node: ""
|
|
5
|
+
figma-file: ""
|
|
6
|
+
related: [DropdownList, DropdownListItem, Portal, OverlayBase, MobileView, SelectField, Suggest, DatePickerExtended]
|
|
7
|
+
tokens: [Dropdown_Background, DropdownList_*, DropdownMobile_*, DropdownMobileList_*]
|
|
8
|
+
stories: stories/Dropdown/Dropdown.stories.tsx
|
|
9
|
+
version: "1.0"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Dropdown
|
|
13
|
+
|
|
14
|
+
## Назначение
|
|
15
|
+
|
|
16
|
+
Основной overlay-компонент библиотеки для выпадающих меню. Renders в
|
|
17
|
+
`Portal` на `document.body`, **автоматически разветвляется**:
|
|
18
|
+
- **Desktop (`DropdownDesktop`)** — абсолютно позиционированный блок
|
|
19
|
+
рядом с `targetRef` (top/bottom auto-flip, left/right alignment,
|
|
20
|
+
ширина content/target/min-target), слушает window resize, document
|
|
21
|
+
scroll и ResizeObserver для reposition; блокирует внешний скролл
|
|
22
|
+
и клавиатурные события при открытии.
|
|
23
|
+
- **Mobile (`DropdownMobile`)** — полноэкранный bottom-overlay
|
|
24
|
+
(`OverlayBase direction=BOTTOM`), с композицией
|
|
25
|
+
`Header`/`Body`/`Footer`/`Close`.
|
|
26
|
+
|
|
27
|
+
Имеет внутренние compound-структуры:
|
|
28
|
+
- **`DropdownList`** + `DropdownList.Item` — список с клавиатурной
|
|
29
|
+
навигацией (↑/↓, scroll-into-view, active-descendant через
|
|
30
|
+
`DropdownListContext`).
|
|
31
|
+
- **`DropdownMobileList`** + `DropdownMobileListItem` — аналог для
|
|
32
|
+
мобильной ветки.
|
|
33
|
+
- **`DropdownMobileInput` / `DropdownMobileMaskedInput`** —
|
|
34
|
+
специальные input'ы в header'е мобильного Dropdown'а.
|
|
35
|
+
|
|
36
|
+
Используй когда: строишь любой выпадающий UI (Select, Suggest, Menu,
|
|
37
|
+
DatePicker, custom dropdown). **В большинстве случаев** бери готовую
|
|
38
|
+
обёртку (`SelectField`, `Suggest`, `DatePickerExtended`) — Dropdown
|
|
39
|
+
прямо в UI встречается реже.
|
|
40
|
+
Не используй как: абсолютный "tooltip" — бери `Tooltip`; bottom sheet
|
|
41
|
+
без target'а — `OverlayBase`/`SideOverlay`; модалку — `LightBox`/
|
|
42
|
+
`ModalWindow`.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Файловая структура
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
src/components/Dropdown/
|
|
50
|
+
├── Dropdown.tsx # forwardRef, разветвление MobileView
|
|
51
|
+
├── DropdownListContext.ts # Context { activeDescendant, setActiveDescendant }
|
|
52
|
+
├── index.ts # Barrel
|
|
53
|
+
├── desktop/
|
|
54
|
+
│ ├── DropdownDesktop.tsx # forwardRef, positioning + scroll-block
|
|
55
|
+
│ ├── DropdownList.tsx # FC, клавиатурная навигация, role="listbox"
|
|
56
|
+
│ ├── DropdownListItem.tsx # ListItem (role="option", active, selected)
|
|
57
|
+
│ ├── enums.ts # EDropdownDirection/Alignment/Width
|
|
58
|
+
│ └── index.ts
|
|
59
|
+
├── mobile/
|
|
60
|
+
│ ├── DropdownMobile.tsx # forwardRef, OverlayBase wrapper
|
|
61
|
+
│ ├── DropdownMobileInner.tsx # Внутренняя разметка (animation, overflow)
|
|
62
|
+
│ ├── DropdownMobileHeader.tsx # Header с title/controlButtons
|
|
63
|
+
│ ├── DropdownMobileBody.tsx # Scrollable body
|
|
64
|
+
│ ├── DropdownMobileFooter.tsx # Footer (sticky)
|
|
65
|
+
│ ├── DropdownMobileClose.tsx # Кнопка закрытия
|
|
66
|
+
│ ├── DropdownMobileList.tsx # List для мобильного
|
|
67
|
+
│ ├── DropdownMobileListItem.tsx # Item для мобильного
|
|
68
|
+
│ ├── DropdownMobileInput.tsx # Input в header
|
|
69
|
+
│ ├── DropdownMobileMaskedInput.tsx # MaskedInput в header
|
|
70
|
+
│ ├── DropdownMobileLoader.tsx # Loader
|
|
71
|
+
│ └── index.ts
|
|
72
|
+
└── styles/
|
|
73
|
+
├── DropdownDesktop.module.less
|
|
74
|
+
├── DropdownDesktopList.module.less
|
|
75
|
+
├── DropdownMobile.module.less
|
|
76
|
+
├── DropdownMobileInput.module.less
|
|
77
|
+
└── DropdownMobileMaskedInput.module.less
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Особенности:
|
|
81
|
+
- **Portal → `document.body`** — всегда, независимо от desktop/mobile.
|
|
82
|
+
- **Два параллельных дерева подкомпонентов** (desktop + mobile) с
|
|
83
|
+
похожими именами, но разными props и поведением.
|
|
84
|
+
- **`data-tx`** на desktop и mobile root.
|
|
85
|
+
- **`useToken().scopeClassName`** на DropdownDesktop — получает
|
|
86
|
+
namespace для CSS-переменных из ThemeProvider.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Варианты и props
|
|
91
|
+
|
|
92
|
+
### Root: `IDropdownProps extends IDropdownDesktopProps`
|
|
93
|
+
|
|
94
|
+
| Prop | Тип | Описание |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `opened` | `boolean` | Открыт/закрыт. Controlled. |
|
|
97
|
+
| `setOpened` | `(opened: boolean) => void` | Сеттер, вызываемый из outside-click / mobile close. |
|
|
98
|
+
| `targetRef` | `RefObject<HTMLElement>` | Target для позиционирования (desktop) / точка привязки жеста. |
|
|
99
|
+
| `size` | `EComponentSize` | `MD` default. |
|
|
100
|
+
| `direction` | `EDropdownDirection` | `AUTO` \| `BOTTOM` \| `TOP` — desktop. |
|
|
101
|
+
| `alignment` | `EDropdownAlignment` | `LEFT` \| `RIGHT` — desktop. |
|
|
102
|
+
| `width` | `EDropdownWidth` | `CONTENT` \| `TARGET` \| `MIN_TARGET` — desktop. |
|
|
103
|
+
| `onOpen` | `() => void` | Callback при открытии (после mount). |
|
|
104
|
+
| `onClose` | `() => void` | Callback при закрытии. |
|
|
105
|
+
| `mobileViewProps` | `Omit<IDropdownMobileProps, "opened" \| "setOpened">` | **Если задан** — на мобильном открывается полноэкранный DropdownMobile. Если не задан — на мобильном тот же desktop-дропдаун. |
|
|
106
|
+
| `...HTMLDivAttributes` | — | — |
|
|
107
|
+
|
|
108
|
+
### `DropdownDesktop` props (также все → root Dropdown)
|
|
109
|
+
|
|
110
|
+
Все выше + `style`, `className`, `...HTMLDivAttributes`.
|
|
111
|
+
|
|
112
|
+
### `DropdownMobile` props (через `mobileViewProps`)
|
|
113
|
+
|
|
114
|
+
- `children` (может быть разный от desktop).
|
|
115
|
+
- `className`.
|
|
116
|
+
- `...HTMLDivAttributes`.
|
|
117
|
+
- (opened, setOpened форвардятся из Dropdown.)
|
|
118
|
+
|
|
119
|
+
### `DropdownList` compound
|
|
120
|
+
|
|
121
|
+
| Prop | Тип | Default | Описание |
|
|
122
|
+
|---|---|---|---|
|
|
123
|
+
| `dropdownOpened` | `boolean` | — | Необходим для подписки на keydown только когда открыто. |
|
|
124
|
+
| `listRef` | `RefObject<HTMLDivElement>` | — | Ссылка на скролл-контейнер. |
|
|
125
|
+
| `loading` | `boolean` | — | Добавляет loader-item в конец списка. |
|
|
126
|
+
| `size` | `EComponentSize` | `MD` | Влияет на классы. |
|
|
127
|
+
| `children` | `DropdownList.Item` | — | Только Item'ы. |
|
|
128
|
+
|
|
129
|
+
Статик: `DropdownList.Item = DropdownListItem`.
|
|
130
|
+
|
|
131
|
+
### Enums
|
|
132
|
+
|
|
133
|
+
**`EDropdownDirection`:**
|
|
134
|
+
- `AUTO = "auto"` — сначала пытается снизу, если не влезает — сверху, иначе снизу.
|
|
135
|
+
- `BOTTOM = "bottom"` — принудительно снизу.
|
|
136
|
+
- `TOP = "top"` — принудительно сверху.
|
|
137
|
+
|
|
138
|
+
**`EDropdownAlignment`:**
|
|
139
|
+
- `LEFT = "left"` — левая граница dropdown'а = левая target'а.
|
|
140
|
+
- `RIGHT = "right"` — правая граница dropdown'а = правая target'а.
|
|
141
|
+
|
|
142
|
+
**`EDropdownWidth`:**
|
|
143
|
+
- `CONTENT = "content"` — по ширине контента.
|
|
144
|
+
- `TARGET = "target"` — `width = targetRect.width`.
|
|
145
|
+
- `MIN_TARGET = "min-target"` — `minWidth = targetRect.width`.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Ключевые особенности реализации
|
|
150
|
+
|
|
151
|
+
### Portal → document.body
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
<Portal container={document.body}>...</Portal>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Dropdown всегда рендерится в `body`, чтобы не зависеть от `overflow:hidden`
|
|
158
|
+
родителей и z-index-стеков. Минус: вычисление позиции в window-координатах,
|
|
159
|
+
а не относительно родителя.
|
|
160
|
+
|
|
161
|
+
### MobileView разветвление — только при `mobileViewProps`
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
{mobileViewProps ? (
|
|
165
|
+
<MobileView fallback={<DropdownDesktop .../>}>
|
|
166
|
+
<DropdownMobile>{mobileViewProps?.children || children}</DropdownMobile>
|
|
167
|
+
</MobileView>
|
|
168
|
+
) : (
|
|
169
|
+
<DropdownDesktop>{children}</DropdownDesktop>
|
|
170
|
+
)}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Если `mobileViewProps` не передан — даже на мобильном будет
|
|
174
|
+
DropdownDesktop.** Это разумно для меню (просто маленький dropdown),
|
|
175
|
+
но для полноэкранных селектов требуется явный `mobileViewProps`.
|
|
176
|
+
|
|
177
|
+
### Desktop: позиционирование через `getBoundingClientRect`
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
const setPosition = useCallback(() => {
|
|
181
|
+
const dropdownRect = dropdownRef.current.getBoundingClientRect();
|
|
182
|
+
const targetRect = targetRef.current.getBoundingClientRect();
|
|
183
|
+
// ...
|
|
184
|
+
setStyleState(css); // { top/bottom, left, width?, minWidth? }
|
|
185
|
+
}, [...]);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**`direction=AUTO`:** сначала пробует low (bottom), если не влезает
|
|
189
|
+
→ top, если тоже не влезает → bottom (скролл). 4px offset между
|
|
190
|
+
target и dropdown (`offset = 4`).
|
|
191
|
+
|
|
192
|
+
**`alignment=LEFT`:** dropdown.left = target.left, если влезает вправо;
|
|
193
|
+
иначе target.right - dropdown.width (с `Math.max(0, ...)`).
|
|
194
|
+
**`alignment=RIGHT`:** зеркально.
|
|
195
|
+
|
|
196
|
+
### Desktop: блокировка скролла и клавиш при открытии
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
document.addEventListener("wheel", wheelHandler, { passive: false });
|
|
200
|
+
document.addEventListener("keydown", keyDownHandler);
|
|
201
|
+
document.body.classList.add(overflowHiddenClassName);
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
- **`wheelHandler`:** preventDefault колеса, если target не внутри
|
|
205
|
+
dropdown'а. Эффект: страница не скроллится, но dropdown (если в нём
|
|
206
|
+
есть scroll) — скроллится.
|
|
207
|
+
- **`keyDownHandler`:** preventDefault на Space/PageUp/PageDown/End/Home/
|
|
208
|
+
Arrows, **но только если `event.target === document.body`**. Это
|
|
209
|
+
защита от "прыжка страницы", когда ни одно поле не сфокусировано.
|
|
210
|
+
Если фокус в input'е — клавиши работают нормально.
|
|
211
|
+
- **`body.classList.add(overflowHiddenClassName)`:** визуально
|
|
212
|
+
"замораживает" страницу.
|
|
213
|
+
|
|
214
|
+
**Эффект отписки:** `removeEventListener` + `classList.remove` —
|
|
215
|
+
обязательны. Memory leak и залипший overflow без них.
|
|
216
|
+
|
|
217
|
+
### Desktop: ResizeObserver + scroll/resize listeners
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
const observer = new ResizeObserver(setPosition);
|
|
221
|
+
observer.observe(dropdownRef.current);
|
|
222
|
+
observer.observe(targetRef.current);
|
|
223
|
+
// + document.addEventListener("scroll", updatePosition, true);
|
|
224
|
+
// + window.addEventListener("resize", updatePosition);
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Три источника изменений:**
|
|
228
|
+
1. **ResizeObserver** — dropdown или target изменили размер (загрузка
|
|
229
|
+
данных).
|
|
230
|
+
2. **`scroll` с `capture=true`** — любой предок target'а скроллится
|
|
231
|
+
→ dropdown должен следовать.
|
|
232
|
+
3. **`resize`** — окно поменяло размер.
|
|
233
|
+
|
|
234
|
+
`updatePosition = () => setTimeout(setPosition)` — пересчёт в следующем
|
|
235
|
+
кадре, чтобы дать браузеру layout'нуть.
|
|
236
|
+
|
|
237
|
+
### Desktop: `{opacity: 0}` при закрытии
|
|
238
|
+
|
|
239
|
+
Первый рендер — `{opacity: 0}`, пока `setPosition` не отработал.
|
|
240
|
+
После первого layout — setStyleState с рассчитанными top/left/width.
|
|
241
|
+
Это **анти-flicker трюк**: если dropdown-ом монтируется и **рендерится
|
|
242
|
+
первым кадром в (0, 0)**, юзер увидит скачок. `opacity: 0` скрывает
|
|
243
|
+
эту микросекунду.
|
|
244
|
+
|
|
245
|
+
### Desktop: `if (!opened) return null`
|
|
246
|
+
|
|
247
|
+
При закрытии компонент **размонтируется**. Это важно:
|
|
248
|
+
- Не платим за event listeners при закрытом dropdown'е.
|
|
249
|
+
- Каждое открытие — новый цикл позиционирования.
|
|
250
|
+
- **Потеря внутреннего state** (scroll position, focus) при закрытии.
|
|
251
|
+
|
|
252
|
+
### Desktop: `useToken().scopeClassName`
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
const { scopeClassName } = useToken();
|
|
256
|
+
const classNames = clsx(styles.dropdownDesktop, sizeToClassNameMap[size], scopeClassName, className);
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**`scopeClassName` нужен**, потому что `Portal` выносит dropdown в
|
|
260
|
+
`body`, и ThemeProvider-scope родителя пропадает. `scopeClassName`
|
|
261
|
+
возвращает класс, под которым активна текущая тема, — dropdown его
|
|
262
|
+
тянет явно.
|
|
263
|
+
|
|
264
|
+
**Без этого** dropdown получит дефолтную (светлую) тему,
|
|
265
|
+
независимо от того, что родитель в `<ThemeProvider theme="dark">`.
|
|
266
|
+
|
|
267
|
+
### Dropdown.onOpen/onClose — effect с `mountedRef`
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
const mountedRef = useRef(false);
|
|
271
|
+
useEffect(() => {
|
|
272
|
+
if (!mountedRef.current) {
|
|
273
|
+
mountedRef.current = true;
|
|
274
|
+
} else if (opened) {
|
|
275
|
+
onOpen?.();
|
|
276
|
+
} else {
|
|
277
|
+
onClose?.();
|
|
278
|
+
}
|
|
279
|
+
}, [opened]);
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**Пропуск первого вызова** (initial mount) — чтобы `onOpen`/`onClose`
|
|
283
|
+
срабатывали только на **реальное** переключение, а не на initial
|
|
284
|
+
`opened=false`. Если удалить `mountedRef` — `onClose` вызовется сразу
|
|
285
|
+
после mount.
|
|
286
|
+
|
|
287
|
+
### DropdownList: keyboard navigation + activeDescendant
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
// Esc/Arrows на document, preventDefault:
|
|
291
|
+
if (keyCode === ARROW_DOWN) nextIndex = (currentIndex + 1) % childrenLength;
|
|
292
|
+
if (keyCode === ARROW_UP) nextIndex = (currentIndex - 1 + childrenLength) % childrenLength;
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
**Wrap-around** — стрелки зацикливают навигацию (вниз с последнего →
|
|
296
|
+
на первый, вверх с первого → на последний).
|
|
297
|
+
|
|
298
|
+
**`scrollContainerToItem(nextIndex)`** — виртуально-скроллит контейнер,
|
|
299
|
+
чтобы активный элемент был в видимой области, с 4px offset.
|
|
300
|
+
|
|
301
|
+
**`DropdownListContext.setActiveDescendant(id)`** — контекст для
|
|
302
|
+
target'а (input/button), который использует `aria-activedescendant=id`
|
|
303
|
+
для screen reader'ов. Target вне `DropdownList`, но в родительском
|
|
304
|
+
контексте.
|
|
305
|
+
|
|
306
|
+
**Initial active:** при открытии — ищет первый `selected` child
|
|
307
|
+
(`child.props.selected === true`), если не найдено — ставит active=0.
|
|
308
|
+
Скроллит в начало при отсутствии selected.
|
|
309
|
+
|
|
310
|
+
### DropdownList: cloneElement + MouseOver/MouseOut
|
|
311
|
+
|
|
312
|
+
Каждый child клонируется с `active={activeListItemIndex === index}` +
|
|
313
|
+
`onMouseOver/Out` переопределены — move mouse на item тоже меняет
|
|
314
|
+
активный.
|
|
315
|
+
|
|
316
|
+
**Dual navigation:** клавиатура и мышь вместе работают. Движение
|
|
317
|
+
мыши "отбрасывает" клавиатурный активный индекс и синхронизирует с
|
|
318
|
+
местом курсора.
|
|
319
|
+
|
|
320
|
+
### Mobile: OverlayBase + body-overflow block
|
|
321
|
+
|
|
322
|
+
```tsx
|
|
323
|
+
<OverlayBase setOpened={setOpened} opened={opened} direction={EOverlayDirection.BOTTOM}>
|
|
324
|
+
{(props) => <DropdownMobileInner {...props} ... />}
|
|
325
|
+
</OverlayBase>
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**OverlayBase** — базовый "шторка" оверлей с анимацией bottom-up,
|
|
329
|
+
swipe-to-close и overlay-backdrop. DropdownMobile — лишь стайлинг
|
|
330
|
+
поверх.
|
|
331
|
+
|
|
332
|
+
**`document.body.classList.add(dropdownMobileBodyOverflowClassName)`**
|
|
333
|
+
— блокирует скролл body, пока открыт мобильный dropdown.
|
|
334
|
+
|
|
335
|
+
### `data-tx={process.env.npm_package_version}` — на desktop и mobile root
|
|
336
|
+
|
|
337
|
+
Стандарт. Оба разветвления ставят `data-tx`. Не удалять.
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Инварианты
|
|
342
|
+
|
|
343
|
+
- **`opened` + `setOpened`** — обязательная пара (controlled).
|
|
344
|
+
- **`targetRef` обязателен** — без него desktop не может позиционироваться.
|
|
345
|
+
Если target недоступен — dropdown не откроется корректно.
|
|
346
|
+
- **Portal → document.body всегда** — не менять на inline.
|
|
347
|
+
- **`scopeClassName` из `useToken()`** — обязателен на DropdownDesktop.
|
|
348
|
+
Без него Portal-теряет контекст темы.
|
|
349
|
+
- **Desktop `if (!opened) return null`** — компонент размонтируется,
|
|
350
|
+
не скрывается через CSS. Не менять.
|
|
351
|
+
- **`data-tx`** на desktop и mobile root.
|
|
352
|
+
- **`mobileViewProps` — необязательный**. Если не задан — мобильная
|
|
353
|
+
версия использует DropdownDesktop.
|
|
354
|
+
- **`DropdownList.Item`** — единственный допустимый child (технически
|
|
355
|
+
клонирование игнорирует не-ReactElement'ы, но API об этом).
|
|
356
|
+
- **Wrap-around navigation в DropdownList** — поведение, не менять.
|
|
357
|
+
- **`mountedRef` в Dropdown.tsx** — skip first effect fire, не удалять.
|
|
358
|
+
- **`wheelHandler` с `passive: false`** — обязательно для preventDefault.
|
|
359
|
+
- **`keyDownHandler` фильтрует по `event.target === document.body`** —
|
|
360
|
+
не preventDefault'ить клавиши в input'ах.
|
|
361
|
+
- **`overflowHiddenClassName` add/remove на body** — аккуратно
|
|
362
|
+
отписываться.
|
|
363
|
+
- **Selected→active логика при открытии DropdownList** — работает
|
|
364
|
+
как "scroll to selected". Не сломать.
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Accessibility
|
|
369
|
+
|
|
370
|
+
- **`role="listbox"`** на DropdownList; `role="option"` на DropdownListItem
|
|
371
|
+
(последнее в соответствующем компоненте).
|
|
372
|
+
- **`aria-activedescendant`** — target (input/button) должен
|
|
373
|
+
выставлять его из `DropdownListContext.activeDescendant`. Screen
|
|
374
|
+
reader объявляет активный option.
|
|
375
|
+
- **Клавиатура:**
|
|
376
|
+
- Esc — закрыть (обрабатывается потребителем, Dropdown сам не ловит).
|
|
377
|
+
- ↑/↓ — навигация по DropdownList.
|
|
378
|
+
- Enter — выбор (DropdownListItem сам обрабатывает).
|
|
379
|
+
- Tab — не работает внутри (DropdownList не в tab-order).
|
|
380
|
+
- **Mobile:** OverlayBase + swipe-to-close. Focus-trap через OverlayBase.
|
|
381
|
+
- **`role="dialog" aria-modal="true"`** — не проставляется автоматически,
|
|
382
|
+
потребитель может передать через `...rest` (как делает
|
|
383
|
+
`DatePickerExtendedDropdown`).
|
|
384
|
+
- **Body-scroll lock** — предотвращает "прыжки" при скролле на
|
|
385
|
+
мобильных.
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## Дизайн-токены
|
|
390
|
+
|
|
391
|
+
Группы в `src/components/DesignTokens/components/`:
|
|
392
|
+
- **`Dropdown.ts`** — `--triplex-next-Dropdown-Background` + border/shadow
|
|
393
|
+
- **`DropdownList.ts`** — hover/active/selected для item'ов
|
|
394
|
+
- **`DropdownMobile.ts`** — фон, border-radius, header/footer
|
|
395
|
+
- **`DropdownMobileList.ts`** — item'ы в мобильной ветке
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
## Связанные компоненты
|
|
400
|
+
|
|
401
|
+
- `Portal` (`src/components/Portal`) — рендер в body.
|
|
402
|
+
- `MobileView` (`src/components/MobileView`) — разветвление.
|
|
403
|
+
- `OverlayBase` (`src/components/Overlay`) — база для мобильного.
|
|
404
|
+
- `useToken` / `ThemeProvider` — scopeClassName.
|
|
405
|
+
- `DropdownList` / `DropdownListItem` — compound-список (desktop).
|
|
406
|
+
- `DropdownMobile*` — mobile compound-семейство.
|
|
407
|
+
- Основные обёртки: `SelectField`, `SelectExtendedField`, `Suggest`,
|
|
408
|
+
`SuggestField`, `DatePickerExtended`, `MultiselectField`.
|
|
409
|
+
- `createSizeToClassNameMap` — утилита для size-класса.
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## Stories
|
|
414
|
+
|
|
415
|
+
`stories/Dropdown/Dropdown.stories.tsx`
|
|
416
|
+
Примеры: `stories/Dropdown/examples/`
|
|
417
|
+
|
|
418
|
+
Типовые stories:
|
|
419
|
+
- `Default` — базовый открывающийся dropdown
|
|
420
|
+
- `Directions` — auto/bottom/top
|
|
421
|
+
- `Alignments` — left/right
|
|
422
|
+
- `Widths` — content/target/min-target
|
|
423
|
+
- `MobileView` — с `mobileViewProps`
|
|
424
|
+
- `DropdownList` — список с item'ами
|
|
425
|
+
- `Playground` — интерактив
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## История изменений
|
|
430
|
+
|
|
431
|
+
| Дата | Изменение |
|
|
432
|
+
|---|---|
|
|
433
|
+
| 2026-04-18 | Создан документ |
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
---
|
|
2
|
+
component: Ellipsis
|
|
3
|
+
category: Typography
|
|
4
|
+
figma-node: ""
|
|
5
|
+
figma-file: ""
|
|
6
|
+
related: [Typography, Tooltip, Spoiler]
|
|
7
|
+
tokens: []
|
|
8
|
+
stories: stories/Ellipsis/Ellipsis.stories.tsx
|
|
9
|
+
version: "1.0"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Ellipsis
|
|
13
|
+
|
|
14
|
+
## Назначение
|
|
15
|
+
|
|
16
|
+
Обрезает многострочный текст с **многоточием** после заданного числа
|
|
17
|
+
строк (`maxLines`). Использует `-webkit-line-clamp` CSS — работает во
|
|
18
|
+
всех современных браузерах (Chromium, Firefox, Safari).
|
|
19
|
+
|
|
20
|
+
Управление числом строк — через CSS-переменную `--ellipsis-line-clamp`,
|
|
21
|
+
устанавливаемую inline-style'ом. `maxLines=1` включает дополнительный
|
|
22
|
+
класс `.oneLine` с `word-break: break-all` (чтобы длинное слово не
|
|
23
|
+
оставляло пустую строку).
|
|
24
|
+
|
|
25
|
+
Используй когда: у тебя блочный контейнер с текстом, который должен
|
|
26
|
+
помещаться в фиксированное количество строк.
|
|
27
|
+
Не используй когда: нужна интерактивность (развернуть/свернуть) — бери
|
|
28
|
+
`Spoiler`; нужен tooltip с полным текстом — оборачивай в `Tooltip`.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Файловая структура
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
src/components/Ellipsis/
|
|
36
|
+
├── Ellipsis.tsx # forwardRef<HTMLDivElement>
|
|
37
|
+
├── index.ts # Barrel
|
|
38
|
+
├── styles/Ellipsis.module.less # -webkit-line-clamp через CSS-var
|
|
39
|
+
└── __tests__/Ellipsis.test.tsx
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Особенности:
|
|
43
|
+
- **`forwardRef`** есть (в отличие от Col/Divider).
|
|
44
|
+
- **`data-tx`** есть.
|
|
45
|
+
- **Единственный проп помимо HTML attributes** — `maxLines`.
|
|
46
|
+
- **Тесты** покрывают базовое поведение.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Варианты и props
|
|
51
|
+
|
|
52
|
+
### `IEllipsisProps extends React.HTMLAttributes<HTMLDivElement>`
|
|
53
|
+
|
|
54
|
+
| Prop | Тип | Обязателен | Описание |
|
|
55
|
+
|---|---|---|---|
|
|
56
|
+
| `maxLines` | `number` | Да | Максимум строк. После `maxLines`-й строки — многоточие. |
|
|
57
|
+
| `children` | `ReactNode` | — | Текст или inline-элементы. |
|
|
58
|
+
| `className` | `string` | — | Через `clsx`. |
|
|
59
|
+
| `...HTMLDivAttributes` | — | — | Стандартные. |
|
|
60
|
+
|
|
61
|
+
**Нет дефолтного `maxLines`** — обязательный проп. Если передать 0
|
|
62
|
+
или отрицательное — получишь пустой контейнер (браузер сочтёт 0-строк).
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Ключевые особенности реализации
|
|
67
|
+
|
|
68
|
+
### `--ellipsis-line-clamp` CSS-переменная
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
style={{ "--ellipsis-line-clamp": maxLines } as React.CSSProperties}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```less
|
|
75
|
+
.ellipsisLineClamp {
|
|
76
|
+
--ellipsis-line-clamp: 1;
|
|
77
|
+
-webkit-line-clamp: var(--ellipsis-line-clamp);
|
|
78
|
+
line-clamp: var(--ellipsis-line-clamp);
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Почему через var, не напрямую inline `WebkitLineClamp`:** чтобы дать
|
|
83
|
+
потребителю возможность переопределить через CSS-scope
|
|
84
|
+
(`.my-class { --ellipsis-line-clamp: 5; }`). Inline-style всегда
|
|
85
|
+
побеждает media-query; переменная может быть перезаписана по
|
|
86
|
+
breakpoint'у.
|
|
87
|
+
|
|
88
|
+
**TypeScript-каст `as React.CSSProperties`** — CSSProperties не
|
|
89
|
+
допускает custom properties без кастинга. Стандартный приём.
|
|
90
|
+
|
|
91
|
+
**Fallback `--ellipsis-line-clamp: 1;`** в базовом классе — защита
|
|
92
|
+
если по каким-то причинам inline var не подхватился.
|
|
93
|
+
|
|
94
|
+
### `-webkit-line-clamp` требует специальной связки
|
|
95
|
+
|
|
96
|
+
```less
|
|
97
|
+
display: -webkit-box;
|
|
98
|
+
-webkit-line-clamp: var(--ellipsis-line-clamp);
|
|
99
|
+
-webkit-box-orient: vertical;
|
|
100
|
+
overflow: hidden;
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Все четыре свойства обязательны.** Без `display: -webkit-box` +
|
|
104
|
+
`-webkit-box-orient: vertical` — `-webkit-line-clamp` игнорируется.
|
|
105
|
+
Без `overflow: hidden` — многоточия не будет.
|
|
106
|
+
|
|
107
|
+
**`line-clamp` (без `-webkit-`)** — стандартное свойство, добавлено
|
|
108
|
+
для будущей совместимости. На данный момент все браузеры всё ещё
|
|
109
|
+
используют vendor-префикс, но standard работает в свежих версиях.
|
|
110
|
+
|
|
111
|
+
### `white-space: normal` (не nowrap)
|
|
112
|
+
|
|
113
|
+
```less
|
|
114
|
+
white-space: normal;
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Важно:** стандартное `white-space: normal` гарантирует, что текст
|
|
118
|
+
может переноситься на новые строки. Без этого `-webkit-line-clamp`
|
|
119
|
+
не сработает — текст просто обрежется справа.
|
|
120
|
+
|
|
121
|
+
### `word-break: break-word` vs `break-all` для `maxLines=1`
|
|
122
|
+
|
|
123
|
+
```less
|
|
124
|
+
word-break: break-word; // default — разрывает только по слову/пробелу
|
|
125
|
+
&.oneLine {
|
|
126
|
+
word-break: break-all; // разрывает посреди слова
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Для `maxLines=1`** — `break-all`. Причина: при одной строке,
|
|
131
|
+
если слишком длинное слово, `break-word` оставит **пустую строку**
|
|
132
|
+
перед словом (оно перенесётся целиком на следующую, но её нет).
|
|
133
|
+
`break-all` гарантирует, что хоть часть слова влезет в одну строку,
|
|
134
|
+
а остальное обрежется многоточием.
|
|
135
|
+
|
|
136
|
+
Для `maxLines > 1` — `break-word` — красивее (слова не разрываются).
|
|
137
|
+
|
|
138
|
+
### Компонент — `<div>`, не `<p>`
|
|
139
|
+
|
|
140
|
+
Не рендерит semantic `<p>` или `<span>`. Потребитель может обернуть
|
|
141
|
+
любой текст. Если нужна семантика — использовать `as` через прокидывание
|
|
142
|
+
(не реализовано) или выбрать `Typography.Text` + обернуть в Ellipsis.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Инварианты
|
|
147
|
+
|
|
148
|
+
- **`maxLines` — обязательный проп**, без дефолта.
|
|
149
|
+
- **Все 4 CSS-свойства** (`display`, `-webkit-line-clamp`, `-webkit-box-orient`,
|
|
150
|
+
`overflow: hidden`) — обязательны. Не удалять по отдельности.
|
|
151
|
+
- **`--ellipsis-line-clamp` CSS-переменная** — источник значения. Inline
|
|
152
|
+
style задаёт, LESS читает. Не хардкодить `WebkitLineClamp`.
|
|
153
|
+
- **`white-space: normal`** — обязателен. Не менять на `nowrap`.
|
|
154
|
+
- **`word-break: break-all` только для `maxLines=1`** — класс `.oneLine`
|
|
155
|
+
добавляется именно при `=== 1`. Не применять для многострочных.
|
|
156
|
+
- **`forwardRef`** — есть.
|
|
157
|
+
- **`data-tx`** — есть, не удалять.
|
|
158
|
+
- **Рендерит `<div>`** — не `<p>`/`<span>`, семантика на потребителе.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Accessibility
|
|
163
|
+
|
|
164
|
+
- **Screen reader получает полный текст** — только `overflow: hidden`
|
|
165
|
+
визуально, но DOM-текст доступен полностью. Screen reader читает всё.
|
|
166
|
+
- **Если нужен alt/full-text на hover** — оборачивать в `Tooltip` с
|
|
167
|
+
полным текстом.
|
|
168
|
+
- **Tab-навигация** — div не focusable по умолчанию. Если дети
|
|
169
|
+
interactive — они доступны обычно.
|
|
170
|
+
- **Нет `title`-атрибута** — Ellipsis не проставляет. Потребитель
|
|
171
|
+
может передать через `...rest`.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Дизайн-токены
|
|
176
|
+
|
|
177
|
+
**Нет собственных.** Чисто CSS-техника обрезания.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Связанные компоненты
|
|
182
|
+
|
|
183
|
+
- `Typography.Text`/`Title` — типичные child'ы (задают шрифт/цвет).
|
|
184
|
+
- `Tooltip` — частая обёртка для показа полного текста при hover'е.
|
|
185
|
+
- `Spoiler` — альтернатива, когда нужна возможность "раскрыть"
|
|
186
|
+
обрезанный текст.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Stories
|
|
191
|
+
|
|
192
|
+
`stories/Ellipsis/Ellipsis.stories.tsx`
|
|
193
|
+
Примеры: `stories/Ellipsis/examples/`
|
|
194
|
+
|
|
195
|
+
Типовые stories:
|
|
196
|
+
- `Default` — 1 строка
|
|
197
|
+
- `MultipleLines` — 2, 3, 5 строк
|
|
198
|
+
- `LongWord` — демонстрация `break-all` на одной строке
|
|
199
|
+
- `WithTooltip` — комбинация с Tooltip для показа полного текста
|
|
200
|
+
- `Playground` — интерактив
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## История изменений
|
|
205
|
+
|
|
206
|
+
| Дата | Изменение |
|
|
207
|
+
|---|---|
|
|
208
|
+
| 2026-04-18 | Создан документ |
|