@snack-uikit/modal 0.19.15-preview-f9bb03b8.0 → 0.20.1
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/CHANGELOG.md +23 -0
- package/README.md +4 -0
- package/dist/cjs/components/ModalCustom/ModalCustom.d.ts +11 -1
- package/dist/cjs/components/ModalCustom/ModalCustom.js +38 -5
- package/dist/cjs/components/ModalCustom/styles.module.css +32 -0
- package/dist/cjs/constants.d.ts +4 -0
- package/dist/cjs/constants.js +5 -1
- package/dist/cjs/helperComponents/OverlayElement/OverlayElement.d.ts +4 -1
- package/dist/cjs/helperComponents/OverlayElement/OverlayElement.js +18 -2
- package/dist/cjs/helperComponents/OverlayElement/styles.module.css +24 -0
- package/dist/cjs/types.d.ts +2 -1
- package/dist/esm/components/ModalCustom/ModalCustom.d.ts +11 -1
- package/dist/esm/components/ModalCustom/ModalCustom.js +34 -4
- package/dist/esm/components/ModalCustom/styles.module.css +32 -0
- package/dist/esm/constants.d.ts +4 -0
- package/dist/esm/constants.js +4 -0
- package/dist/esm/helperComponents/OverlayElement/OverlayElement.d.ts +4 -1
- package/dist/esm/helperComponents/OverlayElement/OverlayElement.js +18 -3
- package/dist/esm/helperComponents/OverlayElement/styles.module.css +24 -0
- package/dist/esm/types.d.ts +2 -1
- package/package.json +6 -6
- package/src/components/ModalCustom/ModalCustom.tsx +57 -5
- package/src/components/ModalCustom/styles.module.scss +37 -0
- package/src/constants.ts +5 -0
- package/src/helperComponents/OverlayElement/OverlayElement.tsx +35 -4
- package/src/helperComponents/OverlayElement/styles.module.scss +18 -0
- package/src/types.ts +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,29 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## 0.20.1 (2026-06-24)
|
|
7
|
+
|
|
8
|
+
### Only dependencies have been changed
|
|
9
|
+
* [@snack-uikit/button@0.19.19]($PUBLIC_PROJECT_URL/blob/master/packages/button/CHANGELOG.md)
|
|
10
|
+
* [@snack-uikit/link@0.18.2]($PUBLIC_PROJECT_URL/blob/master/packages/link/CHANGELOG.md)
|
|
11
|
+
* [@snack-uikit/tooltip@0.18.14]($PUBLIC_PROJECT_URL/blob/master/packages/tooltip/CHANGELOG.md)
|
|
12
|
+
* [@snack-uikit/truncate-string@0.7.13]($PUBLIC_PROJECT_URL/blob/master/packages/truncate-string/CHANGELOG.md)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# 0.20.0 (2026-06-16)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
* **SITE-10721:** add animation for modal ([13f4370](https://github.com/cloud-ru-tech/snack-uikit/commit/13f4370740d84c4d246c9adc512ecc7ec76e1486))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
6
29
|
## 0.19.14 (2026-05-18)
|
|
7
30
|
|
|
8
31
|
### Only dependencies have been changed
|
package/README.md
CHANGED
|
@@ -147,6 +147,8 @@ function Example() {
|
|
|
147
147
|
| size | enum Size: `"s"`, `"m"`, `"l"` | s | Размер модального окна |
|
|
148
148
|
| className | `string` | - | CSS-класс |
|
|
149
149
|
| closeOnPopstate | `boolean` | - | Закрывать при переходе по истории браузера |
|
|
150
|
+
| animationDuration | `number` | - | Длительность анимации открытия/закрытия в миллисекундах. Если не передан — анимации нет. |
|
|
151
|
+
| animationDurationPercent | `number` | 0.4 | Разница в процентах между появлением модального окна и оверлей можно ставить от 0 до 1 |
|
|
150
152
|
## ModalCustom.Header
|
|
151
153
|
### Props
|
|
152
154
|
| name | type | default value | description |
|
|
@@ -183,6 +185,8 @@ function Example() {
|
|
|
183
185
|
| mode | enum Mode: `"regular"`, `"aggressive"`, `"forced"` | regular | Режим отображения модального окна: <br> - __`Regular`__ - есть кнопка закрытия, клик на оверлей и нажатие кнопки `Esc` закрывают модалку <br> - __`Aggressive`__ - есть кнопка закрытия, но выключен клик на оверлей и не работает закрытие по клавише `Esc` <br> - __`Forced`__ - закрыть модальное окно можно только по нажатию на кнопку действия в нижней части |
|
|
184
186
|
| className | `string` | - | CSS-класс |
|
|
185
187
|
| closeOnPopstate | `boolean` | - | Закрывать при переходе по истории браузера |
|
|
188
|
+
| animationDuration | `number` | - | Длительность анимации открытия/закрытия в миллисекундах. Если не передан — анимации нет. |
|
|
189
|
+
| animationDurationPercent | `number` | - | Разница в процентах между появлением модального окна и оверлей можно ставить от 0 до 1 |
|
|
186
190
|
| titleTooltip | `ReactNode` | - | Всплывающая подсказка для заголовка |
|
|
187
191
|
| subtitle | `string` | - | Подзаголовок |
|
|
188
192
|
| content | `ReactNode` | - | Содержимое модального окна |
|
|
@@ -26,8 +26,18 @@ export type ModalCustomProps = WithSupportProps<{
|
|
|
26
26
|
children: ReactNode;
|
|
27
27
|
/** Закрывать при переходе по истории браузера */
|
|
28
28
|
closeOnPopstate?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Длительность анимации открытия/закрытия в миллисекундах.
|
|
31
|
+
* Если не передан — анимации нет.
|
|
32
|
+
*/
|
|
33
|
+
animationDuration?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Разница в процентах между появлением модального окна и оверлей
|
|
36
|
+
* можно ставить от 0 до 1
|
|
37
|
+
*/
|
|
38
|
+
animationDurationPercent?: number;
|
|
29
39
|
}>;
|
|
30
|
-
export declare function ModalCustom({ open, onClose, size, mode, children, className, closeOnPopstate, ...rest }: ModalCustomProps): import("react/jsx-runtime").JSX.Element | null;
|
|
40
|
+
export declare function ModalCustom({ open, onClose, size, mode, children, className, closeOnPopstate, animationDuration, animationDurationPercent, ...rest }: ModalCustomProps): import("react/jsx-runtime").JSX.Element | null;
|
|
31
41
|
export declare namespace ModalCustom {
|
|
32
42
|
type HeaderProps = ModalHeaderProps;
|
|
33
43
|
type BodyProps = ModalBodyProps;
|
|
@@ -19,6 +19,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
19
19
|
exports.ModalCustom = ModalCustom;
|
|
20
20
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
21
21
|
const classnames_1 = __importDefault(require("classnames"));
|
|
22
|
+
const react_1 = require("react");
|
|
22
23
|
const react_modal_1 = __importDefault(require("react-modal"));
|
|
23
24
|
const utils_1 = require("@snack-uikit/utils");
|
|
24
25
|
const constants_1 = require("../../constants");
|
|
@@ -33,9 +34,27 @@ function ModalCustom(_a) {
|
|
|
33
34
|
mode = constants_1.MODE.Regular,
|
|
34
35
|
children,
|
|
35
36
|
className,
|
|
36
|
-
closeOnPopstate
|
|
37
|
+
closeOnPopstate,
|
|
38
|
+
animationDuration,
|
|
39
|
+
animationDurationPercent = 0.4
|
|
37
40
|
} = _a,
|
|
38
|
-
rest = __rest(_a, ["open", "onClose", "size", "mode", "children", "className", "closeOnPopstate"]);
|
|
41
|
+
rest = __rest(_a, ["open", "onClose", "size", "mode", "children", "className", "closeOnPopstate", "animationDuration", "animationDurationPercent"]);
|
|
42
|
+
const animDuration = animationDuration !== null && animationDuration !== void 0 ? animationDuration : 0;
|
|
43
|
+
const isAnimated = animDuration > 0;
|
|
44
|
+
const [shouldRender, setShouldRender] = (0, react_1.useState)(open);
|
|
45
|
+
const [animationState, setAnimationState] = (0, react_1.useState)(constants_1.ANIMATION_STATE.Entering);
|
|
46
|
+
(0, react_1.useEffect)(() => {
|
|
47
|
+
if (!isAnimated) return;
|
|
48
|
+
if (open) {
|
|
49
|
+
setShouldRender(true);
|
|
50
|
+
setAnimationState(constants_1.ANIMATION_STATE.Entering);
|
|
51
|
+
} else {
|
|
52
|
+
setAnimationState(constants_1.ANIMATION_STATE.Exiting);
|
|
53
|
+
const closeTotalDuration = animDuration + Math.round(animDuration * animationDurationPercent);
|
|
54
|
+
const timer = setTimeout(() => setShouldRender(false), closeTotalDuration);
|
|
55
|
+
return () => clearTimeout(timer);
|
|
56
|
+
}
|
|
57
|
+
}, [open, isAnimated, animDuration, animationDurationPercent]);
|
|
39
58
|
const handleCloseButtonClick = () => {
|
|
40
59
|
onClose();
|
|
41
60
|
};
|
|
@@ -48,9 +67,15 @@ function ModalCustom(_a) {
|
|
|
48
67
|
(0, utils_1.useModalOpenState)(open, () => hasCloseButton && onClose(), {
|
|
49
68
|
closeOnPopstate
|
|
50
69
|
});
|
|
51
|
-
|
|
70
|
+
const isRendered = isAnimated ? shouldRender : open;
|
|
71
|
+
if (!isRendered) {
|
|
52
72
|
return null;
|
|
53
73
|
}
|
|
74
|
+
const animDelay = Math.round(animDuration * animationDurationPercent);
|
|
75
|
+
const contentStyle = isAnimated ? {
|
|
76
|
+
'--_modal-anim-duration': `${animDuration}ms`,
|
|
77
|
+
'--_modal-anim-delay': `${animDelay}ms`
|
|
78
|
+
} : undefined;
|
|
54
79
|
return (0, jsx_runtime_1.jsxs)(react_modal_1.default, {
|
|
55
80
|
data: Object.assign(Object.assign({}, (0, utils_2.getDataTestAttributes)(rest)), {
|
|
56
81
|
size
|
|
@@ -61,9 +86,17 @@ function ModalCustom(_a) {
|
|
|
61
86
|
overlayElement: (_, content) => (0, jsx_runtime_1.jsx)(helperComponents_1.OverlayElement, {
|
|
62
87
|
blur: [constants_1.MODE.Forced, constants_1.MODE.Aggressive].includes(mode),
|
|
63
88
|
content: content,
|
|
64
|
-
onClose: handleClose
|
|
89
|
+
onClose: handleClose,
|
|
90
|
+
animationDuration: isAnimated ? animDuration : undefined,
|
|
91
|
+
animationState: isAnimated ? animationState : undefined
|
|
92
|
+
}),
|
|
93
|
+
className: (0, classnames_1.default)(styles_module_scss_1.default.modal, className, isAnimated && {
|
|
94
|
+
[styles_module_scss_1.default.entering]: animationState === constants_1.ANIMATION_STATE.Entering,
|
|
95
|
+
[styles_module_scss_1.default.exiting]: animationState === constants_1.ANIMATION_STATE.Exiting
|
|
65
96
|
}),
|
|
66
|
-
|
|
97
|
+
style: contentStyle ? {
|
|
98
|
+
content: contentStyle
|
|
99
|
+
} : undefined,
|
|
67
100
|
children: [hasCloseButton && (0, jsx_runtime_1.jsx)("div", {
|
|
68
101
|
className: styles_module_scss_1.default.headerElements,
|
|
69
102
|
children: (0, jsx_runtime_1.jsx)(helperComponents_1.ButtonClose, {
|
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
@keyframes modal-blur-rise{
|
|
2
|
+
from{
|
|
3
|
+
transform:translate(-50%, calc(-50% + 3px)) scale(0.97);
|
|
4
|
+
opacity:0;
|
|
5
|
+
filter:blur(4px);
|
|
6
|
+
}
|
|
7
|
+
to{
|
|
8
|
+
transform:translate(-50%, -50%);
|
|
9
|
+
opacity:1;
|
|
10
|
+
filter:blur(0);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
@keyframes modal-blur-fall{
|
|
14
|
+
from{
|
|
15
|
+
transform:translate(-50%, -50%);
|
|
16
|
+
opacity:1;
|
|
17
|
+
filter:blur(0);
|
|
18
|
+
}
|
|
19
|
+
to{
|
|
20
|
+
transform:translate(-50%, calc(-50% + 3px)) scale(0.97);
|
|
21
|
+
opacity:0;
|
|
22
|
+
filter:blur(4px);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
1
25
|
.modal{
|
|
2
26
|
position:fixed;
|
|
3
27
|
top:50%;
|
|
@@ -30,6 +54,14 @@
|
|
|
30
54
|
width:var(--size-modal-width-l, 1872px);
|
|
31
55
|
}
|
|
32
56
|
|
|
57
|
+
.entering{
|
|
58
|
+
animation:modal-blur-rise var(--_modal-anim-duration, 400ms) cubic-bezier(0.4, 0, 0.2, 1) var(--_modal-anim-delay, 0ms) both;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.exiting{
|
|
62
|
+
animation:modal-blur-fall var(--_modal-anim-duration, 400ms) cubic-bezier(0.4, 0, 0.2, 1) both;
|
|
63
|
+
}
|
|
64
|
+
|
|
33
65
|
.headerElements{
|
|
34
66
|
padding-top:var(--space-modal-header-elements-top, 16px);
|
|
35
67
|
padding-right:var(--space-modal-header-elements-side, 16px);
|
package/dist/cjs/constants.d.ts
CHANGED
|
@@ -17,6 +17,10 @@ export declare const CONTENT_ALIGN: {
|
|
|
17
17
|
readonly Default: "default";
|
|
18
18
|
readonly Center: "center";
|
|
19
19
|
};
|
|
20
|
+
export declare const ANIMATION_STATE: {
|
|
21
|
+
readonly Entering: "entering";
|
|
22
|
+
readonly Exiting: "exiting";
|
|
23
|
+
};
|
|
20
24
|
export declare const TEST_IDS: {
|
|
21
25
|
overlay: string;
|
|
22
26
|
closeButton: string;
|
package/dist/cjs/constants.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.TEST_IDS = exports.CONTENT_ALIGN = exports.ALIGN = exports.MODE = exports.SIZE = void 0;
|
|
6
|
+
exports.TEST_IDS = exports.ANIMATION_STATE = exports.CONTENT_ALIGN = exports.ALIGN = exports.MODE = exports.SIZE = void 0;
|
|
7
7
|
exports.SIZE = {
|
|
8
8
|
S: 's',
|
|
9
9
|
M: 'm',
|
|
@@ -23,6 +23,10 @@ exports.CONTENT_ALIGN = {
|
|
|
23
23
|
Default: 'default',
|
|
24
24
|
Center: 'center'
|
|
25
25
|
};
|
|
26
|
+
exports.ANIMATION_STATE = {
|
|
27
|
+
Entering: 'entering',
|
|
28
|
+
Exiting: 'exiting'
|
|
29
|
+
};
|
|
26
30
|
exports.TEST_IDS = {
|
|
27
31
|
overlay: 'modal__overlay',
|
|
28
32
|
closeButton: 'modal__close-button',
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { ReactElement } from 'react';
|
|
2
|
+
import { AnimationState } from '../../types';
|
|
2
3
|
export type OverlayElementProps = {
|
|
3
4
|
onClose(): void;
|
|
4
5
|
content: ReactElement;
|
|
5
6
|
blur?: boolean;
|
|
7
|
+
animationDuration?: number;
|
|
8
|
+
animationState?: AnimationState;
|
|
6
9
|
};
|
|
7
|
-
export declare function OverlayElement({ onClose, content, blur }: OverlayElementProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export declare function OverlayElement({ onClose, content, blur, animationDuration, animationState, }: OverlayElementProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -10,22 +10,38 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
10
10
|
});
|
|
11
11
|
exports.OverlayElement = OverlayElement;
|
|
12
12
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
13
|
+
const classnames_1 = __importDefault(require("classnames"));
|
|
13
14
|
const constants_1 = require("../../constants");
|
|
14
15
|
const styles_module_scss_1 = __importDefault(require('./styles.module.css'));
|
|
15
16
|
function OverlayElement(_ref) {
|
|
16
17
|
let {
|
|
17
18
|
onClose,
|
|
18
19
|
content,
|
|
19
|
-
blur = false
|
|
20
|
+
blur = false,
|
|
21
|
+
animationDuration,
|
|
22
|
+
animationState
|
|
20
23
|
} = _ref;
|
|
21
24
|
const handleClick = e => {
|
|
22
25
|
e.stopPropagation();
|
|
23
26
|
onClose();
|
|
24
27
|
};
|
|
28
|
+
// Backdrop появляется сразу при открытии (без задержки).
|
|
29
|
+
// При закрытии: ждет пока модалка исчезнет (exit-delay = 40% от длительности диалога).
|
|
30
|
+
// Длительность backdrop = 62.5% от длительности диалога (пропорция 250ms / 400ms из пресета).
|
|
31
|
+
const backdropDuration = animationDuration !== undefined ? Math.round(animationDuration * 0.625) : undefined;
|
|
32
|
+
const exitDelay = animationDuration !== undefined ? Math.round(animationDuration * 0.4) : 0;
|
|
33
|
+
const overlayStyle = animationDuration !== undefined ? {
|
|
34
|
+
'--_modal-anim-duration': `${backdropDuration}ms`,
|
|
35
|
+
'--_modal-anim-exit-delay': `${exitDelay}ms`
|
|
36
|
+
} : undefined;
|
|
25
37
|
return (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, {
|
|
26
38
|
children: [(0, jsx_runtime_1.jsx)("div", {
|
|
27
|
-
className: styles_module_scss_1.default.modalOverlay,
|
|
39
|
+
className: (0, classnames_1.default)(styles_module_scss_1.default.modalOverlay, animationState && {
|
|
40
|
+
[styles_module_scss_1.default.overlayEntering]: animationState === constants_1.ANIMATION_STATE.Entering,
|
|
41
|
+
[styles_module_scss_1.default.overlayExiting]: animationState === constants_1.ANIMATION_STATE.Exiting
|
|
42
|
+
}),
|
|
28
43
|
"data-blur": blur || undefined,
|
|
44
|
+
style: overlayStyle,
|
|
29
45
|
onClick: handleClick,
|
|
30
46
|
"data-test-id": constants_1.TEST_IDS.overlay
|
|
31
47
|
}), content]
|
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
@keyframes backdrop-fade-in{
|
|
2
|
+
from{
|
|
3
|
+
opacity:0;
|
|
4
|
+
}
|
|
5
|
+
to{
|
|
6
|
+
opacity:1;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
@keyframes backdrop-fade-out{
|
|
10
|
+
from{
|
|
11
|
+
opacity:1;
|
|
12
|
+
}
|
|
13
|
+
to{
|
|
14
|
+
opacity:0;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
1
17
|
.modalOverlay{
|
|
2
18
|
position:fixed;
|
|
3
19
|
top:0;
|
|
@@ -9,4 +25,12 @@
|
|
|
9
25
|
}
|
|
10
26
|
.modalOverlay[data-blur]{
|
|
11
27
|
backdrop-filter:blur(var(--background-blur-background-blur, 16px));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.overlayEntering{
|
|
31
|
+
animation:backdrop-fade-in var(--_modal-anim-duration, 250ms) ease-out both;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.overlayExiting{
|
|
35
|
+
animation:backdrop-fade-out var(--_modal-anim-duration, 250ms) ease-out var(--_modal-anim-exit-delay, 0ms) both;
|
|
12
36
|
}
|
package/dist/cjs/types.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ValueOf } from '@snack-uikit/utils';
|
|
2
|
-
import { ALIGN, CONTENT_ALIGN, MODE, SIZE } from './constants';
|
|
2
|
+
import { ALIGN, ANIMATION_STATE, CONTENT_ALIGN, MODE, SIZE } from './constants';
|
|
3
3
|
export type Size = ValueOf<typeof SIZE>;
|
|
4
4
|
export type Mode = ValueOf<typeof MODE>;
|
|
5
5
|
export type Align = ValueOf<typeof ALIGN>;
|
|
6
6
|
export type ContentAlign = ValueOf<typeof CONTENT_ALIGN>;
|
|
7
|
+
export type AnimationState = ValueOf<typeof ANIMATION_STATE>;
|
|
@@ -26,8 +26,18 @@ export type ModalCustomProps = WithSupportProps<{
|
|
|
26
26
|
children: ReactNode;
|
|
27
27
|
/** Закрывать при переходе по истории браузера */
|
|
28
28
|
closeOnPopstate?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Длительность анимации открытия/закрытия в миллисекундах.
|
|
31
|
+
* Если не передан — анимации нет.
|
|
32
|
+
*/
|
|
33
|
+
animationDuration?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Разница в процентах между появлением модального окна и оверлей
|
|
36
|
+
* можно ставить от 0 до 1
|
|
37
|
+
*/
|
|
38
|
+
animationDurationPercent?: number;
|
|
29
39
|
}>;
|
|
30
|
-
export declare function ModalCustom({ open, onClose, size, mode, children, className, closeOnPopstate, ...rest }: ModalCustomProps): import("react/jsx-runtime").JSX.Element | null;
|
|
40
|
+
export declare function ModalCustom({ open, onClose, size, mode, children, className, closeOnPopstate, animationDuration, animationDurationPercent, ...rest }: ModalCustomProps): import("react/jsx-runtime").JSX.Element | null;
|
|
31
41
|
export declare namespace ModalCustom {
|
|
32
42
|
type HeaderProps = ModalHeaderProps;
|
|
33
43
|
type BodyProps = ModalBodyProps;
|
|
@@ -11,14 +11,33 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
11
11
|
};
|
|
12
12
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
13
13
|
import cn from 'classnames';
|
|
14
|
+
import { useEffect, useState } from 'react';
|
|
14
15
|
import RCModal from 'react-modal';
|
|
15
16
|
import { isBrowser, useModalOpenState } from '@snack-uikit/utils';
|
|
16
|
-
import { MODE, SIZE } from '../../constants';
|
|
17
|
+
import { ANIMATION_STATE, MODE, SIZE } from '../../constants';
|
|
17
18
|
import { ButtonClose, ModalBody, ModalFooter, ModalHeader, OverlayElement, } from '../../helperComponents';
|
|
18
19
|
import styles from './styles.module.css';
|
|
19
20
|
import { getDataTestAttributes } from './utils';
|
|
20
21
|
export function ModalCustom(_a) {
|
|
21
|
-
var { open, onClose, size = SIZE.S, mode = MODE.Regular, children, className, closeOnPopstate } = _a, rest = __rest(_a, ["open", "onClose", "size", "mode", "children", "className", "closeOnPopstate"]);
|
|
22
|
+
var { open, onClose, size = SIZE.S, mode = MODE.Regular, children, className, closeOnPopstate, animationDuration, animationDurationPercent = 0.4 } = _a, rest = __rest(_a, ["open", "onClose", "size", "mode", "children", "className", "closeOnPopstate", "animationDuration", "animationDurationPercent"]);
|
|
23
|
+
const animDuration = animationDuration !== null && animationDuration !== void 0 ? animationDuration : 0;
|
|
24
|
+
const isAnimated = animDuration > 0;
|
|
25
|
+
const [shouldRender, setShouldRender] = useState(open);
|
|
26
|
+
const [animationState, setAnimationState] = useState(ANIMATION_STATE.Entering);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!isAnimated)
|
|
29
|
+
return;
|
|
30
|
+
if (open) {
|
|
31
|
+
setShouldRender(true);
|
|
32
|
+
setAnimationState(ANIMATION_STATE.Entering);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
setAnimationState(ANIMATION_STATE.Exiting);
|
|
36
|
+
const closeTotalDuration = animDuration + Math.round(animDuration * animationDurationPercent);
|
|
37
|
+
const timer = setTimeout(() => setShouldRender(false), closeTotalDuration);
|
|
38
|
+
return () => clearTimeout(timer);
|
|
39
|
+
}
|
|
40
|
+
}, [open, isAnimated, animDuration, animationDurationPercent]);
|
|
22
41
|
const handleCloseButtonClick = () => {
|
|
23
42
|
onClose();
|
|
24
43
|
};
|
|
@@ -31,10 +50,21 @@ export function ModalCustom(_a) {
|
|
|
31
50
|
useModalOpenState(open, () => hasCloseButton && onClose(), {
|
|
32
51
|
closeOnPopstate,
|
|
33
52
|
});
|
|
34
|
-
|
|
53
|
+
const isRendered = isAnimated ? shouldRender : open;
|
|
54
|
+
if (!isRendered) {
|
|
35
55
|
return null;
|
|
36
56
|
}
|
|
37
|
-
|
|
57
|
+
const animDelay = Math.round(animDuration * animationDurationPercent);
|
|
58
|
+
const contentStyle = isAnimated
|
|
59
|
+
? {
|
|
60
|
+
'--_modal-anim-duration': `${animDuration}ms`,
|
|
61
|
+
'--_modal-anim-delay': `${animDelay}ms`,
|
|
62
|
+
}
|
|
63
|
+
: undefined;
|
|
64
|
+
return (_jsxs(RCModal, { data: Object.assign(Object.assign({}, getDataTestAttributes(rest)), { size }), isOpen: true, onRequestClose: handleClose, appElement: isBrowser() ? document.body : undefined, overlayElement: (_, content) => (_jsx(OverlayElement, { blur: [MODE.Forced, MODE.Aggressive].includes(mode), content: content, onClose: handleClose, animationDuration: isAnimated ? animDuration : undefined, animationState: isAnimated ? animationState : undefined })), className: cn(styles.modal, className, isAnimated && {
|
|
65
|
+
[styles.entering]: animationState === ANIMATION_STATE.Entering,
|
|
66
|
+
[styles.exiting]: animationState === ANIMATION_STATE.Exiting,
|
|
67
|
+
}), style: contentStyle ? { content: contentStyle } : undefined, children: [hasCloseButton && (_jsx("div", { className: styles.headerElements, children: _jsx(ButtonClose, { onClick: handleCloseButtonClick }) })), children] }));
|
|
38
68
|
}
|
|
39
69
|
(function (ModalCustom) {
|
|
40
70
|
ModalCustom.Header = ModalHeader;
|
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
@keyframes modal-blur-rise{
|
|
2
|
+
from{
|
|
3
|
+
transform:translate(-50%, calc(-50% + 3px)) scale(0.97);
|
|
4
|
+
opacity:0;
|
|
5
|
+
filter:blur(4px);
|
|
6
|
+
}
|
|
7
|
+
to{
|
|
8
|
+
transform:translate(-50%, -50%);
|
|
9
|
+
opacity:1;
|
|
10
|
+
filter:blur(0);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
@keyframes modal-blur-fall{
|
|
14
|
+
from{
|
|
15
|
+
transform:translate(-50%, -50%);
|
|
16
|
+
opacity:1;
|
|
17
|
+
filter:blur(0);
|
|
18
|
+
}
|
|
19
|
+
to{
|
|
20
|
+
transform:translate(-50%, calc(-50% + 3px)) scale(0.97);
|
|
21
|
+
opacity:0;
|
|
22
|
+
filter:blur(4px);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
1
25
|
.modal{
|
|
2
26
|
position:fixed;
|
|
3
27
|
top:50%;
|
|
@@ -30,6 +54,14 @@
|
|
|
30
54
|
width:var(--size-modal-width-l, 1872px);
|
|
31
55
|
}
|
|
32
56
|
|
|
57
|
+
.entering{
|
|
58
|
+
animation:modal-blur-rise var(--_modal-anim-duration, 400ms) cubic-bezier(0.4, 0, 0.2, 1) var(--_modal-anim-delay, 0ms) both;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.exiting{
|
|
62
|
+
animation:modal-blur-fall var(--_modal-anim-duration, 400ms) cubic-bezier(0.4, 0, 0.2, 1) both;
|
|
63
|
+
}
|
|
64
|
+
|
|
33
65
|
.headerElements{
|
|
34
66
|
padding-top:var(--space-modal-header-elements-top, 16px);
|
|
35
67
|
padding-right:var(--space-modal-header-elements-side, 16px);
|
package/dist/esm/constants.d.ts
CHANGED
|
@@ -17,6 +17,10 @@ export declare const CONTENT_ALIGN: {
|
|
|
17
17
|
readonly Default: "default";
|
|
18
18
|
readonly Center: "center";
|
|
19
19
|
};
|
|
20
|
+
export declare const ANIMATION_STATE: {
|
|
21
|
+
readonly Entering: "entering";
|
|
22
|
+
readonly Exiting: "exiting";
|
|
23
|
+
};
|
|
20
24
|
export declare const TEST_IDS: {
|
|
21
25
|
overlay: string;
|
|
22
26
|
closeButton: string;
|
package/dist/esm/constants.js
CHANGED
|
@@ -17,6 +17,10 @@ export const CONTENT_ALIGN = {
|
|
|
17
17
|
Default: 'default',
|
|
18
18
|
Center: 'center',
|
|
19
19
|
};
|
|
20
|
+
export const ANIMATION_STATE = {
|
|
21
|
+
Entering: 'entering',
|
|
22
|
+
Exiting: 'exiting',
|
|
23
|
+
};
|
|
20
24
|
export const TEST_IDS = {
|
|
21
25
|
overlay: 'modal__overlay',
|
|
22
26
|
closeButton: 'modal__close-button',
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { ReactElement } from 'react';
|
|
2
|
+
import { AnimationState } from '../../types';
|
|
2
3
|
export type OverlayElementProps = {
|
|
3
4
|
onClose(): void;
|
|
4
5
|
content: ReactElement;
|
|
5
6
|
blur?: boolean;
|
|
7
|
+
animationDuration?: number;
|
|
8
|
+
animationState?: AnimationState;
|
|
6
9
|
};
|
|
7
|
-
export declare function OverlayElement({ onClose, content, blur }: OverlayElementProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export declare function OverlayElement({ onClose, content, blur, animationDuration, animationState, }: OverlayElementProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import
|
|
2
|
+
import cn from 'classnames';
|
|
3
|
+
import { ANIMATION_STATE, TEST_IDS } from '../../constants';
|
|
3
4
|
import styles from './styles.module.css';
|
|
4
|
-
export function OverlayElement({ onClose, content, blur = false }) {
|
|
5
|
+
export function OverlayElement({ onClose, content, blur = false, animationDuration, animationState, }) {
|
|
5
6
|
const handleClick = e => {
|
|
6
7
|
e.stopPropagation();
|
|
7
8
|
onClose();
|
|
8
9
|
};
|
|
9
|
-
|
|
10
|
+
// Backdrop появляется сразу при открытии (без задержки).
|
|
11
|
+
// При закрытии: ждет пока модалка исчезнет (exit-delay = 40% от длительности диалога).
|
|
12
|
+
// Длительность backdrop = 62.5% от длительности диалога (пропорция 250ms / 400ms из пресета).
|
|
13
|
+
const backdropDuration = animationDuration !== undefined ? Math.round(animationDuration * 0.625) : undefined;
|
|
14
|
+
const exitDelay = animationDuration !== undefined ? Math.round(animationDuration * 0.4) : 0;
|
|
15
|
+
const overlayStyle = animationDuration !== undefined
|
|
16
|
+
? {
|
|
17
|
+
'--_modal-anim-duration': `${backdropDuration}ms`,
|
|
18
|
+
'--_modal-anim-exit-delay': `${exitDelay}ms`,
|
|
19
|
+
}
|
|
20
|
+
: undefined;
|
|
21
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: cn(styles.modalOverlay, animationState && {
|
|
22
|
+
[styles.overlayEntering]: animationState === ANIMATION_STATE.Entering,
|
|
23
|
+
[styles.overlayExiting]: animationState === ANIMATION_STATE.Exiting,
|
|
24
|
+
}), "data-blur": blur || undefined, style: overlayStyle, onClick: handleClick, "data-test-id": TEST_IDS.overlay }), content] }));
|
|
10
25
|
}
|
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
@keyframes backdrop-fade-in{
|
|
2
|
+
from{
|
|
3
|
+
opacity:0;
|
|
4
|
+
}
|
|
5
|
+
to{
|
|
6
|
+
opacity:1;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
@keyframes backdrop-fade-out{
|
|
10
|
+
from{
|
|
11
|
+
opacity:1;
|
|
12
|
+
}
|
|
13
|
+
to{
|
|
14
|
+
opacity:0;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
1
17
|
.modalOverlay{
|
|
2
18
|
position:fixed;
|
|
3
19
|
top:0;
|
|
@@ -9,4 +25,12 @@
|
|
|
9
25
|
}
|
|
10
26
|
.modalOverlay[data-blur]{
|
|
11
27
|
backdrop-filter:blur(var(--background-blur-background-blur, 16px));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.overlayEntering{
|
|
31
|
+
animation:backdrop-fade-in var(--_modal-anim-duration, 250ms) ease-out both;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.overlayExiting{
|
|
35
|
+
animation:backdrop-fade-out var(--_modal-anim-duration, 250ms) ease-out var(--_modal-anim-exit-delay, 0ms) both;
|
|
12
36
|
}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ValueOf } from '@snack-uikit/utils';
|
|
2
|
-
import { ALIGN, CONTENT_ALIGN, MODE, SIZE } from './constants';
|
|
2
|
+
import { ALIGN, ANIMATION_STATE, CONTENT_ALIGN, MODE, SIZE } from './constants';
|
|
3
3
|
export type Size = ValueOf<typeof SIZE>;
|
|
4
4
|
export type Mode = ValueOf<typeof MODE>;
|
|
5
5
|
export type Align = ValueOf<typeof ALIGN>;
|
|
6
6
|
export type ContentAlign = ValueOf<typeof CONTENT_ALIGN>;
|
|
7
|
+
export type AnimationState = ValueOf<typeof ANIMATION_STATE>;
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
6
|
"title": "Modal",
|
|
7
|
-
"version": "0.
|
|
7
|
+
"version": "0.20.1",
|
|
8
8
|
"sideEffects": [
|
|
9
9
|
"*.css",
|
|
10
10
|
"*.woff",
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
"license": "Apache-2.0",
|
|
37
37
|
"scripts": {},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@snack-uikit/button": "0.19.19
|
|
39
|
+
"@snack-uikit/button": "0.19.19",
|
|
40
40
|
"@snack-uikit/icon-predefined": "0.7.12",
|
|
41
41
|
"@snack-uikit/icons": "0.27.7",
|
|
42
|
-
"@snack-uikit/link": "0.18.2
|
|
42
|
+
"@snack-uikit/link": "0.18.2",
|
|
43
43
|
"@snack-uikit/loaders": "0.9.11",
|
|
44
44
|
"@snack-uikit/scroll": "0.11.0",
|
|
45
|
-
"@snack-uikit/tooltip": "0.18.14
|
|
46
|
-
"@snack-uikit/truncate-string": "0.7.13
|
|
45
|
+
"@snack-uikit/tooltip": "0.18.14",
|
|
46
|
+
"@snack-uikit/truncate-string": "0.7.13",
|
|
47
47
|
"@snack-uikit/typography": "0.8.13",
|
|
48
48
|
"@snack-uikit/utils": "4.0.2",
|
|
49
49
|
"classnames": "2.5.1",
|
|
@@ -52,5 +52,5 @@
|
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/react-modal": "3.16.0"
|
|
54
54
|
},
|
|
55
|
-
"gitHead": "
|
|
55
|
+
"gitHead": "dfbff14ac89d87d7a86abd70d307e92bf11906cb"
|
|
56
56
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import cn from 'classnames';
|
|
2
|
-
import { ReactNode } from 'react';
|
|
2
|
+
import { CSSProperties, ReactNode, useEffect, useState } from 'react';
|
|
3
3
|
import RCModal from 'react-modal';
|
|
4
4
|
|
|
5
5
|
import { isBrowser, useModalOpenState, WithSupportProps } from '@snack-uikit/utils';
|
|
6
6
|
|
|
7
|
-
import { MODE, SIZE } from '../../constants';
|
|
7
|
+
import { ANIMATION_STATE, MODE, SIZE } from '../../constants';
|
|
8
8
|
import {
|
|
9
9
|
ButtonClose,
|
|
10
10
|
ModalBody,
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
ModalHeaderProps,
|
|
16
16
|
OverlayElement,
|
|
17
17
|
} from '../../helperComponents';
|
|
18
|
-
import { Mode, Size } from '../../types';
|
|
18
|
+
import { AnimationState, Mode, Size } from '../../types';
|
|
19
19
|
import styles from './styles.module.scss';
|
|
20
20
|
import { getDataTestAttributes } from './utils';
|
|
21
21
|
|
|
@@ -43,6 +43,16 @@ export type ModalCustomProps = WithSupportProps<{
|
|
|
43
43
|
children: ReactNode;
|
|
44
44
|
/** Закрывать при переходе по истории браузера */
|
|
45
45
|
closeOnPopstate?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Длительность анимации открытия/закрытия в миллисекундах.
|
|
48
|
+
* Если не передан — анимации нет.
|
|
49
|
+
*/
|
|
50
|
+
animationDuration?: number;
|
|
51
|
+
/**
|
|
52
|
+
* Разница в процентах между появлением модального окна и оверлей
|
|
53
|
+
* можно ставить от 0 до 1
|
|
54
|
+
*/
|
|
55
|
+
animationDurationPercent?: number;
|
|
46
56
|
}>;
|
|
47
57
|
|
|
48
58
|
export function ModalCustom({
|
|
@@ -53,8 +63,30 @@ export function ModalCustom({
|
|
|
53
63
|
children,
|
|
54
64
|
className,
|
|
55
65
|
closeOnPopstate,
|
|
66
|
+
animationDuration,
|
|
67
|
+
animationDurationPercent = 0.4,
|
|
56
68
|
...rest
|
|
57
69
|
}: ModalCustomProps) {
|
|
70
|
+
const animDuration = animationDuration ?? 0;
|
|
71
|
+
const isAnimated = animDuration > 0;
|
|
72
|
+
|
|
73
|
+
const [shouldRender, setShouldRender] = useState(open);
|
|
74
|
+
const [animationState, setAnimationState] = useState<AnimationState>(ANIMATION_STATE.Entering);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!isAnimated) return;
|
|
78
|
+
|
|
79
|
+
if (open) {
|
|
80
|
+
setShouldRender(true);
|
|
81
|
+
setAnimationState(ANIMATION_STATE.Entering);
|
|
82
|
+
} else {
|
|
83
|
+
setAnimationState(ANIMATION_STATE.Exiting);
|
|
84
|
+
const closeTotalDuration = animDuration + Math.round(animDuration * animationDurationPercent);
|
|
85
|
+
const timer = setTimeout(() => setShouldRender(false), closeTotalDuration);
|
|
86
|
+
return () => clearTimeout(timer);
|
|
87
|
+
}
|
|
88
|
+
}, [open, isAnimated, animDuration, animationDurationPercent]);
|
|
89
|
+
|
|
58
90
|
const handleCloseButtonClick = () => {
|
|
59
91
|
onClose();
|
|
60
92
|
};
|
|
@@ -71,10 +103,20 @@ export function ModalCustom({
|
|
|
71
103
|
closeOnPopstate,
|
|
72
104
|
});
|
|
73
105
|
|
|
74
|
-
|
|
106
|
+
const isRendered = isAnimated ? shouldRender : open;
|
|
107
|
+
|
|
108
|
+
if (!isRendered) {
|
|
75
109
|
return null;
|
|
76
110
|
}
|
|
77
111
|
|
|
112
|
+
const animDelay = Math.round(animDuration * animationDurationPercent);
|
|
113
|
+
const contentStyle: CSSProperties | undefined = isAnimated
|
|
114
|
+
? ({
|
|
115
|
+
'--_modal-anim-duration': `${animDuration}ms`,
|
|
116
|
+
'--_modal-anim-delay': `${animDelay}ms`,
|
|
117
|
+
} as CSSProperties)
|
|
118
|
+
: undefined;
|
|
119
|
+
|
|
78
120
|
return (
|
|
79
121
|
<RCModal
|
|
80
122
|
data={{ ...getDataTestAttributes(rest), size }}
|
|
@@ -86,9 +128,19 @@ export function ModalCustom({
|
|
|
86
128
|
blur={([MODE.Forced, MODE.Aggressive] as Mode[]).includes(mode)}
|
|
87
129
|
content={content}
|
|
88
130
|
onClose={handleClose}
|
|
131
|
+
animationDuration={isAnimated ? animDuration : undefined}
|
|
132
|
+
animationState={isAnimated ? animationState : undefined}
|
|
89
133
|
/>
|
|
90
134
|
)}
|
|
91
|
-
className={cn(
|
|
135
|
+
className={cn(
|
|
136
|
+
styles.modal,
|
|
137
|
+
className,
|
|
138
|
+
isAnimated && {
|
|
139
|
+
[styles.entering]: animationState === ANIMATION_STATE.Entering,
|
|
140
|
+
[styles.exiting]: animationState === ANIMATION_STATE.Exiting,
|
|
141
|
+
},
|
|
142
|
+
)}
|
|
143
|
+
style={contentStyle ? { content: contentStyle } : undefined}
|
|
92
144
|
>
|
|
93
145
|
{hasCloseButton && (
|
|
94
146
|
<div className={styles.headerElements}>
|
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
$sizes: 's', 'm', 'l';
|
|
4
4
|
|
|
5
|
+
@keyframes modal-blur-rise {
|
|
6
|
+
from {
|
|
7
|
+
transform: translate(-50%, calc(-50% + 3px)) scale(0.97);
|
|
8
|
+
opacity: 0;
|
|
9
|
+
filter: blur(4px);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
to {
|
|
13
|
+
transform: translate(-50%, -50%);
|
|
14
|
+
opacity: 1;
|
|
15
|
+
filter: blur(0);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@keyframes modal-blur-fall {
|
|
20
|
+
from {
|
|
21
|
+
transform: translate(-50%, -50%);
|
|
22
|
+
opacity: 1;
|
|
23
|
+
filter: blur(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
to {
|
|
27
|
+
transform: translate(-50%, calc(-50% + 3px)) scale(0.97);
|
|
28
|
+
opacity: 0;
|
|
29
|
+
filter: blur(4px);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
5
33
|
.modal {
|
|
6
34
|
position: fixed;
|
|
7
35
|
top: 50%;
|
|
@@ -26,6 +54,15 @@ $sizes: 's', 'm', 'l';
|
|
|
26
54
|
@include styles-tokens-modal.composite-var(styles-tokens-modal.$modal, 'window', $size);
|
|
27
55
|
}
|
|
28
56
|
}
|
|
57
|
+
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.entering {
|
|
61
|
+
animation: modal-blur-rise var(--_modal-anim-duration, 400ms) cubic-bezier(0.4, 0, 0.2, 1) var(--_modal-anim-delay, 0ms) both;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.exiting {
|
|
65
|
+
animation: modal-blur-fall var(--_modal-anim-duration, 400ms) cubic-bezier(0.4, 0, 0.2, 1) both;
|
|
29
66
|
}
|
|
30
67
|
|
|
31
68
|
.headerElements {
|
package/src/constants.ts
CHANGED
|
@@ -21,6 +21,11 @@ export const CONTENT_ALIGN = {
|
|
|
21
21
|
Center: 'center',
|
|
22
22
|
} as const;
|
|
23
23
|
|
|
24
|
+
export const ANIMATION_STATE = {
|
|
25
|
+
Entering: 'entering',
|
|
26
|
+
Exiting: 'exiting',
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
24
29
|
export const TEST_IDS = {
|
|
25
30
|
overlay: 'modal__overlay',
|
|
26
31
|
closeButton: 'modal__close-button',
|
|
@@ -1,26 +1,57 @@
|
|
|
1
|
-
import
|
|
1
|
+
import cn from 'classnames';
|
|
2
|
+
import { CSSProperties, MouseEventHandler, ReactElement } from 'react';
|
|
2
3
|
|
|
3
|
-
import { TEST_IDS } from '../../constants';
|
|
4
|
+
import { ANIMATION_STATE, TEST_IDS } from '../../constants';
|
|
5
|
+
import { AnimationState } from '../../types';
|
|
4
6
|
import styles from './styles.module.scss';
|
|
5
7
|
|
|
6
8
|
export type OverlayElementProps = {
|
|
7
9
|
onClose(): void;
|
|
8
10
|
content: ReactElement;
|
|
9
11
|
blur?: boolean;
|
|
12
|
+
animationDuration?: number;
|
|
13
|
+
animationState?: AnimationState;
|
|
10
14
|
};
|
|
11
15
|
|
|
12
|
-
export function OverlayElement({
|
|
16
|
+
export function OverlayElement({
|
|
17
|
+
onClose,
|
|
18
|
+
content,
|
|
19
|
+
blur = false,
|
|
20
|
+
animationDuration,
|
|
21
|
+
animationState,
|
|
22
|
+
}: OverlayElementProps) {
|
|
13
23
|
const handleClick: MouseEventHandler = e => {
|
|
14
24
|
e.stopPropagation();
|
|
15
25
|
onClose();
|
|
16
26
|
};
|
|
17
27
|
|
|
28
|
+
// Backdrop появляется сразу при открытии (без задержки).
|
|
29
|
+
// При закрытии: ждет пока модалка исчезнет (exit-delay = 40% от длительности диалога).
|
|
30
|
+
// Длительность backdrop = 62.5% от длительности диалога (пропорция 250ms / 400ms из пресета).
|
|
31
|
+
const backdropDuration = animationDuration !== undefined ? Math.round(animationDuration * 0.625) : undefined;
|
|
32
|
+
const exitDelay = animationDuration !== undefined ? Math.round(animationDuration * 0.4) : 0;
|
|
33
|
+
|
|
34
|
+
const overlayStyle: CSSProperties | undefined =
|
|
35
|
+
animationDuration !== undefined
|
|
36
|
+
? ({
|
|
37
|
+
'--_modal-anim-duration': `${backdropDuration}ms`,
|
|
38
|
+
'--_modal-anim-exit-delay': `${exitDelay}ms`,
|
|
39
|
+
} as CSSProperties)
|
|
40
|
+
: undefined;
|
|
41
|
+
|
|
18
42
|
return (
|
|
19
43
|
<>
|
|
20
44
|
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
|
21
45
|
<div
|
|
22
|
-
className={
|
|
46
|
+
className={cn(
|
|
47
|
+
styles.modalOverlay,
|
|
48
|
+
animationState && {
|
|
49
|
+
[styles.overlayEntering]: animationState === ANIMATION_STATE.Entering,
|
|
50
|
+
[styles.overlayExiting]: animationState === ANIMATION_STATE.Exiting,
|
|
51
|
+
},
|
|
52
|
+
)}
|
|
23
53
|
data-blur={blur || undefined}
|
|
54
|
+
style={overlayStyle}
|
|
24
55
|
onClick={handleClick}
|
|
25
56
|
data-test-id={TEST_IDS.overlay}
|
|
26
57
|
/>
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
@use '@snack-uikit/figma-tokens/build/scss/components/styles-tokens-modal';
|
|
2
2
|
|
|
3
|
+
@keyframes backdrop-fade-in {
|
|
4
|
+
from { opacity: 0; }
|
|
5
|
+
to { opacity: 1; }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
@keyframes backdrop-fade-out {
|
|
9
|
+
from { opacity: 1; }
|
|
10
|
+
to { opacity: 0; }
|
|
11
|
+
}
|
|
12
|
+
|
|
3
13
|
.modalOverlay {
|
|
4
14
|
position: fixed;
|
|
5
15
|
top: 0;
|
|
@@ -15,3 +25,11 @@
|
|
|
15
25
|
backdrop-filter: blur(styles-tokens-modal.simple-var(styles-tokens-modal.$background-blur-background-blur));
|
|
16
26
|
}
|
|
17
27
|
}
|
|
28
|
+
|
|
29
|
+
.overlayEntering {
|
|
30
|
+
animation: backdrop-fade-in var(--_modal-anim-duration, 250ms) ease-out both;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.overlayExiting {
|
|
34
|
+
animation: backdrop-fade-out var(--_modal-anim-duration, 250ms) ease-out var(--_modal-anim-exit-delay, 0ms) both;
|
|
35
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ValueOf } from '@snack-uikit/utils';
|
|
2
2
|
|
|
3
|
-
import { ALIGN, CONTENT_ALIGN, MODE, SIZE } from './constants';
|
|
3
|
+
import { ALIGN, ANIMATION_STATE, CONTENT_ALIGN, MODE, SIZE } from './constants';
|
|
4
4
|
|
|
5
5
|
export type Size = ValueOf<typeof SIZE>;
|
|
6
6
|
|
|
@@ -9,3 +9,5 @@ export type Mode = ValueOf<typeof MODE>;
|
|
|
9
9
|
export type Align = ValueOf<typeof ALIGN>;
|
|
10
10
|
|
|
11
11
|
export type ContentAlign = ValueOf<typeof CONTENT_ALIGN>;
|
|
12
|
+
|
|
13
|
+
export type AnimationState = ValueOf<typeof ANIMATION_STATE>;
|