@edux-design/modals 0.1.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 +13 -0
- package/README.md +63 -0
- package/dist/index.js +219 -0
- package/dist/index.mjs +173 -0
- package/package.json +37 -0
- package/src/demos/Modal.stories.jsx +168 -0
- package/src/elements/Modal.jsx +176 -0
- package/src/elements/Modal.test.jsx +111 -0
- package/src/index.js +13 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# @edux-design/modals
|
|
2
|
+
|
|
3
|
+
Centered modal primitives built on top of Radix Dialog and Edux design tokens.
|
|
4
|
+
|
|
5
|
+
Exports:
|
|
6
|
+
|
|
7
|
+
- `Modal`
|
|
8
|
+
- `ModalTrigger`
|
|
9
|
+
- `ModalClose`
|
|
10
|
+
- `ModalPortal`
|
|
11
|
+
- `ModalOverlay`
|
|
12
|
+
- `ModalContent`
|
|
13
|
+
- `ModalHeader`
|
|
14
|
+
- `ModalBody`
|
|
15
|
+
- `ModalFooter`
|
|
16
|
+
- `ModalTitle`
|
|
17
|
+
- `ModalDescription`
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```jsx
|
|
22
|
+
import {
|
|
23
|
+
Modal,
|
|
24
|
+
ModalTrigger,
|
|
25
|
+
ModalContent,
|
|
26
|
+
ModalHeader,
|
|
27
|
+
ModalBody,
|
|
28
|
+
ModalFooter,
|
|
29
|
+
ModalClose,
|
|
30
|
+
ModalTitle,
|
|
31
|
+
ModalDescription,
|
|
32
|
+
} from "@edux-design/modals";
|
|
33
|
+
import { Button } from "@edux-design/buttons";
|
|
34
|
+
|
|
35
|
+
export function PublishModal() {
|
|
36
|
+
return (
|
|
37
|
+
<Modal>
|
|
38
|
+
<ModalTrigger asChild>
|
|
39
|
+
<Button variant="primary">Open Modal</Button>
|
|
40
|
+
</ModalTrigger>
|
|
41
|
+
<ModalContent size="medium">
|
|
42
|
+
<ModalHeader>
|
|
43
|
+
<ModalTitle>Publish changes</ModalTitle>
|
|
44
|
+
<ModalDescription>
|
|
45
|
+
Review content before making it visible.
|
|
46
|
+
</ModalDescription>
|
|
47
|
+
</ModalHeader>
|
|
48
|
+
<ModalBody>
|
|
49
|
+
<p>Modal body content.</p>
|
|
50
|
+
</ModalBody>
|
|
51
|
+
<ModalFooter>
|
|
52
|
+
<ModalClose asChild>
|
|
53
|
+
<Button variant="secondary">Cancel</Button>
|
|
54
|
+
</ModalClose>
|
|
55
|
+
<Button variant="primary">Publish</Button>
|
|
56
|
+
</ModalFooter>
|
|
57
|
+
</ModalContent>
|
|
58
|
+
</Modal>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`ModalContent` accepts `size`, `backdropProps`, `backDropProps`, `className`, and `contentProps`. Storybook examples live in `src/demos/Modal.stories.jsx`.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
|
|
29
|
+
// src/index.js
|
|
30
|
+
var index_exports = {};
|
|
31
|
+
__export(index_exports, {
|
|
32
|
+
Modal: () => Modal,
|
|
33
|
+
ModalBody: () => ModalBody,
|
|
34
|
+
ModalClose: () => ModalClose,
|
|
35
|
+
ModalContent: () => ModalContent,
|
|
36
|
+
ModalDescription: () => ModalDescription,
|
|
37
|
+
ModalFooter: () => ModalFooter,
|
|
38
|
+
ModalHeader: () => ModalHeader,
|
|
39
|
+
ModalOverlay: () => ModalOverlay,
|
|
40
|
+
ModalPortal: () => ModalPortal,
|
|
41
|
+
ModalTitle: () => ModalTitle,
|
|
42
|
+
ModalTrigger: () => ModalTrigger
|
|
43
|
+
});
|
|
44
|
+
module.exports = __toCommonJS(index_exports);
|
|
45
|
+
|
|
46
|
+
// src/elements/Modal.jsx
|
|
47
|
+
var import_react = __toESM(require("react"));
|
|
48
|
+
var DialogPrimitive = __toESM(require("@radix-ui/react-dialog"));
|
|
49
|
+
var import_utils = require("@edux-design/utils");
|
|
50
|
+
var MODAL_OVERLAY_Z_INDEX = 2147483645;
|
|
51
|
+
var MODAL_CONTENT_Z_INDEX = 2147483646;
|
|
52
|
+
var getSizeClasses = (size) => {
|
|
53
|
+
switch (size) {
|
|
54
|
+
case "small":
|
|
55
|
+
return "w-[min(92vw,420px)]";
|
|
56
|
+
case "large":
|
|
57
|
+
return "w-[min(92vw,840px)]";
|
|
58
|
+
case "full":
|
|
59
|
+
return "w-[min(96vw,1200px)] h-[min(92vh,900px)]";
|
|
60
|
+
case "medium":
|
|
61
|
+
default:
|
|
62
|
+
return "w-[min(92vw,640px)]";
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var Modal = ({
|
|
66
|
+
open,
|
|
67
|
+
defaultOpen,
|
|
68
|
+
onOpenChange,
|
|
69
|
+
onOpen,
|
|
70
|
+
onClose,
|
|
71
|
+
children,
|
|
72
|
+
...props
|
|
73
|
+
}) => {
|
|
74
|
+
const handleOpenChange = (0, import_react.useCallback)(
|
|
75
|
+
(nextOpen) => {
|
|
76
|
+
onOpenChange == null ? void 0 : onOpenChange(nextOpen);
|
|
77
|
+
if (nextOpen) {
|
|
78
|
+
onOpen == null ? void 0 : onOpen();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
onClose == null ? void 0 : onClose();
|
|
82
|
+
},
|
|
83
|
+
[onClose, onOpen, onOpenChange]
|
|
84
|
+
);
|
|
85
|
+
return /* @__PURE__ */ import_react.default.createElement(
|
|
86
|
+
DialogPrimitive.Root,
|
|
87
|
+
{
|
|
88
|
+
open,
|
|
89
|
+
defaultOpen,
|
|
90
|
+
onOpenChange: handleOpenChange,
|
|
91
|
+
...props
|
|
92
|
+
},
|
|
93
|
+
children
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
var ModalTrigger = DialogPrimitive.Trigger;
|
|
97
|
+
var ModalClose = DialogPrimitive.Close;
|
|
98
|
+
var ModalPortal = DialogPrimitive.Portal;
|
|
99
|
+
var ModalOverlay = (0, import_react.forwardRef)(
|
|
100
|
+
({ className, style, ...props }, ref) => /* @__PURE__ */ import_react.default.createElement(
|
|
101
|
+
DialogPrimitive.Overlay,
|
|
102
|
+
{
|
|
103
|
+
ref,
|
|
104
|
+
className: (0, import_utils.cx)(
|
|
105
|
+
"fixed inset-0 bg-bg-focus-overlay opacity-70",
|
|
106
|
+
"data-[state=open]:animate-drawer-fade-in data-[state=closed]:animate-drawer-fade-out",
|
|
107
|
+
className
|
|
108
|
+
),
|
|
109
|
+
style: { zIndex: MODAL_OVERLAY_Z_INDEX, ...style },
|
|
110
|
+
...props
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
);
|
|
114
|
+
ModalOverlay.displayName = "ModalOverlay";
|
|
115
|
+
var ModalContent = (0, import_react.forwardRef)(
|
|
116
|
+
({
|
|
117
|
+
children,
|
|
118
|
+
size = "medium",
|
|
119
|
+
className,
|
|
120
|
+
style,
|
|
121
|
+
backdropProps,
|
|
122
|
+
backDropProps,
|
|
123
|
+
contentProps = {},
|
|
124
|
+
...restProps
|
|
125
|
+
}, ref) => {
|
|
126
|
+
const resolvedBackdropProps = backdropProps ?? backDropProps ?? {};
|
|
127
|
+
return /* @__PURE__ */ import_react.default.createElement(ModalPortal, null, /* @__PURE__ */ import_react.default.createElement(
|
|
128
|
+
ModalOverlay,
|
|
129
|
+
{
|
|
130
|
+
...resolvedBackdropProps,
|
|
131
|
+
className: (0, import_utils.cx)(resolvedBackdropProps.className),
|
|
132
|
+
style: { ...resolvedBackdropProps.style }
|
|
133
|
+
}
|
|
134
|
+
), /* @__PURE__ */ import_react.default.createElement(
|
|
135
|
+
DialogPrimitive.Content,
|
|
136
|
+
{
|
|
137
|
+
ref,
|
|
138
|
+
className: (0, import_utils.cx)(
|
|
139
|
+
"fixed left-1/2 top-1/2 max-h-[92vh] -translate-x-1/2 -translate-y-1/2",
|
|
140
|
+
"bg-bg-base text-fg-base shadow-xl outline-none rounded-2xl border border-border-subtle",
|
|
141
|
+
"flex flex-col overflow-hidden",
|
|
142
|
+
"data-[state=open]:animate-fade-in data-[state=closed]:animate-fade-out",
|
|
143
|
+
getSizeClasses(size),
|
|
144
|
+
className
|
|
145
|
+
),
|
|
146
|
+
style: { zIndex: MODAL_CONTENT_Z_INDEX, ...style },
|
|
147
|
+
...contentProps,
|
|
148
|
+
...restProps
|
|
149
|
+
},
|
|
150
|
+
children
|
|
151
|
+
));
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
ModalContent.displayName = "ModalContent";
|
|
155
|
+
var ModalHeader = (0, import_react.forwardRef)(({ className, ...props }, ref) => /* @__PURE__ */ import_react.default.createElement(
|
|
156
|
+
"div",
|
|
157
|
+
{
|
|
158
|
+
ref,
|
|
159
|
+
className: (0, import_utils.cx)(
|
|
160
|
+
"flex flex-col gap-4 px-20 py-16 border-b border-border-subtle",
|
|
161
|
+
className
|
|
162
|
+
),
|
|
163
|
+
...props
|
|
164
|
+
}
|
|
165
|
+
));
|
|
166
|
+
ModalHeader.displayName = "ModalHeader";
|
|
167
|
+
var ModalBody = (0, import_react.forwardRef)(({ className, ...props }, ref) => /* @__PURE__ */ import_react.default.createElement(
|
|
168
|
+
"div",
|
|
169
|
+
{
|
|
170
|
+
ref,
|
|
171
|
+
className: (0, import_utils.cx)("flex-1 overflow-auto px-20 py-16", className),
|
|
172
|
+
...props
|
|
173
|
+
}
|
|
174
|
+
));
|
|
175
|
+
ModalBody.displayName = "ModalBody";
|
|
176
|
+
var ModalFooter = (0, import_react.forwardRef)(({ className, ...props }, ref) => /* @__PURE__ */ import_react.default.createElement(
|
|
177
|
+
"div",
|
|
178
|
+
{
|
|
179
|
+
ref,
|
|
180
|
+
className: (0, import_utils.cx)(
|
|
181
|
+
"flex items-center justify-end gap-8 px-20 py-16 border-t border-border-subtle",
|
|
182
|
+
className
|
|
183
|
+
),
|
|
184
|
+
...props
|
|
185
|
+
}
|
|
186
|
+
));
|
|
187
|
+
ModalFooter.displayName = "ModalFooter";
|
|
188
|
+
var ModalTitle = (0, import_react.forwardRef)(({ className, ...props }, ref) => /* @__PURE__ */ import_react.default.createElement(
|
|
189
|
+
DialogPrimitive.Title,
|
|
190
|
+
{
|
|
191
|
+
ref,
|
|
192
|
+
className: (0, import_utils.cx)("text-lg font-semibold text-fg-base", className),
|
|
193
|
+
...props
|
|
194
|
+
}
|
|
195
|
+
));
|
|
196
|
+
ModalTitle.displayName = "ModalTitle";
|
|
197
|
+
var ModalDescription = (0, import_react.forwardRef)(({ className, ...props }, ref) => /* @__PURE__ */ import_react.default.createElement(
|
|
198
|
+
DialogPrimitive.Description,
|
|
199
|
+
{
|
|
200
|
+
ref,
|
|
201
|
+
className: (0, import_utils.cx)("text-sm text-fg-subtle", className),
|
|
202
|
+
...props
|
|
203
|
+
}
|
|
204
|
+
));
|
|
205
|
+
ModalDescription.displayName = "ModalDescription";
|
|
206
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
207
|
+
0 && (module.exports = {
|
|
208
|
+
Modal,
|
|
209
|
+
ModalBody,
|
|
210
|
+
ModalClose,
|
|
211
|
+
ModalContent,
|
|
212
|
+
ModalDescription,
|
|
213
|
+
ModalFooter,
|
|
214
|
+
ModalHeader,
|
|
215
|
+
ModalOverlay,
|
|
216
|
+
ModalPortal,
|
|
217
|
+
ModalTitle,
|
|
218
|
+
ModalTrigger
|
|
219
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// src/elements/Modal.jsx
|
|
2
|
+
import React, { forwardRef, useCallback } from "react";
|
|
3
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
4
|
+
import { cx } from "@edux-design/utils";
|
|
5
|
+
var MODAL_OVERLAY_Z_INDEX = 2147483645;
|
|
6
|
+
var MODAL_CONTENT_Z_INDEX = 2147483646;
|
|
7
|
+
var getSizeClasses = (size) => {
|
|
8
|
+
switch (size) {
|
|
9
|
+
case "small":
|
|
10
|
+
return "w-[min(92vw,420px)]";
|
|
11
|
+
case "large":
|
|
12
|
+
return "w-[min(92vw,840px)]";
|
|
13
|
+
case "full":
|
|
14
|
+
return "w-[min(96vw,1200px)] h-[min(92vh,900px)]";
|
|
15
|
+
case "medium":
|
|
16
|
+
default:
|
|
17
|
+
return "w-[min(92vw,640px)]";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var Modal = ({
|
|
21
|
+
open,
|
|
22
|
+
defaultOpen,
|
|
23
|
+
onOpenChange,
|
|
24
|
+
onOpen,
|
|
25
|
+
onClose,
|
|
26
|
+
children,
|
|
27
|
+
...props
|
|
28
|
+
}) => {
|
|
29
|
+
const handleOpenChange = useCallback(
|
|
30
|
+
(nextOpen) => {
|
|
31
|
+
onOpenChange == null ? void 0 : onOpenChange(nextOpen);
|
|
32
|
+
if (nextOpen) {
|
|
33
|
+
onOpen == null ? void 0 : onOpen();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
onClose == null ? void 0 : onClose();
|
|
37
|
+
},
|
|
38
|
+
[onClose, onOpen, onOpenChange]
|
|
39
|
+
);
|
|
40
|
+
return /* @__PURE__ */ React.createElement(
|
|
41
|
+
DialogPrimitive.Root,
|
|
42
|
+
{
|
|
43
|
+
open,
|
|
44
|
+
defaultOpen,
|
|
45
|
+
onOpenChange: handleOpenChange,
|
|
46
|
+
...props
|
|
47
|
+
},
|
|
48
|
+
children
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
var ModalTrigger = DialogPrimitive.Trigger;
|
|
52
|
+
var ModalClose = DialogPrimitive.Close;
|
|
53
|
+
var ModalPortal = DialogPrimitive.Portal;
|
|
54
|
+
var ModalOverlay = forwardRef(
|
|
55
|
+
({ className, style, ...props }, ref) => /* @__PURE__ */ React.createElement(
|
|
56
|
+
DialogPrimitive.Overlay,
|
|
57
|
+
{
|
|
58
|
+
ref,
|
|
59
|
+
className: cx(
|
|
60
|
+
"fixed inset-0 bg-bg-focus-overlay opacity-70",
|
|
61
|
+
"data-[state=open]:animate-drawer-fade-in data-[state=closed]:animate-drawer-fade-out",
|
|
62
|
+
className
|
|
63
|
+
),
|
|
64
|
+
style: { zIndex: MODAL_OVERLAY_Z_INDEX, ...style },
|
|
65
|
+
...props
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
);
|
|
69
|
+
ModalOverlay.displayName = "ModalOverlay";
|
|
70
|
+
var ModalContent = forwardRef(
|
|
71
|
+
({
|
|
72
|
+
children,
|
|
73
|
+
size = "medium",
|
|
74
|
+
className,
|
|
75
|
+
style,
|
|
76
|
+
backdropProps,
|
|
77
|
+
backDropProps,
|
|
78
|
+
contentProps = {},
|
|
79
|
+
...restProps
|
|
80
|
+
}, ref) => {
|
|
81
|
+
const resolvedBackdropProps = backdropProps ?? backDropProps ?? {};
|
|
82
|
+
return /* @__PURE__ */ React.createElement(ModalPortal, null, /* @__PURE__ */ React.createElement(
|
|
83
|
+
ModalOverlay,
|
|
84
|
+
{
|
|
85
|
+
...resolvedBackdropProps,
|
|
86
|
+
className: cx(resolvedBackdropProps.className),
|
|
87
|
+
style: { ...resolvedBackdropProps.style }
|
|
88
|
+
}
|
|
89
|
+
), /* @__PURE__ */ React.createElement(
|
|
90
|
+
DialogPrimitive.Content,
|
|
91
|
+
{
|
|
92
|
+
ref,
|
|
93
|
+
className: cx(
|
|
94
|
+
"fixed left-1/2 top-1/2 max-h-[92vh] -translate-x-1/2 -translate-y-1/2",
|
|
95
|
+
"bg-bg-base text-fg-base shadow-xl outline-none rounded-2xl border border-border-subtle",
|
|
96
|
+
"flex flex-col overflow-hidden",
|
|
97
|
+
"data-[state=open]:animate-fade-in data-[state=closed]:animate-fade-out",
|
|
98
|
+
getSizeClasses(size),
|
|
99
|
+
className
|
|
100
|
+
),
|
|
101
|
+
style: { zIndex: MODAL_CONTENT_Z_INDEX, ...style },
|
|
102
|
+
...contentProps,
|
|
103
|
+
...restProps
|
|
104
|
+
},
|
|
105
|
+
children
|
|
106
|
+
));
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
ModalContent.displayName = "ModalContent";
|
|
110
|
+
var ModalHeader = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ React.createElement(
|
|
111
|
+
"div",
|
|
112
|
+
{
|
|
113
|
+
ref,
|
|
114
|
+
className: cx(
|
|
115
|
+
"flex flex-col gap-4 px-20 py-16 border-b border-border-subtle",
|
|
116
|
+
className
|
|
117
|
+
),
|
|
118
|
+
...props
|
|
119
|
+
}
|
|
120
|
+
));
|
|
121
|
+
ModalHeader.displayName = "ModalHeader";
|
|
122
|
+
var ModalBody = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ React.createElement(
|
|
123
|
+
"div",
|
|
124
|
+
{
|
|
125
|
+
ref,
|
|
126
|
+
className: cx("flex-1 overflow-auto px-20 py-16", className),
|
|
127
|
+
...props
|
|
128
|
+
}
|
|
129
|
+
));
|
|
130
|
+
ModalBody.displayName = "ModalBody";
|
|
131
|
+
var ModalFooter = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ React.createElement(
|
|
132
|
+
"div",
|
|
133
|
+
{
|
|
134
|
+
ref,
|
|
135
|
+
className: cx(
|
|
136
|
+
"flex items-center justify-end gap-8 px-20 py-16 border-t border-border-subtle",
|
|
137
|
+
className
|
|
138
|
+
),
|
|
139
|
+
...props
|
|
140
|
+
}
|
|
141
|
+
));
|
|
142
|
+
ModalFooter.displayName = "ModalFooter";
|
|
143
|
+
var ModalTitle = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ React.createElement(
|
|
144
|
+
DialogPrimitive.Title,
|
|
145
|
+
{
|
|
146
|
+
ref,
|
|
147
|
+
className: cx("text-lg font-semibold text-fg-base", className),
|
|
148
|
+
...props
|
|
149
|
+
}
|
|
150
|
+
));
|
|
151
|
+
ModalTitle.displayName = "ModalTitle";
|
|
152
|
+
var ModalDescription = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ React.createElement(
|
|
153
|
+
DialogPrimitive.Description,
|
|
154
|
+
{
|
|
155
|
+
ref,
|
|
156
|
+
className: cx("text-sm text-fg-subtle", className),
|
|
157
|
+
...props
|
|
158
|
+
}
|
|
159
|
+
));
|
|
160
|
+
ModalDescription.displayName = "ModalDescription";
|
|
161
|
+
export {
|
|
162
|
+
Modal,
|
|
163
|
+
ModalBody,
|
|
164
|
+
ModalClose,
|
|
165
|
+
ModalContent,
|
|
166
|
+
ModalDescription,
|
|
167
|
+
ModalFooter,
|
|
168
|
+
ModalHeader,
|
|
169
|
+
ModalOverlay,
|
|
170
|
+
ModalPortal,
|
|
171
|
+
ModalTitle,
|
|
172
|
+
ModalTrigger
|
|
173
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@edux-design/modals",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"sideEffects": [
|
|
6
|
+
"**/*.css"
|
|
7
|
+
],
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"lint": "eslint . --max-warnings 0",
|
|
18
|
+
"build": "tsup src/index.js --format esm,cjs --external react,react-dom,@edux-design/utils,@radix-ui/react-dialog",
|
|
19
|
+
"dev": "tsup src/index.js --watch --format esm,cjs --external react,react-dom,@edux-design/utils,@radix-ui/react-dialog",
|
|
20
|
+
"generate:component": "turbo gen react-component",
|
|
21
|
+
"check-types": "tsc --noEmit"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"react": ">=18",
|
|
25
|
+
"react-dom": ">=18",
|
|
26
|
+
"@edux-design/utils": "*",
|
|
27
|
+
"@radix-ui/react-dialog": "1.1.5"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"typescript": "^5.9.2",
|
|
31
|
+
"tailwindcss": "^4.1.12",
|
|
32
|
+
"postcss": "^8.5.6",
|
|
33
|
+
"tsup": "^8.5.0",
|
|
34
|
+
"@tailwindcss/cli": "^4.1.12",
|
|
35
|
+
"@tailwindcss/postcss": "^4.1.12"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Modal,
|
|
4
|
+
ModalTrigger,
|
|
5
|
+
ModalContent,
|
|
6
|
+
ModalHeader,
|
|
7
|
+
ModalBody,
|
|
8
|
+
ModalFooter,
|
|
9
|
+
ModalClose,
|
|
10
|
+
ModalTitle,
|
|
11
|
+
ModalDescription,
|
|
12
|
+
} from "../index";
|
|
13
|
+
import { Button } from "@edux-design/buttons";
|
|
14
|
+
|
|
15
|
+
const meta = {
|
|
16
|
+
title: "Components/Modal",
|
|
17
|
+
component: Modal,
|
|
18
|
+
parameters: {
|
|
19
|
+
layout: "centered",
|
|
20
|
+
docs: {
|
|
21
|
+
description: {
|
|
22
|
+
component: `
|
|
23
|
+
Centered **Modal** built on Radix Dialog primitives.
|
|
24
|
+
|
|
25
|
+
Use for confirmations, forms, and focused blocking workflows.
|
|
26
|
+
`,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
argTypes: {
|
|
31
|
+
size: {
|
|
32
|
+
control: "select",
|
|
33
|
+
options: ["small", "medium", "large", "full"],
|
|
34
|
+
description: "Width preset for the modal content.",
|
|
35
|
+
table: { defaultValue: { summary: "medium" } },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default meta;
|
|
41
|
+
|
|
42
|
+
export const Basic = {
|
|
43
|
+
render: (args) => (
|
|
44
|
+
<Modal>
|
|
45
|
+
<ModalTrigger asChild>
|
|
46
|
+
<Button variant="primary">Open Modal</Button>
|
|
47
|
+
</ModalTrigger>
|
|
48
|
+
<ModalContent {...args}>
|
|
49
|
+
<ModalHeader>
|
|
50
|
+
<ModalTitle>Publish changes</ModalTitle>
|
|
51
|
+
<ModalDescription>
|
|
52
|
+
Review your changes before making them visible to learners.
|
|
53
|
+
</ModalDescription>
|
|
54
|
+
</ModalHeader>
|
|
55
|
+
<ModalBody>
|
|
56
|
+
<p className="text-sm text-fg-subtle">
|
|
57
|
+
This modal supports title, description, body, and footer regions.
|
|
58
|
+
</p>
|
|
59
|
+
</ModalBody>
|
|
60
|
+
<ModalFooter>
|
|
61
|
+
<ModalClose asChild>
|
|
62
|
+
<Button variant="secondary">Cancel</Button>
|
|
63
|
+
</ModalClose>
|
|
64
|
+
<Button variant="primary">Publish</Button>
|
|
65
|
+
</ModalFooter>
|
|
66
|
+
</ModalContent>
|
|
67
|
+
</Modal>
|
|
68
|
+
),
|
|
69
|
+
args: {
|
|
70
|
+
size: "medium",
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const Controlled = {
|
|
75
|
+
render: (args) => {
|
|
76
|
+
const [open, setOpen] = useState(false);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<Modal open={open} onOpenChange={setOpen}>
|
|
80
|
+
<ModalTrigger asChild>
|
|
81
|
+
<Button variant="primary">
|
|
82
|
+
{open ? "Close Modal" : "Open Modal"}
|
|
83
|
+
</Button>
|
|
84
|
+
</ModalTrigger>
|
|
85
|
+
<ModalContent {...args}>
|
|
86
|
+
<ModalHeader>
|
|
87
|
+
<ModalTitle>Controlled modal</ModalTitle>
|
|
88
|
+
<ModalDescription>
|
|
89
|
+
State is managed externally via open and onOpenChange.
|
|
90
|
+
</ModalDescription>
|
|
91
|
+
</ModalHeader>
|
|
92
|
+
<ModalBody>
|
|
93
|
+
<p className="text-sm text-fg-subtle">
|
|
94
|
+
Use this variant when the parent controls the workflow.
|
|
95
|
+
</p>
|
|
96
|
+
</ModalBody>
|
|
97
|
+
<ModalFooter>
|
|
98
|
+
<Button variant="bare" onClick={() => setOpen(false)}>
|
|
99
|
+
Dismiss
|
|
100
|
+
</Button>
|
|
101
|
+
</ModalFooter>
|
|
102
|
+
</ModalContent>
|
|
103
|
+
</Modal>
|
|
104
|
+
);
|
|
105
|
+
},
|
|
106
|
+
args: {
|
|
107
|
+
size: "medium",
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const Sizes = {
|
|
112
|
+
render: () => (
|
|
113
|
+
<div className="flex flex-wrap gap-12">
|
|
114
|
+
{["small", "medium", "large", "full"].map((size) => (
|
|
115
|
+
<Modal key={size}>
|
|
116
|
+
<ModalTrigger asChild>
|
|
117
|
+
<Button variant="secondary">Size: {size}</Button>
|
|
118
|
+
</ModalTrigger>
|
|
119
|
+
<ModalContent size={size}>
|
|
120
|
+
<ModalHeader>
|
|
121
|
+
<ModalTitle>{size} modal</ModalTitle>
|
|
122
|
+
<ModalDescription>
|
|
123
|
+
Demonstrates the {size} content width preset.
|
|
124
|
+
</ModalDescription>
|
|
125
|
+
</ModalHeader>
|
|
126
|
+
<ModalBody>
|
|
127
|
+
<p className="text-sm text-fg-subtle">
|
|
128
|
+
These presets use existing spacing, border, shadow, and
|
|
129
|
+
foreground/background tokens.
|
|
130
|
+
</p>
|
|
131
|
+
</ModalBody>
|
|
132
|
+
</ModalContent>
|
|
133
|
+
</Modal>
|
|
134
|
+
))}
|
|
135
|
+
</div>
|
|
136
|
+
),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export const CustomBackdrop = {
|
|
140
|
+
render: (args) => (
|
|
141
|
+
<Modal>
|
|
142
|
+
<ModalTrigger asChild>
|
|
143
|
+
<Button variant="primary">Open With Backdrop</Button>
|
|
144
|
+
</ModalTrigger>
|
|
145
|
+
<ModalContent
|
|
146
|
+
{...args}
|
|
147
|
+
backdropProps={{
|
|
148
|
+
className: "bg-bg-primary-subtle opacity-80",
|
|
149
|
+
}}
|
|
150
|
+
>
|
|
151
|
+
<ModalHeader>
|
|
152
|
+
<ModalTitle>Custom backdrop</ModalTitle>
|
|
153
|
+
<ModalDescription>
|
|
154
|
+
Backdrop styles can be customized via backdropProps.
|
|
155
|
+
</ModalDescription>
|
|
156
|
+
</ModalHeader>
|
|
157
|
+
<ModalBody>
|
|
158
|
+
<p className="text-sm text-fg-subtle">
|
|
159
|
+
Override color and opacity without changing the modal surface.
|
|
160
|
+
</p>
|
|
161
|
+
</ModalBody>
|
|
162
|
+
</ModalContent>
|
|
163
|
+
</Modal>
|
|
164
|
+
),
|
|
165
|
+
args: {
|
|
166
|
+
size: "medium",
|
|
167
|
+
},
|
|
168
|
+
};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import React, { forwardRef, useCallback } from "react";
|
|
2
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
3
|
+
import { cx } from "@edux-design/utils";
|
|
4
|
+
|
|
5
|
+
const MODAL_OVERLAY_Z_INDEX = 2147483645;
|
|
6
|
+
const MODAL_CONTENT_Z_INDEX = 2147483646;
|
|
7
|
+
|
|
8
|
+
const getSizeClasses = (size) => {
|
|
9
|
+
switch (size) {
|
|
10
|
+
case "small":
|
|
11
|
+
return "w-[min(92vw,420px)]";
|
|
12
|
+
case "large":
|
|
13
|
+
return "w-[min(92vw,840px)]";
|
|
14
|
+
case "full":
|
|
15
|
+
return "w-[min(96vw,1200px)] h-[min(92vh,900px)]";
|
|
16
|
+
case "medium":
|
|
17
|
+
default:
|
|
18
|
+
return "w-[min(92vw,640px)]";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const Modal = ({
|
|
23
|
+
open,
|
|
24
|
+
defaultOpen,
|
|
25
|
+
onOpenChange,
|
|
26
|
+
onOpen,
|
|
27
|
+
onClose,
|
|
28
|
+
children,
|
|
29
|
+
...props
|
|
30
|
+
}) => {
|
|
31
|
+
const handleOpenChange = useCallback(
|
|
32
|
+
(nextOpen) => {
|
|
33
|
+
onOpenChange?.(nextOpen);
|
|
34
|
+
if (nextOpen) {
|
|
35
|
+
onOpen?.();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
onClose?.();
|
|
39
|
+
},
|
|
40
|
+
[onClose, onOpen, onOpenChange]
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<DialogPrimitive.Root
|
|
45
|
+
open={open}
|
|
46
|
+
defaultOpen={defaultOpen}
|
|
47
|
+
onOpenChange={handleOpenChange}
|
|
48
|
+
{...props}
|
|
49
|
+
>
|
|
50
|
+
{children}
|
|
51
|
+
</DialogPrimitive.Root>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const ModalTrigger = DialogPrimitive.Trigger;
|
|
56
|
+
export const ModalClose = DialogPrimitive.Close;
|
|
57
|
+
export const ModalPortal = DialogPrimitive.Portal;
|
|
58
|
+
|
|
59
|
+
export const ModalOverlay = forwardRef(
|
|
60
|
+
({ className, style, ...props }, ref) => (
|
|
61
|
+
<DialogPrimitive.Overlay
|
|
62
|
+
ref={ref}
|
|
63
|
+
className={cx(
|
|
64
|
+
"fixed inset-0 bg-bg-focus-overlay opacity-70",
|
|
65
|
+
"data-[state=open]:animate-drawer-fade-in data-[state=closed]:animate-drawer-fade-out",
|
|
66
|
+
className
|
|
67
|
+
)}
|
|
68
|
+
style={{ zIndex: MODAL_OVERLAY_Z_INDEX, ...style }}
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
ModalOverlay.displayName = "ModalOverlay";
|
|
75
|
+
|
|
76
|
+
export const ModalContent = forwardRef(
|
|
77
|
+
(
|
|
78
|
+
{
|
|
79
|
+
children,
|
|
80
|
+
size = "medium",
|
|
81
|
+
className,
|
|
82
|
+
style,
|
|
83
|
+
backdropProps,
|
|
84
|
+
backDropProps,
|
|
85
|
+
contentProps = {},
|
|
86
|
+
...restProps
|
|
87
|
+
},
|
|
88
|
+
ref
|
|
89
|
+
) => {
|
|
90
|
+
const resolvedBackdropProps = backdropProps ?? backDropProps ?? {};
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<ModalPortal>
|
|
94
|
+
<ModalOverlay
|
|
95
|
+
{...resolvedBackdropProps}
|
|
96
|
+
className={cx(resolvedBackdropProps.className)}
|
|
97
|
+
style={{ ...resolvedBackdropProps.style }}
|
|
98
|
+
/>
|
|
99
|
+
<DialogPrimitive.Content
|
|
100
|
+
ref={ref}
|
|
101
|
+
className={cx(
|
|
102
|
+
"fixed left-1/2 top-1/2 max-h-[92vh] -translate-x-1/2 -translate-y-1/2",
|
|
103
|
+
"bg-bg-base text-fg-base shadow-xl outline-none rounded-2xl border border-border-subtle",
|
|
104
|
+
"flex flex-col overflow-hidden",
|
|
105
|
+
"data-[state=open]:animate-fade-in data-[state=closed]:animate-fade-out",
|
|
106
|
+
getSizeClasses(size),
|
|
107
|
+
className
|
|
108
|
+
)}
|
|
109
|
+
style={{ zIndex: MODAL_CONTENT_Z_INDEX, ...style }}
|
|
110
|
+
{...contentProps}
|
|
111
|
+
{...restProps}
|
|
112
|
+
>
|
|
113
|
+
{children}
|
|
114
|
+
</DialogPrimitive.Content>
|
|
115
|
+
</ModalPortal>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
ModalContent.displayName = "ModalContent";
|
|
121
|
+
|
|
122
|
+
export const ModalHeader = forwardRef(({ className, ...props }, ref) => (
|
|
123
|
+
<div
|
|
124
|
+
ref={ref}
|
|
125
|
+
className={cx(
|
|
126
|
+
"flex flex-col gap-4 px-20 py-16 border-b border-border-subtle",
|
|
127
|
+
className
|
|
128
|
+
)}
|
|
129
|
+
{...props}
|
|
130
|
+
/>
|
|
131
|
+
));
|
|
132
|
+
|
|
133
|
+
ModalHeader.displayName = "ModalHeader";
|
|
134
|
+
|
|
135
|
+
export const ModalBody = forwardRef(({ className, ...props }, ref) => (
|
|
136
|
+
<div
|
|
137
|
+
ref={ref}
|
|
138
|
+
className={cx("flex-1 overflow-auto px-20 py-16", className)}
|
|
139
|
+
{...props}
|
|
140
|
+
/>
|
|
141
|
+
));
|
|
142
|
+
|
|
143
|
+
ModalBody.displayName = "ModalBody";
|
|
144
|
+
|
|
145
|
+
export const ModalFooter = forwardRef(({ className, ...props }, ref) => (
|
|
146
|
+
<div
|
|
147
|
+
ref={ref}
|
|
148
|
+
className={cx(
|
|
149
|
+
"flex items-center justify-end gap-8 px-20 py-16 border-t border-border-subtle",
|
|
150
|
+
className
|
|
151
|
+
)}
|
|
152
|
+
{...props}
|
|
153
|
+
/>
|
|
154
|
+
));
|
|
155
|
+
|
|
156
|
+
ModalFooter.displayName = "ModalFooter";
|
|
157
|
+
|
|
158
|
+
export const ModalTitle = forwardRef(({ className, ...props }, ref) => (
|
|
159
|
+
<DialogPrimitive.Title
|
|
160
|
+
ref={ref}
|
|
161
|
+
className={cx("text-lg font-semibold text-fg-base", className)}
|
|
162
|
+
{...props}
|
|
163
|
+
/>
|
|
164
|
+
));
|
|
165
|
+
|
|
166
|
+
ModalTitle.displayName = "ModalTitle";
|
|
167
|
+
|
|
168
|
+
export const ModalDescription = forwardRef(({ className, ...props }, ref) => (
|
|
169
|
+
<DialogPrimitive.Description
|
|
170
|
+
ref={ref}
|
|
171
|
+
className={cx("text-sm text-fg-subtle", className)}
|
|
172
|
+
{...props}
|
|
173
|
+
/>
|
|
174
|
+
));
|
|
175
|
+
|
|
176
|
+
ModalDescription.displayName = "ModalDescription";
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import {
|
|
4
|
+
Modal,
|
|
5
|
+
ModalContent,
|
|
6
|
+
ModalHeader,
|
|
7
|
+
ModalBody,
|
|
8
|
+
ModalFooter,
|
|
9
|
+
ModalTitle,
|
|
10
|
+
ModalDescription,
|
|
11
|
+
} from "./Modal";
|
|
12
|
+
|
|
13
|
+
let lastRootProps;
|
|
14
|
+
|
|
15
|
+
vi.mock("@radix-ui/react-dialog", () => {
|
|
16
|
+
const Passthrough = ({ children, ...props }) => (
|
|
17
|
+
<div {...props}>{children}</div>
|
|
18
|
+
);
|
|
19
|
+
const Buttonish = ({ children, ...props }) => (
|
|
20
|
+
<button type="button" {...props}>
|
|
21
|
+
{children}
|
|
22
|
+
</button>
|
|
23
|
+
);
|
|
24
|
+
const Content = React.forwardRef((props, ref) => (
|
|
25
|
+
<div ref={ref} data-testid="radix-content" {...props} />
|
|
26
|
+
));
|
|
27
|
+
Content.displayName = "RadixContent";
|
|
28
|
+
const Overlay = React.forwardRef((props, ref) => (
|
|
29
|
+
<div ref={ref} data-testid="radix-overlay" {...props} />
|
|
30
|
+
));
|
|
31
|
+
Overlay.displayName = "RadixOverlay";
|
|
32
|
+
const Root = ({ children, ...props }) => {
|
|
33
|
+
lastRootProps = props;
|
|
34
|
+
return <div {...props}>{children}</div>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
Root,
|
|
39
|
+
Trigger: Buttonish,
|
|
40
|
+
Portal: Passthrough,
|
|
41
|
+
Overlay,
|
|
42
|
+
Content,
|
|
43
|
+
Close: Buttonish,
|
|
44
|
+
Title: Passthrough,
|
|
45
|
+
Description: Passthrough,
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("ModalContent", () => {
|
|
50
|
+
it("renders overlay with default styling and supports custom backdrop props", () => {
|
|
51
|
+
render(
|
|
52
|
+
<Modal>
|
|
53
|
+
<ModalContent
|
|
54
|
+
size="large"
|
|
55
|
+
backdropProps={{ className: "custom-backdrop" }}
|
|
56
|
+
>
|
|
57
|
+
Body
|
|
58
|
+
</ModalContent>
|
|
59
|
+
</Modal>
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const overlay = screen.getByTestId("radix-overlay");
|
|
63
|
+
expect(overlay).toHaveClass("bg-bg-focus-overlay");
|
|
64
|
+
expect(overlay).toHaveClass("custom-backdrop");
|
|
65
|
+
expect(overlay).toHaveStyle({ zIndex: "2147483645" });
|
|
66
|
+
|
|
67
|
+
const content = screen.getByTestId("radix-content");
|
|
68
|
+
expect(content).toHaveClass("left-1/2");
|
|
69
|
+
expect(content).toHaveClass("w-[min(92vw,840px)]");
|
|
70
|
+
expect(content).toHaveStyle({ zIndex: "2147483646" });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("composes header, body, footer, title, and description sections", () => {
|
|
74
|
+
render(
|
|
75
|
+
<Modal>
|
|
76
|
+
<ModalContent>
|
|
77
|
+
<ModalHeader>
|
|
78
|
+
<ModalTitle>Header</ModalTitle>
|
|
79
|
+
<ModalDescription>Details</ModalDescription>
|
|
80
|
+
</ModalHeader>
|
|
81
|
+
<ModalBody>Body</ModalBody>
|
|
82
|
+
<ModalFooter>Footer</ModalFooter>
|
|
83
|
+
</ModalContent>
|
|
84
|
+
</Modal>
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(screen.getByText("Header")).toHaveClass("font-semibold");
|
|
88
|
+
expect(screen.getByText("Details")).toHaveClass("text-fg-subtle");
|
|
89
|
+
expect(screen.getByText("Body")).toHaveClass("overflow-auto");
|
|
90
|
+
expect(screen.getByText("Footer")).toHaveClass("border-t");
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("Modal", () => {
|
|
95
|
+
it("fires onOpen and onClose from onOpenChange", () => {
|
|
96
|
+
const onOpen = vi.fn();
|
|
97
|
+
const onClose = vi.fn();
|
|
98
|
+
|
|
99
|
+
render(
|
|
100
|
+
<Modal onOpen={onOpen} onClose={onClose}>
|
|
101
|
+
<ModalContent>Body</ModalContent>
|
|
102
|
+
</Modal>
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
lastRootProps.onOpenChange(true);
|
|
106
|
+
lastRootProps.onOpenChange(false);
|
|
107
|
+
|
|
108
|
+
expect(onOpen).toHaveBeenCalledTimes(1);
|
|
109
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
110
|
+
});
|
|
111
|
+
});
|