@sberbusiness/triplex-next-mcp-server 0.36.0-beta0 → 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/bundle/components/AlertContext.md +268 -0
- package/bundle/components/Button.md +5 -2
- package/bundle/components/ButtonDropdown.md +9 -1
- package/bundle/components/ButtonDropdownExtended.md +422 -0
- package/bundle/components/ButtonIcon.md +7 -1
- package/bundle/components/Caption.md +291 -0
- package/bundle/components/CodeText.md +197 -0
- package/bundle/components/Col.md +311 -162
- package/bundle/components/CollapsibleTree.md +6 -3
- package/bundle/components/CollapsibleTreeExtended.md +6 -2
- package/bundle/components/Dropdown.md +748 -346
- package/bundle/components/LightBox.md +2252 -233
- package/bundle/components/List.md +26 -1
- package/bundle/components/ListActionItem.md +157 -0
- package/bundle/components/ListItem.md +34 -9
- package/bundle/components/ListItemSelectable.md +2 -2
- package/bundle/components/ListMaster.md +4 -5
- package/bundle/components/ListTableItem.md +391 -0
- package/bundle/components/Notification.md +21 -21
- package/bundle/components/Page.md +1 -0
- 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/commits.md +118 -24
- package/bundle/guides/context.md +1 -1
- package/bundle/guides/tests.md +42 -0
- package/bundle/manifest.json +89 -14
- package/bundle/release-notes/1.37.0.mdx +42 -1
- 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,13 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
component: LightBox
|
|
3
|
-
category:
|
|
4
|
-
|
|
5
|
-
figma-file: ""
|
|
6
|
-
related: [ModalWindow, Overlay, TopOverlay, Portal, MobileView, FocusTrap]
|
|
3
|
+
category: LightBox
|
|
4
|
+
related: [Page, TopOverlay, Portal, FocusTrapExtended, LoaderScreen, Button, ModalWindow]
|
|
7
5
|
tokens:
|
|
8
|
-
- --
|
|
9
|
-
- --
|
|
10
|
-
- --
|
|
6
|
+
- --triplex-next-LightBox-Backdrop_Background
|
|
7
|
+
- --triplex-next-LightBox-Content_Background
|
|
8
|
+
- --triplex-next-LightBox-SideOverlay_Background
|
|
9
|
+
- --triplex-next-scroll-width
|
|
11
10
|
stories: stories/LightBox/LightBox.stories.tsx
|
|
12
11
|
version: "1.0"
|
|
13
12
|
---
|
|
@@ -16,307 +15,2327 @@ version: "1.0"
|
|
|
16
15
|
|
|
17
16
|
## Назначение
|
|
18
17
|
|
|
19
|
-
Полноэкранный
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
Полноэкранный диалог поверх страницы для отображения крупного контента: просмотр документа,
|
|
19
|
+
пошаговый сценарий, галерея. Рендерится через портал в отдельную DOM-ноду, блокирует скролл
|
|
20
|
+
страницы, держит фокус внутри (focus trap). Составной компонент: контентная область
|
|
21
|
+
(`LightBox.Content`), кнопки управления (`LightBox.Controls` — закрыть/назад/вперёд),
|
|
22
|
+
боковые панели-сайдбары (`LightBox.LeftSidebar` / `LightBox.RightSidebar`), выезжающая справа
|
|
23
|
+
панель (`LightBox.SideOverlay`) и верхняя панель (`LightBox.TopOverlay`).
|
|
24
24
|
|
|
25
|
-
Используй когда: нужен
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
Используй когда: контенту нужен весь экран и собственный сценарий навигации (листание,
|
|
26
|
+
вложенные панели).
|
|
27
|
+
Не используй когда: достаточно компактного диалога с кнопками действий — возьми `ModalWindow`;
|
|
28
|
+
нужна нотификация — возьми соответствующий компонент уведомлений.
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
## Файловая структура
|
|
32
|
-
|
|
33
|
-
```text
|
|
34
|
-
src/components/LightBox/
|
|
35
|
-
├── LightBox.tsx # Основной компонент + Object.assign
|
|
36
|
-
├── LightBoxContent.tsx # Скроллируемый контент, paddingTop = высота controls
|
|
37
|
-
├── enums.ts # ELightBoxSize (SM/MD/LG/XL)
|
|
38
|
-
├── index.ts # Barrel (+ re-export SideOverlay, enums)
|
|
39
|
-
├── LightBoxControls/
|
|
40
|
-
│ ├── LightBoxControls.tsx # Container с data-lightbox-component="controls"
|
|
41
|
-
│ ├── LightBoxClose.tsx
|
|
42
|
-
│ ├── LightBoxNext.tsx
|
|
43
|
-
│ └── LightBoxPrev.tsx
|
|
44
|
-
├── LightBoxSidebars/
|
|
45
|
-
│ ├── LightBoxLeftSidebar.tsx # forwardRef, ResizeObserver, show/hide по minVisibleWidth
|
|
46
|
-
│ └── LightBoxRightSidebar.tsx
|
|
47
|
-
├── LightBoxSideOverlay/
|
|
48
|
-
│ ├── LightBoxSideOverlay.tsx # Выезжающая справа панель (Overlay direction=RIGHT)
|
|
49
|
-
│ ├── LightBoxSideOverlayCloseDesktop.tsx
|
|
50
|
-
│ ├── LightBoxSideOverlayCloseMobile.tsx
|
|
51
|
-
│ ├── LightBoxSideOverlayLoader.tsx
|
|
52
|
-
│ └── index.ts # Re-export SideOverlay
|
|
53
|
-
├── LightBoxViewManager/
|
|
54
|
-
│ ├── LightBoxViewManager.tsx # Вычисляет CSS-переменные --lightBox-screen-*
|
|
55
|
-
│ └── LightBoxViewManagerConsts.ts
|
|
56
|
-
└── styles/
|
|
57
|
-
├── LightBox.module.less
|
|
58
|
-
├── LightBoxControls.module.less
|
|
59
|
-
└── LightBoxScroll.module.less
|
|
60
|
-
```
|
|
30
|
+
Внутрь `LightBox.Content` кладётся `Page` (Header/Body/Footer). Если контента мало —
|
|
31
|
+
`Page.Body` типа `FIRST`; если много — тип `SECOND` с несколькими `Island` внутри.
|
|
61
32
|
|
|
62
33
|
---
|
|
63
34
|
|
|
64
|
-
##
|
|
35
|
+
## Варианты и props
|
|
65
36
|
|
|
66
|
-
|
|
67
|
-
<LightBox size={ELightBoxSize.LG} isLoading={loading}>
|
|
68
|
-
<LightBox.Controls>
|
|
69
|
-
<LightBox.Controls.Prev onClick={...} />
|
|
70
|
-
<LightBox.Controls.Next onClick={...} />
|
|
71
|
-
<LightBox.Controls.Close onClick={onClose} />
|
|
72
|
-
</LightBox.Controls>
|
|
37
|
+
### Размеры (`ELightBoxSize`)
|
|
73
38
|
|
|
74
|
-
|
|
75
|
-
{/* Левая панель — список разделов */}
|
|
76
|
-
</LightBox.LeftSidebar>
|
|
39
|
+
Ограничивают максимальную ширину контентной области:
|
|
77
40
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
41
|
+
| Значение | Max-width контента |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `SM` | 664px |
|
|
44
|
+
| `MD` (по умолчанию) | 864px |
|
|
45
|
+
| `LG` | 1064px |
|
|
46
|
+
| `XL` | 1264px |
|
|
81
47
|
|
|
82
|
-
|
|
48
|
+
### Обязательные props (`LightBox`)
|
|
83
49
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
50
|
+
| Prop | Тип | Описание |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| `children` | `React.ReactElement[]` | Массив элементов: `LightBox.Content` и опционально `LightBox.Controls`, сайдбары, оверлеи |
|
|
87
53
|
|
|
88
|
-
|
|
89
|
-
</LightBox>
|
|
90
|
-
```
|
|
54
|
+
### Опциональные props (`LightBox`)
|
|
91
55
|
|
|
92
|
-
|
|
93
|
-
|
|
56
|
+
| Prop | Тип | По умолчанию | Описание |
|
|
57
|
+
|---|---|---|---|
|
|
58
|
+
| `size` | `ELightBoxSize` | `MD` | Максимальная ширина контента |
|
|
59
|
+
| `isLoading` | `boolean` | — | Состояние загрузки: деактивирует focus trap; `LightBox.Content` показывает `LoaderScreen` |
|
|
60
|
+
| `isSideOverlayOpened` | `boolean` | — | Флаг открытой боковой панели — добавляет модификатор на корневой элемент (блокирует скролл контента) |
|
|
61
|
+
| `isTopOverlayOpened` | `boolean` | — | Флаг открытой верхней панели — аналогичный модификатор |
|
|
62
|
+
| `focusTrapProps` | `IFocusTrapExtendedProps` | — | Переопределение настроек `FocusTrapExtended` |
|
|
63
|
+
| `forwardRef` | `React.MutableRefObject<HTMLElement \| null>` | — | Ref на корневой контейнер (исторически — prop, а не `React.forwardRef`) |
|
|
64
|
+
| `mountNode` | `HTMLDivElement` | нода `#LightBox-next-mount-node` (создаётся автоматически) | DOM-нода для портала |
|
|
65
|
+
| `lightBoxViewManagerNodeId` | `string` | `LightBox-next-view-manager-node` | Id DOM-элемента, задающего левую/правую визуальные границы лайтбокса (split-mode). Известная особенность: если элемента с кастомным id нет в DOM, fallback-нода создаётся с дефолтным id |
|
|
66
|
+
|
|
67
|
+
Открытие/закрытие управляется снаружи условным рендером: `{isOpen && <LightBox>...</LightBox>}` —
|
|
68
|
+
собственного prop `opened` нет.
|
|
69
|
+
|
|
70
|
+
### Ключевые props субкомпонентов
|
|
71
|
+
|
|
72
|
+
- `LightBox.Content`: `isLoading`, `loaderScreenProps`; автоматически добавляет `padding-top`
|
|
73
|
+
на высоту `LightBox.Controls`.
|
|
74
|
+
- `LightBox.Controls.Close`: `onClick` (обязателен), `title` (по умолчанию «Закрыть»);
|
|
75
|
+
срабатывает по Esc.
|
|
76
|
+
- `LightBox.Controls.Prev` / `Next`: `onClick` (обязателен), `clickByArrowLeft` /
|
|
77
|
+
`clickByArrowRight` — клик по стрелкам клавиатуры, `dataTutorialId`.
|
|
78
|
+
- `LightBox.LeftSidebar` / `RightSidebar`: `fixed` (фиксация панели), `minVisibleWidth`
|
|
79
|
+
(порог ширины в px, ниже которого содержимое скрывается; по умолчанию 100), `onShow`/`onHide`.
|
|
80
|
+
Отслеживают собственную ширину через `ResizeObserver`.
|
|
81
|
+
- `LightBox.SideOverlay`: `opened` (управляется снаружи), `onOpen`/`onClose` (вызываются по
|
|
82
|
+
окончании CSS-анимации), `isLoading`, `isTopLevelSideOverlayOpened` (открыт другой SideOverlay
|
|
83
|
+
поверх), `isTopOverlayOpened`, `size` (`EComponentSize` SM/MD/LG),
|
|
84
|
+
субкомпоненты `SideOverlay.CloseDesktop` / `SideOverlay.CloseMobile`.
|
|
85
|
+
- `LightBox.TopOverlay` — реэкспорт компонента `TopOverlay`.
|
|
94
86
|
|
|
95
87
|
---
|
|
96
88
|
|
|
97
|
-
##
|
|
98
|
-
|
|
99
|
-
### LightBox — `ILightBoxProps extends HTMLAttributes<HTMLDivElement>`
|
|
89
|
+
## Дизайн-токены
|
|
100
90
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
| `forwardRef` | `MutableRefObject<HTMLElement \| null>` | — | **Не `ref`**, а отдельный prop. Корневой компонент — не `forwardRef`. |
|
|
107
|
-
| `mountNode` | `HTMLDivElement` | auto | Куда рендерить через Portal. По умолчанию — DOM-элемент `#LightBox-next-mount-node` (создаётся при первом монтировании). |
|
|
108
|
-
| `lightBoxViewManagerNodeId` | `string` | `"LightBox-next-view-manager-node"` | DOM-элемент, по границам которого позиционируется LightBox (для кейса "LightBox не на весь экран"). |
|
|
109
|
-
| `isLoading` | `boolean` | — | Показывает loader, блокирует FocusTrap. |
|
|
110
|
-
| `isSideOverlayOpened` | `boolean` | — | Родитель контролирует — включает классы `.lightBoxSideOverlayActive` на корне. |
|
|
111
|
-
| `isTopOverlayOpened` | `boolean` | — | Аналогично для TopOverlay. |
|
|
112
|
-
|
|
113
|
-
### ELightBoxSize
|
|
114
|
-
|
|
115
|
-
```ts
|
|
116
|
-
enum ELightBoxSize { SM = "sm", MD = "md", LG = "lg", XL = "xl" }
|
|
91
|
+
```css
|
|
92
|
+
--triplex-next-LightBox-Backdrop_Background
|
|
93
|
+
--triplex-next-LightBox-Content_Background
|
|
94
|
+
--triplex-next-LightBox-SideOverlay_Background
|
|
95
|
+
--triplex-next-scroll-width
|
|
117
96
|
```
|
|
118
97
|
|
|
119
|
-
|
|
98
|
+
`--triplex-next-scroll-width` — глобальная переменная ширины скроллбара, проставляется утилитой
|
|
99
|
+
`addClassNameWithScrollbarWidth` при открытии лайтбокса. Ширины размеров (`664/864/1064/1264`)
|
|
100
|
+
заданы LESS-константами `@lightBox-content-max-width-*`.
|
|
120
101
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
| Prop | Тип | Описание |
|
|
124
|
-
|---|---|---|
|
|
125
|
-
| `isLoading` | `boolean` | Накладывает `LoaderScreen` поверх контента. |
|
|
126
|
-
| `loadingTitle` | `ReactNode` | Подпись под спиннером. |
|
|
102
|
+
---
|
|
127
103
|
|
|
128
|
-
|
|
129
|
-
- Слушает resize через `react-resize-detector` (debounce 100ms) и `WindowResizeListener`.
|
|
104
|
+
## Инварианты
|
|
130
105
|
|
|
131
|
-
|
|
106
|
+
- **Публичный API**: имена props `ILightBoxProps`, значения `ELightBoxSize`, компаундные ключи
|
|
107
|
+
`LightBox.Content` / `SideOverlay` / `TopOverlay` / `Controls` / `LeftSidebar` / `RightSidebar`
|
|
108
|
+
и `Controls.Close` / `Prev` / `Next` — не переименовывать.
|
|
109
|
+
- **`forwardRef`-паттерн**: `LightBox` принимает ref через prop `forwardRef`
|
|
110
|
+
(`MutableRefObject`) — не менять на `React.forwardRef` (breaking change);
|
|
111
|
+
`LightBoxSideOverlay`, `LightBoxLeftSidebar`, `LightBoxRightSidebar` обёрнуты в
|
|
112
|
+
`React.forwardRef` — не убирать.
|
|
113
|
+
- **Экспортируемые константы** `lightBoxMountNodeIdDefault` (`LightBox-next-mount-node`) и
|
|
114
|
+
`lightBoxViewManagerNodeIdDefault` (`LightBox-next-view-manager-node`) — часть публичного API.
|
|
115
|
+
- **`data-lightbox-component="controls"`** на контейнере `LightBox.Controls` — по нему
|
|
116
|
+
`LightBox.Content` вычисляет `padding-top`. Не удалять.
|
|
117
|
+
- **`data-test-id`** `lightBox-prev` / `lightBox-next` на кнопках стрелок — используются
|
|
118
|
+
автотестами потребителей.
|
|
119
|
+
- **Глобальные классы** `global-LB-less-or-equal-media-point-0` / `global-LB-more-media-point-0`
|
|
120
|
+
(`LightBoxViewManagerConsts`) заменяют media query по ширине контейнера; связаны со стилями в
|
|
121
|
+
`src/styles/components/lightbox.less`.
|
|
122
|
+
- Константы `LightBoxViewManagerConsts` (ширина стрелки 64, отступы 16/16, breakpoint 1024 +
|
|
123
|
+
ширина скроллбара) синхронизированы с LESS — менять только парой.
|
|
124
|
+
- Пока смонтирован хотя бы один лайтбокс, на `document.documentElement` висит класс блокировки
|
|
125
|
+
скролла; ведётся счётчик смонтированных лайтбоксов — класс снимается при размонтировании
|
|
126
|
+
последнего (при переключении роутов второй лайтбокс может смонтироваться раньше, чем
|
|
127
|
+
размонтируется первый). Аналогичный счётчик в `LightBoxViewManager` управляет классами
|
|
128
|
+
mount-ноды (`LightBoxMountNodeViewManager`, `LB-*`).
|
|
129
|
+
- Баррел `index.ts` реэкспортирует `LightBox`, `enums`, `LightBoxSideOverlay` — сохранять.
|
|
130
|
+
- React 17 совместимость (синхронизация в release-0): React 18-only API не использовать.
|
|
132
131
|
|
|
133
|
-
|
|
134
|
-
|---|---|---|---|
|
|
135
|
-
| `fixed` | `boolean` | — | Закрепляет панель (не скроллится вместе с контентом). |
|
|
136
|
-
| `minVisibleWidth` | `number` | `100` | Ширина (px), ниже которой контент панели скрывается классом `.hidden`. |
|
|
137
|
-
| `onShow` / `onHide` | `() => void` | — | Callback'и при пересечении порога ширины. Хранятся в ref — замыкание на актуальное значение. |
|
|
132
|
+
---
|
|
138
133
|
|
|
139
|
-
|
|
134
|
+
## Accessibility
|
|
140
135
|
|
|
141
|
-
|
|
136
|
+
- Корневой элемент и `SideOverlay` — `role="dialog"` + `aria-modal="true"`.
|
|
137
|
+
- **Focus trap** (`FocusTrapExtended`): активен, пока `isLoading` не установлен; у `SideOverlay` —
|
|
138
|
+
пока панель открыта и не анимируется. Начальный фокус — элемент с data-атрибутом
|
|
139
|
+
`FocusTrapUtils.firstInteractionElementDataAttr` (потребитель помечает его сам, например
|
|
140
|
+
заголовок с `tabIndex={-1}`).
|
|
141
|
+
- **Клавиатура**: Esc — клик по `Controls.Close`; ArrowLeft / ArrowRight — клик по
|
|
142
|
+
`Controls.Prev` / `Controls.Next` при включённых `clickByArrowLeft` / `clickByArrowRight`.
|
|
143
|
+
При открытом `SideOverlay` клавиатурные триггеры контролов лайтбокса не активны
|
|
144
|
+
(рендерится вариант кнопок без триггера).
|
|
145
|
+
- Кнопки `Controls.Close` имеют `data-exclude-modal-focus` — исключаются из начального фокуса
|
|
146
|
+
(у `Prev`/`Next` атрибута нет).
|
|
147
|
+
- `title` у `Controls.Close` по умолчанию «Закрыть» — для других языков потребитель передаёт
|
|
148
|
+
свой `title`.
|
|
142
149
|
|
|
143
|
-
|
|
144
|
-
|---|---|---|---|
|
|
145
|
-
| `opened` | `boolean` | — | Управляемое состояние. |
|
|
146
|
-
| `onClose` / `onOpen` | `() => void` | — | Срабатывают после завершения `transitionend` (не сразу при смене `opened`). |
|
|
147
|
-
| `focusTrapProps` | `FocusTrapProps` | — | Собственный FocusTrap внутри LightBox'овского (вложенные trap'ы). |
|
|
148
|
-
| `isLoading` | `boolean` | — | Показывает внутренний loader. |
|
|
149
|
-
| `isTopLevelSideOverlayOpened` | `boolean` | — | Если сверху открыт ещё один overlay — отключить scroll в текущем. |
|
|
150
|
-
| `isTopOverlayOpened` | `boolean` | — | Аналогично. |
|
|
151
|
-
| `size` | `EComponentSize` | `MD` | Ширина панели. |
|
|
150
|
+
---
|
|
152
151
|
|
|
153
|
-
|
|
154
|
-
- Внутри — `OverlayBase direction={RIGHT}` + `OverlayMask` + `FocusTrap`.
|
|
155
|
-
- Отслеживает `opening`/`closing` state'ы по `prevOpened` (useRef) — для анимированных переходов.
|
|
152
|
+
## Связанные компоненты
|
|
156
153
|
|
|
157
|
-
|
|
154
|
+
- `LightBoxContent` (`LightBoxContent.tsx`) — контентная область; следит за высотой контролов и шириной окна
|
|
155
|
+
- `LightBoxControls` + `LightBoxClose` / `LightBoxPrev` / `LightBoxNext` (`LightBoxControls/`) — кнопки управления; стрелки построены на общем внутреннем `LightBoxArrow` (не экспортируется)
|
|
156
|
+
- `LightBoxLeftSidebar` / `LightBoxRightSidebar` (`LightBoxSidebars/`) — сайдбары с автоскрытием содержимого по ширине (общий хук `useLightBoxSidebarVisibility`)
|
|
157
|
+
- `LightBoxSideOverlay` (`LightBoxSideOverlay/`) — выезжающая справа панель с собственным focus trap, лоадером и кнопками закрытия CloseDesktop/CloseMobile
|
|
158
|
+
- `LightBoxViewManager` (`LightBoxViewManager/`) — внутренний менеджер позиционирования: следит за границами view-manager-ноды, проставляет breakpoint-классы и CSS-переменные
|
|
159
|
+
- `TopOverlay` (`src/components/TopOverlay`) — верхняя панель, реэкспортируется как `LightBox.TopOverlay`
|
|
160
|
+
- `Page` (`src/components/Page`) — рекомендуемая структура содержимого `LightBox.Content`
|
|
161
|
+
- `ModalWindow` — альтернатива для компактных диалогов
|
|
158
162
|
|
|
159
|
-
|
|
163
|
+
---
|
|
160
164
|
|
|
161
|
-
|
|
165
|
+
## Stories
|
|
162
166
|
|
|
163
|
-
|
|
167
|
+
Основные истории: `stories/LightBox/LightBox.stories.tsx`
|
|
168
|
+
Файлы примеров: `stories/LightBox/examples/`
|
|
169
|
+
Визуальная регрессия: `stories/LightBox/LightBoxInitialOpened.stories.tsx` (лайтбокс, открытый
|
|
170
|
+
сразу при рендере — для скриншот-тестов; baseline в `__screenshots__/`).
|
|
164
171
|
|
|
165
|
-
|
|
172
|
+
| Story | Example file | Что демонстрирует |
|
|
173
|
+
|---|---|---|
|
|
174
|
+
| `Playground` | — | Интерактивный контроль size / isLoading / контролы / sticky header и footer |
|
|
175
|
+
| `Default` | `DefaultExample.tsx` | Базовая конфигурация с контролами и sticky-шапкой/футером |
|
|
176
|
+
| `Sizes` | `SizesExample.tsx` | Размеры SM / MD / LG / XL |
|
|
177
|
+
| `SmallContent` | `SmallContentExample.tsx` | Малый контент с `Page.Body` типа FIRST |
|
|
178
|
+
| `SplitMode` | `SplitModeExample.tsx` | Позиционирование по границам кастомной view-manager-ноды |
|
|
179
|
+
| `WithSidebars` | `WithSidebarsExample.tsx` | Левый и правый сайдбары |
|
|
180
|
+
| `WithFixedSidebars` | `WithFixedSidebarsExample.tsx` | Фиксированные сайдбары (`fixed`) |
|
|
181
|
+
| `WithOneSidebar` | `WithOneSidebarExample.tsx` | Только один сайдбар |
|
|
182
|
+
| `WithSideOverlay` | `WithSideOverlayExample.tsx` | Боковая выезжающая панель |
|
|
183
|
+
| `WithSideOverlayLoading` | `WithSideOverlayLoadingExample.tsx` | SideOverlay в состоянии загрузки |
|
|
184
|
+
| `WithTopOverlay` | `WithTopOverlayExample.tsx` | Верхняя панель |
|
|
185
|
+
| `WithTopOverlayInSideOverlay` | `WithTopOverlayInSideOverlayExample.tsx` | TopOverlay внутри SideOverlay |
|
|
186
|
+
| `LightBoxInitialOpened/Default` | — | Visual regression: лайтбокс, открытый сразу при рендере (`LightBoxInitialOpened.stories.tsx`, baseline в `__screenshots__/`) |
|
|
166
187
|
|
|
167
188
|
---
|
|
168
189
|
|
|
169
|
-
##
|
|
190
|
+
## История изменений
|
|
191
|
+
|
|
192
|
+
| Дата | Изменение |
|
|
193
|
+
|---|---|
|
|
194
|
+
| 2026-07-22 | Создан документ (Phase 1 AI-Ready). В том же изменении — AI-рефакторинг: JSDoc на props, общий `LightBoxArrow` для Prev/Next, хук `useLightBoxSidebarVisibility` для сайдбаров, устранение eslint-подавлений; публичный API не менялся |
|
|
195
|
+
| 2026-07-22 | Добавлен `export` к props-интерфейсам `ILightBoxControlsProps`, `ILightBoxCloseProps`, `ILightBoxPrevProps`, `ILightBoxNextProps`, `ILightBoxSideOverlayLoaderProps` (аддитивно, для консистентности с остальными интерфейсами компонента) |
|
|
196
|
+
| 2026-07-22 | Багфиксы по ревью PR #474: клавиатурные триггеры контролов целятся в видимую кнопку (стрелки не работали на desktop, Esc — на mobile); таймер-хак блокировки скролла заменён счётчиком смонтированных лайтбоксов; `LightBoxViewManager` снимает классы mount-ноды при размонтировании последнего менеджера |
|
|
170
197
|
|
|
171
|
-
|
|
198
|
+
---
|
|
172
199
|
|
|
173
|
-
|
|
174
|
-
<>
|
|
175
|
-
<Portal container={lightBoxMountNode.current}>{renderLightBox()}</Portal>
|
|
176
|
-
<LightBoxViewManager lightBoxViewManagerNode={...} lightBoxMountNode={...} />
|
|
177
|
-
</>
|
|
178
|
-
```
|
|
200
|
+
## Примеры использования
|
|
179
201
|
|
|
180
|
-
|
|
181
|
-
- **ViewManager** рендерится в `#LightBox-next-view-manager-node` (по умолчанию в body, но потребитель может создать его в нужном контейнере) и **вычисляет CSS-переменные** `--lightBox-screen-left/width/top` через `<style>`-тег на mountNode.
|
|
182
|
-
- Это позволяет LightBox'у "уменьшаться" до размеров конкретной области (например, если есть глобальный sidebar — LightBox не должен его перекрывать).
|
|
202
|
+
### Default
|
|
183
203
|
|
|
184
|
-
|
|
204
|
+
`stories/LightBox/examples/DefaultExample.tsx`
|
|
185
205
|
|
|
186
206
|
```tsx
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
207
|
+
import React, { useState } from "react";
|
|
208
|
+
import {
|
|
209
|
+
LightBox,
|
|
210
|
+
Page,
|
|
211
|
+
Button,
|
|
212
|
+
EButtonTheme,
|
|
213
|
+
Gap,
|
|
214
|
+
Title,
|
|
215
|
+
Text,
|
|
216
|
+
EFontType,
|
|
217
|
+
ETextSize,
|
|
218
|
+
ETitleSize,
|
|
219
|
+
EHeaderPageType,
|
|
220
|
+
EFooterPageType,
|
|
221
|
+
EBodyPageType,
|
|
222
|
+
Island,
|
|
223
|
+
IslandBody,
|
|
224
|
+
EIslandType,
|
|
225
|
+
FocusTrapUtils,
|
|
226
|
+
EComponentSize,
|
|
227
|
+
} from "@sberbusiness/triplex-next";
|
|
228
|
+
import { DefaulticonStrokePrdIcon20 } from "@sberbusiness/icons-next";
|
|
229
|
+
|
|
230
|
+
const POEM_LINES: string[] = [
|
|
231
|
+
"Мой дядя самых честных правил,",
|
|
232
|
+
"Когда не в шутку занемог,",
|
|
233
|
+
"Он уважать себя заставил",
|
|
234
|
+
"И лучше выдумать не мог.",
|
|
235
|
+
"Его пример другим наука;",
|
|
236
|
+
"Но, боже мой, какая скука",
|
|
237
|
+
"С больным сидеть и день и ночь,",
|
|
238
|
+
"Не отходя ни шагу прочь!",
|
|
239
|
+
"Какое низкое коварство",
|
|
240
|
+
"Полуживого забавлять,",
|
|
241
|
+
"Ему подушки поправлять,",
|
|
242
|
+
"Печально подносить лекарство,",
|
|
243
|
+
"Вздыхать и думать про себя:",
|
|
244
|
+
"Когда же черт возьмёт тебя…",
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
const PoemBlock: React.FC = () => (
|
|
248
|
+
<Island type={EIslandType.TYPE_1} size={EComponentSize.MD}>
|
|
249
|
+
<IslandBody>
|
|
250
|
+
{POEM_LINES.map((line) => (
|
|
251
|
+
<React.Fragment key={line}>
|
|
252
|
+
{line}
|
|
253
|
+
<br />
|
|
254
|
+
</React.Fragment>
|
|
255
|
+
))}
|
|
256
|
+
</IslandBody>
|
|
257
|
+
</Island>
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
const DefaultExample = () => {
|
|
261
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
262
|
+
|
|
263
|
+
const handleOpen = () => setIsOpen(true);
|
|
264
|
+
const handleClose = () => setIsOpen(false);
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<div>
|
|
268
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD} onClick={handleOpen}>
|
|
269
|
+
Открыть LightBox
|
|
270
|
+
</Button>
|
|
271
|
+
|
|
272
|
+
{isOpen ? (
|
|
273
|
+
<LightBox isLoading={false} isSideOverlayOpened={false} isTopOverlayOpened={false}>
|
|
274
|
+
<LightBox.Content key="content" isLoading={false}>
|
|
275
|
+
<Page>
|
|
276
|
+
<Page.Header type={EHeaderPageType.FIRST} sticky>
|
|
277
|
+
<Page.Header.Title>
|
|
278
|
+
<Page.Header.Title.Content>
|
|
279
|
+
<Title
|
|
280
|
+
tag="h1"
|
|
281
|
+
size={ETitleSize.H1}
|
|
282
|
+
tabIndex={-1}
|
|
283
|
+
// Устанавливает фокус на первый элемент при открытии LightBox.
|
|
284
|
+
{...{ [FocusTrapUtils.firstInteractionElementDataAttr]: true }}
|
|
285
|
+
>
|
|
286
|
+
Евгений Онегин
|
|
287
|
+
</Title>
|
|
288
|
+
<Gap size={8} />
|
|
289
|
+
<Text tag="div" size={ETextSize.B3} type={EFontType.SECONDARY}>
|
|
290
|
+
Русский поэт, драматург и прозаик, заложивший основы русского
|
|
291
|
+
реалистического направления.
|
|
292
|
+
</Text>
|
|
293
|
+
</Page.Header.Title.Content>
|
|
294
|
+
<Page.Header.Title.Controls>
|
|
295
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD}>
|
|
296
|
+
Button text
|
|
297
|
+
</Button>
|
|
298
|
+
<Button
|
|
299
|
+
icon={<DefaulticonStrokePrdIcon20 paletteIndex={0} />}
|
|
300
|
+
theme={EButtonTheme.SECONDARY}
|
|
301
|
+
size={EComponentSize.MD}
|
|
302
|
+
/>
|
|
303
|
+
</Page.Header.Title.Controls>
|
|
304
|
+
</Page.Header.Title>
|
|
305
|
+
</Page.Header>
|
|
306
|
+
|
|
307
|
+
<Page.Body type={EBodyPageType.SECOND}>
|
|
308
|
+
{[0, 1, 2].map((index) => (
|
|
309
|
+
<React.Fragment key={index}>
|
|
310
|
+
<PoemBlock />
|
|
311
|
+
{index < 2 && <Gap size={16} />}
|
|
312
|
+
</React.Fragment>
|
|
313
|
+
))}
|
|
314
|
+
</Page.Body>
|
|
315
|
+
|
|
316
|
+
<Page.Footer type={EFooterPageType.FIRST} sticky>
|
|
317
|
+
<Page.Footer.Description>
|
|
318
|
+
<Page.Footer.Description.Content>А. С. Пушкин</Page.Footer.Description.Content>
|
|
319
|
+
<Page.Footer.Description.Controls>
|
|
320
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD}>
|
|
321
|
+
Button text
|
|
322
|
+
</Button>
|
|
323
|
+
<Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD}>
|
|
324
|
+
Button text
|
|
325
|
+
</Button>
|
|
326
|
+
</Page.Footer.Description.Controls>
|
|
327
|
+
</Page.Footer.Description>
|
|
328
|
+
</Page.Footer>
|
|
329
|
+
</Page>
|
|
330
|
+
</LightBox.Content>
|
|
331
|
+
|
|
332
|
+
<LightBox.Controls>
|
|
333
|
+
<LightBox.Controls.Close title="Закрыть" data-test-id="lightbox-close" onClick={handleClose} />
|
|
334
|
+
<LightBox.Controls.Prev
|
|
335
|
+
title="Назад"
|
|
336
|
+
clickByArrowLeft
|
|
337
|
+
onClick={() => console.log("Prev clicked")}
|
|
338
|
+
/>
|
|
339
|
+
<LightBox.Controls.Next
|
|
340
|
+
title="Вперёд"
|
|
341
|
+
clickByArrowRight
|
|
342
|
+
onClick={() => console.log("Next clicked")}
|
|
343
|
+
/>
|
|
344
|
+
</LightBox.Controls>
|
|
345
|
+
</LightBox>
|
|
346
|
+
) : null}
|
|
347
|
+
</div>
|
|
348
|
+
);
|
|
349
|
+
};
|
|
350
|
+
export default DefaultExample;
|
|
190
351
|
```
|
|
191
352
|
|
|
192
|
-
|
|
193
|
-
- На desktop — `FocusTrap` с `initialFocus = getFirstInteractionElementByDataAttr` из `FocusTrapUtils`.
|
|
194
|
-
- `active={!isLoading}` — trap неактивен во время загрузки.
|
|
353
|
+
### Sizes
|
|
195
354
|
|
|
196
|
-
|
|
355
|
+
`stories/LightBox/examples/SizesExample.tsx`
|
|
197
356
|
|
|
198
357
|
```tsx
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
358
|
+
import React, { useState } from "react";
|
|
359
|
+
import {
|
|
360
|
+
LightBox,
|
|
361
|
+
ELightBoxSize,
|
|
362
|
+
Page,
|
|
363
|
+
Button,
|
|
364
|
+
EButtonTheme,
|
|
365
|
+
Gap,
|
|
366
|
+
Title,
|
|
367
|
+
Text,
|
|
368
|
+
EFontType,
|
|
369
|
+
ETextSize,
|
|
370
|
+
ETitleSize,
|
|
371
|
+
EHeaderPageType,
|
|
372
|
+
EFooterPageType,
|
|
373
|
+
EBodyPageType,
|
|
374
|
+
Island,
|
|
375
|
+
IslandBody,
|
|
376
|
+
EIslandType,
|
|
377
|
+
FocusTrapUtils,
|
|
378
|
+
EComponentSize,
|
|
379
|
+
} from "@sberbusiness/triplex-next";
|
|
380
|
+
|
|
381
|
+
const POEM_LINES: string[] = [
|
|
382
|
+
"Мой дядя самых честных правил,",
|
|
383
|
+
"Когда не в шутку занемог,",
|
|
384
|
+
"Он уважать себя заставил",
|
|
385
|
+
"И лучше выдумать не мог.",
|
|
386
|
+
"Его пример другим наука;",
|
|
387
|
+
"Но, боже мой, какая скука",
|
|
388
|
+
"С больным сидеть и день и ночь,",
|
|
389
|
+
"Не отходя ни шагу прочь!",
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
const PoemBlock: React.FC = () => (
|
|
393
|
+
<Island type={EIslandType.TYPE_1} size={EComponentSize.MD}>
|
|
394
|
+
<IslandBody>
|
|
395
|
+
{POEM_LINES.map((line) => (
|
|
396
|
+
<React.Fragment key={line}>
|
|
397
|
+
{line}
|
|
398
|
+
<br />
|
|
399
|
+
</React.Fragment>
|
|
400
|
+
))}
|
|
401
|
+
</IslandBody>
|
|
402
|
+
</Island>
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
const SIZES = Object.values(ELightBoxSize);
|
|
406
|
+
|
|
407
|
+
export const SizesExample = () => {
|
|
408
|
+
const [openedSize, setOpenedSize] = useState<ELightBoxSize | null>(null);
|
|
409
|
+
|
|
410
|
+
const handleClose = () => setOpenedSize(null);
|
|
411
|
+
|
|
412
|
+
return (
|
|
413
|
+
<div>
|
|
414
|
+
<div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
|
|
415
|
+
{SIZES.map((size) => (
|
|
416
|
+
<Button
|
|
417
|
+
key={size}
|
|
418
|
+
theme={EButtonTheme.SECONDARY}
|
|
419
|
+
size={EComponentSize.MD}
|
|
420
|
+
onClick={() => setOpenedSize(size)}
|
|
421
|
+
>
|
|
422
|
+
{size.toUpperCase()}
|
|
423
|
+
</Button>
|
|
424
|
+
))}
|
|
425
|
+
</div>
|
|
426
|
+
|
|
427
|
+
{openedSize !== null ? (
|
|
428
|
+
<LightBox isLoading={false} isSideOverlayOpened={false} isTopOverlayOpened={false} size={openedSize}>
|
|
429
|
+
<LightBox.Content key="content" isLoading={false}>
|
|
430
|
+
<Page>
|
|
431
|
+
<Page.Header type={EHeaderPageType.FIRST} sticky>
|
|
432
|
+
<Page.Header.Title>
|
|
433
|
+
<Page.Header.Title.Content>
|
|
434
|
+
<Title
|
|
435
|
+
tag="h1"
|
|
436
|
+
size={ETitleSize.H1}
|
|
437
|
+
tabIndex={-1}
|
|
438
|
+
{...{ [FocusTrapUtils.firstInteractionElementDataAttr]: true }}
|
|
439
|
+
>
|
|
440
|
+
LightBox {openedSize.toUpperCase()}
|
|
441
|
+
</Title>
|
|
442
|
+
<Gap size={8} />
|
|
443
|
+
<Text tag="div" size={ETextSize.B3} type={EFontType.SECONDARY}>
|
|
444
|
+
Размер контента: {openedSize.toUpperCase()}
|
|
445
|
+
</Text>
|
|
446
|
+
</Page.Header.Title.Content>
|
|
447
|
+
</Page.Header.Title>
|
|
448
|
+
</Page.Header>
|
|
449
|
+
|
|
450
|
+
<Page.Body type={EBodyPageType.SECOND}>
|
|
451
|
+
<PoemBlock />
|
|
452
|
+
<Gap size={16} />
|
|
453
|
+
<PoemBlock />
|
|
454
|
+
</Page.Body>
|
|
455
|
+
|
|
456
|
+
<Page.Footer type={EFooterPageType.FIRST} sticky>
|
|
457
|
+
<Page.Footer.Description>
|
|
458
|
+
<Page.Footer.Description.Controls>
|
|
459
|
+
<Button
|
|
460
|
+
theme={EButtonTheme.GENERAL}
|
|
461
|
+
size={EComponentSize.MD}
|
|
462
|
+
onClick={handleClose}
|
|
463
|
+
>
|
|
464
|
+
Закрыть
|
|
465
|
+
</Button>
|
|
466
|
+
</Page.Footer.Description.Controls>
|
|
467
|
+
</Page.Footer.Description>
|
|
468
|
+
</Page.Footer>
|
|
469
|
+
</Page>
|
|
470
|
+
</LightBox.Content>
|
|
471
|
+
|
|
472
|
+
<LightBox.Controls>
|
|
473
|
+
<LightBox.Controls.Close title="Закрыть" data-test-id="lightbox-close" onClick={handleClose} />
|
|
474
|
+
</LightBox.Controls>
|
|
475
|
+
</LightBox>
|
|
476
|
+
) : null}
|
|
477
|
+
</div>
|
|
478
|
+
);
|
|
479
|
+
};
|
|
207
480
|
```
|
|
208
481
|
|
|
209
|
-
|
|
482
|
+
### SmallContent
|
|
210
483
|
|
|
211
|
-
|
|
484
|
+
`stories/LightBox/examples/SmallContentExample.tsx`
|
|
212
485
|
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
486
|
+
```tsx
|
|
487
|
+
import React, { useState } from "react";
|
|
488
|
+
import {
|
|
489
|
+
LightBox,
|
|
490
|
+
Page,
|
|
491
|
+
Button,
|
|
492
|
+
EButtonTheme,
|
|
493
|
+
Gap,
|
|
494
|
+
Title,
|
|
495
|
+
Text,
|
|
496
|
+
EFontType,
|
|
497
|
+
ETextSize,
|
|
498
|
+
ETitleSize,
|
|
499
|
+
EHeaderPageType,
|
|
500
|
+
EFooterPageType,
|
|
501
|
+
EBodyPageType,
|
|
502
|
+
FocusTrapUtils,
|
|
503
|
+
EComponentSize,
|
|
504
|
+
} from "@sberbusiness/triplex-next";
|
|
505
|
+
import { DefaulticonStrokePrdIcon20 } from "@sberbusiness/icons-next";
|
|
506
|
+
|
|
507
|
+
export const SmallContentExample = () => {
|
|
508
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
509
|
+
|
|
510
|
+
const handleOpen = () => setIsOpen(true);
|
|
511
|
+
const handleClose = () => setIsOpen(false);
|
|
512
|
+
|
|
513
|
+
return (
|
|
514
|
+
<div>
|
|
515
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD} onClick={handleOpen}>
|
|
516
|
+
Открыть LightBox
|
|
517
|
+
</Button>
|
|
518
|
+
|
|
519
|
+
{isOpen ? (
|
|
520
|
+
<LightBox isLoading={false} isSideOverlayOpened={false} isTopOverlayOpened={false}>
|
|
521
|
+
<LightBox.Content key="content" isLoading={false}>
|
|
522
|
+
<Page>
|
|
523
|
+
<Page.Header type={EHeaderPageType.FIRST}>
|
|
524
|
+
<Page.Header.Title>
|
|
525
|
+
<Page.Header.Title.Content>
|
|
526
|
+
<Title
|
|
527
|
+
tag="h1"
|
|
528
|
+
size={ETitleSize.H1}
|
|
529
|
+
tabIndex={-1}
|
|
530
|
+
// Устанавливает фокус на первый элемент при открытии LightBox.
|
|
531
|
+
{...{ [FocusTrapUtils.firstInteractionElementDataAttr]: true }}
|
|
532
|
+
>
|
|
533
|
+
Евгений Онегин
|
|
534
|
+
</Title>
|
|
535
|
+
<Gap size={8} />
|
|
536
|
+
<Text tag="div" size={ETextSize.B3} type={EFontType.SECONDARY}>
|
|
537
|
+
Русский поэт, драматург и прозаик, заложивший основы русского
|
|
538
|
+
реалистического направления.
|
|
539
|
+
</Text>
|
|
540
|
+
</Page.Header.Title.Content>
|
|
541
|
+
<Page.Header.Title.Controls>
|
|
542
|
+
<Button
|
|
543
|
+
icon={<DefaulticonStrokePrdIcon20 paletteIndex={0} />}
|
|
544
|
+
theme={EButtonTheme.SECONDARY}
|
|
545
|
+
size={EComponentSize.MD}
|
|
546
|
+
/>
|
|
547
|
+
<Button
|
|
548
|
+
icon={<DefaulticonStrokePrdIcon20 paletteIndex={0} />}
|
|
549
|
+
theme={EButtonTheme.SECONDARY}
|
|
550
|
+
size={EComponentSize.MD}
|
|
551
|
+
/>
|
|
552
|
+
</Page.Header.Title.Controls>
|
|
553
|
+
</Page.Header.Title>
|
|
554
|
+
</Page.Header>
|
|
555
|
+
|
|
556
|
+
<Page.Body type={EBodyPageType.FIRST}>
|
|
557
|
+
Если контента мало, рекомендуется использовать Page.Body типа FIRST, чтобы не было
|
|
558
|
+
пустого пространства.
|
|
559
|
+
</Page.Body>
|
|
560
|
+
|
|
561
|
+
<Page.Footer type={EFooterPageType.FIRST}>
|
|
562
|
+
<Page.Footer.Description>
|
|
563
|
+
<Page.Footer.Description.Content>А. С. Пушкин</Page.Footer.Description.Content>
|
|
564
|
+
<Page.Footer.Description.Controls>
|
|
565
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD}>
|
|
566
|
+
Button text
|
|
567
|
+
</Button>
|
|
568
|
+
<Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD}>
|
|
569
|
+
Button text
|
|
570
|
+
</Button>
|
|
571
|
+
</Page.Footer.Description.Controls>
|
|
572
|
+
</Page.Footer.Description>
|
|
573
|
+
</Page.Footer>
|
|
574
|
+
</Page>
|
|
575
|
+
</LightBox.Content>
|
|
576
|
+
|
|
577
|
+
<LightBox.Controls>
|
|
578
|
+
<LightBox.Controls.Close title="Закрыть" data-test-id="lightbox-close" onClick={handleClose} />
|
|
579
|
+
<LightBox.Controls.Prev
|
|
580
|
+
title="Назад"
|
|
581
|
+
clickByArrowLeft
|
|
582
|
+
onClick={() => console.log("Prev clicked")}
|
|
583
|
+
/>
|
|
584
|
+
<LightBox.Controls.Next
|
|
585
|
+
title="Вперёд"
|
|
586
|
+
clickByArrowRight
|
|
587
|
+
onClick={() => console.log("Next clicked")}
|
|
588
|
+
/>
|
|
589
|
+
</LightBox.Controls>
|
|
590
|
+
</LightBox>
|
|
591
|
+
) : null}
|
|
592
|
+
</div>
|
|
593
|
+
);
|
|
594
|
+
};
|
|
220
595
|
```
|
|
221
596
|
|
|
222
|
-
|
|
223
|
-
- `setTimeout(100)` — фикс роутерного бага: новый LightBox монтируется **до** размонтирования старого, классы удаляются старым — повторно добавляем через 100ms.
|
|
597
|
+
### SplitMode
|
|
224
598
|
|
|
225
|
-
|
|
599
|
+
`stories/LightBox/examples/SplitModeExample.tsx`
|
|
226
600
|
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
|
|
601
|
+
```tsx
|
|
602
|
+
import React, { useState } from "react";
|
|
603
|
+
import {
|
|
604
|
+
LightBox,
|
|
605
|
+
Page,
|
|
606
|
+
Button,
|
|
607
|
+
EButtonTheme,
|
|
608
|
+
Gap,
|
|
609
|
+
Title,
|
|
610
|
+
Text,
|
|
611
|
+
EFontType,
|
|
612
|
+
ETextSize,
|
|
613
|
+
ETitleSize,
|
|
614
|
+
EHeaderPageType,
|
|
615
|
+
EFooterPageType,
|
|
616
|
+
EBodyPageType,
|
|
617
|
+
Island,
|
|
618
|
+
IslandBody,
|
|
619
|
+
EIslandType,
|
|
620
|
+
FocusTrapUtils,
|
|
621
|
+
EComponentSize,
|
|
622
|
+
} from "@sberbusiness/triplex-next";
|
|
623
|
+
import { DefaulticonStrokePrdIcon20 } from "@sberbusiness/icons-next";
|
|
624
|
+
import "../styles.less";
|
|
625
|
+
|
|
626
|
+
const POEM_LINES: string[] = [
|
|
627
|
+
"Мой дядя самых честных правил,",
|
|
628
|
+
"Когда не в шутку занемог,",
|
|
629
|
+
"Он уважать себя заставил",
|
|
630
|
+
"И лучше выдумать не мог.",
|
|
631
|
+
"Его пример другим наука;",
|
|
632
|
+
"Но, боже мой, какая скука",
|
|
633
|
+
"С больным сидеть и день и ночь,",
|
|
634
|
+
"Не отходя ни шагу прочь!",
|
|
635
|
+
"Какое низкое коварство",
|
|
636
|
+
"Полуживого забавлять,",
|
|
637
|
+
"Ему подушки поправлять,",
|
|
638
|
+
"Печально подносить лекарство,",
|
|
639
|
+
"Вздыхать и думать про себя:",
|
|
640
|
+
"Когда же черт возьмёт тебя…",
|
|
641
|
+
];
|
|
642
|
+
|
|
643
|
+
const PoemBlock: React.FC = () => (
|
|
644
|
+
<Island type={EIslandType.TYPE_1} size={EComponentSize.MD}>
|
|
645
|
+
<IslandBody>
|
|
646
|
+
{POEM_LINES.map((line) => (
|
|
647
|
+
<React.Fragment key={line}>
|
|
648
|
+
{line}
|
|
649
|
+
<br />
|
|
650
|
+
</React.Fragment>
|
|
651
|
+
))}
|
|
652
|
+
</IslandBody>
|
|
653
|
+
</Island>
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
export const SplitModeExample = () => {
|
|
657
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
658
|
+
|
|
659
|
+
const handleOpen = () => setIsOpen(true);
|
|
660
|
+
const handleClose = () => setIsOpen(false);
|
|
661
|
+
|
|
662
|
+
return (
|
|
663
|
+
<div>
|
|
664
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD} onClick={handleOpen}>
|
|
665
|
+
Открыть LightBox
|
|
666
|
+
</Button>
|
|
667
|
+
|
|
668
|
+
<div id="LightBox-next-view-manager-node-example" />
|
|
669
|
+
|
|
670
|
+
{isOpen ? (
|
|
671
|
+
<LightBox
|
|
672
|
+
isLoading={false}
|
|
673
|
+
isSideOverlayOpened={false}
|
|
674
|
+
isTopOverlayOpened={false}
|
|
675
|
+
lightBoxViewManagerNodeId="LightBox-next-view-manager-node-example"
|
|
676
|
+
>
|
|
677
|
+
<LightBox.Content key="content" isLoading={false}>
|
|
678
|
+
<Page>
|
|
679
|
+
<Page.Header type={EHeaderPageType.FIRST} sticky>
|
|
680
|
+
<Page.Header.Title>
|
|
681
|
+
<Page.Header.Title.Content>
|
|
682
|
+
<Title
|
|
683
|
+
tag="h1"
|
|
684
|
+
size={ETitleSize.H1}
|
|
685
|
+
tabIndex={-1}
|
|
686
|
+
// Устанавливает фокус на первый элемент при открытии LightBox.
|
|
687
|
+
{...{ [FocusTrapUtils.firstInteractionElementDataAttr]: true }}
|
|
688
|
+
>
|
|
689
|
+
Евгений Онегин
|
|
690
|
+
</Title>
|
|
691
|
+
<Gap size={8} />
|
|
692
|
+
<Text tag="div" size={ETextSize.B3} type={EFontType.SECONDARY}>
|
|
693
|
+
Русский поэт, драматург и прозаик, заложивший основы русского
|
|
694
|
+
реалистического направления.
|
|
695
|
+
</Text>
|
|
696
|
+
</Page.Header.Title.Content>
|
|
697
|
+
<Page.Header.Title.Controls>
|
|
698
|
+
<Button
|
|
699
|
+
icon={<DefaulticonStrokePrdIcon20 paletteIndex={0} />}
|
|
700
|
+
theme={EButtonTheme.SECONDARY}
|
|
701
|
+
size={EComponentSize.MD}
|
|
702
|
+
/>
|
|
703
|
+
<Button
|
|
704
|
+
icon={<DefaulticonStrokePrdIcon20 paletteIndex={0} />}
|
|
705
|
+
theme={EButtonTheme.SECONDARY}
|
|
706
|
+
size={EComponentSize.MD}
|
|
707
|
+
/>
|
|
708
|
+
</Page.Header.Title.Controls>
|
|
709
|
+
</Page.Header.Title>
|
|
710
|
+
</Page.Header>
|
|
711
|
+
|
|
712
|
+
<Page.Body type={EBodyPageType.SECOND}>
|
|
713
|
+
{[0, 1, 2].map((index) => (
|
|
714
|
+
<React.Fragment key={index}>
|
|
715
|
+
<PoemBlock />
|
|
716
|
+
{index < 2 && <Gap size={16} />}
|
|
717
|
+
</React.Fragment>
|
|
718
|
+
))}
|
|
719
|
+
</Page.Body>
|
|
720
|
+
|
|
721
|
+
<Page.Footer type={EFooterPageType.FIRST} sticky>
|
|
722
|
+
<Page.Footer.Description>
|
|
723
|
+
<Page.Footer.Description.Content>А. С. Пушкин</Page.Footer.Description.Content>
|
|
724
|
+
<Page.Footer.Description.Controls>
|
|
725
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD}>
|
|
726
|
+
Button text
|
|
727
|
+
</Button>
|
|
728
|
+
<Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD}>
|
|
729
|
+
Button text
|
|
730
|
+
</Button>
|
|
731
|
+
</Page.Footer.Description.Controls>
|
|
732
|
+
</Page.Footer.Description>
|
|
733
|
+
</Page.Footer>
|
|
734
|
+
</Page>
|
|
735
|
+
</LightBox.Content>
|
|
736
|
+
|
|
737
|
+
<LightBox.Controls>
|
|
738
|
+
<LightBox.Controls.Close title="Закрыть" data-test-id="lightbox-close" onClick={handleClose} />
|
|
739
|
+
<LightBox.Controls.Prev
|
|
740
|
+
title="Назад"
|
|
741
|
+
clickByArrowLeft
|
|
742
|
+
onClick={() => console.log("Prev clicked")}
|
|
743
|
+
/>
|
|
744
|
+
<LightBox.Controls.Next
|
|
745
|
+
title="Вперёд"
|
|
746
|
+
clickByArrowRight
|
|
747
|
+
onClick={() => console.log("Next clicked")}
|
|
748
|
+
/>
|
|
749
|
+
</LightBox.Controls>
|
|
750
|
+
</LightBox>
|
|
751
|
+
) : null}
|
|
752
|
+
</div>
|
|
753
|
+
);
|
|
754
|
+
};
|
|
230
755
|
```
|
|
231
756
|
|
|
232
|
-
|
|
233
|
-
- Debounce 100ms, изменение применяется при `deltaTop > 1` — антидрожание.
|
|
757
|
+
### WithSidebars
|
|
234
758
|
|
|
235
|
-
|
|
759
|
+
`stories/LightBox/examples/WithSidebarsExample.tsx`
|
|
236
760
|
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
761
|
+
```tsx
|
|
762
|
+
import React, { useState } from "react";
|
|
763
|
+
import {
|
|
764
|
+
LightBox,
|
|
765
|
+
Page,
|
|
766
|
+
Button,
|
|
767
|
+
EButtonTheme,
|
|
768
|
+
Gap,
|
|
769
|
+
Title,
|
|
770
|
+
Text,
|
|
771
|
+
EFontType,
|
|
772
|
+
ETextSize,
|
|
773
|
+
ETitleSize,
|
|
774
|
+
EHeaderPageType,
|
|
775
|
+
EFooterPageType,
|
|
776
|
+
EBodyPageType,
|
|
777
|
+
Island,
|
|
778
|
+
IslandBody,
|
|
779
|
+
EIslandType,
|
|
780
|
+
FocusTrapUtils,
|
|
781
|
+
EComponentSize,
|
|
782
|
+
MobileView,
|
|
783
|
+
} from "@sberbusiness/triplex-next";
|
|
784
|
+
import { DefaulticonStrokePrdIcon20 } from "@sberbusiness/icons-next";
|
|
785
|
+
|
|
786
|
+
const POEM_LINES: string[] = [
|
|
787
|
+
"Мой дядя самых честных правил,",
|
|
788
|
+
"Когда не в шутку занемог,",
|
|
789
|
+
"Он уважать себя заставил",
|
|
790
|
+
"И лучше выдумать не мог.",
|
|
791
|
+
"Его пример другим наука;",
|
|
792
|
+
"Но, боже мой, какая скука",
|
|
793
|
+
"С больным сидеть и день и ночь,",
|
|
794
|
+
"Не отходя ни шагу прочь!",
|
|
795
|
+
"Какое низкое коварство",
|
|
796
|
+
"Полуживого забавлять,",
|
|
797
|
+
"Ему подушки поправлять,",
|
|
798
|
+
"Печально подносить лекарство,",
|
|
799
|
+
"Вздыхать и думать про себя:",
|
|
800
|
+
"Когда же черт возьмёт тебя…",
|
|
801
|
+
];
|
|
802
|
+
|
|
803
|
+
const PoemBlock: React.FC = () => (
|
|
804
|
+
<Island type={EIslandType.TYPE_1} size={EComponentSize.MD}>
|
|
805
|
+
<IslandBody>
|
|
806
|
+
{POEM_LINES.map((line) => (
|
|
807
|
+
<React.Fragment key={line}>
|
|
808
|
+
{line}
|
|
809
|
+
<br />
|
|
810
|
+
</React.Fragment>
|
|
811
|
+
))}
|
|
812
|
+
</IslandBody>
|
|
813
|
+
</Island>
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
export const WithSidebarsExample = () => {
|
|
817
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
818
|
+
const [isLeftSidebarVisible, setIsLeftSidebarVisible] = useState(true);
|
|
819
|
+
const [isRightSidebarVisible, setIsRightSidebarVisible] = useState(true);
|
|
820
|
+
|
|
821
|
+
const renderLightBoxControls = () => (
|
|
822
|
+
<LightBox.Controls key="controls">
|
|
823
|
+
<LightBox.Controls.Close title="Закрыть" data-test-id="lightbox-close" onClick={() => setIsOpen(false)} />
|
|
824
|
+
<LightBox.Controls.Prev title="Назад" clickByArrowLeft onClick={() => console.log("Prev clicked")} />
|
|
825
|
+
<LightBox.Controls.Next title="Вперёд" clickByArrowRight onClick={() => console.log("Next clicked")} />
|
|
826
|
+
</LightBox.Controls>
|
|
827
|
+
);
|
|
828
|
+
|
|
829
|
+
return (
|
|
830
|
+
<div>
|
|
831
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD} onClick={() => setIsOpen(true)}>
|
|
832
|
+
Открыть LightBox
|
|
833
|
+
</Button>
|
|
834
|
+
|
|
835
|
+
{isOpen ? (
|
|
836
|
+
<LightBox isLoading={false}>
|
|
837
|
+
<LightBox.Content key="content" isLoading={false}>
|
|
838
|
+
<Page>
|
|
839
|
+
<Page.Header type={EHeaderPageType.FIRST} sticky>
|
|
840
|
+
<Page.Header.Title>
|
|
841
|
+
<Page.Header.Title.Content>
|
|
842
|
+
<MobileView
|
|
843
|
+
fallback={
|
|
844
|
+
<Title
|
|
845
|
+
tag="h1"
|
|
846
|
+
size={ETitleSize.H1}
|
|
847
|
+
tabIndex={-1}
|
|
848
|
+
// Устанавливает фокус на первый элемент при открытии LightBox.
|
|
849
|
+
{...{ [FocusTrapUtils.firstInteractionElementDataAttr]: true }}
|
|
850
|
+
>
|
|
851
|
+
Евгений Онегин
|
|
852
|
+
</Title>
|
|
853
|
+
}
|
|
854
|
+
>
|
|
855
|
+
<Title tag="h2" size={ETitleSize.H2}>
|
|
856
|
+
Евгений Онегин
|
|
857
|
+
</Title>
|
|
858
|
+
</MobileView>
|
|
859
|
+
|
|
860
|
+
<Gap size={8} />
|
|
861
|
+
<Text tag="div" size={ETextSize.B3} type={EFontType.SECONDARY}>
|
|
862
|
+
Русский поэт, драматург и прозаик, заложивший основы русского
|
|
863
|
+
реалистического направления.
|
|
864
|
+
</Text>
|
|
865
|
+
</Page.Header.Title.Content>
|
|
866
|
+
<Page.Header.Title.Controls>
|
|
867
|
+
<Button
|
|
868
|
+
icon={<DefaulticonStrokePrdIcon20 paletteIndex={0} />}
|
|
869
|
+
theme={EButtonTheme.SECONDARY}
|
|
870
|
+
size={EComponentSize.MD}
|
|
871
|
+
/>
|
|
872
|
+
<Button
|
|
873
|
+
icon={<DefaulticonStrokePrdIcon20 paletteIndex={0} />}
|
|
874
|
+
theme={EButtonTheme.SECONDARY}
|
|
875
|
+
size={EComponentSize.MD}
|
|
876
|
+
/>
|
|
877
|
+
</Page.Header.Title.Controls>
|
|
878
|
+
</Page.Header.Title>
|
|
879
|
+
</Page.Header>
|
|
880
|
+
|
|
881
|
+
<Page.Body type={EBodyPageType.SECOND}>
|
|
882
|
+
<Island type={EIslandType.TYPE_1} size={EComponentSize.MD}>
|
|
883
|
+
<IslandBody>
|
|
884
|
+
Left sidebar: {isLeftSidebarVisible ? "visible" : "hidden"}
|
|
885
|
+
<br />
|
|
886
|
+
Right sidebar: {isRightSidebarVisible ? "visible" : "hidden"}
|
|
887
|
+
</IslandBody>
|
|
888
|
+
</Island>
|
|
889
|
+
<Gap size={16} />
|
|
890
|
+
{[0, 1, 2].map((index) => (
|
|
891
|
+
<React.Fragment key={index}>
|
|
892
|
+
<PoemBlock />
|
|
893
|
+
{index < 2 && <Gap size={16} />}
|
|
894
|
+
</React.Fragment>
|
|
895
|
+
))}
|
|
896
|
+
</Page.Body>
|
|
897
|
+
|
|
898
|
+
<Page.Footer type={EFooterPageType.FIRST} sticky>
|
|
899
|
+
<Page.Footer.Description>
|
|
900
|
+
<Page.Footer.Description.Content>А. С. Пушкин</Page.Footer.Description.Content>
|
|
901
|
+
<Page.Footer.Description.Controls>
|
|
902
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD}>
|
|
903
|
+
Button text
|
|
904
|
+
</Button>
|
|
905
|
+
<Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD}>
|
|
906
|
+
Button text
|
|
907
|
+
</Button>
|
|
908
|
+
</Page.Footer.Description.Controls>
|
|
909
|
+
</Page.Footer.Description>
|
|
910
|
+
</Page.Footer>
|
|
911
|
+
</Page>
|
|
912
|
+
</LightBox.Content>
|
|
913
|
+
|
|
914
|
+
<LightBox.LeftSidebar
|
|
915
|
+
key="left-sidebar"
|
|
916
|
+
minVisibleWidth={140}
|
|
917
|
+
onShow={() => setIsLeftSidebarVisible(true)}
|
|
918
|
+
onHide={() => setIsLeftSidebarVisible(false)}
|
|
919
|
+
>
|
|
920
|
+
<Island type={EIslandType.TYPE_1} size={EComponentSize.MD}>
|
|
921
|
+
<IslandBody>Left Sidebar</IslandBody>
|
|
922
|
+
</Island>
|
|
923
|
+
</LightBox.LeftSidebar>
|
|
924
|
+
|
|
925
|
+
<LightBox.RightSidebar
|
|
926
|
+
key="right-sidebar"
|
|
927
|
+
onShow={() => setIsRightSidebarVisible(true)}
|
|
928
|
+
onHide={() => setIsRightSidebarVisible(false)}
|
|
929
|
+
>
|
|
930
|
+
<Island type={EIslandType.TYPE_1} size={EComponentSize.MD}>
|
|
931
|
+
<IslandBody>Right Sidebar</IslandBody>
|
|
932
|
+
</Island>
|
|
933
|
+
</LightBox.RightSidebar>
|
|
934
|
+
|
|
935
|
+
{renderLightBoxControls()}
|
|
936
|
+
</LightBox>
|
|
937
|
+
) : null}
|
|
938
|
+
</div>
|
|
939
|
+
);
|
|
940
|
+
};
|
|
243
941
|
```
|
|
244
942
|
|
|
245
|
-
|
|
246
|
-
- FocusTrap активен только когда `opened && !opening && !closing`.
|
|
247
|
-
|
|
248
|
-
### `LeftSidebar.onShow/onHide` через ref
|
|
249
|
-
|
|
250
|
-
Чтобы `ResizeObserver`-коллбэк не пересоздавался при смене обработчиков, они пишутся в `onShowRef`/`onHideRef` через `useEffect`. Паттерн "stable callback".
|
|
251
|
-
|
|
252
|
-
---
|
|
253
|
-
|
|
254
|
-
## Инварианты
|
|
943
|
+
### WithFixedSidebars
|
|
255
944
|
|
|
256
|
-
|
|
257
|
-
- **`data-lightbox-component="controls"`** атрибут на Controls-контейнере — **необходим** для работы `LightBoxContent` (querySelector). При замене на ref нужно править `LightBoxContent.tsx`.
|
|
258
|
-
- **IDs `LightBox-next-mount-node` и `LightBox-next-view-manager-node`** — константы в модуле, переименование — breaking.
|
|
259
|
-
- **`forwardRef` prop (а не `ref`)** — публичный API. LightBoxBase — не `forwardRef`; `Object.assign` поверх. Не переименовывать в `ref`.
|
|
260
|
-
- **`ELightBoxSize`** — собственный enum, не `EComponentSize`; есть значение `XL`.
|
|
261
|
-
- **Safari span `tempElSafariBug`** — не удалять без регрессии (см. DCBSWT-2866).
|
|
262
|
-
- **`setTimeout(100)` на body-lock** — костыль против роутерных гонок, не убирать.
|
|
263
|
-
- **`LightBox.SideOverlay.CloseDesktop`/`.CloseMobile`** — две отдельные кнопки, не одна. Mobile/desktop визуально разные.
|
|
264
|
-
- **`Portal` + `lightBoxMountNode`** — LightBox никогда не рендерится inline. При SSR потребителю нужен ClientOnly-wrapper.
|
|
265
|
-
- **`MobileView` fallback wraps FocusTrap, children — нет**: на mobile focus trap не активен.
|
|
266
|
-
- **`onOpen`/`onClose` SideOverlay** вызываются по `transitionend`, не по смене `opened`. Важно для консьюмеров, ждущих завершения анимации.
|
|
945
|
+
`stories/LightBox/examples/WithFixedSidebarsExample.tsx`
|
|
267
946
|
|
|
268
|
-
|
|
947
|
+
```tsx
|
|
948
|
+
import React, { useState } from "react";
|
|
949
|
+
import {
|
|
950
|
+
LightBox,
|
|
951
|
+
Page,
|
|
952
|
+
Button,
|
|
953
|
+
EButtonTheme,
|
|
954
|
+
Gap,
|
|
955
|
+
Title,
|
|
956
|
+
Text,
|
|
957
|
+
EFontType,
|
|
958
|
+
ETextSize,
|
|
959
|
+
ETitleSize,
|
|
960
|
+
EHeaderPageType,
|
|
961
|
+
EFooterPageType,
|
|
962
|
+
EBodyPageType,
|
|
963
|
+
Island,
|
|
964
|
+
IslandBody,
|
|
965
|
+
EIslandType,
|
|
966
|
+
FocusTrapUtils,
|
|
967
|
+
EComponentSize,
|
|
968
|
+
MobileView,
|
|
969
|
+
} from "@sberbusiness/triplex-next";
|
|
970
|
+
import { DefaulticonStrokePrdIcon20 } from "@sberbusiness/icons-next";
|
|
971
|
+
|
|
972
|
+
const POEM_LINES: string[] = [
|
|
973
|
+
"Мой дядя самых честных правил,",
|
|
974
|
+
"Когда не в шутку занемог,",
|
|
975
|
+
"Он уважать себя заставил",
|
|
976
|
+
"И лучше выдумать не мог.",
|
|
977
|
+
"Его пример другим наука;",
|
|
978
|
+
"Но, боже мой, какая скука",
|
|
979
|
+
"С больным сидеть и день и ночь,",
|
|
980
|
+
"Не отходя ни шагу прочь!",
|
|
981
|
+
"Какое низкое коварство",
|
|
982
|
+
"Полуживого забавлять,",
|
|
983
|
+
"Ему подушки поправлять,",
|
|
984
|
+
"Печально подносить лекарство,",
|
|
985
|
+
"Вздыхать и думать про себя:",
|
|
986
|
+
"Когда же черт возьмёт тебя…",
|
|
987
|
+
];
|
|
988
|
+
|
|
989
|
+
const PoemBlock: React.FC = () => (
|
|
990
|
+
<Island type={EIslandType.TYPE_1} size={EComponentSize.MD}>
|
|
991
|
+
<IslandBody>
|
|
992
|
+
{POEM_LINES.map((line) => (
|
|
993
|
+
<React.Fragment key={line}>
|
|
994
|
+
{line}
|
|
995
|
+
<br />
|
|
996
|
+
</React.Fragment>
|
|
997
|
+
))}
|
|
998
|
+
</IslandBody>
|
|
999
|
+
</Island>
|
|
1000
|
+
);
|
|
1001
|
+
|
|
1002
|
+
export const WithFixedSidebarsExample = () => {
|
|
1003
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
1004
|
+
|
|
1005
|
+
const renderLightBoxControls = () => (
|
|
1006
|
+
<LightBox.Controls key="controls">
|
|
1007
|
+
<LightBox.Controls.Close title="Закрыть" data-test-id="lightbox-close" onClick={() => setIsOpen(false)} />
|
|
1008
|
+
<LightBox.Controls.Prev title="Назад" clickByArrowLeft onClick={() => console.log("Prev clicked")} />
|
|
1009
|
+
<LightBox.Controls.Next title="Вперёд" clickByArrowRight onClick={() => console.log("Next clicked")} />
|
|
1010
|
+
</LightBox.Controls>
|
|
1011
|
+
);
|
|
1012
|
+
|
|
1013
|
+
return (
|
|
1014
|
+
<div>
|
|
1015
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD} onClick={() => setIsOpen(true)}>
|
|
1016
|
+
Открыть LightBox
|
|
1017
|
+
</Button>
|
|
1018
|
+
|
|
1019
|
+
{isOpen ? (
|
|
1020
|
+
<LightBox isLoading={false}>
|
|
1021
|
+
<LightBox.Content key="content" isLoading={false}>
|
|
1022
|
+
<Page>
|
|
1023
|
+
<Page.Header type={EHeaderPageType.FIRST} sticky>
|
|
1024
|
+
<Page.Header.Title>
|
|
1025
|
+
<Page.Header.Title.Content>
|
|
1026
|
+
<MobileView
|
|
1027
|
+
fallback={
|
|
1028
|
+
<Title
|
|
1029
|
+
tag="h1"
|
|
1030
|
+
size={ETitleSize.H1}
|
|
1031
|
+
tabIndex={-1}
|
|
1032
|
+
// Устанавливает фокус на первый элемент при открытии LightBox.
|
|
1033
|
+
{...{ [FocusTrapUtils.firstInteractionElementDataAttr]: true }}
|
|
1034
|
+
>
|
|
1035
|
+
Евгений Онегин
|
|
1036
|
+
</Title>
|
|
1037
|
+
}
|
|
1038
|
+
>
|
|
1039
|
+
<Title tag="h2" size={ETitleSize.H2}>
|
|
1040
|
+
Евгений Онегин
|
|
1041
|
+
</Title>
|
|
1042
|
+
</MobileView>
|
|
1043
|
+
|
|
1044
|
+
<Gap size={8} />
|
|
1045
|
+
<Text tag="div" size={ETextSize.B3} type={EFontType.SECONDARY}>
|
|
1046
|
+
Русский поэт, драматург и прозаик, заложивший основы русского
|
|
1047
|
+
реалистического направления.
|
|
1048
|
+
</Text>
|
|
1049
|
+
</Page.Header.Title.Content>
|
|
1050
|
+
<Page.Header.Title.Controls>
|
|
1051
|
+
<Button
|
|
1052
|
+
icon={<DefaulticonStrokePrdIcon20 paletteIndex={0} />}
|
|
1053
|
+
theme={EButtonTheme.SECONDARY}
|
|
1054
|
+
size={EComponentSize.MD}
|
|
1055
|
+
/>
|
|
1056
|
+
<Button
|
|
1057
|
+
icon={<DefaulticonStrokePrdIcon20 paletteIndex={0} />}
|
|
1058
|
+
theme={EButtonTheme.SECONDARY}
|
|
1059
|
+
size={EComponentSize.MD}
|
|
1060
|
+
/>
|
|
1061
|
+
</Page.Header.Title.Controls>
|
|
1062
|
+
</Page.Header.Title>
|
|
1063
|
+
</Page.Header>
|
|
1064
|
+
|
|
1065
|
+
<Page.Body type={EBodyPageType.SECOND}>
|
|
1066
|
+
{[0, 1, 2].map((index) => (
|
|
1067
|
+
<React.Fragment key={index}>
|
|
1068
|
+
<PoemBlock />
|
|
1069
|
+
{index < 2 && <Gap size={16} />}
|
|
1070
|
+
</React.Fragment>
|
|
1071
|
+
))}
|
|
1072
|
+
</Page.Body>
|
|
1073
|
+
|
|
1074
|
+
<Page.Footer type={EFooterPageType.FIRST} sticky>
|
|
1075
|
+
<Page.Footer.Description>
|
|
1076
|
+
<Page.Footer.Description.Content>А. С. Пушкин</Page.Footer.Description.Content>
|
|
1077
|
+
<Page.Footer.Description.Controls>
|
|
1078
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD}>
|
|
1079
|
+
Button text
|
|
1080
|
+
</Button>
|
|
1081
|
+
<Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD}>
|
|
1082
|
+
Button text
|
|
1083
|
+
</Button>
|
|
1084
|
+
</Page.Footer.Description.Controls>
|
|
1085
|
+
</Page.Footer.Description>
|
|
1086
|
+
</Page.Footer>
|
|
1087
|
+
</Page>
|
|
1088
|
+
</LightBox.Content>
|
|
1089
|
+
|
|
1090
|
+
<LightBox.LeftSidebar fixed key="left-sidebar">
|
|
1091
|
+
<Island type={EIslandType.TYPE_1} size={EComponentSize.MD}>
|
|
1092
|
+
<IslandBody>Left Sidebar</IslandBody>
|
|
1093
|
+
</Island>
|
|
1094
|
+
</LightBox.LeftSidebar>
|
|
1095
|
+
|
|
1096
|
+
<LightBox.RightSidebar fixed key="right-sidebar">
|
|
1097
|
+
<Island type={EIslandType.TYPE_1} size={EComponentSize.MD}>
|
|
1098
|
+
<IslandBody>Right Sidebar</IslandBody>
|
|
1099
|
+
</Island>
|
|
1100
|
+
</LightBox.RightSidebar>
|
|
1101
|
+
|
|
1102
|
+
{renderLightBoxControls()}
|
|
1103
|
+
</LightBox>
|
|
1104
|
+
) : null}
|
|
1105
|
+
</div>
|
|
1106
|
+
);
|
|
1107
|
+
};
|
|
1108
|
+
```
|
|
269
1109
|
|
|
270
|
-
|
|
1110
|
+
### WithOneSidebar
|
|
271
1111
|
|
|
272
|
-
|
|
273
|
-
- FocusTrap удерживает фокус внутри (desktop). Потребитель должен проставить tabbable-элементы и осмысленные labels.
|
|
274
|
-
- Initial focus — первый элемент с `data-focus-initial` (см. `FocusTrapUtils.getFirstInteractionElementByDataAttr`) или первый tabbable.
|
|
275
|
-
- `allowOutsideClick: true` — клик вне модалки не ломает trap (но и не закрывает — консьюмер сам вешает onClose).
|
|
276
|
-
- **Mobile — без focus trap'а**. Семантика диалога остаётся через `role="dialog"`, но скринридеры могут не удерживать фокус — закладывайте это для мобильных UX-тестов.
|
|
277
|
-
- Контент не анонсируется при открытии (нет `aria-labelledby`/`aria-describedby` по умолчанию) — потребитель должен добавить через `...rest`.
|
|
1112
|
+
`stories/LightBox/examples/WithOneSidebarExample.tsx`
|
|
278
1113
|
|
|
279
|
-
|
|
1114
|
+
```tsx
|
|
1115
|
+
import React, { useState } from "react";
|
|
1116
|
+
import {
|
|
1117
|
+
LightBox,
|
|
1118
|
+
Page,
|
|
1119
|
+
Button,
|
|
1120
|
+
EButtonTheme,
|
|
1121
|
+
Gap,
|
|
1122
|
+
Title,
|
|
1123
|
+
Text,
|
|
1124
|
+
EFontType,
|
|
1125
|
+
ETextSize,
|
|
1126
|
+
ETitleSize,
|
|
1127
|
+
EHeaderPageType,
|
|
1128
|
+
EFooterPageType,
|
|
1129
|
+
EBodyPageType,
|
|
1130
|
+
Island,
|
|
1131
|
+
IslandBody,
|
|
1132
|
+
EIslandType,
|
|
1133
|
+
FocusTrapUtils,
|
|
1134
|
+
EComponentSize,
|
|
1135
|
+
MobileView,
|
|
1136
|
+
} from "@sberbusiness/triplex-next";
|
|
1137
|
+
import { DefaulticonStrokePrdIcon20 } from "@sberbusiness/icons-next";
|
|
1138
|
+
|
|
1139
|
+
const POEM_LINES: string[] = [
|
|
1140
|
+
"Мой дядя самых честных правил,",
|
|
1141
|
+
"Когда не в шутку занемог,",
|
|
1142
|
+
"Он уважать себя заставил",
|
|
1143
|
+
"И лучше выдумать не мог.",
|
|
1144
|
+
"Его пример другим наука;",
|
|
1145
|
+
"Но, боже мой, какая скука",
|
|
1146
|
+
"С больным сидеть и день и ночь,",
|
|
1147
|
+
"Не отходя ни шагу прочь!",
|
|
1148
|
+
"Какое низкое коварство",
|
|
1149
|
+
"Полуживого забавлять,",
|
|
1150
|
+
"Ему подушки поправлять,",
|
|
1151
|
+
"Печально подносить лекарство,",
|
|
1152
|
+
"Вздыхать и думать про себя:",
|
|
1153
|
+
"Когда же черт возьмёт тебя…",
|
|
1154
|
+
];
|
|
1155
|
+
|
|
1156
|
+
const PoemBlock: React.FC = () => (
|
|
1157
|
+
<Island type={EIslandType.TYPE_1} size={EComponentSize.MD}>
|
|
1158
|
+
<IslandBody>
|
|
1159
|
+
{POEM_LINES.map((line) => (
|
|
1160
|
+
<React.Fragment key={line}>
|
|
1161
|
+
{line}
|
|
1162
|
+
<br />
|
|
1163
|
+
</React.Fragment>
|
|
1164
|
+
))}
|
|
1165
|
+
</IslandBody>
|
|
1166
|
+
</Island>
|
|
1167
|
+
);
|
|
1168
|
+
|
|
1169
|
+
export const WithOneSidebarExample = () => {
|
|
1170
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
1171
|
+
|
|
1172
|
+
const renderLightBoxControls = () => (
|
|
1173
|
+
<LightBox.Controls key="controls">
|
|
1174
|
+
<LightBox.Controls.Close title="Закрыть" data-test-id="lightbox-close" onClick={() => setIsOpen(false)} />
|
|
1175
|
+
<LightBox.Controls.Prev title="Назад" clickByArrowLeft onClick={() => console.log("Prev clicked")} />
|
|
1176
|
+
<LightBox.Controls.Next title="Вперёд" clickByArrowRight onClick={() => console.log("Next clicked")} />
|
|
1177
|
+
</LightBox.Controls>
|
|
1178
|
+
);
|
|
1179
|
+
|
|
1180
|
+
return (
|
|
1181
|
+
<div>
|
|
1182
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD} onClick={() => setIsOpen(true)}>
|
|
1183
|
+
Открыть LightBox
|
|
1184
|
+
</Button>
|
|
1185
|
+
|
|
1186
|
+
{isOpen ? (
|
|
1187
|
+
<LightBox isLoading={false}>
|
|
1188
|
+
<LightBox.Content key="content" isLoading={false}>
|
|
1189
|
+
<Page>
|
|
1190
|
+
<Page.Header type={EHeaderPageType.FIRST} sticky>
|
|
1191
|
+
<Page.Header.Title>
|
|
1192
|
+
<Page.Header.Title.Content>
|
|
1193
|
+
<MobileView
|
|
1194
|
+
fallback={
|
|
1195
|
+
<Title
|
|
1196
|
+
tag="h1"
|
|
1197
|
+
size={ETitleSize.H1}
|
|
1198
|
+
tabIndex={-1}
|
|
1199
|
+
// Устанавливает фокус на первый элемент при открытии LightBox.
|
|
1200
|
+
{...{ [FocusTrapUtils.firstInteractionElementDataAttr]: true }}
|
|
1201
|
+
>
|
|
1202
|
+
Евгений Онегин
|
|
1203
|
+
</Title>
|
|
1204
|
+
}
|
|
1205
|
+
>
|
|
1206
|
+
<Title tag="h2" size={ETitleSize.H2}>
|
|
1207
|
+
Евгений Онегин
|
|
1208
|
+
</Title>
|
|
1209
|
+
</MobileView>
|
|
1210
|
+
|
|
1211
|
+
<Gap size={8} />
|
|
1212
|
+
<Text tag="div" size={ETextSize.B3} type={EFontType.SECONDARY}>
|
|
1213
|
+
Русский поэт, драматург и прозаик, заложивший основы русского
|
|
1214
|
+
реалистического направления.
|
|
1215
|
+
</Text>
|
|
1216
|
+
</Page.Header.Title.Content>
|
|
1217
|
+
<Page.Header.Title.Controls>
|
|
1218
|
+
<Button
|
|
1219
|
+
icon={<DefaulticonStrokePrdIcon20 paletteIndex={0} />}
|
|
1220
|
+
theme={EButtonTheme.SECONDARY}
|
|
1221
|
+
size={EComponentSize.MD}
|
|
1222
|
+
/>
|
|
1223
|
+
<Button
|
|
1224
|
+
icon={<DefaulticonStrokePrdIcon20 paletteIndex={0} />}
|
|
1225
|
+
theme={EButtonTheme.SECONDARY}
|
|
1226
|
+
size={EComponentSize.MD}
|
|
1227
|
+
/>
|
|
1228
|
+
</Page.Header.Title.Controls>
|
|
1229
|
+
</Page.Header.Title>
|
|
1230
|
+
</Page.Header>
|
|
1231
|
+
|
|
1232
|
+
<Page.Body type={EBodyPageType.SECOND}>
|
|
1233
|
+
{[0, 1, 2].map((index) => (
|
|
1234
|
+
<React.Fragment key={index}>
|
|
1235
|
+
<PoemBlock />
|
|
1236
|
+
{index < 2 && <Gap size={16} />}
|
|
1237
|
+
</React.Fragment>
|
|
1238
|
+
))}
|
|
1239
|
+
</Page.Body>
|
|
1240
|
+
|
|
1241
|
+
<Page.Footer type={EFooterPageType.FIRST} sticky>
|
|
1242
|
+
<Page.Footer.Description>
|
|
1243
|
+
<Page.Footer.Description.Content>А. С. Пушкин</Page.Footer.Description.Content>
|
|
1244
|
+
<Page.Footer.Description.Controls>
|
|
1245
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD}>
|
|
1246
|
+
Button text
|
|
1247
|
+
</Button>
|
|
1248
|
+
<Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD}>
|
|
1249
|
+
Button text
|
|
1250
|
+
</Button>
|
|
1251
|
+
</Page.Footer.Description.Controls>
|
|
1252
|
+
</Page.Footer.Description>
|
|
1253
|
+
</Page.Footer>
|
|
1254
|
+
</Page>
|
|
1255
|
+
</LightBox.Content>
|
|
1256
|
+
|
|
1257
|
+
<LightBox.RightSidebar key="right-sidebar">
|
|
1258
|
+
<Island type={EIslandType.TYPE_1} size={EComponentSize.MD}>
|
|
1259
|
+
<IslandBody>Right Sidebar</IslandBody>
|
|
1260
|
+
</Island>
|
|
1261
|
+
</LightBox.RightSidebar>
|
|
1262
|
+
|
|
1263
|
+
{renderLightBoxControls()}
|
|
1264
|
+
</LightBox>
|
|
1265
|
+
) : null}
|
|
1266
|
+
</div>
|
|
1267
|
+
);
|
|
1268
|
+
};
|
|
1269
|
+
```
|
|
280
1270
|
|
|
281
|
-
|
|
1271
|
+
### WithSideOverlay
|
|
282
1272
|
|
|
283
|
-
|
|
1273
|
+
`stories/LightBox/examples/WithSideOverlayExample.tsx`
|
|
284
1274
|
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
1275
|
+
```tsx
|
|
1276
|
+
import React, { useState } from "react";
|
|
1277
|
+
import {
|
|
1278
|
+
LightBox,
|
|
1279
|
+
Page,
|
|
1280
|
+
Button,
|
|
1281
|
+
EButtonTheme,
|
|
1282
|
+
Gap,
|
|
1283
|
+
Title,
|
|
1284
|
+
Text,
|
|
1285
|
+
EFontType,
|
|
1286
|
+
ETextSize,
|
|
1287
|
+
ETitleSize,
|
|
1288
|
+
EHeaderPageType,
|
|
1289
|
+
EFooterPageType,
|
|
1290
|
+
EBodyPageType,
|
|
1291
|
+
Island,
|
|
1292
|
+
IslandBody,
|
|
1293
|
+
EIslandType,
|
|
1294
|
+
FocusTrapUtils,
|
|
1295
|
+
EComponentSize,
|
|
1296
|
+
MobileView,
|
|
1297
|
+
} from "@sberbusiness/triplex-next";
|
|
1298
|
+
|
|
1299
|
+
const POEM_LINES: string[] = [
|
|
1300
|
+
"Мой дядя самых честных правил,",
|
|
1301
|
+
"Когда не в шутку занемог,",
|
|
1302
|
+
"Он уважать себя заставил",
|
|
1303
|
+
"И лучше выдумать не мог.",
|
|
1304
|
+
"Его пример другим наука;",
|
|
1305
|
+
"Но, боже мой, какая скука",
|
|
1306
|
+
"С больным сидеть и день и ночь,",
|
|
1307
|
+
"Не отходя ни шагу прочь!",
|
|
1308
|
+
"Какое низкое коварство",
|
|
1309
|
+
"Полуживого забавлять,",
|
|
1310
|
+
"Ему подушки поправлять,",
|
|
1311
|
+
"Печально подносить лекарство,",
|
|
1312
|
+
"Вздыхать и думать про себя:",
|
|
1313
|
+
"Когда же черт возьмёт тебя…",
|
|
1314
|
+
];
|
|
1315
|
+
|
|
1316
|
+
const PoemBlock: React.FC = () => (
|
|
1317
|
+
<Island type={EIslandType.TYPE_1} size={EComponentSize.MD}>
|
|
1318
|
+
<IslandBody>
|
|
1319
|
+
{POEM_LINES.map((line) => (
|
|
1320
|
+
<React.Fragment key={line}>
|
|
1321
|
+
{line}
|
|
1322
|
+
<br />
|
|
1323
|
+
</React.Fragment>
|
|
1324
|
+
))}
|
|
1325
|
+
</IslandBody>
|
|
1326
|
+
</Island>
|
|
1327
|
+
);
|
|
1328
|
+
|
|
1329
|
+
export const WithSideOverlayExample = () => {
|
|
1330
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
1331
|
+
const [openedSideOverlayLG, setOpenedSideOverlayLG] = useState(false);
|
|
1332
|
+
const [openedSideOverlayMD, setOpenedSideOverlayMD] = useState(false);
|
|
1333
|
+
const [openedSideOverlaySM, setOpenedSideOverlaySM] = useState(false);
|
|
1334
|
+
|
|
1335
|
+
const handleOpenSideOverlayLG = () => setOpenedSideOverlayLG(true);
|
|
1336
|
+
const handleCloseSideOverlayLG = () => setOpenedSideOverlayLG(false);
|
|
1337
|
+
const handleOpenSideOverlayMD = () => setOpenedSideOverlayMD(true);
|
|
1338
|
+
const handleCloseSideOverlayMD = () => setOpenedSideOverlayMD(false);
|
|
1339
|
+
const handleOpenSideOverlaySM = () => setOpenedSideOverlaySM(true);
|
|
1340
|
+
const handleCloseSideOverlaySM = () => setOpenedSideOverlaySM(false);
|
|
1341
|
+
|
|
1342
|
+
const handleOpen = () => setIsOpen(true);
|
|
1343
|
+
const handleClose = () => setIsOpen(false);
|
|
1344
|
+
|
|
1345
|
+
const renderLightBoxControls = () => (
|
|
1346
|
+
<LightBox.Controls key="controls">
|
|
1347
|
+
<LightBox.Controls.Close title="Закрыть" data-test-id="lightbox-close" onClick={handleClose} />
|
|
1348
|
+
</LightBox.Controls>
|
|
1349
|
+
);
|
|
1350
|
+
|
|
1351
|
+
const renderLightBoxSideOverlayLG = () => (
|
|
1352
|
+
<LightBox.SideOverlay
|
|
1353
|
+
key="sideOverlayLG"
|
|
1354
|
+
opened={openedSideOverlayLG}
|
|
1355
|
+
size={EComponentSize.LG}
|
|
1356
|
+
isTopLevelSideOverlayOpened={openedSideOverlayMD || openedSideOverlaySM}
|
|
1357
|
+
>
|
|
1358
|
+
<Page>
|
|
1359
|
+
<Page.Header type={EHeaderPageType.FIRST} sticky>
|
|
1360
|
+
<Page.Header.Title>
|
|
1361
|
+
<Page.Header.Title.Content>
|
|
1362
|
+
<MobileView
|
|
1363
|
+
fallback={
|
|
1364
|
+
<Title
|
|
1365
|
+
tag="h1"
|
|
1366
|
+
size={ETitleSize.H1}
|
|
1367
|
+
tabIndex={-1}
|
|
1368
|
+
// Устанавливает фокус на первый элемент при открытии LightBox.
|
|
1369
|
+
{...{ [FocusTrapUtils.firstInteractionElementDataAttr]: true }}
|
|
1370
|
+
>
|
|
1371
|
+
Евгений Онегин
|
|
1372
|
+
</Title>
|
|
1373
|
+
}
|
|
1374
|
+
>
|
|
1375
|
+
<Title tag="h2" size={ETitleSize.H2}>
|
|
1376
|
+
Евгений Онегин
|
|
1377
|
+
</Title>
|
|
1378
|
+
</MobileView>
|
|
1379
|
+
</Page.Header.Title.Content>
|
|
1380
|
+
<Page.Header.Title.Controls>
|
|
1381
|
+
{/* Кнопка закрытия SideOverlay для мобильного устройства. Отображается только на мобильном устройстве, внутри заголовка SideOverlay. */}
|
|
1382
|
+
<LightBox.SideOverlay.CloseMobile
|
|
1383
|
+
data-test-id="lightbox-side-overlay-close"
|
|
1384
|
+
onClick={handleCloseSideOverlayLG}
|
|
1385
|
+
/>
|
|
1386
|
+
</Page.Header.Title.Controls>
|
|
1387
|
+
</Page.Header.Title>
|
|
1388
|
+
</Page.Header>
|
|
1389
|
+
|
|
1390
|
+
<Page.Body type={EBodyPageType.SECOND}>
|
|
1391
|
+
{[0, 1, 2].map((index) => (
|
|
1392
|
+
<React.Fragment key={index}>
|
|
1393
|
+
<PoemBlock />
|
|
1394
|
+
{index < 2 && <Gap size={16} />}
|
|
1395
|
+
</React.Fragment>
|
|
1396
|
+
))}
|
|
1397
|
+
</Page.Body>
|
|
1398
|
+
|
|
1399
|
+
<Page.Footer type={EFooterPageType.FIRST} sticky>
|
|
1400
|
+
<Page.Footer.Description>
|
|
1401
|
+
<Page.Footer.Description.Content>А. С. Пушкин</Page.Footer.Description.Content>
|
|
1402
|
+
<Page.Footer.Description.Controls>
|
|
1403
|
+
<Button
|
|
1404
|
+
theme={EButtonTheme.GENERAL}
|
|
1405
|
+
size={EComponentSize.MD}
|
|
1406
|
+
onClick={handleOpenSideOverlayMD}
|
|
1407
|
+
>
|
|
1408
|
+
Открыть SideOverlay MD
|
|
1409
|
+
</Button>
|
|
1410
|
+
</Page.Footer.Description.Controls>
|
|
1411
|
+
</Page.Footer.Description>
|
|
1412
|
+
</Page.Footer>
|
|
1413
|
+
</Page>
|
|
1414
|
+
|
|
1415
|
+
{/* Кнопка закрытия SideOverlay для десктопа. Отображается только на десктопе, справа от заголовка SideOverlay. */}
|
|
1416
|
+
<LightBox.SideOverlay.CloseDesktop
|
|
1417
|
+
data-test-id="lightbox-side-overlay-close"
|
|
1418
|
+
clickByEsc={!openedSideOverlayMD && !openedSideOverlaySM}
|
|
1419
|
+
onClick={handleCloseSideOverlayLG}
|
|
1420
|
+
/>
|
|
1421
|
+
</LightBox.SideOverlay>
|
|
1422
|
+
);
|
|
1423
|
+
|
|
1424
|
+
const renderLightBoxSideOverlayMD = () => (
|
|
1425
|
+
<LightBox.SideOverlay
|
|
1426
|
+
key="sideOverlayMD"
|
|
1427
|
+
opened={openedSideOverlayMD}
|
|
1428
|
+
size={EComponentSize.MD}
|
|
1429
|
+
isTopLevelSideOverlayOpened={openedSideOverlaySM}
|
|
1430
|
+
>
|
|
1431
|
+
<Page>
|
|
1432
|
+
<Page.Header type={EHeaderPageType.FIRST} sticky>
|
|
1433
|
+
<Page.Header.Title>
|
|
1434
|
+
<Page.Header.Title.Content>
|
|
1435
|
+
<MobileView
|
|
1436
|
+
fallback={
|
|
1437
|
+
<Title
|
|
1438
|
+
tag="h1"
|
|
1439
|
+
size={ETitleSize.H1}
|
|
1440
|
+
tabIndex={-1}
|
|
1441
|
+
// Устанавливает фокус на первый элемент при открытии LightBox.
|
|
1442
|
+
{...{ [FocusTrapUtils.firstInteractionElementDataAttr]: true }}
|
|
1443
|
+
>
|
|
1444
|
+
Евгений Онегин
|
|
1445
|
+
</Title>
|
|
1446
|
+
}
|
|
1447
|
+
>
|
|
1448
|
+
<Title tag="h2" size={ETitleSize.H2}>
|
|
1449
|
+
Евгений Онегин
|
|
1450
|
+
</Title>
|
|
1451
|
+
</MobileView>
|
|
1452
|
+
</Page.Header.Title.Content>
|
|
1453
|
+
<Page.Header.Title.Controls>
|
|
1454
|
+
<LightBox.SideOverlay.CloseMobile
|
|
1455
|
+
data-test-id="lightbox-side-overlay-close"
|
|
1456
|
+
onClick={handleCloseSideOverlayMD}
|
|
1457
|
+
/>
|
|
1458
|
+
</Page.Header.Title.Controls>
|
|
1459
|
+
</Page.Header.Title>
|
|
1460
|
+
</Page.Header>
|
|
1461
|
+
|
|
1462
|
+
<Page.Body type={EBodyPageType.SECOND}>
|
|
1463
|
+
{[0, 1, 2].map((index) => (
|
|
1464
|
+
<React.Fragment key={index}>
|
|
1465
|
+
<PoemBlock />
|
|
1466
|
+
{index < 2 && <Gap size={16} />}
|
|
1467
|
+
</React.Fragment>
|
|
1468
|
+
))}
|
|
1469
|
+
</Page.Body>
|
|
1470
|
+
|
|
1471
|
+
<Page.Footer type={EFooterPageType.FIRST} sticky>
|
|
1472
|
+
<Page.Footer.Description>
|
|
1473
|
+
<Page.Footer.Description.Content>А. С. Пушкин</Page.Footer.Description.Content>
|
|
1474
|
+
<Page.Footer.Description.Controls>
|
|
1475
|
+
<Button
|
|
1476
|
+
theme={EButtonTheme.GENERAL}
|
|
1477
|
+
size={EComponentSize.MD}
|
|
1478
|
+
onClick={handleOpenSideOverlaySM}
|
|
1479
|
+
>
|
|
1480
|
+
Открыть SideOverlay SM
|
|
1481
|
+
</Button>
|
|
1482
|
+
</Page.Footer.Description.Controls>
|
|
1483
|
+
</Page.Footer.Description>
|
|
1484
|
+
</Page.Footer>
|
|
1485
|
+
</Page>
|
|
1486
|
+
|
|
1487
|
+
<LightBox.SideOverlay.CloseDesktop
|
|
1488
|
+
data-test-id="lightbox-side-overlay-close"
|
|
1489
|
+
clickByEsc={!openedSideOverlaySM}
|
|
1490
|
+
onClick={handleCloseSideOverlayMD}
|
|
1491
|
+
/>
|
|
1492
|
+
</LightBox.SideOverlay>
|
|
1493
|
+
);
|
|
1494
|
+
|
|
1495
|
+
const renderLightBoxSideOverlaySM = () => (
|
|
1496
|
+
<LightBox.SideOverlay key="sideOverlaySM" opened={openedSideOverlaySM} size={EComponentSize.SM}>
|
|
1497
|
+
<Page>
|
|
1498
|
+
<Page.Header type={EHeaderPageType.FIRST} sticky>
|
|
1499
|
+
<Page.Header.Title>
|
|
1500
|
+
<Page.Header.Title.Content>
|
|
1501
|
+
<MobileView
|
|
1502
|
+
fallback={
|
|
1503
|
+
<Title
|
|
1504
|
+
tag="h1"
|
|
1505
|
+
size={ETitleSize.H1}
|
|
1506
|
+
tabIndex={-1}
|
|
1507
|
+
// Устанавливает фокус на первый элемент при открытии LightBox.
|
|
1508
|
+
{...{ [FocusTrapUtils.firstInteractionElementDataAttr]: true }}
|
|
1509
|
+
>
|
|
1510
|
+
Евгений Онегин
|
|
1511
|
+
</Title>
|
|
1512
|
+
}
|
|
1513
|
+
>
|
|
1514
|
+
<Title tag="h2" size={ETitleSize.H2}>
|
|
1515
|
+
Евгений Онегин
|
|
1516
|
+
</Title>
|
|
1517
|
+
</MobileView>
|
|
1518
|
+
</Page.Header.Title.Content>
|
|
1519
|
+
<Page.Header.Title.Controls>
|
|
1520
|
+
<LightBox.SideOverlay.CloseMobile
|
|
1521
|
+
data-test-id="lightbox-side-overlay-close"
|
|
1522
|
+
onClick={handleCloseSideOverlaySM}
|
|
1523
|
+
/>
|
|
1524
|
+
</Page.Header.Title.Controls>
|
|
1525
|
+
</Page.Header.Title>
|
|
1526
|
+
</Page.Header>
|
|
1527
|
+
|
|
1528
|
+
<Page.Body type={EBodyPageType.FIRST}>
|
|
1529
|
+
Если контента мало, рекомендуется использовать Page.Body типа FIRST, чтобы не было пустого
|
|
1530
|
+
пространства.
|
|
1531
|
+
</Page.Body>
|
|
1532
|
+
|
|
1533
|
+
<Page.Footer type={EFooterPageType.FIRST} sticky={openedSideOverlaySM}>
|
|
1534
|
+
<Page.Footer.Description>
|
|
1535
|
+
<Page.Footer.Description.Content>А. С. Пушкин</Page.Footer.Description.Content>
|
|
1536
|
+
<Page.Footer.Description.Controls>
|
|
1537
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD}>
|
|
1538
|
+
Button text
|
|
1539
|
+
</Button>
|
|
1540
|
+
<Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD}>
|
|
1541
|
+
Button text
|
|
1542
|
+
</Button>
|
|
1543
|
+
</Page.Footer.Description.Controls>
|
|
1544
|
+
</Page.Footer.Description>
|
|
1545
|
+
</Page.Footer>
|
|
1546
|
+
</Page>
|
|
1547
|
+
|
|
1548
|
+
<LightBox.SideOverlay.CloseDesktop
|
|
1549
|
+
data-test-id="lightbox-side-overlay-close"
|
|
1550
|
+
clickByEsc
|
|
1551
|
+
onClick={handleCloseSideOverlaySM}
|
|
1552
|
+
/>
|
|
1553
|
+
</LightBox.SideOverlay>
|
|
1554
|
+
);
|
|
1555
|
+
|
|
1556
|
+
const renderLightBoxSideOverlays = () => (
|
|
1557
|
+
<>
|
|
1558
|
+
{renderLightBoxSideOverlayLG()}
|
|
1559
|
+
{renderLightBoxSideOverlayMD()}
|
|
1560
|
+
{renderLightBoxSideOverlaySM()}
|
|
1561
|
+
</>
|
|
1562
|
+
);
|
|
1563
|
+
|
|
1564
|
+
return (
|
|
1565
|
+
<div>
|
|
1566
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD} onClick={handleOpen}>
|
|
1567
|
+
Открыть LightBox
|
|
1568
|
+
</Button>
|
|
1569
|
+
|
|
1570
|
+
{isOpen ? (
|
|
1571
|
+
<LightBox isLoading={false} isSideOverlayOpened={openedSideOverlayLG} isTopOverlayOpened={false}>
|
|
1572
|
+
<LightBox.Content key="content" isLoading={false}>
|
|
1573
|
+
<Page>
|
|
1574
|
+
<Page.Header type={EHeaderPageType.FIRST} sticky>
|
|
1575
|
+
<Page.Header.Title>
|
|
1576
|
+
<Page.Header.Title.Content>
|
|
1577
|
+
<MobileView
|
|
1578
|
+
fallback={
|
|
1579
|
+
<Title
|
|
1580
|
+
tag="h1"
|
|
1581
|
+
size={ETitleSize.H1}
|
|
1582
|
+
tabIndex={-1}
|
|
1583
|
+
// Устанавливает фокус на первый элемент при открытии LightBox.
|
|
1584
|
+
{...{ [FocusTrapUtils.firstInteractionElementDataAttr]: true }}
|
|
1585
|
+
>
|
|
1586
|
+
Евгений Онегин
|
|
1587
|
+
</Title>
|
|
1588
|
+
}
|
|
1589
|
+
>
|
|
1590
|
+
<Title tag="h2" size={ETitleSize.H2}>
|
|
1591
|
+
Евгений Онегин
|
|
1592
|
+
</Title>
|
|
1593
|
+
</MobileView>
|
|
1594
|
+
|
|
1595
|
+
<Gap size={8} />
|
|
1596
|
+
<Text tag="div" size={ETextSize.B3} type={EFontType.SECONDARY}>
|
|
1597
|
+
Русский поэт, драматург и прозаик, заложивший основы русского
|
|
1598
|
+
реалистического направления.
|
|
1599
|
+
</Text>
|
|
1600
|
+
</Page.Header.Title.Content>
|
|
1601
|
+
<Page.Header.Title.Controls>
|
|
1602
|
+
<Button
|
|
1603
|
+
theme={EButtonTheme.GENERAL}
|
|
1604
|
+
size={EComponentSize.MD}
|
|
1605
|
+
onClick={handleOpenSideOverlayLG}
|
|
1606
|
+
>
|
|
1607
|
+
SideOverlay LG
|
|
1608
|
+
</Button>
|
|
1609
|
+
</Page.Header.Title.Controls>
|
|
1610
|
+
</Page.Header.Title>
|
|
1611
|
+
</Page.Header>
|
|
1612
|
+
|
|
1613
|
+
<Page.Body type={EBodyPageType.SECOND}>
|
|
1614
|
+
{[0, 1, 2].map((index) => (
|
|
1615
|
+
<React.Fragment key={index}>
|
|
1616
|
+
<PoemBlock />
|
|
1617
|
+
{index < 2 && <Gap size={16} />}
|
|
1618
|
+
</React.Fragment>
|
|
1619
|
+
))}
|
|
1620
|
+
</Page.Body>
|
|
1621
|
+
|
|
1622
|
+
<Page.Footer type={EFooterPageType.FIRST} sticky>
|
|
1623
|
+
<Page.Footer.Description>
|
|
1624
|
+
<Page.Footer.Description.Content>А. С. Пушкин</Page.Footer.Description.Content>
|
|
1625
|
+
<Page.Footer.Description.Controls>
|
|
1626
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD}>
|
|
1627
|
+
Button text
|
|
1628
|
+
</Button>
|
|
1629
|
+
<Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD}>
|
|
1630
|
+
Button text
|
|
1631
|
+
</Button>
|
|
1632
|
+
</Page.Footer.Description.Controls>
|
|
1633
|
+
</Page.Footer.Description>
|
|
1634
|
+
</Page.Footer>
|
|
1635
|
+
</Page>
|
|
1636
|
+
</LightBox.Content>
|
|
1637
|
+
|
|
1638
|
+
{renderLightBoxControls()}
|
|
1639
|
+
|
|
1640
|
+
{renderLightBoxSideOverlays()}
|
|
1641
|
+
</LightBox>
|
|
1642
|
+
) : null}
|
|
1643
|
+
</div>
|
|
1644
|
+
);
|
|
1645
|
+
};
|
|
289
1646
|
```
|
|
290
1647
|
|
|
291
|
-
|
|
1648
|
+
### WithSideOverlayLoading
|
|
292
1649
|
|
|
293
|
-
|
|
1650
|
+
`stories/LightBox/examples/WithSideOverlayLoadingExample.tsx`
|
|
294
1651
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
1652
|
+
```tsx
|
|
1653
|
+
import React, { useState } from "react";
|
|
1654
|
+
import {
|
|
1655
|
+
LightBox,
|
|
1656
|
+
Page,
|
|
1657
|
+
Button,
|
|
1658
|
+
EButtonTheme,
|
|
1659
|
+
Gap,
|
|
1660
|
+
Title,
|
|
1661
|
+
Text,
|
|
1662
|
+
EFontType,
|
|
1663
|
+
ETextSize,
|
|
1664
|
+
ETitleSize,
|
|
1665
|
+
EHeaderPageType,
|
|
1666
|
+
EFooterPageType,
|
|
1667
|
+
EBodyPageType,
|
|
1668
|
+
Island,
|
|
1669
|
+
IslandBody,
|
|
1670
|
+
EIslandType,
|
|
1671
|
+
FocusTrapUtils,
|
|
1672
|
+
EComponentSize,
|
|
1673
|
+
} from "@sberbusiness/triplex-next";
|
|
1674
|
+
|
|
1675
|
+
const POEM_LINES: string[] = [
|
|
1676
|
+
"Мой дядя самых честных правил,",
|
|
1677
|
+
"Когда не в шутку занемог,",
|
|
1678
|
+
"Он уважать себя заставил",
|
|
1679
|
+
"И лучше выдумать не мог.",
|
|
1680
|
+
"Его пример другим наука;",
|
|
1681
|
+
"Но, боже мой, какая скука",
|
|
1682
|
+
"С больным сидеть и день и ночь,",
|
|
1683
|
+
"Не отходя ни шагу прочь!",
|
|
1684
|
+
"Какое низкое коварство",
|
|
1685
|
+
"Полуживого забавлять,",
|
|
1686
|
+
"Ему подушки поправлять,",
|
|
1687
|
+
"Печально подносить лекарство,",
|
|
1688
|
+
"Вздыхать и думать про себя:",
|
|
1689
|
+
"Когда же черт возьмёт тебя…",
|
|
1690
|
+
];
|
|
1691
|
+
|
|
1692
|
+
const PoemBlock: React.FC = () => (
|
|
1693
|
+
<Island type={EIslandType.TYPE_1} size={EComponentSize.MD}>
|
|
1694
|
+
<IslandBody>
|
|
1695
|
+
{POEM_LINES.map((line) => (
|
|
1696
|
+
<React.Fragment key={line}>
|
|
1697
|
+
{line}
|
|
1698
|
+
<br />
|
|
1699
|
+
</React.Fragment>
|
|
1700
|
+
))}
|
|
1701
|
+
</IslandBody>
|
|
1702
|
+
</Island>
|
|
1703
|
+
);
|
|
1704
|
+
|
|
1705
|
+
export const WithSideOverlayLoadingExample = () => {
|
|
1706
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
1707
|
+
const [openedSideOverlayLG, setOpenedSideOverlayLG] = useState(false);
|
|
1708
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
1709
|
+
|
|
1710
|
+
const handleOpenSideOverlayLG = () => setOpenedSideOverlayLG(true);
|
|
1711
|
+
const handleCloseSideOverlayLG = () => {
|
|
1712
|
+
setIsLoading(false);
|
|
1713
|
+
setOpenedSideOverlayLG(false);
|
|
1714
|
+
};
|
|
1715
|
+
|
|
1716
|
+
const handleOpen = () => setIsOpen(true);
|
|
1717
|
+
const handleClose = () => setIsOpen(false);
|
|
1718
|
+
|
|
1719
|
+
const renderLightBoxControls = () => (
|
|
1720
|
+
<LightBox.Controls key="controls">
|
|
1721
|
+
<LightBox.Controls.Close title="Закрыть" data-test-id="lightbox-close" onClick={handleClose} />
|
|
1722
|
+
</LightBox.Controls>
|
|
1723
|
+
);
|
|
1724
|
+
|
|
1725
|
+
const renderLightBoxSideOverlayLG = () => (
|
|
1726
|
+
<LightBox.SideOverlay
|
|
1727
|
+
key="sideOverlayLG"
|
|
1728
|
+
opened={openedSideOverlayLG}
|
|
1729
|
+
size={EComponentSize.LG}
|
|
1730
|
+
isTopLevelSideOverlayOpened={false}
|
|
1731
|
+
isLoading={isLoading}
|
|
1732
|
+
>
|
|
1733
|
+
<Page>
|
|
1734
|
+
<Page.Header type={EHeaderPageType.FIRST} sticky={openedSideOverlayLG}>
|
|
1735
|
+
<Page.Header.Title>
|
|
1736
|
+
<Page.Header.Title.Content>
|
|
1737
|
+
<Title
|
|
1738
|
+
tag="h1"
|
|
1739
|
+
size={ETitleSize.H1}
|
|
1740
|
+
tabIndex={-1}
|
|
1741
|
+
// Устанавливает фокус на первый элемент при открытии LightBox.
|
|
1742
|
+
{...{ [FocusTrapUtils.firstInteractionElementDataAttr]: true }}
|
|
1743
|
+
>
|
|
1744
|
+
Евгений Онегин
|
|
1745
|
+
</Title>
|
|
1746
|
+
</Page.Header.Title.Content>
|
|
1747
|
+
<Page.Header.Title.Controls>
|
|
1748
|
+
{/* Кнопка закрытия SideOverlay для мобильного устройства. Отображается только на мобильном устройстве, внутри заголовка SideOverlay. */}
|
|
1749
|
+
<LightBox.SideOverlay.CloseMobile
|
|
1750
|
+
data-test-id="lightbox-side-overlay-close"
|
|
1751
|
+
onClick={handleCloseSideOverlayLG}
|
|
1752
|
+
/>
|
|
1753
|
+
</Page.Header.Title.Controls>
|
|
1754
|
+
</Page.Header.Title>
|
|
1755
|
+
</Page.Header>
|
|
1756
|
+
|
|
1757
|
+
<Page.Body type={EBodyPageType.SECOND}>
|
|
1758
|
+
{[0, 1, 2].map((index) => (
|
|
1759
|
+
<React.Fragment key={index}>
|
|
1760
|
+
<PoemBlock />
|
|
1761
|
+
{index < 2 && <Gap size={16} />}
|
|
1762
|
+
</React.Fragment>
|
|
1763
|
+
))}
|
|
1764
|
+
</Page.Body>
|
|
1765
|
+
|
|
1766
|
+
<Page.Footer type={EFooterPageType.FIRST} sticky={openedSideOverlayLG}>
|
|
1767
|
+
<Page.Footer.Description>
|
|
1768
|
+
<Page.Footer.Description.Content>А. С. Пушкин</Page.Footer.Description.Content>
|
|
1769
|
+
<Page.Footer.Description.Controls>
|
|
1770
|
+
<Button
|
|
1771
|
+
theme={EButtonTheme.GENERAL}
|
|
1772
|
+
size={EComponentSize.MD}
|
|
1773
|
+
onClick={() => setIsLoading(true)}
|
|
1774
|
+
>
|
|
1775
|
+
Loading on
|
|
1776
|
+
</Button>
|
|
1777
|
+
</Page.Footer.Description.Controls>
|
|
1778
|
+
</Page.Footer.Description>
|
|
1779
|
+
</Page.Footer>
|
|
1780
|
+
</Page>
|
|
1781
|
+
|
|
1782
|
+
{/* Кнопка закрытия SideOverlay для десктопа. Отображается только на десктопе, справа от заголовка SideOverlay. */}
|
|
1783
|
+
<LightBox.SideOverlay.CloseDesktop
|
|
1784
|
+
data-test-id="lightbox-side-overlay-close"
|
|
1785
|
+
clickByEsc
|
|
1786
|
+
onClick={handleCloseSideOverlayLG}
|
|
1787
|
+
/>
|
|
1788
|
+
</LightBox.SideOverlay>
|
|
1789
|
+
);
|
|
1790
|
+
|
|
1791
|
+
return (
|
|
1792
|
+
<div>
|
|
1793
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD} onClick={handleOpen}>
|
|
1794
|
+
Открыть LightBox
|
|
1795
|
+
</Button>
|
|
1796
|
+
|
|
1797
|
+
{isOpen ? (
|
|
1798
|
+
<LightBox isLoading={false} isSideOverlayOpened={openedSideOverlayLG} isTopOverlayOpened={false}>
|
|
1799
|
+
<LightBox.Content key="content" isLoading={false}>
|
|
1800
|
+
<Page>
|
|
1801
|
+
<Page.Header type={EHeaderPageType.FIRST} sticky>
|
|
1802
|
+
<Page.Header.Title>
|
|
1803
|
+
<Page.Header.Title.Content>
|
|
1804
|
+
<Title
|
|
1805
|
+
tag="h1"
|
|
1806
|
+
size={ETitleSize.H1}
|
|
1807
|
+
tabIndex={-1}
|
|
1808
|
+
// Устанавливает фокус на первый элемент при открытии LightBox.
|
|
1809
|
+
{...{ [FocusTrapUtils.firstInteractionElementDataAttr]: true }}
|
|
1810
|
+
>
|
|
1811
|
+
Евгений Онегин
|
|
1812
|
+
</Title>
|
|
1813
|
+
<Gap size={8} />
|
|
1814
|
+
<Text tag="div" size={ETextSize.B3} type={EFontType.SECONDARY}>
|
|
1815
|
+
Русский поэт, драматург и прозаик, заложивший основы русского
|
|
1816
|
+
реалистического направления.
|
|
1817
|
+
</Text>
|
|
1818
|
+
</Page.Header.Title.Content>
|
|
1819
|
+
<Page.Header.Title.Controls>
|
|
1820
|
+
<Button
|
|
1821
|
+
theme={EButtonTheme.GENERAL}
|
|
1822
|
+
size={EComponentSize.MD}
|
|
1823
|
+
onClick={handleOpenSideOverlayLG}
|
|
1824
|
+
>
|
|
1825
|
+
SideOverlay LG
|
|
1826
|
+
</Button>
|
|
1827
|
+
</Page.Header.Title.Controls>
|
|
1828
|
+
</Page.Header.Title>
|
|
1829
|
+
</Page.Header>
|
|
1830
|
+
|
|
1831
|
+
<Page.Body type={EBodyPageType.SECOND}>
|
|
1832
|
+
{[0, 1, 2].map((index) => (
|
|
1833
|
+
<React.Fragment key={index}>
|
|
1834
|
+
<PoemBlock />
|
|
1835
|
+
{index < 2 && <Gap size={16} />}
|
|
1836
|
+
</React.Fragment>
|
|
1837
|
+
))}
|
|
1838
|
+
</Page.Body>
|
|
1839
|
+
|
|
1840
|
+
<Page.Footer type={EFooterPageType.FIRST} sticky>
|
|
1841
|
+
<Page.Footer.Description>
|
|
1842
|
+
<Page.Footer.Description.Content>А. С. Пушкин</Page.Footer.Description.Content>
|
|
1843
|
+
<Page.Footer.Description.Controls>
|
|
1844
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD}>
|
|
1845
|
+
Button text
|
|
1846
|
+
</Button>
|
|
1847
|
+
<Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD}>
|
|
1848
|
+
Button text
|
|
1849
|
+
</Button>
|
|
1850
|
+
</Page.Footer.Description.Controls>
|
|
1851
|
+
</Page.Footer.Description>
|
|
1852
|
+
</Page.Footer>
|
|
1853
|
+
</Page>
|
|
1854
|
+
</LightBox.Content>
|
|
1855
|
+
|
|
1856
|
+
{renderLightBoxControls()}
|
|
1857
|
+
|
|
1858
|
+
{renderLightBoxSideOverlayLG()}
|
|
1859
|
+
</LightBox>
|
|
1860
|
+
) : null}
|
|
1861
|
+
</div>
|
|
1862
|
+
);
|
|
1863
|
+
};
|
|
1864
|
+
```
|
|
298
1865
|
|
|
299
|
-
|
|
300
|
-
- `Overlay` / `OverlayBase` / `OverlayMask` — основа для SideOverlay.
|
|
301
|
-
- `TopOverlay` — верхняя выезжающая панель (реэкспортируется как `LightBox.TopOverlay`).
|
|
302
|
-
- `Portal` — используется для render'а.
|
|
303
|
-
- `MobileView` — разграничение desktop/mobile.
|
|
304
|
-
- `LoaderScreen` — loader внутри Content.
|
|
305
|
-
- `focus-trap-react` — FocusTrap.
|
|
306
|
-
- `react-resize-detector` — resize-listener в Content и ViewManager.
|
|
1866
|
+
### WithTopOverlay
|
|
307
1867
|
|
|
308
|
-
|
|
1868
|
+
`stories/LightBox/examples/WithTopOverlayExample.tsx`
|
|
309
1869
|
|
|
310
|
-
|
|
1870
|
+
```tsx
|
|
1871
|
+
import React, { useState } from "react";
|
|
1872
|
+
import {
|
|
1873
|
+
LightBox,
|
|
1874
|
+
Page,
|
|
1875
|
+
Button,
|
|
1876
|
+
EButtonTheme,
|
|
1877
|
+
Gap,
|
|
1878
|
+
Title,
|
|
1879
|
+
Text,
|
|
1880
|
+
EFontType,
|
|
1881
|
+
ETextSize,
|
|
1882
|
+
ETitleSize,
|
|
1883
|
+
EHeaderPageType,
|
|
1884
|
+
EFooterPageType,
|
|
1885
|
+
EBodyPageType,
|
|
1886
|
+
Island,
|
|
1887
|
+
IslandBody,
|
|
1888
|
+
EIslandType,
|
|
1889
|
+
FocusTrapUtils,
|
|
1890
|
+
EComponentSize,
|
|
1891
|
+
Confirm,
|
|
1892
|
+
} from "@sberbusiness/triplex-next";
|
|
1893
|
+
|
|
1894
|
+
const POEM_LINES: string[] = [
|
|
1895
|
+
"Мой дядя самых честных правил,",
|
|
1896
|
+
"Когда не в шутку занемог,",
|
|
1897
|
+
"Он уважать себя заставил",
|
|
1898
|
+
"И лучше выдумать не мог.",
|
|
1899
|
+
"Его пример другим наука;",
|
|
1900
|
+
"Но, боже мой, какая скука",
|
|
1901
|
+
"С больным сидеть и день и ночь,",
|
|
1902
|
+
"Не отходя ни шагу прочь!",
|
|
1903
|
+
"Какое низкое коварство",
|
|
1904
|
+
"Полуживого забавлять,",
|
|
1905
|
+
"Ему подушки поправлять,",
|
|
1906
|
+
"Печально подносить лекарство,",
|
|
1907
|
+
"Вздыхать и думать про себя:",
|
|
1908
|
+
"Когда же черт возьмёт тебя…",
|
|
1909
|
+
];
|
|
1910
|
+
|
|
1911
|
+
const PoemBlock: React.FC = () => (
|
|
1912
|
+
<Island type={EIslandType.TYPE_1} size={EComponentSize.MD}>
|
|
1913
|
+
<IslandBody>
|
|
1914
|
+
{POEM_LINES.map((line) => (
|
|
1915
|
+
<React.Fragment key={line}>
|
|
1916
|
+
{line}
|
|
1917
|
+
<br />
|
|
1918
|
+
</React.Fragment>
|
|
1919
|
+
))}
|
|
1920
|
+
</IslandBody>
|
|
1921
|
+
</Island>
|
|
1922
|
+
);
|
|
1923
|
+
|
|
1924
|
+
export const WithTopOverlayExample = () => {
|
|
1925
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
1926
|
+
const [openedTopOverlay, setOpenedTopOverlay] = useState(false);
|
|
1927
|
+
const [closeConfirmed, setCloseConfirmed] = useState(false);
|
|
1928
|
+
|
|
1929
|
+
const handleOpenTopOverlay = () => setOpenedTopOverlay(true);
|
|
1930
|
+
const handleCloseTopOverlay = () => {
|
|
1931
|
+
if (closeConfirmed) {
|
|
1932
|
+
setIsOpen(false);
|
|
1933
|
+
setCloseConfirmed(false);
|
|
1934
|
+
} else {
|
|
1935
|
+
setOpenedTopOverlay(false);
|
|
1936
|
+
}
|
|
1937
|
+
};
|
|
1938
|
+
|
|
1939
|
+
const handleOpen = () => setIsOpen(true);
|
|
1940
|
+
const handleClose = () => setOpenedTopOverlay(true);
|
|
1941
|
+
|
|
1942
|
+
const renderLightBoxControls = () => (
|
|
1943
|
+
<LightBox.Controls key="controls">
|
|
1944
|
+
<LightBox.Controls.Close title="Закрыть" data-test-id="lightbox-close" onClick={handleClose} />
|
|
1945
|
+
<LightBox.Controls.Prev title="Назад" clickByArrowLeft onClick={() => console.log("Prev clicked")} />
|
|
1946
|
+
<LightBox.Controls.Next title="Вперёд" clickByArrowRight onClick={() => console.log("Next clicked")} />
|
|
1947
|
+
</LightBox.Controls>
|
|
1948
|
+
);
|
|
1949
|
+
|
|
1950
|
+
const renderTopOverlay = () => (
|
|
1951
|
+
<LightBox.TopOverlay opened={openedTopOverlay} onClose={handleCloseTopOverlay} onOpen={handleOpenTopOverlay}>
|
|
1952
|
+
<Confirm>
|
|
1953
|
+
<Confirm.Content>
|
|
1954
|
+
<Confirm.Content.Title>Внимание</Confirm.Content.Title>
|
|
1955
|
+
<Confirm.Content.SubTitle>
|
|
1956
|
+
Несохранённые данные будут утеряны. Вы уверены, что хотите покинуть форму редактирования?
|
|
1957
|
+
</Confirm.Content.SubTitle>
|
|
1958
|
+
</Confirm.Content>
|
|
1959
|
+
<Confirm.Controls>
|
|
1960
|
+
<Button
|
|
1961
|
+
theme={EButtonTheme.SECONDARY}
|
|
1962
|
+
size={EComponentSize.MD}
|
|
1963
|
+
onClick={() => setOpenedTopOverlay(false)}
|
|
1964
|
+
>
|
|
1965
|
+
Отмена
|
|
1966
|
+
</Button>
|
|
1967
|
+
<Button
|
|
1968
|
+
theme={EButtonTheme.DANGER}
|
|
1969
|
+
size={EComponentSize.MD}
|
|
1970
|
+
onClick={() => {
|
|
1971
|
+
handleCloseTopOverlay();
|
|
1972
|
+
setCloseConfirmed(true);
|
|
1973
|
+
}}
|
|
1974
|
+
>
|
|
1975
|
+
Покинуть форму
|
|
1976
|
+
</Button>
|
|
1977
|
+
</Confirm.Controls>
|
|
1978
|
+
<Confirm.Close
|
|
1979
|
+
title="Закрыть"
|
|
1980
|
+
// Закрыть по Esc, если TopOverlay открыт.
|
|
1981
|
+
clickByEsc={openedTopOverlay}
|
|
1982
|
+
onClick={() => setOpenedTopOverlay(false)}
|
|
1983
|
+
/>
|
|
1984
|
+
</Confirm>
|
|
1985
|
+
</LightBox.TopOverlay>
|
|
1986
|
+
);
|
|
1987
|
+
|
|
1988
|
+
return (
|
|
1989
|
+
<div>
|
|
1990
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD} onClick={handleOpen}>
|
|
1991
|
+
Открыть LightBox
|
|
1992
|
+
</Button>
|
|
1993
|
+
|
|
1994
|
+
{isOpen ? (
|
|
1995
|
+
<LightBox isLoading={false} isSideOverlayOpened={false} isTopOverlayOpened={openedTopOverlay}>
|
|
1996
|
+
<LightBox.Content key="content" isLoading={false}>
|
|
1997
|
+
<Page>
|
|
1998
|
+
<Page.Header type={EHeaderPageType.FIRST} sticky>
|
|
1999
|
+
<Page.Header.Title>
|
|
2000
|
+
<Page.Header.Title.Content>
|
|
2001
|
+
<Title
|
|
2002
|
+
tag="h1"
|
|
2003
|
+
size={ETitleSize.H1}
|
|
2004
|
+
tabIndex={-1}
|
|
2005
|
+
// Устанавливает фокус на первый элемент при открытии LightBox.
|
|
2006
|
+
{...{ [FocusTrapUtils.firstInteractionElementDataAttr]: true }}
|
|
2007
|
+
>
|
|
2008
|
+
Евгений Онегин
|
|
2009
|
+
</Title>
|
|
2010
|
+
<Gap size={8} />
|
|
2011
|
+
<Text tag="div" size={ETextSize.B3} type={EFontType.SECONDARY}>
|
|
2012
|
+
Русский поэт, драматург и прозаик, заложивший основы русского
|
|
2013
|
+
реалистического направления.
|
|
2014
|
+
</Text>
|
|
2015
|
+
</Page.Header.Title.Content>
|
|
2016
|
+
<Page.Header.Title.Controls>
|
|
2017
|
+
<Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD}>
|
|
2018
|
+
Button text
|
|
2019
|
+
</Button>
|
|
2020
|
+
</Page.Header.Title.Controls>
|
|
2021
|
+
</Page.Header.Title>
|
|
2022
|
+
</Page.Header>
|
|
2023
|
+
|
|
2024
|
+
<Page.Body type={EBodyPageType.SECOND}>
|
|
2025
|
+
{[0, 1, 2].map((index) => (
|
|
2026
|
+
<React.Fragment key={index}>
|
|
2027
|
+
<PoemBlock />
|
|
2028
|
+
{index < 2 && <Gap size={16} />}
|
|
2029
|
+
</React.Fragment>
|
|
2030
|
+
))}
|
|
2031
|
+
</Page.Body>
|
|
2032
|
+
|
|
2033
|
+
<Page.Footer type={EFooterPageType.FIRST} sticky>
|
|
2034
|
+
<Page.Footer.Description>
|
|
2035
|
+
<Page.Footer.Description.Content>А. С. Пушкин</Page.Footer.Description.Content>
|
|
2036
|
+
<Page.Footer.Description.Controls>
|
|
2037
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD}>
|
|
2038
|
+
Button text
|
|
2039
|
+
</Button>
|
|
2040
|
+
<Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD}>
|
|
2041
|
+
Button text
|
|
2042
|
+
</Button>
|
|
2043
|
+
</Page.Footer.Description.Controls>
|
|
2044
|
+
</Page.Footer.Description>
|
|
2045
|
+
</Page.Footer>
|
|
2046
|
+
</Page>
|
|
2047
|
+
|
|
2048
|
+
{renderTopOverlay()}
|
|
2049
|
+
</LightBox.Content>
|
|
2050
|
+
|
|
2051
|
+
{renderLightBoxControls()}
|
|
2052
|
+
</LightBox>
|
|
2053
|
+
) : null}
|
|
2054
|
+
</div>
|
|
2055
|
+
);
|
|
2056
|
+
};
|
|
2057
|
+
```
|
|
311
2058
|
|
|
312
|
-
|
|
313
|
-
`stories/LightBox/LightBoxInitialOpened.stories.tsx`
|
|
314
|
-
Примеры: `stories/LightBox/examples/`
|
|
2059
|
+
### WithTopOverlayInSideOverlay
|
|
315
2060
|
|
|
316
|
-
|
|
2061
|
+
`stories/LightBox/examples/WithTopOverlayInSideOverlayExample.tsx`
|
|
317
2062
|
|
|
318
|
-
|
|
2063
|
+
```tsx
|
|
2064
|
+
import React, { useState } from "react";
|
|
2065
|
+
import {
|
|
2066
|
+
LightBox,
|
|
2067
|
+
Page,
|
|
2068
|
+
Button,
|
|
2069
|
+
EButtonTheme,
|
|
2070
|
+
Gap,
|
|
2071
|
+
Title,
|
|
2072
|
+
Text,
|
|
2073
|
+
EFontType,
|
|
2074
|
+
ETextSize,
|
|
2075
|
+
ETitleSize,
|
|
2076
|
+
EHeaderPageType,
|
|
2077
|
+
EFooterPageType,
|
|
2078
|
+
EBodyPageType,
|
|
2079
|
+
Island,
|
|
2080
|
+
IslandBody,
|
|
2081
|
+
EIslandType,
|
|
2082
|
+
FocusTrapUtils,
|
|
2083
|
+
EComponentSize,
|
|
2084
|
+
MobileView,
|
|
2085
|
+
Confirm,
|
|
2086
|
+
EConfirmParentComponent,
|
|
2087
|
+
} from "@sberbusiness/triplex-next";
|
|
2088
|
+
|
|
2089
|
+
const POEM_LINES: string[] = [
|
|
2090
|
+
"Мой дядя самых честных правил,",
|
|
2091
|
+
"Когда не в шутку занемог,",
|
|
2092
|
+
"Он уважать себя заставил",
|
|
2093
|
+
"И лучше выдумать не мог.",
|
|
2094
|
+
"Его пример другим наука;",
|
|
2095
|
+
"Но, боже мой, какая скука",
|
|
2096
|
+
"С больным сидеть и день и ночь,",
|
|
2097
|
+
"Не отходя ни шагу прочь!",
|
|
2098
|
+
"Какое низкое коварство",
|
|
2099
|
+
"Полуживого забавлять,",
|
|
2100
|
+
"Ему подушки поправлять,",
|
|
2101
|
+
"Печально подносить лекарство,",
|
|
2102
|
+
"Вздыхать и думать про себя:",
|
|
2103
|
+
"Когда же черт возьмёт тебя…",
|
|
2104
|
+
];
|
|
2105
|
+
|
|
2106
|
+
const PoemBlock: React.FC = () => (
|
|
2107
|
+
<Island type={EIslandType.TYPE_1} size={EComponentSize.MD}>
|
|
2108
|
+
<IslandBody>
|
|
2109
|
+
{POEM_LINES.map((line) => (
|
|
2110
|
+
<React.Fragment key={line}>
|
|
2111
|
+
{line}
|
|
2112
|
+
<br />
|
|
2113
|
+
</React.Fragment>
|
|
2114
|
+
))}
|
|
2115
|
+
</IslandBody>
|
|
2116
|
+
</Island>
|
|
2117
|
+
);
|
|
2118
|
+
|
|
2119
|
+
export const WithTopOverlayInSideOverlayExample = () => {
|
|
2120
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
2121
|
+
const [openedSideOverlayMD, setOpenedSideOverlayMD] = useState(false);
|
|
2122
|
+
|
|
2123
|
+
const [openedTopOverlay, setOpenedTopOverlay] = useState(false);
|
|
2124
|
+
const [closeConfirmed, setCloseConfirmed] = useState(false);
|
|
2125
|
+
|
|
2126
|
+
const handleOpenTopOverlay = () => setOpenedTopOverlay(true);
|
|
2127
|
+
const handleCloseTopOverlay = () => {
|
|
2128
|
+
if (closeConfirmed) {
|
|
2129
|
+
setOpenedSideOverlayMD(false);
|
|
2130
|
+
setCloseConfirmed(false);
|
|
2131
|
+
} else {
|
|
2132
|
+
setOpenedTopOverlay(false);
|
|
2133
|
+
}
|
|
2134
|
+
};
|
|
2135
|
+
|
|
2136
|
+
const handleOpenSideOverlayMD = () => setOpenedSideOverlayMD(true);
|
|
2137
|
+
const handleCloseSideOverlayMD = () => setOpenedTopOverlay(true);
|
|
2138
|
+
|
|
2139
|
+
const handleOpen = () => setIsOpen(true);
|
|
2140
|
+
const handleClose = () => setIsOpen(false);
|
|
2141
|
+
|
|
2142
|
+
const renderLightBoxControls = () => (
|
|
2143
|
+
<LightBox.Controls key="controls">
|
|
2144
|
+
<LightBox.Controls.Close title="Закрыть" data-test-id="lightbox-close" onClick={handleClose} />
|
|
2145
|
+
<LightBox.Controls.Prev title="Назад" clickByArrowLeft onClick={() => console.log("Prev clicked")} />
|
|
2146
|
+
<LightBox.Controls.Next title="Вперёд" clickByArrowRight onClick={() => console.log("Next clicked")} />
|
|
2147
|
+
</LightBox.Controls>
|
|
2148
|
+
);
|
|
2149
|
+
|
|
2150
|
+
const renderTopOverlay = () => (
|
|
2151
|
+
<LightBox.TopOverlay opened={openedTopOverlay} onClose={handleCloseTopOverlay} onOpen={handleOpenTopOverlay}>
|
|
2152
|
+
<Confirm parentComponent={EConfirmParentComponent.SIDE_OVERLAY_MD}>
|
|
2153
|
+
<Confirm.Content>
|
|
2154
|
+
<Confirm.Content.Title>Внимание</Confirm.Content.Title>
|
|
2155
|
+
<Confirm.Content.SubTitle>
|
|
2156
|
+
Несохранённые данные будут утеряны. Вы уверены, что хотите покинуть форму редактирования?
|
|
2157
|
+
</Confirm.Content.SubTitle>
|
|
2158
|
+
</Confirm.Content>
|
|
2159
|
+
<Confirm.Controls>
|
|
2160
|
+
<Button
|
|
2161
|
+
theme={EButtonTheme.SECONDARY}
|
|
2162
|
+
size={EComponentSize.MD}
|
|
2163
|
+
onClick={() => setOpenedTopOverlay(false)}
|
|
2164
|
+
>
|
|
2165
|
+
Отмена
|
|
2166
|
+
</Button>
|
|
2167
|
+
<Button
|
|
2168
|
+
theme={EButtonTheme.DANGER}
|
|
2169
|
+
size={EComponentSize.MD}
|
|
2170
|
+
onClick={() => {
|
|
2171
|
+
handleCloseTopOverlay();
|
|
2172
|
+
setCloseConfirmed(true);
|
|
2173
|
+
}}
|
|
2174
|
+
>
|
|
2175
|
+
Покинуть форму
|
|
2176
|
+
</Button>
|
|
2177
|
+
</Confirm.Controls>
|
|
2178
|
+
<Confirm.Close
|
|
2179
|
+
title="Закрыть"
|
|
2180
|
+
// Закрыть по Esc, если TopOverlay открыт.
|
|
2181
|
+
clickByEsc={openedTopOverlay}
|
|
2182
|
+
onClick={() => setOpenedTopOverlay(false)}
|
|
2183
|
+
/>
|
|
2184
|
+
</Confirm>
|
|
2185
|
+
</LightBox.TopOverlay>
|
|
2186
|
+
);
|
|
2187
|
+
|
|
2188
|
+
const renderLightBoxSideOverlayMD = () => (
|
|
2189
|
+
<LightBox.SideOverlay
|
|
2190
|
+
key="sideOverlayMD"
|
|
2191
|
+
opened={openedSideOverlayMD}
|
|
2192
|
+
size={EComponentSize.MD}
|
|
2193
|
+
isTopLevelSideOverlayOpened={false}
|
|
2194
|
+
isTopOverlayOpened={openedTopOverlay}
|
|
2195
|
+
>
|
|
2196
|
+
<Page>
|
|
2197
|
+
<Page.Header type={EHeaderPageType.FIRST} sticky>
|
|
2198
|
+
<Page.Header.Title>
|
|
2199
|
+
<Page.Header.Title.Content>
|
|
2200
|
+
<MobileView
|
|
2201
|
+
fallback={
|
|
2202
|
+
<Title
|
|
2203
|
+
tag="h1"
|
|
2204
|
+
size={ETitleSize.H1}
|
|
2205
|
+
tabIndex={-1}
|
|
2206
|
+
// Устанавливает фокус на первый элемент при открытии LightBox.
|
|
2207
|
+
{...{ [FocusTrapUtils.firstInteractionElementDataAttr]: true }}
|
|
2208
|
+
>
|
|
2209
|
+
Евгений Онегин
|
|
2210
|
+
</Title>
|
|
2211
|
+
}
|
|
2212
|
+
>
|
|
2213
|
+
<Title tag="h2" size={ETitleSize.H2}>
|
|
2214
|
+
Евгений Онегин
|
|
2215
|
+
</Title>
|
|
2216
|
+
</MobileView>
|
|
2217
|
+
</Page.Header.Title.Content>
|
|
2218
|
+
<Page.Header.Title.Controls>
|
|
2219
|
+
<LightBox.SideOverlay.CloseMobile
|
|
2220
|
+
data-test-id="lightbox-side-overlay-close"
|
|
2221
|
+
onClick={handleCloseSideOverlayMD}
|
|
2222
|
+
/>
|
|
2223
|
+
</Page.Header.Title.Controls>
|
|
2224
|
+
</Page.Header.Title>
|
|
2225
|
+
</Page.Header>
|
|
2226
|
+
|
|
2227
|
+
<Page.Body type={EBodyPageType.SECOND}>
|
|
2228
|
+
{[0, 1, 2].map((index) => (
|
|
2229
|
+
<React.Fragment key={index}>
|
|
2230
|
+
<PoemBlock />
|
|
2231
|
+
{index < 2 && <Gap size={16} />}
|
|
2232
|
+
</React.Fragment>
|
|
2233
|
+
))}
|
|
2234
|
+
</Page.Body>
|
|
2235
|
+
|
|
2236
|
+
<Page.Footer type={EFooterPageType.FIRST} sticky>
|
|
2237
|
+
<Page.Footer.Description>
|
|
2238
|
+
<Page.Footer.Description.Content>А. С. Пушкин</Page.Footer.Description.Content>
|
|
2239
|
+
<Page.Footer.Description.Controls>
|
|
2240
|
+
<Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD}>
|
|
2241
|
+
Button text
|
|
2242
|
+
</Button>
|
|
2243
|
+
</Page.Footer.Description.Controls>
|
|
2244
|
+
</Page.Footer.Description>
|
|
2245
|
+
</Page.Footer>
|
|
2246
|
+
</Page>
|
|
2247
|
+
|
|
2248
|
+
<LightBox.SideOverlay.CloseDesktop
|
|
2249
|
+
data-test-id="lightbox-side-overlay-close"
|
|
2250
|
+
clickByEsc={!openedTopOverlay}
|
|
2251
|
+
onClick={handleCloseSideOverlayMD}
|
|
2252
|
+
/>
|
|
2253
|
+
|
|
2254
|
+
{renderTopOverlay()}
|
|
2255
|
+
</LightBox.SideOverlay>
|
|
2256
|
+
);
|
|
2257
|
+
|
|
2258
|
+
return (
|
|
2259
|
+
<div>
|
|
2260
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD} onClick={handleOpen}>
|
|
2261
|
+
Открыть LightBox
|
|
2262
|
+
</Button>
|
|
2263
|
+
|
|
2264
|
+
{isOpen ? (
|
|
2265
|
+
<LightBox isLoading={false} isSideOverlayOpened={openedSideOverlayMD} isTopOverlayOpened={false}>
|
|
2266
|
+
<LightBox.Content key="content" isLoading={false}>
|
|
2267
|
+
<Page>
|
|
2268
|
+
<Page.Header type={EHeaderPageType.FIRST} sticky>
|
|
2269
|
+
<Page.Header.Title>
|
|
2270
|
+
<Page.Header.Title.Content>
|
|
2271
|
+
<MobileView
|
|
2272
|
+
fallback={
|
|
2273
|
+
<Title
|
|
2274
|
+
tag="h1"
|
|
2275
|
+
size={ETitleSize.H1}
|
|
2276
|
+
tabIndex={-1}
|
|
2277
|
+
// Устанавливает фокус на первый элемент при открытии LightBox.
|
|
2278
|
+
{...{ [FocusTrapUtils.firstInteractionElementDataAttr]: true }}
|
|
2279
|
+
>
|
|
2280
|
+
Евгений Онегин
|
|
2281
|
+
</Title>
|
|
2282
|
+
}
|
|
2283
|
+
>
|
|
2284
|
+
<Title tag="h2" size={ETitleSize.H2}>
|
|
2285
|
+
Евгений Онегин
|
|
2286
|
+
</Title>
|
|
2287
|
+
</MobileView>
|
|
2288
|
+
|
|
2289
|
+
<Gap size={8} />
|
|
2290
|
+
<Text tag="div" size={ETextSize.B3} type={EFontType.SECONDARY}>
|
|
2291
|
+
Русский поэт, драматург и прозаик, заложивший основы русского
|
|
2292
|
+
реалистического направления.
|
|
2293
|
+
</Text>
|
|
2294
|
+
</Page.Header.Title.Content>
|
|
2295
|
+
<Page.Header.Title.Controls>
|
|
2296
|
+
<Button
|
|
2297
|
+
theme={EButtonTheme.GENERAL}
|
|
2298
|
+
size={EComponentSize.MD}
|
|
2299
|
+
onClick={handleOpenSideOverlayMD}
|
|
2300
|
+
>
|
|
2301
|
+
SideOverlay MD
|
|
2302
|
+
</Button>
|
|
2303
|
+
</Page.Header.Title.Controls>
|
|
2304
|
+
</Page.Header.Title>
|
|
2305
|
+
</Page.Header>
|
|
2306
|
+
|
|
2307
|
+
<Page.Body type={EBodyPageType.SECOND}>
|
|
2308
|
+
{[0, 1, 2].map((index) => (
|
|
2309
|
+
<React.Fragment key={index}>
|
|
2310
|
+
<PoemBlock />
|
|
2311
|
+
{index < 2 && <Gap size={16} />}
|
|
2312
|
+
</React.Fragment>
|
|
2313
|
+
))}
|
|
2314
|
+
</Page.Body>
|
|
2315
|
+
|
|
2316
|
+
<Page.Footer type={EFooterPageType.FIRST} sticky>
|
|
2317
|
+
<Page.Footer.Description>
|
|
2318
|
+
<Page.Footer.Description.Content>А. С. Пушкин</Page.Footer.Description.Content>
|
|
2319
|
+
<Page.Footer.Description.Controls>
|
|
2320
|
+
<Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD}>
|
|
2321
|
+
Button text
|
|
2322
|
+
</Button>
|
|
2323
|
+
<Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD}>
|
|
2324
|
+
Button text
|
|
2325
|
+
</Button>
|
|
2326
|
+
</Page.Footer.Description.Controls>
|
|
2327
|
+
</Page.Footer.Description>
|
|
2328
|
+
</Page.Footer>
|
|
2329
|
+
</Page>
|
|
2330
|
+
</LightBox.Content>
|
|
2331
|
+
|
|
2332
|
+
{renderLightBoxControls()}
|
|
2333
|
+
|
|
2334
|
+
{renderLightBoxSideOverlayMD()}
|
|
2335
|
+
</LightBox>
|
|
2336
|
+
) : null}
|
|
2337
|
+
</div>
|
|
2338
|
+
);
|
|
2339
|
+
};
|
|
2340
|
+
```
|
|
319
2341
|
|
|
320
|
-
| Дата | Изменение |
|
|
321
|
-
|---|---|
|
|
322
|
-
| 2026-04-18 | Создан документ |
|