@sberbusiness/triplex-next-mcp-server 0.24.0-beta2 → 1.40.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/bundle/components/AlertContext.md +268 -0
- package/bundle/components/BodyPage.md +186 -0
- package/bundle/components/Button.md +93 -85
- package/bundle/components/ButtonDropdown.md +36 -70
- package/bundle/components/ButtonDropdownExtended.md +422 -0
- package/bundle/components/ButtonIcon.md +41 -48
- package/bundle/components/Caption.md +291 -0
- package/bundle/components/Checkbox.md +43 -122
- package/bundle/components/CheckboxXGroup.md +109 -46
- package/bundle/components/CheckboxYGroup.md +77 -49
- package/bundle/components/CodeText.md +197 -0
- package/bundle/components/Col.md +311 -162
- package/bundle/components/CollapsibleTree.md +584 -0
- package/bundle/components/CollapsibleTreeExtended.md +422 -0
- package/bundle/components/Dropdown.md +748 -346
- package/bundle/components/EmptyView.md +14 -15
- package/bundle/components/FooterPage.md +203 -0
- package/bundle/components/Gap.md +115 -57
- package/bundle/components/HeaderPage.md +226 -0
- package/bundle/components/HelpBox.md +45 -84
- package/bundle/components/ImageGallery.md +423 -0
- package/bundle/components/ImageGalleryExtended.md +606 -0
- package/bundle/components/LightBox.md +2252 -233
- package/bundle/components/List.md +216 -183
- package/bundle/components/ListActionItem.md +157 -0
- package/bundle/components/ListItem.md +276 -0
- package/bundle/components/ListItemControlsButton.md +103 -0
- package/bundle/components/ListItemControlsButtonDropdown.md +107 -0
- package/bundle/components/ListItemSelectable.md +99 -0
- package/bundle/components/ListItemTable.md +390 -0
- package/bundle/components/ListMaster.md +876 -156
- package/bundle/components/ListMasterFooter.md +80 -0
- package/bundle/components/ListMasterHeader.md +86 -0
- package/bundle/components/ListSortable.md +153 -0
- package/bundle/components/ListSortableItem.md +175 -0
- package/bundle/components/ListTableItem.md +391 -0
- package/bundle/components/ModalWindow.md +427 -237
- package/bundle/components/MonthYearField.md +145 -109
- package/bundle/components/Notification.md +625 -155
- package/bundle/components/Overlay.md +271 -170
- package/bundle/components/Page.md +284 -186
- package/bundle/components/Pagination.md +206 -229
- package/bundle/components/Portal.md +131 -67
- package/bundle/components/Row.md +171 -69
- package/bundle/components/Text.md +320 -0
- package/bundle/components/TextField.md +364 -97
- package/bundle/components/Title.md +296 -0
- package/bundle/guides/codestyle.md +38 -11
- package/bundle/guides/commits.md +119 -26
- package/bundle/guides/context.md +34 -30
- package/bundle/guides/stories.md +110 -40
- package/bundle/guides/template.md +3 -7
- package/bundle/guides/tests.md +48 -2
- package/bundle/manifest.json +275 -30
- package/bundle/release-notes/1.26.0.mdx +33 -2
- package/bundle/release-notes/1.27.0.mdx +43 -0
- package/bundle/release-notes/1.28.0.mdx +25 -0
- package/bundle/release-notes/1.29.0.mdx +47 -0
- package/bundle/release-notes/1.30.0.mdx +56 -0
- package/bundle/release-notes/1.31.0.mdx +52 -0
- package/bundle/release-notes/1.32.0.mdx +35 -0
- package/bundle/release-notes/1.33.0.mdx +61 -0
- package/bundle/release-notes/1.34.0.mdx +35 -0
- package/bundle/release-notes/1.35.0.mdx +21 -0
- package/bundle/release-notes/1.35.1.mdx +11 -0
- package/bundle/release-notes/1.36.0.mdx +21 -0
- package/bundle/release-notes/1.37.0.mdx +71 -0
- package/bundle/release-notes/1.38.0.mdx +36 -0
- package/bundle/release-notes/1.39.0.mdx +66 -0
- package/bundle/release-notes/1.40.0.mdx +27 -0
- package/bundle/release-notes/1.41.0.mdx +7 -0
- package/package.json +1 -1
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
component: ModalWindow
|
|
3
|
-
category:
|
|
4
|
-
|
|
5
|
-
figma-file: ""
|
|
6
|
-
related: [LightBox, Portal, Island, Page, TriggerClickOnKeyDownEvent, Button]
|
|
3
|
+
category: ModalWindow
|
|
4
|
+
related: [ModalWindowContent, ModalWindowHeader, ModalWindowBody, ModalWindowFooter, ModalWindowClose, ModalWindowViewManager, Portal, FocusTrap, Page, HeaderPage, FooterPage, Island, LoaderScreen]
|
|
7
5
|
tokens:
|
|
8
|
-
- --
|
|
9
|
-
- --
|
|
10
|
-
- --modalWindow-screen-top
|
|
11
|
-
- --modal-window-top
|
|
6
|
+
- --triplex-next-ModalWindow-Backdrop_Background
|
|
7
|
+
- --triplex-next-ModalWindow-Background
|
|
12
8
|
stories: stories/ModalWindow/ModalWindow.stories.tsx
|
|
13
9
|
version: "1.0"
|
|
14
10
|
---
|
|
@@ -17,292 +13,486 @@ version: "1.0"
|
|
|
17
13
|
|
|
18
14
|
## Назначение
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
focus-trap, Escape-close, анимированное появление/исчезновение (CSSTransition).
|
|
22
|
-
Подкомпоненты: `Header` (на базе `HeaderPage`), `Body` (на базе `Island`),
|
|
23
|
-
`Footer` (на базе `FooterPage`), `Content` (scroll-container с Page внутри),
|
|
24
|
-
`Close` (Button с Escape-handler), и `ViewManager` (вычисляет CSS-переменные
|
|
25
|
-
позиционирования).
|
|
16
|
+
Модальное окно поверх контента страницы с затемнённым backdrop, ловушкой фокуса и блокировкой скролла `body`. Рендерится через `Portal` в собственный wrapper (`#ufs-modal-window-wrapper`), позиционируется по координатам, которые вычисляет вспомогательный `ModalWindowViewManager` (CSS-переменные `--modalWindow-screen-*`).
|
|
26
17
|
|
|
27
|
-
Используй когда:
|
|
28
|
-
Для полноэкранного — `LightBox`. Для поповера — `Overlay`.
|
|
18
|
+
Используй когда: нужно вывести критичный контент (форму, подтверждение, мастер-форму), который должен прервать взаимодействие со страницей и удержать фокус.
|
|
29
19
|
|
|
30
20
|
---
|
|
31
21
|
|
|
32
|
-
##
|
|
33
|
-
|
|
34
|
-
```text
|
|
35
|
-
src/components/ModalWindow/
|
|
36
|
-
├── ModalWindow.tsx # forwardRef, Portal + CSSTransition + FocusTrap
|
|
37
|
-
├── index.ts # Barrel всех подкомпонентов
|
|
38
|
-
├── components/
|
|
39
|
-
│ ├── ModalWindowHeader.tsx # Обёртка над HeaderPage type=FIRST, + .Title
|
|
40
|
-
│ ├── ModalWindowBody.tsx # Обёртка над Island TYPE_1, MD
|
|
41
|
-
│ ├── ModalWindowContent.tsx # <div> + <Page> + optional LoaderScreen
|
|
42
|
-
│ ├── ModalWindowFooter.tsx # Обёртка над FooterPage type=FIRST, + .Description
|
|
43
|
-
│ ├── ModalWindowClose.tsx # Button + TriggerClickOnKeyDownEvent(ESCAPE)
|
|
44
|
-
│ └── ModalWindowViewManager.tsx # CSS-переменные --modalWindow-screen-*
|
|
45
|
-
└── styles/*.module.less
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
---
|
|
49
|
-
|
|
50
|
-
## Compound-структура
|
|
51
|
-
|
|
52
|
-
```tsx
|
|
53
|
-
<ModalWindow
|
|
54
|
-
isOpen={isOpen}
|
|
55
|
-
closeButton={<ModalWindow.Close onClick={onClose} />}
|
|
56
|
-
size={EComponentSize.MD}
|
|
57
|
-
onExited={onAnimationClose}
|
|
58
|
-
>
|
|
59
|
-
<ModalWindow.Content isLoading={loading}>
|
|
60
|
-
<ModalWindow.Header>
|
|
61
|
-
<ModalWindow.Header.Title>Заголовок</ModalWindow.Header.Title>
|
|
62
|
-
</ModalWindow.Header>
|
|
63
|
-
|
|
64
|
-
<ModalWindow.Body>
|
|
65
|
-
Основной контент...
|
|
66
|
-
</ModalWindow.Body>
|
|
67
|
-
|
|
68
|
-
<ModalWindow.Footer>
|
|
69
|
-
<ModalWindow.Footer.Description>Подпись</ModalWindow.Footer.Description>
|
|
70
|
-
<Button>ОК</Button>
|
|
71
|
-
</ModalWindow.Footer>
|
|
72
|
-
</ModalWindow.Content>
|
|
73
|
-
</ModalWindow>
|
|
74
|
-
```
|
|
22
|
+
## Варианты и props
|
|
75
23
|
|
|
76
|
-
|
|
77
|
-
`ModalWindowContent`, `ModalWindowFooter`, `ModalWindowClose`) экспортируются
|
|
78
|
-
из barrel, но **не навешаны** на `ModalWindow` через `Object.assign`. Нужно
|
|
79
|
-
импортировать их **именованно**: `import { ModalWindow, ModalWindowHeader } from ...`.
|
|
24
|
+
`ModalWindow` принимает все стандартные `React.HTMLAttributes<HTMLDivElement>` (они пробрасываются на корневой `role="dialog"` элемент) плюс собственные props.
|
|
80
25
|
|
|
81
|
-
|
|
26
|
+
### Обязательные props
|
|
82
27
|
|
|
83
|
-
|
|
28
|
+
| Prop | Тип | Описание |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `isOpen` | `boolean` | Открыто ли окно. Управляет монтированием Portal и анимацией `CSSTransition`. |
|
|
31
|
+
| `children` | `React.ReactElement` | Контент окна. Обычно — `ModalWindowContent` с вложенными `ModalWindowHeader`, `ModalWindowBody`, `ModalWindowFooter`. |
|
|
32
|
+
| `closeButton` | `React.ReactNode` | Кнопка закрытия (обычно `ModalWindowClose`). Рендерится внутри корневого dialog-элемента поверх контента. |
|
|
84
33
|
|
|
85
|
-
###
|
|
34
|
+
### Опциональные props
|
|
86
35
|
|
|
87
|
-
| Prop | Тип |
|
|
36
|
+
| Prop | Тип | По умолчанию | Описание |
|
|
88
37
|
|---|---|---|---|
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `topPosition` | `number` | `100` | Отступ сверху в px. Пишется в CSS-переменную `--modal-window-top`. |
|
|
96
|
-
| `containerClassName` | `string` | — | className внешнего контейнера (с backdrop'ом). |
|
|
97
|
-
| `className`, `...HTMLDivAttributes` | — | — | Стандартные, идут на `div[role="dialog"]`. |
|
|
38
|
+
| `size` | `EComponentSize` | `EComponentSize.MD` | Размер окна: `SM` / `MD` / `LG`. Определяет `max-width` контента и `min-height` body. |
|
|
39
|
+
| `topPosition` | `number` | `100` | Отступ от верхней границы экрана, px. Прокидывается в CSS-переменную `--modal-window-top`. |
|
|
40
|
+
| `containerClassName` | `string` | — | Кастомный класс на контейнер (внешний `div` с `position: fixed`, который содержит backdrop и dialog). |
|
|
41
|
+
| `className` | `string` | — | Кастомный класс на корневой `role="dialog"` элемент. |
|
|
42
|
+
| `focusTrapProps` | `FocusTrapProps` | — | Свойства `focus-trap-react`. Внутренние `allowOutsideClick: true` и `preventScroll: true` мерджатся с пользовательскими `focusTrapOptions`. |
|
|
43
|
+
| `onExited` | `() => void` | — | Callback после завершения анимации закрытия (`CSSTransition.onExited`). |
|
|
98
44
|
|
|
99
|
-
###
|
|
45
|
+
### Ограничения использования
|
|
100
46
|
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
- `
|
|
104
|
-
-
|
|
47
|
+
- **Controlled-only.** Состояние открытия полностью контролируется потребителем через `isOpen`. Компонент сам не закрывается на клик по backdrop'у — обработчик `Escape` живёт в `ModalWindowClose`, не в `ModalWindow`.
|
|
48
|
+
- **`children` — единый `React.ReactElement`.** Это требование `CSSTransition` для анимирования одного дочернего узла. Не передавай массив или фрагмент — оборачивай контент в `ModalWindowContent`.
|
|
49
|
+
- **Rendering lifecycle.** При первом рендере синхронно создаётся локальный `mountNode` под `#ufs-modal-window-wrapper` (через `useState` lazy initializer) — это нужно, чтобы `FocusTrap` нашёл tabbable-узлы при активации. Сама модалка показывается/скрывается через `CSSTransition` (`mountOnEnter` / `unmountOnExit`); body-классы `modal-open` + `no-hash-overflow-hidden` ставятся в `onEnter` и снимаются в `onExited` (≈300ms на анимацию). При unmount-е компонента `mountNode` отвязывается от DOM.
|
|
50
|
+
- **`ModalWindowViewManager` рендерится всегда** рядом с Portal — он отвечает за вычисление координат рамки, в которой живёт окно (через `useResizeDetector`), и инжектит `<style>` с CSS-переменными `--modalWindow-screen-{left,top,width}` в `:root`. Не пытайся убрать его из дерева.
|
|
105
51
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
- Обёртка над `<HeaderPage type={FIRST}>`.
|
|
109
|
-
- Статический `.Title = HeaderPage.Title`.
|
|
52
|
+
---
|
|
110
53
|
|
|
111
|
-
|
|
54
|
+
## Дизайн-токены
|
|
112
55
|
|
|
113
|
-
|
|
114
|
-
-
|
|
56
|
+
```text
|
|
57
|
+
--triplex-next-ModalWindow-Backdrop_Background
|
|
58
|
+
--triplex-next-ModalWindow-Background
|
|
59
|
+
```
|
|
115
60
|
|
|
116
|
-
|
|
61
|
+
Помимо токенов, компонент инжектит и читает несколько runtime CSS-переменных, не относящихся к дизайн-системе:
|
|
117
62
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
63
|
+
```text
|
|
64
|
+
--modalWindow-screen-left # координата слева, выставляется ModalWindowViewManager
|
|
65
|
+
--modalWindow-screen-top # высота шапки лайаута, выставляется ModalWindowViewManager
|
|
66
|
+
--modalWindow-screen-width # ширина области, выставляется ModalWindowViewManager
|
|
67
|
+
--modal-window-top # отступ контента от верхней границы окна (= prop topPosition)
|
|
68
|
+
```
|
|
124
69
|
|
|
125
|
-
|
|
70
|
+
Имена этих переменных — публичный контракт для лайаута приложения, не переименовывать без согласования.
|
|
126
71
|
|
|
127
|
-
|
|
128
|
-
- Статический `.Description = FooterPage.Description`.
|
|
72
|
+
---
|
|
129
73
|
|
|
130
|
-
|
|
74
|
+
## Инварианты
|
|
131
75
|
|
|
132
|
-
|
|
76
|
+
- **`forwardRef`** на `ModalWindow` пробрасывает ref на корневой `role="dialog"` `<div>` — не убирать.
|
|
77
|
+
- **`role="dialog"` + `aria-modal="true"`** на корневом dialog-элементе — не менять.
|
|
78
|
+
- **Body-классы `modal-open` + `no-hash-overflow-hidden`** добавляются при открытии и убираются при закрытии и unmount. Имена согласованы с внешним лайаутом сббола, не переименовывать.
|
|
79
|
+
- **Portal-wrapper id `ufs-modal-window-wrapper`** и класс `ufs-modal-window-portal-node` — публичный контракт, на него могут опираться e2e-тесты и внешние стили.
|
|
80
|
+
- **`ModalWindowViewManager` node id `modalWindowViewManagerNodeId` (`"modalWindowViewManagerNodeId"`)** — публичный контракт лайаута: контейнер `<div id="modalWindowViewManagerNodeId">` создаётся вне React-дерева приложения и используется как граница позиционирования.
|
|
81
|
+
- **Animation timeout 300ms** (`animationExitTime`) — синхронизирован с CSS-анимацией `modalWindowContentAnimationOnExit` и `keyframes-fadeOut`. Менять только синхронно с less-стилями.
|
|
82
|
+
- **Barrel export в `src/components/ModalWindow/index.ts`** — все семь экспортов (`ModalWindow`, `ModalWindowBody`, `ModalWindowClose`, `ModalWindowContent`, `ModalWindowFooter`, `ModalWindowHeader`, `ModalWindowViewManager`) — публичный API.
|
|
83
|
+
- **`displayName = "ModalWindow"`** и displayName всех субкомпонентов — публичный API React DevTools/snapshot-тестов.
|
|
133
84
|
|
|
134
85
|
---
|
|
135
86
|
|
|
136
|
-
##
|
|
87
|
+
## Accessibility
|
|
137
88
|
|
|
138
|
-
|
|
89
|
+
- **Роль и aria.** Корневой dialog-элемент рендерится с `role="dialog"` и `aria-modal="true"`. Дополнительные aria-атрибуты (`aria-label`, `aria-labelledby`, `aria-describedby`) потребитель **обязан** передать сам через `...rest` — компонент не хардкодит текст, библиотека мультиязычная.
|
|
90
|
+
- **Focus trap.** Активная ловушка фокуса (`focus-trap-react`) включается синхронно с `isOpen`. По умолчанию: `allowOutsideClick: true` (клик за пределами не сбрасывает trap, но не блокируется), `preventScroll: true` (фокус не скроллит layout), `fallbackFocus` указывает на сам `role="dialog"` узел (у него `tabIndex={-1}`). Расширить настройки можно через `focusTrapProps.focusTrapOptions`.
|
|
91
|
+
- **Почему нужен `fallbackFocus`.** Ловушка активируется синхронно при открытии. Если в этот момент внутри dialog ещё нет ни одного tabbable-узла (контент грузится асинхронно, показан лоадер, `closeButton` отрисуется тиком позже), `focus-trap` без fallback падает с `"must have at least one container with at least one tabbable node"`. `fallbackFocus` на dialog снимает это падение — фокус временно встаёт на сам контейнер.
|
|
92
|
+
- **Закрытие по Escape.** Обработчик ESC живёт в `ModalWindowClose` (через `TriggerClickOnKeyDownEvent` + `KeyDownListener` на window keydown). Если `closeButton` заменён на кастомный — потребитель сам реализует ESC-handler.
|
|
93
|
+
- **Возврат фокуса.** За возврат фокуса на триггер при закрытии отвечает `focus-trap-react` (стандартное поведение). Если триггер был внутри другой ловушки фокуса — настрой `returnFocusOnDeactivate` через `focusTrapProps`.
|
|
94
|
+
- **Блокировка фоновой страницы.** Body-классы `modal-open` + `no-hash-overflow-hidden` блокируют скролл страницы. Это визуальная блокировка, не aria — для технологий screen-reader полагаемся на `aria-modal="true"`.
|
|
139
95
|
|
|
140
|
-
|
|
141
|
-
let wrapperNode = document.querySelector<HTMLDivElement>(`#${modalNodeName}-wrapper`);
|
|
142
|
-
if (!wrapperNode) { /* create & append to body */ }
|
|
96
|
+
---
|
|
143
97
|
|
|
144
|
-
|
|
145
|
-
mountNode.current.className = `${modalNodeName}-portal-node`;
|
|
146
|
-
wrapperNode.appendChild(mountNode.current);
|
|
147
|
-
```
|
|
98
|
+
## Связанные компоненты
|
|
148
99
|
|
|
149
|
-
|
|
150
|
-
- При закрытии portal-node удаляется, wrapper — остаётся (для повторного использования).
|
|
151
|
-
- **ID wrapper'а `ufs-modal-window-wrapper`** — известная "точка привязки" для интеграции с Layout'ом (сббол).
|
|
100
|
+
Все экспортируются из `@sberbusiness/triplex-next` через barrel `src/components/ModalWindow/index.ts`:
|
|
152
101
|
|
|
153
|
-
|
|
102
|
+
- **`ModalWindowContent`** — контейнер контента. Оборачивает дочерние секции в `Page` (`triplex-next/Page`). Имеет `isLoading?: boolean` и `loaderScreenProps?: ILoaderScreenMiddleProps` — при `isLoading` поверх контента показывается `LoaderScreen` типа `middle`; кастомизация лоадера (текст, кнопки и др.) передаётся через `loaderScreenProps`. Свой `displayName`. Свои props через интерфейс `IModalWindowContentProps` — экспортируется.
|
|
103
|
+
- **`ModalWindowHeader`** — заголовок. Тривиальная обёртка над `HeaderPage` с фиксированным `type=FIRST` и доп. отступом справа под кнопку закрытия (через less). Экспонирует статическое поле `Title = HeaderPage.Title`. Тип `IModalWindowHeaderProps` (Omit `children`/`type` от `IHeaderPageTypeFirstProps`).
|
|
104
|
+
- **`ModalWindowBody`** — тело. Тривиальная обёртка над `Island` с фиксированными `type=TYPE_1` и `size=MD`. Тип `IModalWindowBodyProps extends IIslandProps`.
|
|
105
|
+
- **`ModalWindowFooter`** — футер. Тривиальная обёртка над `FooterPage` с фиксированным `type=FIRST`. Экспонирует статическое поле `Description = FooterPage.Description`.
|
|
106
|
+
- **`ModalWindowClose`** — кнопка закрытия. Обёртка над `Button` (`theme=SECONDARY`, `size=MD`, иконка `CrossStrokeSrvIcon20`) внутри `TriggerClickOnKeyDownEvent` с `EVENT_KEY_CODES.ESCAPE`. **Текст `title` не имеет дефолта внутри компонента** — библиотека мультиязычная, локализованную подсказку (например, `title="Закрыть"`) передаёт потребитель через props. Тип `IModalWindowCloseProps` (Omit `size`/`theme`/`icon` от `IButtonSecondaryProps`).
|
|
107
|
+
- **`ModalWindowViewManager`** — невидимый компонент-сенсор. Создаёт DOM-ноду `<div id="modalWindowViewManagerNodeId">` (если её нет в `body`) и рендерит туда через `Portal` resize-сенсор + `<style>`-инжект CSS-переменных `--modalWindow-screen-*` в `:root`. Подключается автоматически из `ModalWindow`; экспортируется отдельно для случая, когда лайаут хочет позиционировать ноду вручную (например, рисовать модалку только в области рабочей зоны без шапки).
|
|
154
108
|
|
|
155
|
-
|
|
156
|
-
const bodyClassNameModalOpen = ["modal-open", "no-hash-overflow-hidden"];
|
|
157
|
-
```
|
|
109
|
+
---
|
|
158
110
|
|
|
159
|
-
|
|
160
|
-
- Класс `modal-open` — от внешнего Layout (сббол), блюрит фоновый контент.
|
|
161
|
-
- Снимаются при закрытии/unmount.
|
|
111
|
+
## Stories
|
|
162
112
|
|
|
163
|
-
|
|
113
|
+
Основные истории: `stories/ModalWindow/ModalWindow.stories.tsx`
|
|
114
|
+
Файлы примеров: `stories/ModalWindow/examples/`
|
|
164
115
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
onExited={handleCloseModal} // удалить body-классы + unmount portal + onExited()
|
|
173
|
-
mountOnEnter
|
|
174
|
-
unmountOnExit
|
|
175
|
-
>
|
|
176
|
-
```
|
|
116
|
+
| Story | Example file | Что демонстрирует |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| `Playground` | — | Интерактивный контроль `isLoading` и `size`; кнопки эмитят `action()` |
|
|
119
|
+
| `Default` | `Default.tsx` | Минимальный пример: открытие/закрытие, заголовок, body, футер |
|
|
120
|
+
| `Sizes` | `Sizes.tsx` | Размеры `SM` / `MD` / `LG`, для каждого — отдельный триггер |
|
|
121
|
+
| `WithLongContent` | `WithLongContent.tsx` | Длинный контент с прилипающими header/footer |
|
|
122
|
+
| `LoadingState` | `LoadingState.tsx` | Состояние загрузки (`isLoading=true` + `loaderScreenProps`) |
|
|
177
123
|
|
|
178
|
-
|
|
179
|
-
- `appear` — чтобы `onEnter` сработал на самой первой анимации.
|
|
180
|
-
- `unmountOnExit` — DOM-элемент удаляется после анимации, `renderPortal` сбрасывается в false.
|
|
124
|
+
Все стори, кроме скриншотных проверок, исключены из визуальных тестов на уровне `meta.parameters.testRunner: { skip: true }` — модалка анимируется и реализована через Portal вне Storybook canvas, скриншоты были бы пустыми.
|
|
181
125
|
|
|
182
|
-
|
|
126
|
+
---
|
|
183
127
|
|
|
184
|
-
|
|
185
|
-
const [renderPortal, setRenderPortal] = useState(false);
|
|
186
|
-
useEffect(() => { if (isOpen) { mountPortalNode(); setRenderPortal(true); } }, [isOpen]);
|
|
187
|
-
```
|
|
128
|
+
## История изменений
|
|
188
129
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
-
|
|
130
|
+
| Дата | Изменение |
|
|
131
|
+
|---|---|
|
|
132
|
+
| 2026-05-04 | Создан документ. AI-рефакторинг: добавлены JSDoc на компоненты и props, экспортирован `IModalWindowContentProps`, унифицированы импорты в `ModalWindowClose`, добавлен `displayName` субкомпонентам. Stories мигрированы в modern pattern (папка `examples/`, `?raw`, публичные импорты). Добавлены unit-тесты на `ModalWindowContent`, `ModalWindowClose`, `ModalWindowBody`, `ModalWindowHeader`, `ModalWindowFooter`. |
|
|
133
|
+
| 2026-05-06 | Возвращён `forwardRef` в `ModalWindowContent` и `ModalWindowViewManager` (обязательный инвариант проекта). Удалён захардкоженный `title="Закрыть"` из `ModalWindowClose` — для мультиязычной библиотеки текст подсказки должен приходить от потребителя. **Breaking:** теперь `title` не подставляется автоматически. |
|
|
134
|
+
| 2026-06-25 | Fix: устранено падение `focus-trap` `"must have at least one container with at least one tabbable node"` при открытии модалки без tabbable-узлов (асинхронный контент/лоадер). Добавлены `fallbackFocus` на dialog-узел и `tabIndex={-1}` на `role="dialog"`. Регрессия проявилась после синхронной активации ловушки в рефакторинге 1.28.0. Добавлен интеграционный тест на реальном `focus-trap`. |
|
|
135
|
+
| 2026-06-30 | Fix: модалка не отображалась под React 18 `StrictMode`. На искусственном unmount/mount-цикле cleanup в `useEffect` открепляет портальный `mountNode`, а инициализатор `useState` повторно не вызывается — узел оставался вне DOM. Теперь `useEffect` при (повторном) mount возвращает узел в wrapper, если тот откреплён. Wrapper вынесен в `getOrCreateWrapperNode`. Добавлен регрессионный тест на реальном `Portal` (`ModalWindow.strictMode.test.tsx`). |
|
|
192
136
|
|
|
193
|
-
|
|
137
|
+
---
|
|
194
138
|
|
|
195
|
-
|
|
196
|
-
const topPositionStyle = { "--modal-window-top": `${topPosition}px` };
|
|
197
|
-
```
|
|
139
|
+
## Примеры использования
|
|
198
140
|
|
|
199
|
-
|
|
200
|
-
- CSS-переменная именно `--modal-window-top` (kebab с дефисами), **не в naming-convention `--triplex-next-*`**. Это исторический паттерн, не переименовывать.
|
|
141
|
+
### Default
|
|
201
142
|
|
|
202
|
-
|
|
143
|
+
`stories/ModalWindow/examples/Default.tsx`
|
|
203
144
|
|
|
204
145
|
```tsx
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
146
|
+
import React, { useState } from "react";
|
|
147
|
+
import {
|
|
148
|
+
Button,
|
|
149
|
+
EButtonTheme,
|
|
150
|
+
EComponentSize,
|
|
151
|
+
ETextSize,
|
|
152
|
+
ETitleSize,
|
|
153
|
+
Gap,
|
|
154
|
+
ModalWindow,
|
|
155
|
+
ModalWindowBody,
|
|
156
|
+
ModalWindowClose,
|
|
157
|
+
ModalWindowContent,
|
|
158
|
+
ModalWindowFooter,
|
|
159
|
+
ModalWindowHeader,
|
|
160
|
+
Text,
|
|
161
|
+
Title,
|
|
162
|
+
} from "@sberbusiness/triplex-next";
|
|
163
|
+
|
|
164
|
+
export const Default = () => {
|
|
165
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
166
|
+
|
|
167
|
+
const handleOpen = () => setIsOpen(true);
|
|
168
|
+
const handleClose = () => setIsOpen(false);
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<div>
|
|
172
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD} onClick={handleOpen}>
|
|
173
|
+
Открыть модальное окно
|
|
174
|
+
</Button>
|
|
175
|
+
|
|
176
|
+
<ModalWindow
|
|
177
|
+
isOpen={isOpen}
|
|
178
|
+
size={EComponentSize.MD}
|
|
179
|
+
closeButton={<ModalWindowClose onClick={handleClose} title="Закрыть" />}
|
|
180
|
+
onExited={() => {}}
|
|
181
|
+
>
|
|
182
|
+
<ModalWindowContent>
|
|
183
|
+
<ModalWindowHeader>
|
|
184
|
+
<ModalWindowHeader.Title>
|
|
185
|
+
<ModalWindowHeader.Title.Content>
|
|
186
|
+
<Title tag="h1" size={ETitleSize.H1}>
|
|
187
|
+
Заголовок окна
|
|
188
|
+
</Title>
|
|
189
|
+
</ModalWindowHeader.Title.Content>
|
|
190
|
+
</ModalWindowHeader.Title>
|
|
191
|
+
</ModalWindowHeader>
|
|
192
|
+
|
|
193
|
+
<ModalWindowBody>
|
|
194
|
+
<Text tag="div" size={ETextSize.B2}>
|
|
195
|
+
Содержимое модального окна. Здесь может быть любой контент: формы, текст, изображения и
|
|
196
|
+
другие элементы интерфейса.
|
|
197
|
+
</Text>
|
|
198
|
+
<Gap size={16} />
|
|
199
|
+
<Text tag="div" size={ETextSize.B2}>
|
|
200
|
+
Фокус автоматически удерживается внутри окна, нажатие Escape закрывает его.
|
|
201
|
+
</Text>
|
|
202
|
+
</ModalWindowBody>
|
|
203
|
+
|
|
204
|
+
<ModalWindowFooter>
|
|
205
|
+
<ModalWindowFooter.Description>
|
|
206
|
+
<ModalWindowFooter.Description.Controls>
|
|
207
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD} onClick={handleClose}>
|
|
208
|
+
Button text
|
|
209
|
+
</Button>
|
|
210
|
+
<Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD} onClick={handleClose}>
|
|
211
|
+
Button text
|
|
212
|
+
</Button>
|
|
213
|
+
</ModalWindowFooter.Description.Controls>
|
|
214
|
+
</ModalWindowFooter.Description>
|
|
215
|
+
</ModalWindowFooter>
|
|
216
|
+
</ModalWindowContent>
|
|
217
|
+
</ModalWindow>
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
};
|
|
208
221
|
```
|
|
209
222
|
|
|
210
|
-
|
|
211
|
-
- Кнопка **обязательно должна быть в дереве** (не опциональна). Если `closeButton` не передан — TS ошибка (prop обязателен).
|
|
223
|
+
### Sizes
|
|
212
224
|
|
|
213
|
-
|
|
225
|
+
`stories/ModalWindow/examples/Sizes.tsx`
|
|
214
226
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
227
|
+
```tsx
|
|
228
|
+
import React, { useState } from "react";
|
|
229
|
+
import {
|
|
230
|
+
Button,
|
|
231
|
+
EButtonTheme,
|
|
232
|
+
EComponentSize,
|
|
233
|
+
ETextSize,
|
|
234
|
+
ETitleSize,
|
|
235
|
+
MobileView,
|
|
236
|
+
ModalWindow,
|
|
237
|
+
ModalWindowBody,
|
|
238
|
+
ModalWindowClose,
|
|
239
|
+
ModalWindowContent,
|
|
240
|
+
ModalWindowFooter,
|
|
241
|
+
ModalWindowHeader,
|
|
242
|
+
Text,
|
|
243
|
+
Title,
|
|
244
|
+
} from "@sberbusiness/triplex-next";
|
|
245
|
+
|
|
246
|
+
interface ISizeItemProps {
|
|
247
|
+
size: EComponentSize;
|
|
248
|
+
label: string;
|
|
222
249
|
}
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
- Default id `modalWindowViewManagerNodeId` создаётся в body. Используется для позиционирования относительно окна (если есть глобальный sidebar, Layout может переопределить эту node).
|
|
226
|
-
- **Отличие от LightBox**: переменные в `:root` — глобальные; любая ModalWindow подстраивается.
|
|
227
|
-
|
|
228
|
-
### `focus-trap-react` v11 (используется `FocusTrap` как default, не `.FocusTrap`)
|
|
229
250
|
|
|
230
|
-
|
|
231
|
-
|
|
251
|
+
const SizeItem = ({ size, label }: ISizeItemProps) => {
|
|
252
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
253
|
+
|
|
254
|
+
const handleOpen = () => setIsOpen(true);
|
|
255
|
+
const handleClose = () => setIsOpen(false);
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<div>
|
|
259
|
+
<div style={{ marginBottom: "8px", fontSize: "16px", fontWeight: 700 }}>{label}</div>
|
|
260
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD} onClick={handleOpen}>
|
|
261
|
+
Открыть {label}
|
|
262
|
+
</Button>
|
|
263
|
+
|
|
264
|
+
<ModalWindow
|
|
265
|
+
isOpen={isOpen}
|
|
266
|
+
size={size}
|
|
267
|
+
closeButton={<ModalWindowClose onClick={handleClose} title="Закрыть" />}
|
|
268
|
+
>
|
|
269
|
+
<ModalWindowContent>
|
|
270
|
+
<ModalWindowHeader>
|
|
271
|
+
<ModalWindowHeader.Title>
|
|
272
|
+
<ModalWindowHeader.Title.Content>
|
|
273
|
+
<MobileView
|
|
274
|
+
fallback={
|
|
275
|
+
<Title tag="h1" size={ETitleSize.H1}>
|
|
276
|
+
Размер {label}
|
|
277
|
+
</Title>
|
|
278
|
+
}
|
|
279
|
+
>
|
|
280
|
+
<Title tag="h2" size={ETitleSize.H2}>
|
|
281
|
+
Размер {label}
|
|
282
|
+
</Title>
|
|
283
|
+
</MobileView>
|
|
284
|
+
</ModalWindowHeader.Title.Content>
|
|
285
|
+
</ModalWindowHeader.Title>
|
|
286
|
+
</ModalWindowHeader>
|
|
287
|
+
|
|
288
|
+
<ModalWindowBody>
|
|
289
|
+
<Text tag="div" size={ETextSize.B2}>
|
|
290
|
+
Пример модального окна размера {label}.
|
|
291
|
+
</Text>
|
|
292
|
+
</ModalWindowBody>
|
|
293
|
+
|
|
294
|
+
<ModalWindowFooter>
|
|
295
|
+
<ModalWindowFooter.Description>
|
|
296
|
+
<ModalWindowFooter.Description.Controls>
|
|
297
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD} onClick={handleClose}>
|
|
298
|
+
Button text
|
|
299
|
+
</Button>
|
|
300
|
+
<Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD} onClick={handleClose}>
|
|
301
|
+
Button text
|
|
302
|
+
</Button>
|
|
303
|
+
</ModalWindowFooter.Description.Controls>
|
|
304
|
+
</ModalWindowFooter.Description>
|
|
305
|
+
</ModalWindowFooter>
|
|
306
|
+
</ModalWindowContent>
|
|
307
|
+
</ModalWindow>
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
export const Sizes = () => (
|
|
313
|
+
<div style={{ display: "flex", gap: 16, flexWrap: "wrap" }}>
|
|
314
|
+
<SizeItem size={EComponentSize.SM} label="SM" />
|
|
315
|
+
<SizeItem size={EComponentSize.MD} label="MD" />
|
|
316
|
+
<SizeItem size={EComponentSize.LG} label="LG" />
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
232
319
|
```
|
|
233
320
|
|
|
234
|
-
|
|
321
|
+
### WithLongContent
|
|
235
322
|
|
|
236
|
-
|
|
323
|
+
`stories/ModalWindow/examples/WithLongContent.tsx`
|
|
237
324
|
|
|
238
325
|
```tsx
|
|
239
|
-
|
|
326
|
+
import React, { useState } from "react";
|
|
327
|
+
import {
|
|
328
|
+
Button,
|
|
329
|
+
EButtonTheme,
|
|
330
|
+
EComponentSize,
|
|
331
|
+
ETextSize,
|
|
332
|
+
ETitleSize,
|
|
333
|
+
Gap,
|
|
334
|
+
MobileView,
|
|
335
|
+
ModalWindow,
|
|
336
|
+
ModalWindowBody,
|
|
337
|
+
ModalWindowClose,
|
|
338
|
+
ModalWindowContent,
|
|
339
|
+
ModalWindowFooter,
|
|
340
|
+
ModalWindowHeader,
|
|
341
|
+
Text,
|
|
342
|
+
Title,
|
|
343
|
+
} from "@sberbusiness/triplex-next";
|
|
344
|
+
|
|
345
|
+
const PARAGRAPHS = Array.from({ length: 10 }, (_, i) => i + 1);
|
|
346
|
+
|
|
347
|
+
export const WithLongContent = () => {
|
|
348
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
349
|
+
|
|
350
|
+
const handleOpen = () => setIsOpen(true);
|
|
351
|
+
const handleClose = () => setIsOpen(false);
|
|
352
|
+
|
|
353
|
+
return (
|
|
354
|
+
<div>
|
|
355
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD} onClick={handleOpen}>
|
|
356
|
+
Открыть с длинным контентом
|
|
357
|
+
</Button>
|
|
358
|
+
|
|
359
|
+
<ModalWindow
|
|
360
|
+
isOpen={isOpen}
|
|
361
|
+
size={EComponentSize.MD}
|
|
362
|
+
closeButton={<ModalWindowClose onClick={handleClose} title="Закрыть" />}
|
|
363
|
+
>
|
|
364
|
+
<ModalWindowContent>
|
|
365
|
+
<ModalWindowHeader>
|
|
366
|
+
<ModalWindowHeader.Title>
|
|
367
|
+
<ModalWindowHeader.Title.Content>
|
|
368
|
+
<MobileView
|
|
369
|
+
fallback={
|
|
370
|
+
<Title tag="h1" size={ETitleSize.H1}>
|
|
371
|
+
Длинный контент
|
|
372
|
+
</Title>
|
|
373
|
+
}
|
|
374
|
+
>
|
|
375
|
+
<Title tag="h2" size={ETitleSize.H2}>
|
|
376
|
+
Длинный контент
|
|
377
|
+
</Title>
|
|
378
|
+
</MobileView>
|
|
379
|
+
</ModalWindowHeader.Title.Content>
|
|
380
|
+
</ModalWindowHeader.Title>
|
|
381
|
+
</ModalWindowHeader>
|
|
382
|
+
|
|
383
|
+
<ModalWindowBody>
|
|
384
|
+
{PARAGRAPHS.map((n, i) => (
|
|
385
|
+
<React.Fragment key={n}>
|
|
386
|
+
<Text tag="div" size={ETextSize.B2}>
|
|
387
|
+
Параграф {n}. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
|
|
388
|
+
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
|
|
389
|
+
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
|
390
|
+
</Text>
|
|
391
|
+
{i < PARAGRAPHS.length - 1 && <Gap size={16} />}
|
|
392
|
+
</React.Fragment>
|
|
393
|
+
))}
|
|
394
|
+
</ModalWindowBody>
|
|
395
|
+
|
|
396
|
+
<ModalWindowFooter>
|
|
397
|
+
<ModalWindowFooter.Description>
|
|
398
|
+
<ModalWindowFooter.Description.Controls>
|
|
399
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD} onClick={handleClose}>
|
|
400
|
+
Button text
|
|
401
|
+
</Button>
|
|
402
|
+
<Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD} onClick={handleClose}>
|
|
403
|
+
Button text
|
|
404
|
+
</Button>
|
|
405
|
+
</ModalWindowFooter.Description.Controls>
|
|
406
|
+
</ModalWindowFooter.Description>
|
|
407
|
+
</ModalWindowFooter>
|
|
408
|
+
</ModalWindowContent>
|
|
409
|
+
</ModalWindow>
|
|
410
|
+
</div>
|
|
411
|
+
);
|
|
412
|
+
};
|
|
240
413
|
```
|
|
241
414
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
---
|
|
245
|
-
|
|
246
|
-
## Инварианты
|
|
247
|
-
|
|
248
|
-
- **`isOpen` управляемый, `children` — один `ReactElement`, `closeButton` — обязателен** — публичный контракт.
|
|
249
|
-
- **ID DOM-контейнеров: `ufs-modal-window-wrapper`, `modalWindowViewManagerNodeId`** — **интеграционные** точки с внешним Layout'ом. Не переименовывать.
|
|
250
|
-
- **Body-классы `modal-open`, `no-hash-overflow-hidden`** — контракт с внешним Layout; изменение — регрессия фонового блюра.
|
|
251
|
-
- **Escape → closeButton click** через `TriggerClickOnKeyDownEvent` — поведенческий контракт `ModalWindowClose`.
|
|
252
|
-
- **CSS-переменная `--modal-window-top`** (kebab-case, не `--triplex-next-ModalWindow-Top`) — публичная, исторический naming.
|
|
253
|
-
- **`CSSTransition classNames="global-modalWindowTransition"`** — строковый класс, нет в CSS-Modules. Стили в глобальном файле.
|
|
254
|
-
- **`ModalWindowHeader/Footer` — обёртки над HeaderPage/FooterPage type=FIRST** — не поддерживают TYPE_2/TYPE_3.
|
|
255
|
-
- **`ModalWindowBody` хардкодит Island TYPE_1/MD** — кастомизация через props не публична.
|
|
256
|
-
- **`ModalWindow.Close` title="Закрыть" + `TriggerClickOnKeyDownEvent` eventKeyCode=ESCAPE** — i18n-debt (rus-hardcode).
|
|
257
|
-
- **`forwardRef<HTMLDivElement>`** — корневая node (dialog), но **подкомпоненты не являются слотами через Object.assign**.
|
|
258
|
-
- **ViewManager пишет в `:root`**, не в scoped-элемент — глобально влияет на любой ModalWindow.
|
|
259
|
-
|
|
260
|
-
---
|
|
261
|
-
|
|
262
|
-
## Accessibility
|
|
263
|
-
|
|
264
|
-
- `role="dialog"` + `aria-modal="true"` — есть на корне.
|
|
265
|
-
- `FocusTrap` активен пока `isOpen` — фокус удерживается внутри.
|
|
266
|
-
- `allowOutsideClick: true` — outside click не ломает trap; закрытие делается **только через `closeButton`** или Escape.
|
|
267
|
-
- **Нет `aria-labelledby`/`aria-describedby` по умолчанию** — потребитель должен добавить через `...rest` или передать `aria-label`.
|
|
268
|
-
- `ModalWindowClose` — Button с `title="Закрыть"` (рус), tabIndex нормальный.
|
|
269
|
-
- Escape обрабатывается через TriggerClickOnKeyDownEvent — глобально на window (см. `KeyDownListener-AI`).
|
|
270
|
-
|
|
271
|
-
---
|
|
415
|
+
### LoadingState
|
|
272
416
|
|
|
273
|
-
|
|
417
|
+
`stories/ModalWindow/examples/LoadingState.tsx`
|
|
274
418
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
419
|
+
```tsx
|
|
420
|
+
import React, { useState } from "react";
|
|
421
|
+
import {
|
|
422
|
+
Button,
|
|
423
|
+
EButtonTheme,
|
|
424
|
+
EComponentSize,
|
|
425
|
+
ETextSize,
|
|
426
|
+
ETitleSize,
|
|
427
|
+
MobileView,
|
|
428
|
+
ModalWindow,
|
|
429
|
+
ModalWindowBody,
|
|
430
|
+
ModalWindowClose,
|
|
431
|
+
ModalWindowContent,
|
|
432
|
+
ModalWindowFooter,
|
|
433
|
+
ModalWindowHeader,
|
|
434
|
+
Text,
|
|
435
|
+
Title,
|
|
436
|
+
} from "@sberbusiness/triplex-next";
|
|
437
|
+
|
|
438
|
+
export const LoadingState = () => {
|
|
439
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
440
|
+
|
|
441
|
+
const handleOpen = () => setIsOpen(true);
|
|
442
|
+
const handleClose = () => setIsOpen(false);
|
|
443
|
+
|
|
444
|
+
return (
|
|
445
|
+
<div>
|
|
446
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD} onClick={handleOpen}>
|
|
447
|
+
Открыть с загрузкой
|
|
448
|
+
</Button>
|
|
449
|
+
|
|
450
|
+
<ModalWindow
|
|
451
|
+
isOpen={isOpen}
|
|
452
|
+
size={EComponentSize.MD}
|
|
453
|
+
closeButton={<ModalWindowClose onClick={handleClose} title="Закрыть" />}
|
|
454
|
+
>
|
|
455
|
+
<ModalWindowContent isLoading loaderScreenProps={{ description: "Загрузка данных..." }}>
|
|
456
|
+
<ModalWindowHeader>
|
|
457
|
+
<ModalWindowHeader.Title>
|
|
458
|
+
<ModalWindowHeader.Title.Content>
|
|
459
|
+
<MobileView
|
|
460
|
+
fallback={
|
|
461
|
+
<Title tag="h1" size={ETitleSize.H1}>
|
|
462
|
+
Загрузка
|
|
463
|
+
</Title>
|
|
464
|
+
}
|
|
465
|
+
>
|
|
466
|
+
<Title tag="h2" size={ETitleSize.H2}>
|
|
467
|
+
Загрузка
|
|
468
|
+
</Title>
|
|
469
|
+
</MobileView>
|
|
470
|
+
</ModalWindowHeader.Title.Content>
|
|
471
|
+
</ModalWindowHeader.Title>
|
|
472
|
+
</ModalWindowHeader>
|
|
473
|
+
|
|
474
|
+
<ModalWindowBody>
|
|
475
|
+
<Text tag="div" size={ETextSize.B2}>
|
|
476
|
+
Этот контент скрыт под индикатором загрузки.
|
|
477
|
+
</Text>
|
|
478
|
+
</ModalWindowBody>
|
|
479
|
+
|
|
480
|
+
<ModalWindowFooter>
|
|
481
|
+
<ModalWindowFooter.Description>
|
|
482
|
+
<ModalWindowFooter.Description.Controls>
|
|
483
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD} onClick={handleClose}>
|
|
484
|
+
Button text
|
|
485
|
+
</Button>
|
|
486
|
+
<Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD} onClick={handleClose}>
|
|
487
|
+
Button text
|
|
488
|
+
</Button>
|
|
489
|
+
</ModalWindowFooter.Description.Controls>
|
|
490
|
+
</ModalWindowFooter.Description>
|
|
491
|
+
</ModalWindowFooter>
|
|
492
|
+
</ModalWindowContent>
|
|
493
|
+
</ModalWindow>
|
|
494
|
+
</div>
|
|
495
|
+
);
|
|
496
|
+
};
|
|
279
497
|
```
|
|
280
498
|
|
|
281
|
-
Визуал — от `Island` (внутри Body) и ThemeProvider scope (scopeClassName на контейнере).
|
|
282
|
-
|
|
283
|
-
---
|
|
284
|
-
|
|
285
|
-
## Связанные компоненты
|
|
286
|
-
|
|
287
|
-
- `LightBox` — полноэкранный аналог.
|
|
288
|
-
- `Portal` — используется для render'а.
|
|
289
|
-
- `Island` / `Page` / `HeaderPage` / `FooterPage` — базовые примитивы для Body/Content/Header/Footer.
|
|
290
|
-
- `Button` — в ModalWindowClose.
|
|
291
|
-
- `TriggerClickOnKeyDownEvent` — для Escape handling.
|
|
292
|
-
- `LoaderScreen` — loader в Content.
|
|
293
|
-
- `focus-trap-react`, `react-transition-group` — внешние зависимости.
|
|
294
|
-
|
|
295
|
-
---
|
|
296
|
-
|
|
297
|
-
## Stories
|
|
298
|
-
|
|
299
|
-
`stories/ModalWindow/ModalWindow.stories.tsx`
|
|
300
|
-
Примеры: `stories/ModalWindow/examples/`
|
|
301
|
-
|
|
302
|
-
---
|
|
303
|
-
|
|
304
|
-
## История изменений
|
|
305
|
-
|
|
306
|
-
| Дата | Изменение |
|
|
307
|
-
|---|---|
|
|
308
|
-
| 2026-04-18 | Создан документ |
|