@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.
Files changed (73) hide show
  1. package/README.md +10 -0
  2. package/bundle/components/AlertContext.md +268 -0
  3. package/bundle/components/BodyPage.md +186 -0
  4. package/bundle/components/Button.md +93 -85
  5. package/bundle/components/ButtonDropdown.md +36 -70
  6. package/bundle/components/ButtonDropdownExtended.md +422 -0
  7. package/bundle/components/ButtonIcon.md +41 -48
  8. package/bundle/components/Caption.md +291 -0
  9. package/bundle/components/Checkbox.md +43 -122
  10. package/bundle/components/CheckboxXGroup.md +109 -46
  11. package/bundle/components/CheckboxYGroup.md +77 -49
  12. package/bundle/components/CodeText.md +197 -0
  13. package/bundle/components/Col.md +311 -162
  14. package/bundle/components/CollapsibleTree.md +584 -0
  15. package/bundle/components/CollapsibleTreeExtended.md +422 -0
  16. package/bundle/components/Dropdown.md +748 -346
  17. package/bundle/components/EmptyView.md +14 -15
  18. package/bundle/components/FooterPage.md +203 -0
  19. package/bundle/components/Gap.md +115 -57
  20. package/bundle/components/HeaderPage.md +226 -0
  21. package/bundle/components/HelpBox.md +45 -84
  22. package/bundle/components/ImageGallery.md +423 -0
  23. package/bundle/components/ImageGalleryExtended.md +606 -0
  24. package/bundle/components/LightBox.md +2252 -233
  25. package/bundle/components/List.md +216 -183
  26. package/bundle/components/ListActionItem.md +157 -0
  27. package/bundle/components/ListItem.md +276 -0
  28. package/bundle/components/ListItemControlsButton.md +103 -0
  29. package/bundle/components/ListItemControlsButtonDropdown.md +107 -0
  30. package/bundle/components/ListItemSelectable.md +99 -0
  31. package/bundle/components/ListItemTable.md +390 -0
  32. package/bundle/components/ListMaster.md +876 -156
  33. package/bundle/components/ListMasterFooter.md +80 -0
  34. package/bundle/components/ListMasterHeader.md +86 -0
  35. package/bundle/components/ListSortable.md +153 -0
  36. package/bundle/components/ListSortableItem.md +175 -0
  37. package/bundle/components/ListTableItem.md +391 -0
  38. package/bundle/components/ModalWindow.md +427 -237
  39. package/bundle/components/MonthYearField.md +145 -109
  40. package/bundle/components/Notification.md +625 -155
  41. package/bundle/components/Overlay.md +271 -170
  42. package/bundle/components/Page.md +284 -186
  43. package/bundle/components/Pagination.md +206 -229
  44. package/bundle/components/Portal.md +131 -67
  45. package/bundle/components/Row.md +171 -69
  46. package/bundle/components/Text.md +320 -0
  47. package/bundle/components/TextField.md +364 -97
  48. package/bundle/components/Title.md +296 -0
  49. package/bundle/guides/codestyle.md +38 -11
  50. package/bundle/guides/commits.md +119 -26
  51. package/bundle/guides/context.md +34 -30
  52. package/bundle/guides/stories.md +110 -40
  53. package/bundle/guides/template.md +3 -7
  54. package/bundle/guides/tests.md +48 -2
  55. package/bundle/manifest.json +275 -30
  56. package/bundle/release-notes/1.26.0.mdx +33 -2
  57. package/bundle/release-notes/1.27.0.mdx +43 -0
  58. package/bundle/release-notes/1.28.0.mdx +25 -0
  59. package/bundle/release-notes/1.29.0.mdx +47 -0
  60. package/bundle/release-notes/1.30.0.mdx +56 -0
  61. package/bundle/release-notes/1.31.0.mdx +52 -0
  62. package/bundle/release-notes/1.32.0.mdx +35 -0
  63. package/bundle/release-notes/1.33.0.mdx +61 -0
  64. package/bundle/release-notes/1.34.0.mdx +35 -0
  65. package/bundle/release-notes/1.35.0.mdx +21 -0
  66. package/bundle/release-notes/1.35.1.mdx +11 -0
  67. package/bundle/release-notes/1.36.0.mdx +21 -0
  68. package/bundle/release-notes/1.37.0.mdx +71 -0
  69. package/bundle/release-notes/1.38.0.mdx +36 -0
  70. package/bundle/release-notes/1.39.0.mdx +66 -0
  71. package/bundle/release-notes/1.40.0.mdx +27 -0
  72. package/bundle/release-notes/1.41.0.mdx +7 -0
  73. package/package.json +1 -1
@@ -1,14 +1,10 @@
1
1
  ---
2
2
  component: ModalWindow
3
- category: Overlays
4
- figma-node: ""
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
- - --modalWindow-screen-left
9
- - --modalWindow-screen-width
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
- Модальный диалог средних размеров — контент по центру экрана, backdrop-маска,
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
- **Важно:** сами подкомпоненты (`ModalWindowHeader`, `ModalWindowBody`,
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
- ## Варианты и props
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
- ### ModalWindow — `IModalWindowProps extends HTMLAttributes<HTMLDivElement>`
34
+ ### Опциональные props
86
35
 
87
- | Prop | Тип | Default | Описание |
36
+ | Prop | Тип | По умолчанию | Описание |
88
37
  |---|---|---|---|
89
- | `isOpen` | `boolean` | — (обязателен) | Управляемое состояние. |
90
- | `children` | `ReactElement` | — | Один корневой элемент (обычно `ModalWindowContent`). |
91
- | `closeButton` | `ReactNode` | — (обязателен) | Кнопка закрыть, вставляется рядом с `children`. Обычно `<ModalWindowClose onClick={onClose} />`. |
92
- | `focusTrapProps` | `FocusTrapProps` | — | Переопределение параметров `focus-trap-react`. |
93
- | `onExited` | `() => void` | — | Callback после завершения анимации закрытия. |
94
- | `size` | `EComponentSize` | `MD` | Размер модалки (ширина). |
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
- ### ModalWindowClose — `IModalWindowCloseProps extends Omit<IButtonSecondaryProps, "size" | "theme" | "icon">`
45
+ ### Ограничения использования
100
46
 
101
- - Кнопка с иконкой `CrossStrokeSrvIcon20` в правом-верхнем углу модалки.
102
- - **Автоматически триггерит клик по Escape** через `TriggerClickOnKeyDownEvent` (eventKeyCode=ESCAPE).
103
- - `size=MD` и `theme=SECONDARY` захардкожены — не переопределить.
104
- - **Title "Закрыть"** — хардкод русского, i18n-debt.
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
- ### ModalWindowHeader — `IModalWindowHeaderProps extends Omit<IHeaderPageTypeFirstProps, "children" | "type">`
107
-
108
- - Обёртка над `<HeaderPage type={FIRST}>`.
109
- - Статический `.Title = HeaderPage.Title`.
52
+ ---
110
53
 
111
- ### ModalWindowBody — `IModalWindowBodyProps extends IIslandProps`
54
+ ## Дизайн-токены
112
55
 
113
- - Обёртка над `<Island type={TYPE_1} size={MD}>` с дополнительным классом `modalWindowBody`.
114
- - **Хардкод TYPE_1/MD** — если нужен другой тип/размер Island, текущий API не позволяет.
56
+ ```text
57
+ --triplex-next-ModalWindow-Backdrop_Background
58
+ --triplex-next-ModalWindow-Background
59
+ ```
115
60
 
116
- ### ModalWindowContent — `IModalWindowContentProps extends HTMLAttributes<HTMLDivElement>`
61
+ Помимо токенов, компонент инжектит и читает несколько runtime CSS-переменных, не относящихся к дизайн-системе:
117
62
 
118
- | Prop | Тип | Описание |
119
- |---|---|---|
120
- | `isLoading` | `boolean` | Показывает `LoaderScreen type="middle"` над содержимым. |
121
- | `loadingTitle` | `ReactNode` | Подпись под спиннером. |
122
-
123
- Внутри — обёртывает `children` в `<Page>`. Scroll-контейнер.
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
- ### ModalWindowFooter — `IModalWindowFooterProps extends Omit<IFooterPageTypeFirstProps, "children" | "type">`
70
+ Имена этих переменных — публичный контракт для лайаута приложения, не переименовывать без согласования.
126
71
 
127
- - Обёртка над `<FooterPage type={FIRST}>`.
128
- - Статический `.Description = FooterPage.Description`.
72
+ ---
129
73
 
130
- ### ModalWindowViewManager
74
+ ## Инварианты
131
75
 
132
- Рендерится автоматически внутри `ModalWindow`. Отдельно использовать не нужно (но экспортирован для nuance-сценариев).
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
- ### Два DOM-контейнера: `#ufs-modal-window-wrapper` + свой `portal-node`
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
- ```ts
141
- let wrapperNode = document.querySelector<HTMLDivElement>(`#${modalNodeName}-wrapper`);
142
- if (!wrapperNode) { /* create & append to body */ }
96
+ ---
143
97
 
144
- mountNode.current = document.createElement("div");
145
- mountNode.current.className = `${modalNodeName}-portal-node`;
146
- wrapperNode.appendChild(mountNode.current);
147
- ```
98
+ ## Связанные компоненты
148
99
 
149
- - **Каждая открытая ModalWindow создаёт свой `.portal-node`** внутри общего `#ufs-modal-window-wrapper`. Это позволяет держать несколько модалок параллельно (хотя обычно одна).
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
- ### Body-классы `modal-open` + `no-hash-overflow-hidden`
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
- ```ts
156
- const bodyClassNameModalOpen = ["modal-open", "no-hash-overflow-hidden"];
157
- ```
109
+ ---
158
110
 
159
- - При открытии — **добавляются на `body`** (не на `documentElement`, в отличие от LightBox).
160
- - Класс `modal-open` — от внешнего Layout (сббол), блюрит фоновый контент.
161
- - Снимаются при закрытии/unmount.
111
+ ## Stories
162
112
 
163
- ### Animation — `CSSTransition` + `appear` + `unmountOnExit`
113
+ Основные истории: `stories/ModalWindow/ModalWindow.stories.tsx`
114
+ Файлы примеров: `stories/ModalWindow/examples/`
164
115
 
165
- ```tsx
166
- <CSSTransition
167
- in={isOpen}
168
- timeout={animationExitTime} // 300
169
- classNames="global-modalWindowTransition"
170
- appear enter exit
171
- onEnter={handleOpenModal} // добавить body-классы
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
- - **classNames `global-modalWindowTransition`** — строка, не CSS-Modules. Соответствующие CSS-правила живут в глобальных стилях/index.less.
179
- - `appear` — чтобы `onEnter` сработал на самой первой анимации.
180
- - `unmountOnExit` — DOM-элемент удаляется после анимации, `renderPortal` сбрасывается в false.
124
+ Все стори, кроме скриншотных проверок, исключены из визуальных тестов на уровне `meta.parameters.testRunner: { skip: true }` — модалка анимируется и реализована через Portal вне Storybook canvas, скриншоты были бы пустыми.
181
125
 
182
- ### `renderPortal` state и двойное монтирование
126
+ ---
183
127
 
184
- ```ts
185
- const [renderPortal, setRenderPortal] = useState(false);
186
- useEffect(() => { if (isOpen) { mountPortalNode(); setRenderPortal(true); } }, [isOpen]);
187
- ```
128
+ ## История изменений
188
129
 
189
- - До открытия `renderPortal=false` → ранний return `null` → Portal не рендерится.
190
- - При `isOpen=true` — создаётся mountNode (imperatively), ставится `renderPortal=true` — затем react рендерит Portal.
191
- - **На повторные открытия** — state остаётся true после закрытия (handleCloseModal сбрасывает в false), и при следующем `isOpen=true` — снова создаётся mountNode.
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
- ### `topPosition` через CSS-переменную
137
+ ---
194
138
 
195
- ```tsx
196
- const topPositionStyle = { "--modal-window-top": `${topPosition}px` };
197
- ```
139
+ ## Примеры использования
198
140
 
199
- - Default 100px. Команды могут переопределять, передавая `topPosition`.
200
- - CSS-переменная именно `--modal-window-top` (kebab с дефисами), **не в naming-convention `--triplex-next-*`**. Это исторический паттерн, не переименовывать.
141
+ ### Default
201
142
 
202
- ### ModalWindowClose с Escape-handler
143
+ `stories/ModalWindow/examples/Default.tsx`
203
144
 
204
145
  ```tsx
205
- <TriggerClickOnKeyDownEvent eventKeyCode={EVENT_KEY_CODES.ESCAPE} targetRef={ref}>
206
- <Button ref={ref} icon={<CrossIcon />} ... />
207
- </TriggerClickOnKeyDownEvent>
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
- - Escape нажатие → имитирует click на кнопку → `onClick` → закрытие.
211
- - Кнопка **обязательно должна быть в дереве** (не опциональна). Если `closeButton` не передан — TS ошибка (prop обязателен).
223
+ ### Sizes
212
224
 
213
- ### `ModalWindowViewManager` — CSS-переменные позиционирования
225
+ `stories/ModalWindow/examples/Sizes.tsx`
214
226
 
215
- Аналогично `LightBoxViewManager`: пишет в `:root` (именно в `:root`, а не на `lightBoxMountNode`):
216
-
217
- ```css
218
- :root {
219
- --modalWindow-screen-left: ...;
220
- --modalWindow-screen-width: ...;
221
- --modalWindow-screen-top: ...;
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
- ```ts
231
- import { FocusTrap, FocusTrapProps } from "focus-trap-react";
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
- **Внимание**: старые версии использовали default-import. Не ломать.
321
+ ### WithLongContent
235
322
 
236
- ### ModalWindowBody хардкодит Island type=TYPE_1, size=MD
323
+ `stories/ModalWindow/examples/WithLongContent.tsx`
237
324
 
238
325
  ```tsx
239
- <Island type={EIslandType.TYPE_1} size={EComponentSize.MD} {...islandProps}>
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
- `...islandProps` идут **после** `type`/`size` — потребитель теоретически может переопределить через props. Но это не публичная часть API (not документированная). Не полагайся.
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
- Runtime CSS-переменные:
276
- ```
277
- --modalWindow-screen-left, --modalWindow-screen-width, --modalWindow-screen-top — от ViewManager
278
- --modal-window-top — от topPosition prop
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 | Создан документ |