@elementor/editor-modal-shell 4.2.0-919

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.
@@ -0,0 +1,56 @@
1
+ import * as React from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { SxProps, Theme } from '@elementor/ui';
4
+
5
+ declare const MODAL_Z_INDEX = 99999;
6
+ type ModalShellContextValue = {
7
+ close: () => void;
8
+ };
9
+ declare const useModalShell: () => ModalShellContextValue;
10
+ type ModalShellProps = {
11
+ children: ReactNode;
12
+ onClose?: () => void;
13
+ revealDuration?: number;
14
+ revealDelay?: number;
15
+ container?: HTMLElement;
16
+ sx?: SxProps<Theme>;
17
+ closeOnEsc?: boolean;
18
+ closeOnOutsideClick?: boolean;
19
+ backdrop?: boolean;
20
+ backdropSx?: SxProps<Theme>;
21
+ hideCloseButton?: boolean;
22
+ };
23
+ declare const ModalShell: ({ children, onClose, revealDuration, revealDelay, container, sx, closeOnEsc, closeOnOutsideClick, backdrop, backdropSx, hideCloseButton, }: ModalShellProps) => React.JSX.Element;
24
+
25
+ type ModalHeaderProps = {
26
+ title: string;
27
+ content: ReactNode;
28
+ };
29
+ declare const ModalHeader: ({ title, content }: ModalHeaderProps) => React.JSX.Element;
30
+
31
+ type FooterLinkData = {
32
+ text: string;
33
+ url: string;
34
+ };
35
+ type ModalFooterProps = {
36
+ helpText: string;
37
+ link: FooterLinkData;
38
+ };
39
+ declare const ModalFooter: ({ helpText, link }: ModalFooterProps) => React.JSX.Element;
40
+
41
+ type BackgroundLottieProps = {
42
+ animationData: object;
43
+ loop?: boolean;
44
+ autoplay?: boolean;
45
+ zIndex?: number;
46
+ backgroundColor?: string;
47
+ onComplete?: () => void;
48
+ };
49
+ declare function BackgroundLottie({ animationData, loop, autoplay, zIndex, backgroundColor, onComplete, }: BackgroundLottieProps): React.JSX.Element | null;
50
+
51
+ declare function useAutoplayCarousel<T>(items: T[], intervalMs?: number): {
52
+ selectedItem: T;
53
+ selectItem: (item: T) => void;
54
+ };
55
+
56
+ export { BackgroundLottie, MODAL_Z_INDEX, ModalFooter, ModalHeader, ModalShell, type ModalShellProps, useAutoplayCarousel, useModalShell };
@@ -0,0 +1,56 @@
1
+ import * as React from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { SxProps, Theme } from '@elementor/ui';
4
+
5
+ declare const MODAL_Z_INDEX = 99999;
6
+ type ModalShellContextValue = {
7
+ close: () => void;
8
+ };
9
+ declare const useModalShell: () => ModalShellContextValue;
10
+ type ModalShellProps = {
11
+ children: ReactNode;
12
+ onClose?: () => void;
13
+ revealDuration?: number;
14
+ revealDelay?: number;
15
+ container?: HTMLElement;
16
+ sx?: SxProps<Theme>;
17
+ closeOnEsc?: boolean;
18
+ closeOnOutsideClick?: boolean;
19
+ backdrop?: boolean;
20
+ backdropSx?: SxProps<Theme>;
21
+ hideCloseButton?: boolean;
22
+ };
23
+ declare const ModalShell: ({ children, onClose, revealDuration, revealDelay, container, sx, closeOnEsc, closeOnOutsideClick, backdrop, backdropSx, hideCloseButton, }: ModalShellProps) => React.JSX.Element;
24
+
25
+ type ModalHeaderProps = {
26
+ title: string;
27
+ content: ReactNode;
28
+ };
29
+ declare const ModalHeader: ({ title, content }: ModalHeaderProps) => React.JSX.Element;
30
+
31
+ type FooterLinkData = {
32
+ text: string;
33
+ url: string;
34
+ };
35
+ type ModalFooterProps = {
36
+ helpText: string;
37
+ link: FooterLinkData;
38
+ };
39
+ declare const ModalFooter: ({ helpText, link }: ModalFooterProps) => React.JSX.Element;
40
+
41
+ type BackgroundLottieProps = {
42
+ animationData: object;
43
+ loop?: boolean;
44
+ autoplay?: boolean;
45
+ zIndex?: number;
46
+ backgroundColor?: string;
47
+ onComplete?: () => void;
48
+ };
49
+ declare function BackgroundLottie({ animationData, loop, autoplay, zIndex, backgroundColor, onComplete, }: BackgroundLottieProps): React.JSX.Element | null;
50
+
51
+ declare function useAutoplayCarousel<T>(items: T[], intervalMs?: number): {
52
+ selectedItem: T;
53
+ selectItem: (item: T) => void;
54
+ };
55
+
56
+ export { BackgroundLottie, MODAL_Z_INDEX, ModalFooter, ModalHeader, ModalShell, type ModalShellProps, useAutoplayCarousel, useModalShell };
package/dist/index.js ADDED
@@ -0,0 +1,251 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ BackgroundLottie: () => BackgroundLottie,
34
+ MODAL_Z_INDEX: () => MODAL_Z_INDEX,
35
+ ModalFooter: () => ModalFooter,
36
+ ModalHeader: () => ModalHeader,
37
+ ModalShell: () => ModalShell,
38
+ useAutoplayCarousel: () => useAutoplayCarousel,
39
+ useModalShell: () => useModalShell
40
+ });
41
+ module.exports = __toCommonJS(index_exports);
42
+
43
+ // src/components/modal-shell.tsx
44
+ var React = __toESM(require("react"));
45
+ var import_react = require("react");
46
+ var import_ui = require("@elementor/ui");
47
+ var MODAL_Z_INDEX = 99999;
48
+ var DEFAULT_WIDTH = 800;
49
+ var DEFAULT_HEIGHT = 400;
50
+ var DEFAULT_REVEAL_DURATION_MS = 500;
51
+ var DEFAULT_REVEAL_DELAY_MS = 0;
52
+ var ModalShellContext = (0, import_react.createContext)(null);
53
+ var useModalShell = () => {
54
+ const ctx = (0, import_react.useContext)(ModalShellContext);
55
+ if (!ctx) {
56
+ throw new Error("useModalShell must be used inside a <ModalShell />");
57
+ }
58
+ return ctx;
59
+ };
60
+ var EXIT_TRANSITION_MS = 225;
61
+ var prefersReducedMotion = () => typeof window !== "undefined" && Boolean(window.matchMedia?.("(prefers-reduced-motion: reduce)").matches);
62
+ var ModalShell = ({
63
+ children,
64
+ onClose,
65
+ revealDuration = DEFAULT_REVEAL_DURATION_MS,
66
+ revealDelay = DEFAULT_REVEAL_DELAY_MS,
67
+ container,
68
+ sx = {},
69
+ closeOnEsc = true,
70
+ closeOnOutsideClick = true,
71
+ backdrop = true,
72
+ backdropSx = {},
73
+ hideCloseButton = false
74
+ }) => {
75
+ const portalTarget = container ?? (typeof document !== "undefined" ? document.body : void 0);
76
+ const reducedMotion = prefersReducedMotion();
77
+ const consumerOwnsEnter = revealDuration === 0 || reducedMotion;
78
+ const [open, setOpen] = (0, import_react.useState)(true);
79
+ const startClose = (0, import_react.useCallback)(() => setOpen(false), []);
80
+ const contextValue = (0, import_react.useMemo)(() => ({ close: startClose }), [startClose]);
81
+ const handleDialogClose = (_event, reason) => {
82
+ if (reason === "escapeKeyDown" && !closeOnEsc) {
83
+ return;
84
+ }
85
+ if (reason === "backdropClick" && !closeOnOutsideClick) {
86
+ return;
87
+ }
88
+ startClose();
89
+ };
90
+ const transitionTimeouts = (0, import_react.useMemo)(
91
+ () => ({ enter: 0, exit: reducedMotion ? 0 : EXIT_TRANSITION_MS }),
92
+ [reducedMotion]
93
+ );
94
+ const animationProps = (0, import_react.useMemo)(
95
+ () => consumerOwnsEnter ? {} : {
96
+ animation: `e-modal-shell-reveal ${revealDuration}ms ease ${revealDelay}ms backwards`,
97
+ "@keyframes e-modal-shell-reveal": {
98
+ from: { opacity: 0, transform: "scale(0.95)" },
99
+ to: { opacity: 1, transform: "scale(1)" }
100
+ }
101
+ },
102
+ [consumerOwnsEnter, revealDuration, revealDelay]
103
+ );
104
+ return /* @__PURE__ */ React.createElement(
105
+ import_ui.Dialog,
106
+ {
107
+ open,
108
+ maxWidth: false,
109
+ onClose: handleDialogClose,
110
+ container: portalTarget,
111
+ disableEscapeKeyDown: !closeOnEsc,
112
+ hideBackdrop: !backdrop,
113
+ TransitionProps: { onExited: onClose, timeout: transitionTimeouts },
114
+ slotProps: { backdrop: { transitionDuration: transitionTimeouts, sx: backdropSx } },
115
+ PaperProps: {
116
+ sx: {
117
+ width: DEFAULT_WIDTH,
118
+ height: DEFAULT_HEIGHT,
119
+ maxWidth: "100%",
120
+ overflow: "hidden",
121
+ ...animationProps,
122
+ ...sx
123
+ }
124
+ },
125
+ sx: {
126
+ zIndex: MODAL_Z_INDEX
127
+ }
128
+ },
129
+ /* @__PURE__ */ React.createElement(ModalShellContext.Provider, { value: contextValue }, /* @__PURE__ */ React.createElement(import_ui.Box, { sx: { display: "contents" } }, children, !hideCloseButton && /* @__PURE__ */ React.createElement(
130
+ import_ui.CloseButton,
131
+ {
132
+ onClick: startClose,
133
+ sx: {
134
+ position: "absolute",
135
+ right: 16,
136
+ top: 16,
137
+ zIndex: 3
138
+ }
139
+ }
140
+ )))
141
+ );
142
+ };
143
+
144
+ // src/components/modal-header.tsx
145
+ var React2 = __toESM(require("react"));
146
+ var import_ui2 = require("@elementor/ui");
147
+ var ModalHeader = ({ title, content }) => {
148
+ return /* @__PURE__ */ React2.createElement(import_ui2.Stack, { gap: 0.75 }, /* @__PURE__ */ React2.createElement(import_ui2.Typography, { variant: "h4", color: "text.primary" }, title), /* @__PURE__ */ React2.createElement(import_ui2.Typography, { variant: "subtitle2", color: "text.primary" }, content));
149
+ };
150
+
151
+ // src/components/modal-footer.tsx
152
+ var React3 = __toESM(require("react"));
153
+ var import_ui3 = require("@elementor/ui");
154
+ var ModalFooter = ({ helpText, link }) => {
155
+ return /* @__PURE__ */ React3.createElement(import_ui3.Stack, { direction: "row", alignItems: "center", gap: 1 }, /* @__PURE__ */ React3.createElement(import_ui3.Typography, { variant: "caption", color: "text.tertiary", sx: { fontSize: "11px", lineHeight: "normal" } }, helpText), /* @__PURE__ */ React3.createElement(
156
+ import_ui3.Button,
157
+ {
158
+ href: link.url,
159
+ target: "_blank",
160
+ variant: "text",
161
+ size: "small",
162
+ color: "info",
163
+ sx: { fontSize: "11px" }
164
+ },
165
+ link.text
166
+ ));
167
+ };
168
+
169
+ // src/components/background-lottie.tsx
170
+ var React4 = __toESM(require("react"));
171
+ var import_react2 = require("react");
172
+ var import_lottie_react = __toESM(require("lottie-react"));
173
+ function BackgroundLottie({
174
+ animationData,
175
+ loop = false,
176
+ autoplay = true,
177
+ zIndex = MODAL_Z_INDEX - 1,
178
+ backgroundColor = "transparent",
179
+ onComplete = () => {
180
+ }
181
+ }) {
182
+ const prefersReducedMotion2 = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
183
+ (0, import_react2.useEffect)(() => {
184
+ if (prefersReducedMotion2) {
185
+ onComplete?.();
186
+ }
187
+ }, [prefersReducedMotion2, onComplete]);
188
+ if (prefersReducedMotion2) {
189
+ return null;
190
+ }
191
+ return /* @__PURE__ */ React4.createElement(
192
+ import_lottie_react.default,
193
+ {
194
+ animationData,
195
+ loop,
196
+ autoplay,
197
+ onComplete: loop ? void 0 : onComplete,
198
+ onDataFailed: onComplete,
199
+ rendererSettings: {
200
+ preserveAspectRatio: "xMidYMid slice"
201
+ },
202
+ style: {
203
+ position: "fixed",
204
+ inset: 0,
205
+ width: "100vw",
206
+ height: "100vh",
207
+ zIndex,
208
+ backgroundColor,
209
+ pointerEvents: "none"
210
+ }
211
+ }
212
+ );
213
+ }
214
+
215
+ // src/hooks/use-autoplay-carousel.ts
216
+ var import_react3 = require("react");
217
+ var DEFAULT_INTERVAL_MS = 4e3;
218
+ function useAutoplayCarousel(items, intervalMs = DEFAULT_INTERVAL_MS) {
219
+ const [selectedItem, setSelectedItem] = (0, import_react3.useState)(items[0]);
220
+ const [isAutoPlaying, setIsAutoPlaying] = (0, import_react3.useState)(true);
221
+ const advanceToNextItem = (0, import_react3.useCallback)(() => {
222
+ setSelectedItem((current) => {
223
+ const currentIndex = items.indexOf(current);
224
+ const nextIndex = (currentIndex + 1) % items.length;
225
+ return items[nextIndex];
226
+ });
227
+ }, [items]);
228
+ (0, import_react3.useEffect)(() => {
229
+ if (!isAutoPlaying) {
230
+ return;
231
+ }
232
+ const id = setInterval(advanceToNextItem, intervalMs);
233
+ return () => clearInterval(id);
234
+ }, [isAutoPlaying, advanceToNextItem, intervalMs]);
235
+ const selectItem = (0, import_react3.useCallback)((item) => {
236
+ setSelectedItem(item);
237
+ setIsAutoPlaying(false);
238
+ }, []);
239
+ return { selectedItem, selectItem };
240
+ }
241
+ // Annotate the CommonJS export names for ESM import in node:
242
+ 0 && (module.exports = {
243
+ BackgroundLottie,
244
+ MODAL_Z_INDEX,
245
+ ModalFooter,
246
+ ModalHeader,
247
+ ModalShell,
248
+ useAutoplayCarousel,
249
+ useModalShell
250
+ });
251
+ //# sourceMappingURL=index.js.map
package/dist/index.mjs ADDED
@@ -0,0 +1,208 @@
1
+ // src/components/modal-shell.tsx
2
+ import * as React from "react";
3
+ import { createContext, useCallback, useContext, useMemo, useState } from "react";
4
+ import { Box, CloseButton, Dialog } from "@elementor/ui";
5
+ var MODAL_Z_INDEX = 99999;
6
+ var DEFAULT_WIDTH = 800;
7
+ var DEFAULT_HEIGHT = 400;
8
+ var DEFAULT_REVEAL_DURATION_MS = 500;
9
+ var DEFAULT_REVEAL_DELAY_MS = 0;
10
+ var ModalShellContext = createContext(null);
11
+ var useModalShell = () => {
12
+ const ctx = useContext(ModalShellContext);
13
+ if (!ctx) {
14
+ throw new Error("useModalShell must be used inside a <ModalShell />");
15
+ }
16
+ return ctx;
17
+ };
18
+ var EXIT_TRANSITION_MS = 225;
19
+ var prefersReducedMotion = () => typeof window !== "undefined" && Boolean(window.matchMedia?.("(prefers-reduced-motion: reduce)").matches);
20
+ var ModalShell = ({
21
+ children,
22
+ onClose,
23
+ revealDuration = DEFAULT_REVEAL_DURATION_MS,
24
+ revealDelay = DEFAULT_REVEAL_DELAY_MS,
25
+ container,
26
+ sx = {},
27
+ closeOnEsc = true,
28
+ closeOnOutsideClick = true,
29
+ backdrop = true,
30
+ backdropSx = {},
31
+ hideCloseButton = false
32
+ }) => {
33
+ const portalTarget = container ?? (typeof document !== "undefined" ? document.body : void 0);
34
+ const reducedMotion = prefersReducedMotion();
35
+ const consumerOwnsEnter = revealDuration === 0 || reducedMotion;
36
+ const [open, setOpen] = useState(true);
37
+ const startClose = useCallback(() => setOpen(false), []);
38
+ const contextValue = useMemo(() => ({ close: startClose }), [startClose]);
39
+ const handleDialogClose = (_event, reason) => {
40
+ if (reason === "escapeKeyDown" && !closeOnEsc) {
41
+ return;
42
+ }
43
+ if (reason === "backdropClick" && !closeOnOutsideClick) {
44
+ return;
45
+ }
46
+ startClose();
47
+ };
48
+ const transitionTimeouts = useMemo(
49
+ () => ({ enter: 0, exit: reducedMotion ? 0 : EXIT_TRANSITION_MS }),
50
+ [reducedMotion]
51
+ );
52
+ const animationProps = useMemo(
53
+ () => consumerOwnsEnter ? {} : {
54
+ animation: `e-modal-shell-reveal ${revealDuration}ms ease ${revealDelay}ms backwards`,
55
+ "@keyframes e-modal-shell-reveal": {
56
+ from: { opacity: 0, transform: "scale(0.95)" },
57
+ to: { opacity: 1, transform: "scale(1)" }
58
+ }
59
+ },
60
+ [consumerOwnsEnter, revealDuration, revealDelay]
61
+ );
62
+ return /* @__PURE__ */ React.createElement(
63
+ Dialog,
64
+ {
65
+ open,
66
+ maxWidth: false,
67
+ onClose: handleDialogClose,
68
+ container: portalTarget,
69
+ disableEscapeKeyDown: !closeOnEsc,
70
+ hideBackdrop: !backdrop,
71
+ TransitionProps: { onExited: onClose, timeout: transitionTimeouts },
72
+ slotProps: { backdrop: { transitionDuration: transitionTimeouts, sx: backdropSx } },
73
+ PaperProps: {
74
+ sx: {
75
+ width: DEFAULT_WIDTH,
76
+ height: DEFAULT_HEIGHT,
77
+ maxWidth: "100%",
78
+ overflow: "hidden",
79
+ ...animationProps,
80
+ ...sx
81
+ }
82
+ },
83
+ sx: {
84
+ zIndex: MODAL_Z_INDEX
85
+ }
86
+ },
87
+ /* @__PURE__ */ React.createElement(ModalShellContext.Provider, { value: contextValue }, /* @__PURE__ */ React.createElement(Box, { sx: { display: "contents" } }, children, !hideCloseButton && /* @__PURE__ */ React.createElement(
88
+ CloseButton,
89
+ {
90
+ onClick: startClose,
91
+ sx: {
92
+ position: "absolute",
93
+ right: 16,
94
+ top: 16,
95
+ zIndex: 3
96
+ }
97
+ }
98
+ )))
99
+ );
100
+ };
101
+
102
+ // src/components/modal-header.tsx
103
+ import * as React2 from "react";
104
+ import { Stack, Typography } from "@elementor/ui";
105
+ var ModalHeader = ({ title, content }) => {
106
+ return /* @__PURE__ */ React2.createElement(Stack, { gap: 0.75 }, /* @__PURE__ */ React2.createElement(Typography, { variant: "h4", color: "text.primary" }, title), /* @__PURE__ */ React2.createElement(Typography, { variant: "subtitle2", color: "text.primary" }, content));
107
+ };
108
+
109
+ // src/components/modal-footer.tsx
110
+ import * as React3 from "react";
111
+ import { Button, Stack as Stack2, Typography as Typography2 } from "@elementor/ui";
112
+ var ModalFooter = ({ helpText, link }) => {
113
+ return /* @__PURE__ */ React3.createElement(Stack2, { direction: "row", alignItems: "center", gap: 1 }, /* @__PURE__ */ React3.createElement(Typography2, { variant: "caption", color: "text.tertiary", sx: { fontSize: "11px", lineHeight: "normal" } }, helpText), /* @__PURE__ */ React3.createElement(
114
+ Button,
115
+ {
116
+ href: link.url,
117
+ target: "_blank",
118
+ variant: "text",
119
+ size: "small",
120
+ color: "info",
121
+ sx: { fontSize: "11px" }
122
+ },
123
+ link.text
124
+ ));
125
+ };
126
+
127
+ // src/components/background-lottie.tsx
128
+ import * as React4 from "react";
129
+ import { useEffect } from "react";
130
+ import Lottie from "lottie-react";
131
+ function BackgroundLottie({
132
+ animationData,
133
+ loop = false,
134
+ autoplay = true,
135
+ zIndex = MODAL_Z_INDEX - 1,
136
+ backgroundColor = "transparent",
137
+ onComplete = () => {
138
+ }
139
+ }) {
140
+ const prefersReducedMotion2 = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
141
+ useEffect(() => {
142
+ if (prefersReducedMotion2) {
143
+ onComplete?.();
144
+ }
145
+ }, [prefersReducedMotion2, onComplete]);
146
+ if (prefersReducedMotion2) {
147
+ return null;
148
+ }
149
+ return /* @__PURE__ */ React4.createElement(
150
+ Lottie,
151
+ {
152
+ animationData,
153
+ loop,
154
+ autoplay,
155
+ onComplete: loop ? void 0 : onComplete,
156
+ onDataFailed: onComplete,
157
+ rendererSettings: {
158
+ preserveAspectRatio: "xMidYMid slice"
159
+ },
160
+ style: {
161
+ position: "fixed",
162
+ inset: 0,
163
+ width: "100vw",
164
+ height: "100vh",
165
+ zIndex,
166
+ backgroundColor,
167
+ pointerEvents: "none"
168
+ }
169
+ }
170
+ );
171
+ }
172
+
173
+ // src/hooks/use-autoplay-carousel.ts
174
+ import { useCallback as useCallback2, useEffect as useEffect2, useState as useState2 } from "react";
175
+ var DEFAULT_INTERVAL_MS = 4e3;
176
+ function useAutoplayCarousel(items, intervalMs = DEFAULT_INTERVAL_MS) {
177
+ const [selectedItem, setSelectedItem] = useState2(items[0]);
178
+ const [isAutoPlaying, setIsAutoPlaying] = useState2(true);
179
+ const advanceToNextItem = useCallback2(() => {
180
+ setSelectedItem((current) => {
181
+ const currentIndex = items.indexOf(current);
182
+ const nextIndex = (currentIndex + 1) % items.length;
183
+ return items[nextIndex];
184
+ });
185
+ }, [items]);
186
+ useEffect2(() => {
187
+ if (!isAutoPlaying) {
188
+ return;
189
+ }
190
+ const id = setInterval(advanceToNextItem, intervalMs);
191
+ return () => clearInterval(id);
192
+ }, [isAutoPlaying, advanceToNextItem, intervalMs]);
193
+ const selectItem = useCallback2((item) => {
194
+ setSelectedItem(item);
195
+ setIsAutoPlaying(false);
196
+ }, []);
197
+ return { selectedItem, selectItem };
198
+ }
199
+ export {
200
+ BackgroundLottie,
201
+ MODAL_Z_INDEX,
202
+ ModalFooter,
203
+ ModalHeader,
204
+ ModalShell,
205
+ useAutoplayCarousel,
206
+ useModalShell
207
+ };
208
+ //# sourceMappingURL=index.mjs.map
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@elementor/editor-modal-shell",
3
+ "description": "Reusable modal shell primitives for Elementor editor promotional and announcement modals",
4
+ "version": "4.2.0-919",
5
+ "private": false,
6
+ "author": "Elementor Team",
7
+ "homepage": "https://elementor.com/",
8
+ "license": "GPL-3.0-or-later",
9
+ "main": "dist/index.js",
10
+ "module": "dist/index.mjs",
11
+ "types": "dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.mjs",
16
+ "require": "./dist/index.js"
17
+ },
18
+ "./package.json": "./package.json"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/elementor/elementor.git",
23
+ "directory": "packages/libs/editor-modal-shell"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/elementor/elementor/issues"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "scripts": {
32
+ "build": "tsup --config=../../tsup.build.ts",
33
+ "dev": "tsup --config=../../tsup.dev.ts"
34
+ },
35
+ "peerDependencies": {
36
+ "react": "^18.3.1",
37
+ "react-dom": "^18.3.1"
38
+ },
39
+ "dependencies": {
40
+ "@elementor/icons": "~1.75.1",
41
+ "@elementor/ui": "1.37.5",
42
+ "lottie-react": "^2.4.1"
43
+ },
44
+ "devDependencies": {
45
+ "tsup": "^8.3.5"
46
+ }
47
+ }
@@ -0,0 +1,60 @@
1
+ import * as React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+
4
+ import { BackgroundLottie } from '../background-lottie';
5
+
6
+ const LOTTIE_LABEL = 'mock-lottie-player';
7
+
8
+ jest.mock( 'lottie-react', () => ( {
9
+ __esModule: true,
10
+ default: () => <div aria-label={ LOTTIE_LABEL } />,
11
+ } ) );
12
+
13
+ function mockMatchMedia( reducedMotion: boolean ) {
14
+ Object.defineProperty( window, 'matchMedia', {
15
+ writable: true,
16
+ configurable: true,
17
+ value: jest.fn().mockImplementation( ( query: string ) => ( {
18
+ matches: query === '(prefers-reduced-motion: reduce)' ? reducedMotion : false,
19
+ media: query,
20
+ addListener: jest.fn(),
21
+ removeListener: jest.fn(),
22
+ addEventListener: jest.fn(),
23
+ removeEventListener: jest.fn(),
24
+ dispatchEvent: jest.fn(),
25
+ onchange: null,
26
+ } ) ),
27
+ } );
28
+ }
29
+
30
+ describe( 'BackgroundLottie', () => {
31
+ const animationData = {};
32
+
33
+ it( 'should render the Lottie player by default', () => {
34
+ mockMatchMedia( false );
35
+
36
+ render( <BackgroundLottie animationData={ animationData } /> );
37
+
38
+ expect( screen.getByLabelText( LOTTIE_LABEL ) ).toBeInTheDocument();
39
+ } );
40
+
41
+ describe( 'when prefers-reduced-motion is reduce', () => {
42
+ beforeEach( () => {
43
+ mockMatchMedia( true );
44
+ } );
45
+
46
+ it( 'should not render the Lottie player', () => {
47
+ render( <BackgroundLottie animationData={ animationData } /> );
48
+
49
+ expect( screen.queryByLabelText( LOTTIE_LABEL ) ).not.toBeInTheDocument();
50
+ } );
51
+
52
+ it( 'should call onComplete from a side-effect, not during render', () => {
53
+ const onComplete = jest.fn();
54
+
55
+ render( <BackgroundLottie animationData={ animationData } onComplete={ onComplete } /> );
56
+
57
+ expect( onComplete ).toHaveBeenCalledTimes( 1 );
58
+ } );
59
+ } );
60
+ } );
@@ -0,0 +1,167 @@
1
+ import * as React from 'react';
2
+ import { act, fireEvent, render, renderHook, screen } from '@testing-library/react';
3
+
4
+ import { ModalShell, useModalShell } from '../modal-shell';
5
+
6
+ const EXIT_TRANSITION_MS = 225;
7
+
8
+ function mockMatchMedia( reducedMotion: boolean ) {
9
+ Object.defineProperty( window, 'matchMedia', {
10
+ writable: true,
11
+ configurable: true,
12
+ value: jest.fn().mockImplementation( ( query: string ) => ( {
13
+ matches: query === '(prefers-reduced-motion: reduce)' ? reducedMotion : false,
14
+ media: query,
15
+ addListener: jest.fn(),
16
+ removeListener: jest.fn(),
17
+ addEventListener: jest.fn(),
18
+ removeEventListener: jest.fn(),
19
+ dispatchEvent: jest.fn(),
20
+ onchange: null,
21
+ } ) ),
22
+ } );
23
+ }
24
+
25
+ beforeEach( () => {
26
+ mockMatchMedia( false );
27
+ jest.useFakeTimers();
28
+ } );
29
+
30
+ afterEach( () => {
31
+ if ( jest.isMockFunction( setTimeout ) ) {
32
+ jest.runOnlyPendingTimers();
33
+ }
34
+
35
+ jest.useRealTimers();
36
+ } );
37
+
38
+ describe( 'ModalShell', () => {
39
+ it( 'should render children inside the dialog', () => {
40
+ render(
41
+ <ModalShell>
42
+ <div>shell content</div>
43
+ </ModalShell>
44
+ );
45
+
46
+ expect( screen.getByRole( 'dialog' ) ).toBeInTheDocument();
47
+ expect( screen.getByText( 'shell content' ) ).toBeInTheDocument();
48
+ } );
49
+
50
+ it( 'should render the close button by default', () => {
51
+ render(
52
+ <ModalShell>
53
+ <div>x</div>
54
+ </ModalShell>
55
+ );
56
+
57
+ expect( screen.getByRole( 'button', { name: /close/i } ) ).toBeInTheDocument();
58
+ } );
59
+
60
+ it( 'should not render the close button when hideCloseButton is true', () => {
61
+ render(
62
+ <ModalShell hideCloseButton>
63
+ <div>x</div>
64
+ </ModalShell>
65
+ );
66
+
67
+ expect( screen.queryByRole( 'button', { name: /close/i } ) ).not.toBeInTheDocument();
68
+ } );
69
+
70
+ it( 'should call onClose only after the exit transition completes', () => {
71
+ const onClose = jest.fn();
72
+
73
+ render(
74
+ <ModalShell onClose={ onClose }>
75
+ <div>x</div>
76
+ </ModalShell>
77
+ );
78
+
79
+ fireEvent.click( screen.getByRole( 'button', { name: /close/i } ) );
80
+
81
+ expect( onClose ).not.toHaveBeenCalled();
82
+
83
+ act( () => {
84
+ jest.advanceTimersByTime( EXIT_TRANSITION_MS );
85
+ } );
86
+
87
+ expect( onClose ).toHaveBeenCalledTimes( 1 );
88
+ } );
89
+
90
+ it( 'should ignore Escape key when closeOnEsc is false', () => {
91
+ const onClose = jest.fn();
92
+
93
+ render(
94
+ <ModalShell onClose={ onClose } closeOnEsc={ false }>
95
+ <div>x</div>
96
+ </ModalShell>
97
+ );
98
+
99
+ fireEvent.keyDown( screen.getByRole( 'dialog' ), { key: 'Escape' } );
100
+
101
+ act( () => {
102
+ jest.advanceTimersByTime( EXIT_TRANSITION_MS );
103
+ } );
104
+
105
+ expect( onClose ).not.toHaveBeenCalled();
106
+ } );
107
+
108
+ it( 'should expose a close() function via useModalShell context', () => {
109
+ const onClose = jest.fn();
110
+ const ChildCta = () => {
111
+ const { close } = useModalShell();
112
+ return (
113
+ <button type="button" onClick={ close }>
114
+ trigger
115
+ </button>
116
+ );
117
+ };
118
+
119
+ render(
120
+ <ModalShell onClose={ onClose }>
121
+ <ChildCta />
122
+ </ModalShell>
123
+ );
124
+
125
+ fireEvent.click( screen.getByRole( 'button', { name: 'trigger' } ) );
126
+
127
+ act( () => {
128
+ jest.advanceTimersByTime( EXIT_TRANSITION_MS );
129
+ } );
130
+
131
+ expect( onClose ).toHaveBeenCalledTimes( 1 );
132
+ } );
133
+
134
+ it( 'should throw when useModalShell is used outside a provider', () => {
135
+ const consoleError = jest.spyOn( console, 'error' ).mockImplementation( () => {} );
136
+
137
+ expect( () => renderHook( () => useModalShell() ) ).toThrow(
138
+ 'useModalShell must be used inside a <ModalShell />'
139
+ );
140
+
141
+ consoleError.mockRestore();
142
+ } );
143
+
144
+ describe( 'when prefers-reduced-motion is reduce', () => {
145
+ beforeEach( () => {
146
+ mockMatchMedia( true );
147
+ } );
148
+
149
+ it( 'should close without waiting for an exit transition', () => {
150
+ const onClose = jest.fn();
151
+
152
+ render(
153
+ <ModalShell onClose={ onClose }>
154
+ <div>x</div>
155
+ </ModalShell>
156
+ );
157
+
158
+ fireEvent.click( screen.getByRole( 'button', { name: /close/i } ) );
159
+
160
+ act( () => {
161
+ jest.advanceTimersByTime( 0 );
162
+ } );
163
+
164
+ expect( onClose ).toHaveBeenCalledTimes( 1 );
165
+ } );
166
+ } );
167
+ } );
@@ -0,0 +1,57 @@
1
+ import * as React from 'react';
2
+ import { useEffect } from 'react';
3
+ import Lottie from 'lottie-react';
4
+
5
+ import { MODAL_Z_INDEX } from './modal-shell';
6
+
7
+ type BackgroundLottieProps = {
8
+ animationData: object;
9
+ loop?: boolean;
10
+ autoplay?: boolean;
11
+ zIndex?: number;
12
+ backgroundColor?: string;
13
+ onComplete?: () => void;
14
+ };
15
+
16
+ export function BackgroundLottie( {
17
+ animationData,
18
+ loop = false,
19
+ autoplay = true,
20
+ zIndex = MODAL_Z_INDEX - 1,
21
+ backgroundColor = 'transparent',
22
+ onComplete = () => {},
23
+ }: BackgroundLottieProps ) {
24
+ const prefersReducedMotion =
25
+ typeof window !== 'undefined' && window.matchMedia?.( '(prefers-reduced-motion: reduce)' ).matches;
26
+
27
+ useEffect( () => {
28
+ if ( prefersReducedMotion ) {
29
+ onComplete?.();
30
+ }
31
+ }, [ prefersReducedMotion, onComplete ] );
32
+ if ( prefersReducedMotion ) {
33
+ return null;
34
+ }
35
+
36
+ return (
37
+ <Lottie
38
+ animationData={ animationData }
39
+ loop={ loop }
40
+ autoplay={ autoplay }
41
+ onComplete={ loop ? undefined : onComplete }
42
+ onDataFailed={ onComplete }
43
+ rendererSettings={ {
44
+ preserveAspectRatio: 'xMidYMid slice',
45
+ } }
46
+ style={ {
47
+ position: 'fixed',
48
+ inset: 0,
49
+ width: '100vw',
50
+ height: '100vh',
51
+ zIndex,
52
+ backgroundColor,
53
+ pointerEvents: 'none',
54
+ } }
55
+ />
56
+ );
57
+ }
@@ -0,0 +1,32 @@
1
+ import * as React from 'react';
2
+ import { Button, Stack, Typography } from '@elementor/ui';
3
+
4
+ type FooterLinkData = {
5
+ text: string;
6
+ url: string;
7
+ };
8
+
9
+ type ModalFooterProps = {
10
+ helpText: string;
11
+ link: FooterLinkData;
12
+ };
13
+
14
+ export const ModalFooter = ( { helpText, link }: ModalFooterProps ) => {
15
+ return (
16
+ <Stack direction="row" alignItems="center" gap={ 1 }>
17
+ <Typography variant="caption" color="text.tertiary" sx={ { fontSize: '11px', lineHeight: 'normal' } }>
18
+ { helpText }
19
+ </Typography>
20
+ <Button
21
+ href={ link.url }
22
+ target="_blank"
23
+ variant="text"
24
+ size="small"
25
+ color="info"
26
+ sx={ { fontSize: '11px' } }
27
+ >
28
+ { link.text }
29
+ </Button>
30
+ </Stack>
31
+ );
32
+ };
@@ -0,0 +1,21 @@
1
+ import * as React from 'react';
2
+ import { type ReactNode } from 'react';
3
+ import { Stack, Typography } from '@elementor/ui';
4
+
5
+ type ModalHeaderProps = {
6
+ title: string;
7
+ content: ReactNode;
8
+ };
9
+
10
+ export const ModalHeader = ( { title, content }: ModalHeaderProps ) => {
11
+ return (
12
+ <Stack gap={ 0.75 }>
13
+ <Typography variant="h4" color="text.primary">
14
+ { title }
15
+ </Typography>
16
+ <Typography variant="subtitle2" color="text.primary">
17
+ { content }
18
+ </Typography>
19
+ </Stack>
20
+ );
21
+ };
@@ -0,0 +1,144 @@
1
+ import * as React from 'react';
2
+ import { createContext, type ReactNode, useCallback, useContext, useMemo, useState } from 'react';
3
+ import { Box, CloseButton, Dialog, type SxProps, type Theme } from '@elementor/ui';
4
+
5
+ export const MODAL_Z_INDEX = 99999;
6
+
7
+ const DEFAULT_WIDTH = 800;
8
+ const DEFAULT_HEIGHT = 400;
9
+ const DEFAULT_REVEAL_DURATION_MS = 500;
10
+ const DEFAULT_REVEAL_DELAY_MS = 0;
11
+
12
+ type CloseReason = 'escapeKeyDown' | 'backdropClick';
13
+
14
+ type ModalShellContextValue = {
15
+ close: () => void;
16
+ };
17
+
18
+ const ModalShellContext = createContext< ModalShellContextValue | null >( null );
19
+
20
+ export const useModalShell = (): ModalShellContextValue => {
21
+ const ctx = useContext( ModalShellContext );
22
+
23
+ if ( ! ctx ) {
24
+ throw new Error( 'useModalShell must be used inside a <ModalShell />' );
25
+ }
26
+
27
+ return ctx;
28
+ };
29
+
30
+ const EXIT_TRANSITION_MS = 225;
31
+
32
+ const prefersReducedMotion = (): boolean =>
33
+ typeof window !== 'undefined' && Boolean( window.matchMedia?.( '(prefers-reduced-motion: reduce)' ).matches );
34
+
35
+ export type ModalShellProps = {
36
+ children: ReactNode;
37
+ onClose?: () => void;
38
+ revealDuration?: number;
39
+ revealDelay?: number;
40
+ container?: HTMLElement;
41
+ sx?: SxProps< Theme >;
42
+ closeOnEsc?: boolean;
43
+ closeOnOutsideClick?: boolean;
44
+ backdrop?: boolean;
45
+ backdropSx?: SxProps< Theme >;
46
+ hideCloseButton?: boolean;
47
+ };
48
+
49
+ export const ModalShell = ( {
50
+ children,
51
+ onClose,
52
+ revealDuration = DEFAULT_REVEAL_DURATION_MS,
53
+ revealDelay = DEFAULT_REVEAL_DELAY_MS,
54
+ container,
55
+ sx = {},
56
+ closeOnEsc = true,
57
+ closeOnOutsideClick = true,
58
+ backdrop = true,
59
+ backdropSx = {},
60
+ hideCloseButton = false,
61
+ }: ModalShellProps ) => {
62
+ const portalTarget = container ?? ( typeof document !== 'undefined' ? document.body : undefined );
63
+ const reducedMotion = prefersReducedMotion();
64
+ const consumerOwnsEnter = revealDuration === 0 || reducedMotion;
65
+ const [ open, setOpen ] = useState( true );
66
+
67
+ const startClose = useCallback( () => setOpen( false ), [] );
68
+
69
+ const contextValue = useMemo< ModalShellContextValue >( () => ( { close: startClose } ), [ startClose ] );
70
+
71
+ const handleDialogClose = ( _event: object, reason: CloseReason ) => {
72
+ if ( reason === 'escapeKeyDown' && ! closeOnEsc ) {
73
+ return;
74
+ }
75
+
76
+ if ( reason === 'backdropClick' && ! closeOnOutsideClick ) {
77
+ return;
78
+ }
79
+
80
+ startClose();
81
+ };
82
+
83
+ const transitionTimeouts = useMemo(
84
+ () => ( { enter: 0, exit: reducedMotion ? 0 : EXIT_TRANSITION_MS } ),
85
+ [ reducedMotion ]
86
+ );
87
+
88
+ const animationProps = useMemo(
89
+ () =>
90
+ consumerOwnsEnter
91
+ ? {}
92
+ : {
93
+ animation: `e-modal-shell-reveal ${ revealDuration }ms ease ${ revealDelay }ms backwards`,
94
+ '@keyframes e-modal-shell-reveal': {
95
+ from: { opacity: 0, transform: 'scale(0.95)' },
96
+ to: { opacity: 1, transform: 'scale(1)' },
97
+ },
98
+ },
99
+ [ consumerOwnsEnter, revealDuration, revealDelay ]
100
+ );
101
+
102
+ return (
103
+ <Dialog
104
+ open={ open }
105
+ maxWidth={ false }
106
+ onClose={ handleDialogClose }
107
+ container={ portalTarget }
108
+ disableEscapeKeyDown={ ! closeOnEsc }
109
+ hideBackdrop={ ! backdrop }
110
+ TransitionProps={ { onExited: onClose, timeout: transitionTimeouts } }
111
+ slotProps={ { backdrop: { transitionDuration: transitionTimeouts, sx: backdropSx } } }
112
+ PaperProps={ {
113
+ sx: {
114
+ width: DEFAULT_WIDTH,
115
+ height: DEFAULT_HEIGHT,
116
+ maxWidth: '100%',
117
+ overflow: 'hidden',
118
+ ...animationProps,
119
+ ...sx,
120
+ },
121
+ } }
122
+ sx={ {
123
+ zIndex: MODAL_Z_INDEX,
124
+ } }
125
+ >
126
+ <ModalShellContext.Provider value={ contextValue }>
127
+ <Box sx={ { display: 'contents' } }>
128
+ { children }
129
+ { ! hideCloseButton && (
130
+ <CloseButton
131
+ onClick={ startClose }
132
+ sx={ {
133
+ position: 'absolute',
134
+ right: 16,
135
+ top: 16,
136
+ zIndex: 3,
137
+ } }
138
+ />
139
+ ) }
140
+ </Box>
141
+ </ModalShellContext.Provider>
142
+ </Dialog>
143
+ );
144
+ };
@@ -0,0 +1,59 @@
1
+ import { act, renderHook } from '@testing-library/react';
2
+
3
+ import { useAutoplayCarousel } from '../use-autoplay-carousel';
4
+
5
+ const ITEMS = [ 'a', 'b', 'c' ];
6
+ const INTERVAL_MS = 4000;
7
+
8
+ beforeEach( () => {
9
+ jest.useFakeTimers();
10
+ } );
11
+
12
+ afterEach( () => {
13
+ jest.runOnlyPendingTimers();
14
+ jest.clearAllTimers();
15
+ } );
16
+
17
+ describe( 'useAutoplayCarousel', () => {
18
+ it( 'should initialize with the first item selected', () => {
19
+ const { result } = renderHook( () => useAutoplayCarousel( ITEMS ) );
20
+
21
+ expect( result.current.selectedItem ).toBe( 'a' );
22
+ } );
23
+
24
+ it( 'should advance to the next item after the default interval', () => {
25
+ const { result } = renderHook( () => useAutoplayCarousel( ITEMS ) );
26
+
27
+ act( () => {
28
+ jest.advanceTimersByTime( INTERVAL_MS );
29
+ } );
30
+
31
+ expect( result.current.selectedItem ).toBe( 'b' );
32
+ } );
33
+
34
+ it( 'should cycle back to the first item after reaching the last', () => {
35
+ const { result } = renderHook( () => useAutoplayCarousel( ITEMS ) );
36
+
37
+ act( () => {
38
+ jest.advanceTimersByTime( INTERVAL_MS * ITEMS.length );
39
+ } );
40
+
41
+ expect( result.current.selectedItem ).toBe( 'a' );
42
+ } );
43
+
44
+ it( 'should stop autoplay when an item is manually selected', () => {
45
+ const { result } = renderHook( () => useAutoplayCarousel( ITEMS ) );
46
+
47
+ act( () => {
48
+ result.current.selectItem( 'c' );
49
+ } );
50
+
51
+ expect( result.current.selectedItem ).toBe( 'c' );
52
+
53
+ act( () => {
54
+ jest.advanceTimersByTime( 10_000 );
55
+ } );
56
+
57
+ expect( result.current.selectedItem ).toBe( 'c' );
58
+ } );
59
+ } );
@@ -0,0 +1,33 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+
3
+ const DEFAULT_INTERVAL_MS = 4000;
4
+
5
+ export function useAutoplayCarousel< T >( items: T[], intervalMs = DEFAULT_INTERVAL_MS ) {
6
+ const [ selectedItem, setSelectedItem ] = useState< T >( items[ 0 ] );
7
+ const [ isAutoPlaying, setIsAutoPlaying ] = useState( true );
8
+
9
+ const advanceToNextItem = useCallback( () => {
10
+ setSelectedItem( ( current ) => {
11
+ const currentIndex = items.indexOf( current );
12
+ const nextIndex = ( currentIndex + 1 ) % items.length;
13
+ return items[ nextIndex ];
14
+ } );
15
+ }, [ items ] );
16
+
17
+ useEffect( () => {
18
+ if ( ! isAutoPlaying ) {
19
+ return;
20
+ }
21
+
22
+ const id = setInterval( advanceToNextItem, intervalMs );
23
+
24
+ return () => clearInterval( id );
25
+ }, [ isAutoPlaying, advanceToNextItem, intervalMs ] );
26
+
27
+ const selectItem = useCallback( ( item: T ) => {
28
+ setSelectedItem( item );
29
+ setIsAutoPlaying( false );
30
+ }, [] );
31
+
32
+ return { selectedItem, selectItem };
33
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { ModalShell, MODAL_Z_INDEX, type ModalShellProps, useModalShell } from './components/modal-shell';
2
+ export { ModalHeader } from './components/modal-header';
3
+ export { ModalFooter } from './components/modal-footer';
4
+ export { BackgroundLottie } from './components/background-lottie';
5
+ export { useAutoplayCarousel } from './hooks/use-autoplay-carousel';