@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,233 @@
|
|
|
1
|
+
---
|
|
2
|
+
component: IslandWidget
|
|
3
|
+
category: Layout
|
|
4
|
+
figma-node: ""
|
|
5
|
+
figma-file: ""
|
|
6
|
+
related: [Island, IslandAccordion, ExpandAnimation, MediaWidth]
|
|
7
|
+
tokens: []
|
|
8
|
+
stories: stories/IslandWidget/IslandWidget.stories.tsx
|
|
9
|
+
version: "1.0"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# IslandWidget
|
|
13
|
+
|
|
14
|
+
## Назначение
|
|
15
|
+
|
|
16
|
+
Виджет-карточка: как `Island` (TYPE_1, MD), но:
|
|
17
|
+
- Принимает контент через **render-props** (`renderHeader`/`renderBody`/`renderFooter`) — не через children.
|
|
18
|
+
- На **мобиле (≤ SM_MAX) автоматически сворачивает Body+Footer**, заголовок становится кликабельным, рядом появляется chevron.
|
|
19
|
+
- Поддерживает необязательный **`ExtraFooter`** — дополнительный раздел, который открывается/закрывается управляемо (`open` prop), **снаружи** Island'а.
|
|
20
|
+
- Оптимизирован под работу в группе (`IslandWidgetWrapper` — provider для `hasExtraFooter`).
|
|
21
|
+
|
|
22
|
+
Используй когда: нужен адаптивный виджет на дашборде/пульте, который
|
|
23
|
+
схлопывается на мобиле. Если адаптивность не нужна — `Island` проще.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Файловая структура
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
src/components/IslandWidget/
|
|
31
|
+
├── IslandWidget.tsx # Object.assign с Header/Body/Footer/ExtraFooter
|
|
32
|
+
├── IslandWidgetContext.tsx # { adaptive, disableAdaptiveCollapsing, open }
|
|
33
|
+
├── IslandWidgetLayoutContext.tsx # { hasExtraFooter, setHasExtraFooter }
|
|
34
|
+
├── index.ts # Barrel
|
|
35
|
+
├── components/
|
|
36
|
+
│ ├── IslandWidgetHeader.tsx # FC + статические .Content/.Description, chevron при adaptive
|
|
37
|
+
│ ├── IslandWidgetHeaderContent.tsx
|
|
38
|
+
│ ├── IslandWidgetHeaderDescription.tsx
|
|
39
|
+
│ ├── IslandWidgetBody.tsx # FC
|
|
40
|
+
│ ├── IslandWidgetFooter.tsx # FC + статические .Content/.Controls
|
|
41
|
+
│ ├── IslandWidgetFooterContent.tsx
|
|
42
|
+
│ ├── IslandWidgetFooterControls.tsx
|
|
43
|
+
│ ├── IslandWidgetExtraFooter.tsx # FC, управляемое open (ExpandAnimation)
|
|
44
|
+
│ └── IslandWidgetWrapper.tsx # forwardRef, Provider IslandWidgetLayoutContext
|
|
45
|
+
└── styles/*.module.less
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Compound-структура
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
<IslandWidgetWrapper>
|
|
54
|
+
<IslandWidget
|
|
55
|
+
renderHeader={(props) => (
|
|
56
|
+
<IslandWidget.Header {...props}>
|
|
57
|
+
<IslandWidget.Header.Content>Заголовок</IslandWidget.Header.Content>
|
|
58
|
+
<IslandWidget.Header.Description>Описание</IslandWidget.Header.Description>
|
|
59
|
+
</IslandWidget.Header>
|
|
60
|
+
)}
|
|
61
|
+
renderBody={(props) => <IslandWidget.Body {...props}>{...}</IslandWidget.Body>}
|
|
62
|
+
renderFooter={(props) => (
|
|
63
|
+
<IslandWidget.Footer {...props}>
|
|
64
|
+
<IslandWidget.Footer.Content>...</IslandWidget.Footer.Content>
|
|
65
|
+
<IslandWidget.Footer.Controls>
|
|
66
|
+
<Button>...</Button>
|
|
67
|
+
</IslandWidget.Footer.Controls>
|
|
68
|
+
</IslandWidget.Footer>
|
|
69
|
+
)}
|
|
70
|
+
/>
|
|
71
|
+
<IslandWidget.ExtraFooter open={extraOpen}>
|
|
72
|
+
Дополнительный блок снизу
|
|
73
|
+
</IslandWidget.ExtraFooter>
|
|
74
|
+
</IslandWidgetWrapper>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`IslandWidgetWrapper` опционален — нужен только если используешь `ExtraFooter`
|
|
78
|
+
(иначе `hasExtraFooter` всегда `false` и стили корня не подстраиваются).
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Варианты и props
|
|
83
|
+
|
|
84
|
+
### IslandWidget — `IIslandWidgetProps extends HTMLAttributes<HTMLDivElement>`
|
|
85
|
+
|
|
86
|
+
| Prop | Тип | Обязателен | Описание |
|
|
87
|
+
|---|---|---|---|
|
|
88
|
+
| `renderHeader` | `(props: IIslandWidgetHeaderProps) => ReactNode` | Да | Рендер-функция заголовка. Получает пустой объект — можно использовать `IslandWidget.Header` с любыми child'ами. |
|
|
89
|
+
| `renderBody` | `(props: IIslandWidgetBodyProps) => ReactNode` | Да | Рендер-функция Body. |
|
|
90
|
+
| `renderFooter` | `(props: IIslandWidgetFooterProps) => ReactNode` | Нет | Необязательный Footer. |
|
|
91
|
+
| `disableAdaptiveCollapsing` | `boolean` | Нет (default `false`) | Отключает автоматическое сворачивание на мобиле. При `true` — заголовок некликабельный, chevron не рендерится. |
|
|
92
|
+
|
|
93
|
+
**Почему через render-props, а не children:** IslandWidget оборачивает контент в `ExpandAnimation` на adaptive, и ему нужно знать структуру слотов. Render-props позволяют вставлять слоты именно в нужные места (внутрь/снаружи ExpandAnimation).
|
|
94
|
+
|
|
95
|
+
### IslandWidget.Header — `IIslandWidgetHeaderProps extends HTMLAttributes<HTMLDivElement>`
|
|
96
|
+
|
|
97
|
+
FC (не forwardRef). Не управляемый; читает `IslandWidgetContext` и рендерит chevron если `adaptive && !disableAdaptiveCollapsing`. Имеет статические `.Content` и `.Description`.
|
|
98
|
+
|
|
99
|
+
### IslandWidget.Footer — `IIslandWidgetFooterProps extends HTMLAttributes<HTMLDivElement>`
|
|
100
|
+
|
|
101
|
+
FC. Имеет статические `.Content` и `.Controls` — типовой паттерн "описание + кнопки".
|
|
102
|
+
|
|
103
|
+
### IslandWidget.Body — `IIslandWidgetBodyProps`
|
|
104
|
+
|
|
105
|
+
FC, без подслотов.
|
|
106
|
+
|
|
107
|
+
### IslandWidget.ExtraFooter — `IIslandWidgetExtraFooterProps`
|
|
108
|
+
|
|
109
|
+
| Prop | Тип | Default | Описание |
|
|
110
|
+
|---|---|---|---|
|
|
111
|
+
| `open` | `boolean` | `false` | Управляемое состояние раскрытия. |
|
|
112
|
+
|
|
113
|
+
- Оборачивает содержимое в `ExpandAnimation expanded={open}`.
|
|
114
|
+
- В `useEffect` пишет `setHasExtraFooter(open)` в `IslandWidgetLayoutContext` — корневой `IslandWidget` использует этот флаг для корректировки стилей корня (добавляет `.islandWidgetWithExtraFooter`).
|
|
115
|
+
|
|
116
|
+
### IslandWidgetWrapper
|
|
117
|
+
|
|
118
|
+
ForwardRef. Provider `IslandWidgetLayoutContext`. Нужен только для корректной работы `ExtraFooter`.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Ключевые особенности реализации
|
|
123
|
+
|
|
124
|
+
### Adaptive через `useMatchMedia`
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const adaptive = useMatchMedia(
|
|
128
|
+
`(max-width: ${EScreenWidth.SM_MAX})`,
|
|
129
|
+
window.innerWidth <= parseInt(EScreenWidth.SM_MAX),
|
|
130
|
+
);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
- Первым аргументом — media query, вторым — initial fallback для SSR-safe значения.
|
|
134
|
+
- `window.innerWidth` вычисляется на этапе рендера — **не SSR-safe** (упадёт на сервере). Потребители в SSR должны оборачивать виджет в `ClientOnly`-паттерн.
|
|
135
|
+
|
|
136
|
+
### Управление открытием — внутреннее `useState`
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
const [open, setOpen] = useState(disableAdaptiveCollapsing);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
- Если adaptive не включён — поведение не работает.
|
|
143
|
+
- `open` инициализируется значением `disableAdaptiveCollapsing`: без адаптивного сворачивания виджет открыт всегда (это защита, если `disableAdaptiveCollapsing` меняется рантайм).
|
|
144
|
+
- Нет prop'а `defaultOpen` / `opened` / `onToggle` — открытие полностью неуправляемое.
|
|
145
|
+
- Клик по заголовку → `handleHeaderClick` → `setOpen(!open)`. Срабатывает **только если adaptive**.
|
|
146
|
+
|
|
147
|
+
### ExpandAnimation оборачивает Body + Footer (но не Header)
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
{expandableContent
|
|
151
|
+
? <ExpandAnimation expanded={open}>{renderContent()}</ExpandAnimation>
|
|
152
|
+
: <>{renderContent()}</>}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
На desktop (не adaptive) или при `disableAdaptiveCollapsing=true` — без анимации, контент виден сразу.
|
|
156
|
+
|
|
157
|
+
### `onClick` на wrapper'е Header'а, а не на Header'е
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
<div
|
|
161
|
+
className={clsx({ [styles.islandWidgetHeaderWrapperAdaptive]: adaptive })}
|
|
162
|
+
onClick={adaptive ? handleHeaderClick : undefined}
|
|
163
|
+
>
|
|
164
|
+
{renderHeader({})}
|
|
165
|
+
</div>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
- Click-target — обёртка, а не сам header. Это упрощает CSS (hover только на adaptive).
|
|
169
|
+
- **Нет `role="button"`/`tabIndex`/keyboard-навигации** на wrapper'е — клик только мышью. Это a11y-gap.
|
|
170
|
+
|
|
171
|
+
### `hasExtraFooter` в LayoutContext
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
<div className={clsx(styles.islandWidget, className, {
|
|
175
|
+
[styles.islandWidgetWithExtraFooter]: hasExtraFooter,
|
|
176
|
+
})}>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Корневой Island меняет стиль (например, нижний радиус), когда снизу есть ExtraFooter. Это делает Wrapper'ом эффективным лишь для пары "IslandWidget + ExtraFooter". Без Wrapper'а контекст дефолтный (`hasExtraFooter=false`).
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Инварианты
|
|
184
|
+
|
|
185
|
+
- **Render-props API (`renderHeader`/`renderBody`/`renderFooter`)** — публичный контракт. Не заменять на children без миграции потребителей.
|
|
186
|
+
- **Дочерние `Header`/`Body`/`Footer`** — `React.FC` (без forwardRef). Обёртки легковесные, но не прокидывают ref наружу.
|
|
187
|
+
- **`IslandWidgetHeader` / `IslandWidgetFooter`** — статические подкомпоненты через **не Object.assign**, а через `TIslandWidgetHeader = FC & { Content, Description }` + присваивание полей после определения. При изменении сохранить паттерн.
|
|
188
|
+
- **`IslandWidget.Body`** — не экспортируется в barrel прямо (`export * from "./IslandWidgetBody"` в components/index.ts есть, но проверить отдельно потребителей). Проверь экспорт перед изменением.
|
|
189
|
+
- **`useMatchMedia` + `window.innerWidth` на инициализации** — **не SSR-safe**. Не переносить в SSR-контекст без защиты.
|
|
190
|
+
- **Клик-target — wrapper Header'а**. Нет ARIA/keyboard для коллапса — известный a11y-debt.
|
|
191
|
+
- **`Island` хардкожен как `TYPE_1 / MD`** внутри — если нужен другой тип/размер, нужен рефакторинг с пропагацией через props.
|
|
192
|
+
- **`ExtraFooter` требует `IslandWidgetWrapper`** для корректного стиля нижнего радиуса корневого Island.
|
|
193
|
+
- **`ExpandAnimation`** оборачивает только Body+Footer; Header всегда виден.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Accessibility
|
|
198
|
+
|
|
199
|
+
- Клик по header'у на мобиле **не доступен с клавиатуры** (нет `tabIndex`/`role="button"`/keyboard handler). Known debt. Для критичных сценариев используйте `disableAdaptiveCollapsing` и навигацию через iOS/Android native.
|
|
200
|
+
- Chevron-иконка — `aria-hidden="true"`, правильно.
|
|
201
|
+
- Нет `aria-expanded`/`aria-controls` на wrapper'е — скринридер не анонсирует состояние. Known debt.
|
|
202
|
+
- Остальное — прокидывается через `...htmlDivAttributes`.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Дизайн-токены
|
|
207
|
+
|
|
208
|
+
Нет собственных (все от `Island.TYPE_1`: `--triplex-next-Island-Type1_Background` / `_Shadow`).
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Связанные компоненты
|
|
213
|
+
|
|
214
|
+
- `Island` — корневая карточка (hardcoded TYPE_1/MD внутри).
|
|
215
|
+
- `IslandAccordion` — альтернатива для списка раскрывающихся элементов.
|
|
216
|
+
- `ExpandAnimation` — используется для анимации сворачивания.
|
|
217
|
+
- `MediaWidth.useMatchMedia` — детектирует адаптив.
|
|
218
|
+
- `EScreenWidth` (`@sberbusiness/triplex-next/helpers/breakpoints`) — breakpoint SM_MAX.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Stories
|
|
223
|
+
|
|
224
|
+
`stories/IslandWidget/IslandWidget.stories.tsx`
|
|
225
|
+
Примеры: `stories/IslandWidget/examples/`
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## История изменений
|
|
230
|
+
|
|
231
|
+
| Дата | Изменение |
|
|
232
|
+
|---|---|
|
|
233
|
+
| 2026-04-18 | Создан документ |
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
---
|
|
2
|
+
component: KeyDownListener
|
|
3
|
+
category: Utilities
|
|
4
|
+
figma-node: ""
|
|
5
|
+
figma-file: ""
|
|
6
|
+
related: [ComposedKeyDownListener, WindowResizeListener]
|
|
7
|
+
tokens: []
|
|
8
|
+
stories: ""
|
|
9
|
+
version: "1.0"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# KeyDownListener
|
|
13
|
+
|
|
14
|
+
## Назначение
|
|
15
|
+
|
|
16
|
+
Невизуальный компонент-слушатель `keydown` на `window`. При совпадении
|
|
17
|
+
кода нажатой клавиши с `eventKeyCode` (одно значение или массив) вызывает
|
|
18
|
+
`onMatch(event)`. Рендерит `children` как есть (или `null`) — не добавляет
|
|
19
|
+
обёртку в DOM.
|
|
20
|
+
|
|
21
|
+
Используй когда: нужно отреагировать на глобальное нажатие клавиш
|
|
22
|
+
(Escape для закрытия модалки, Tab/ArrowKey для навигации в кастомных
|
|
23
|
+
виджетах), но не привязываться к конкретному focus-target'у.
|
|
24
|
+
|
|
25
|
+
Для нескольких параллельных слушателей используй `ComposedKeyDownListener` —
|
|
26
|
+
принимает массив конфигов и композитит их в иерархию.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Файловая структура
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
src/components/KeyDownListener/
|
|
34
|
+
├── KeyDownListener.tsx # React.Component (class) с window-keydown
|
|
35
|
+
├── ComposedKeyDownListener.tsx # FC, сворачивает массив listener'ов в tree
|
|
36
|
+
└── index.ts # Barrel: оба компонента
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Нет styles, stories, tests.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Варианты и props
|
|
44
|
+
|
|
45
|
+
### KeyDownListener — `IKeyDownListenerProps`
|
|
46
|
+
|
|
47
|
+
| Prop | Тип | Обязателен | Описание |
|
|
48
|
+
|---|---|---|---|
|
|
49
|
+
| `eventKeyCode` | `number \| number[]` | Да | Код клавиши (число — это `KeyboardEvent.keyCode`, не `key`). Один код или массив. |
|
|
50
|
+
| `onMatch` | `(event: KeyboardEvent) => void` | Да | Вызывается при совпадении кода. |
|
|
51
|
+
| `children` | `React.ReactNode` | Нет | Рендерятся "прозрачно" (компонент не оборачивает в DOM). |
|
|
52
|
+
|
|
53
|
+
Тип кода: `keyCode = (typeof EVENT_KEY_CODES)[keyof typeof EVENT_KEY_CODES]`
|
|
54
|
+
(числа из `@sberbusiness/triplex-next/utils/keyboard`): `ENTER=13`, `ESCAPE=27`,
|
|
55
|
+
`TAB=9`, `ARROW_*=37-40`, и т.д.
|
|
56
|
+
|
|
57
|
+
### ComposedKeyDownListener — `IKeyDownListenerManagerProps`
|
|
58
|
+
|
|
59
|
+
| Prop | Тип | Обязателен | Описание |
|
|
60
|
+
|---|---|---|---|
|
|
61
|
+
| `keyDownListeners` | `IKeyDownListenerProps[]` | Да | Массив конфигов. Для каждого создаётся свой `KeyDownListener`. |
|
|
62
|
+
| `children` | `React.ReactNode` | Нет | Обёрнуты всеми listener'ами в иерархию (последний в массиве — самый внешний). |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Ключевые особенности реализации
|
|
67
|
+
|
|
68
|
+
### `KeyDownListener` — class component
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
export class KeyDownListener extends React.Component<IKeyDownListenerProps> {
|
|
72
|
+
public componentDidMount() { window.addEventListener("keydown", this.handleKeyDown); }
|
|
73
|
+
public componentWillUnmount() { window.removeEventListener("keydown", this.handleKeyDown); }
|
|
74
|
+
public render() { return this.props.children || null; }
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- **Сохранён как class**, хотя весь остальной код библиотеки — функциональные компоненты. Миграция на `useEffect` возможна, но class сохраняет идентичность listener'а через reference handler'а — аналог можно сделать через `useEffect` + стабильный handler (через ref на `onMatch`).
|
|
79
|
+
- Не `forwardRef` — компонент не имеет своего DOM.
|
|
80
|
+
|
|
81
|
+
### Использование `keyCode`, а не `key` / `code`
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
if (typeof eventKeyCode === "number") {
|
|
85
|
+
eventKeyCode === event.keyCode && onMatch(event);
|
|
86
|
+
} else if (eventKeyCode.includes(event.keyCode)) {
|
|
87
|
+
onMatch(event);
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- `event.keyCode` — **deprecated** в современном вебе (замена: `event.key` или `event.code`). Компонент и набор `EVENT_KEY_CODES` сохранили legacy-подход.
|
|
92
|
+
- Числовые константы (13, 27, 40, ...) должны использоваться через `EVENT_KEY_CODES.ENTER` / `.ESCAPE` и т.д., не литерально.
|
|
93
|
+
- Для новых компонентов предпочитать `isKey(event.key, "ENTER")` из `utils/keyboard` — она поддерживает `key`-strings и legacy-fallback.
|
|
94
|
+
|
|
95
|
+
### `onMatch` не кешируется в class state
|
|
96
|
+
|
|
97
|
+
Каждый keydown вытаскивает `this.props.onMatch` актуальный — замыкание не устаревает. При миграции на hooks это поведение нужно воспроизвести через ref.
|
|
98
|
+
|
|
99
|
+
### `ComposedKeyDownListener` — reduce-вариант через цикл
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
let accumulatedElement = children as JSX.Element;
|
|
103
|
+
for (let i = 0; i < keyDownListeners.length; i++) {
|
|
104
|
+
accumulatedElement = <KeyDownListener {...}>{accumulatedElement}</KeyDownListener>;
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- Последний listener в массиве → самый внешний wrapper в tree.
|
|
109
|
+
- Порядок не влияет на результат (все listener'ы слушают `window` независимо), но Reactу важен для reconciliation — не менять порядок без нужды.
|
|
110
|
+
- **Внимание:** ключи не проставляются. При рендере одного и того же конфига это ок, но при динамическом изменении массива React может ломать состояние child'ов — используй stable ordering.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Инварианты
|
|
115
|
+
|
|
116
|
+
- **`eventKeyCode` — `number | number[]`** (не `string`/`KeyboardEvent["key"]`). Публичный контракт. При смене на `key`-строки — breaking change.
|
|
117
|
+
- **`onMatch(event: KeyboardEvent)`** — получает нативное событие, не синтетическое React-событие. Не менять тип на `React.KeyboardEvent`.
|
|
118
|
+
- **Class component** — сохранить при рефакторинге; миграция на FC допустима, но должна воспроизвести `componentWillUnmount`-очистку и актуальный `onMatch`.
|
|
119
|
+
- **`window.addEventListener`** — listener на окне, не на document. Не менять без согласования (влияет на порядок обработки событий с портальными элементами).
|
|
120
|
+
- **`children` рендерятся без обёртки** — компонент "прозрачный" в DOM. Не добавлять `<div>`/`<Fragment>` без нужды.
|
|
121
|
+
- **`ComposedKeyDownListener`** — простой композитор. Не усложнять (например, не добавлять `useMemo`/контексты) без реальной нужды.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Accessibility
|
|
126
|
+
|
|
127
|
+
- Компонент работает на уровне окна, перехватывает keydown до того, как фокусируемый элемент получит шанс обработать — при использовании **в составе интерактивных виджетов** учитывай, что нативный `onKeyDown` на inputе/кнопке сработает **после** глобального listener'а (из-за bubbling), но глобальный listener активен всегда, даже если фокус в другом месте.
|
|
128
|
+
- Не подменяй focus management'ом. Для Escape в модалках предпочитай `focus trap` + `onKeyDown` на overlay'е.
|
|
129
|
+
- Listener активен даже в скрытом state компонента-родителя, если сам KeyDownListener не размонтирован. Контролируй монтирование через условный рендеринг.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Дизайн-токены
|
|
134
|
+
|
|
135
|
+
Нет.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Связанные компоненты
|
|
140
|
+
|
|
141
|
+
- `ComposedKeyDownListener` — обёртка для нескольких listener'ов.
|
|
142
|
+
- `WindowResizeListener` — аналогичный паттерн для resize.
|
|
143
|
+
- `@sberbusiness/triplex-next/utils/keyboard` — `EVENT_KEY_CODES`, `EVENT_KEYS`, `isKey`.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Stories
|
|
148
|
+
|
|
149
|
+
Нет.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## История изменений
|
|
154
|
+
|
|
155
|
+
| Дата | Изменение |
|
|
156
|
+
|---|---|
|
|
157
|
+
| 2026-04-18 | Создан документ |
|