@edux-design/modals 0.1.1 → 0.1.3
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 +12 -0
- package/README.md +7 -5
- package/dist/index.js +76 -38
- package/dist/index.mjs +83 -39
- package/package.json +7 -3
- package/src/demos/Modal.stories.jsx +156 -20
- package/src/elements/Modal.jsx +96 -44
- package/src/elements/Modal.test.jsx +36 -8
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -34,9 +34,9 @@ import { Button } from "@edux-design/buttons";
|
|
|
34
34
|
|
|
35
35
|
export function PublishModal() {
|
|
36
36
|
return (
|
|
37
|
-
<Modal>
|
|
37
|
+
<Modal onClose={() => {}}>
|
|
38
38
|
<ModalTrigger asChild>
|
|
39
|
-
<Button
|
|
39
|
+
<Button>Open Modal</Button>
|
|
40
40
|
</ModalTrigger>
|
|
41
41
|
<ModalContent size="medium">
|
|
42
42
|
<ModalHeader>
|
|
@@ -50,9 +50,9 @@ export function PublishModal() {
|
|
|
50
50
|
</ModalBody>
|
|
51
51
|
<ModalFooter>
|
|
52
52
|
<ModalClose asChild>
|
|
53
|
-
<Button
|
|
53
|
+
<Button isSecondary>Cancel</Button>
|
|
54
54
|
</ModalClose>
|
|
55
|
-
<Button
|
|
55
|
+
<Button>Publish</Button>
|
|
56
56
|
</ModalFooter>
|
|
57
57
|
</ModalContent>
|
|
58
58
|
</Modal>
|
|
@@ -60,4 +60,6 @@ export function PublishModal() {
|
|
|
60
60
|
}
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
`
|
|
63
|
+
Passing `onClose` to `Modal` also enables the default close icon inside `ModalHeader`.
|
|
64
|
+
|
|
65
|
+
`ModalContent` accepts `size`, `backdropProps`, the legacy alias `backDropProps`, `className`, and `contentProps`. Storybook examples live in `src/demos/Modal.stories.jsx`.
|
package/dist/index.js
CHANGED
|
@@ -47,20 +47,48 @@ module.exports = __toCommonJS(index_exports);
|
|
|
47
47
|
var import_react = __toESM(require("react"));
|
|
48
48
|
var DialogPrimitive = __toESM(require("@radix-ui/react-dialog"));
|
|
49
49
|
var import_utils = require("@edux-design/utils");
|
|
50
|
+
var import_buttons = require("@edux-design/buttons");
|
|
51
|
+
var import_icons = require("@edux-design/icons");
|
|
50
52
|
var MODAL_OVERLAY_Z_INDEX = 2147483645;
|
|
51
53
|
var MODAL_CONTENT_Z_INDEX = 2147483646;
|
|
54
|
+
var ModalContext = (0, import_react.createContext)({ hasOnClose: false });
|
|
55
|
+
var MODAL_SECTION_PADDING_X = "px-16";
|
|
56
|
+
var MODAL_SECTION_PADDING_Y = "py-16";
|
|
57
|
+
var MODAL_SURFACE_CLASSES = [
|
|
58
|
+
"fixed left-1/2 top-1/2 max-h-[92vh] -translate-x-1/2 -translate-y-1/2",
|
|
59
|
+
"bg-bg-base text-fg-base shadow-xl outline-none rounded-md border-2 border-border-subtle",
|
|
60
|
+
"flex flex-col overflow-hidden",
|
|
61
|
+
"data-[state=open]:animate-fade-in data-[state=closed]:animate-fade-out"
|
|
62
|
+
].join(" ");
|
|
63
|
+
var MODAL_HEADER_CLASSES = [
|
|
64
|
+
"flex justify-between gap-12",
|
|
65
|
+
MODAL_SECTION_PADDING_X,
|
|
66
|
+
MODAL_SECTION_PADDING_Y,
|
|
67
|
+
"border-b border-border-subtle"
|
|
68
|
+
].join(" ");
|
|
69
|
+
var MODAL_HEADER_CONTENT_CLASSES = "flex min-w-0 flex-1 flex-col gap-4";
|
|
70
|
+
var MODAL_BODY_CLASSES = [
|
|
71
|
+
"flex-1 overflow-auto",
|
|
72
|
+
MODAL_SECTION_PADDING_X,
|
|
73
|
+
MODAL_SECTION_PADDING_Y
|
|
74
|
+
].join(" ");
|
|
75
|
+
var MODAL_FOOTER_CLASSES = [
|
|
76
|
+
"flex items-center justify-end gap-8",
|
|
77
|
+
MODAL_SECTION_PADDING_X,
|
|
78
|
+
MODAL_SECTION_PADDING_Y,
|
|
79
|
+
"border-t border-border-subtle"
|
|
80
|
+
].join(" ");
|
|
81
|
+
var MODAL_TITLE_CLASSES = "text-lg font-semibold text-fg-base";
|
|
82
|
+
var MODAL_DESCRIPTION_CLASSES = "text-sm text-fg-subtle";
|
|
83
|
+
var MODAL_CLOSE_LABEL = "Close modal";
|
|
84
|
+
var MODAL_SIZE_CLASSES = {
|
|
85
|
+
small: "w-[min(92vw,420px)]",
|
|
86
|
+
medium: "w-[min(92vw,640px)]",
|
|
87
|
+
large: "w-[min(92vw,840px)]",
|
|
88
|
+
full: "w-[min(96vw,1200px)] h-[min(92vh,900px)]"
|
|
89
|
+
};
|
|
52
90
|
var getSizeClasses = (size) => {
|
|
53
|
-
|
|
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
|
-
}
|
|
91
|
+
return MODAL_SIZE_CLASSES[size] ?? MODAL_SIZE_CLASSES.medium;
|
|
64
92
|
};
|
|
65
93
|
var Modal = ({
|
|
66
94
|
open,
|
|
@@ -71,6 +99,10 @@ var Modal = ({
|
|
|
71
99
|
children,
|
|
72
100
|
...props
|
|
73
101
|
}) => {
|
|
102
|
+
const contextValue = (0, import_react.useMemo)(
|
|
103
|
+
() => ({ hasOnClose: typeof onClose === "function" }),
|
|
104
|
+
[onClose]
|
|
105
|
+
);
|
|
74
106
|
const handleOpenChange = (0, import_react.useCallback)(
|
|
75
107
|
(nextOpen) => {
|
|
76
108
|
onOpenChange == null ? void 0 : onOpenChange(nextOpen);
|
|
@@ -82,7 +114,7 @@ var Modal = ({
|
|
|
82
114
|
},
|
|
83
115
|
[onClose, onOpen, onOpenChange]
|
|
84
116
|
);
|
|
85
|
-
return /* @__PURE__ */ import_react.default.createElement(
|
|
117
|
+
return /* @__PURE__ */ import_react.default.createElement(ModalContext.Provider, { value: contextValue }, /* @__PURE__ */ import_react.default.createElement(
|
|
86
118
|
DialogPrimitive.Root,
|
|
87
119
|
{
|
|
88
120
|
open,
|
|
@@ -91,11 +123,20 @@ var Modal = ({
|
|
|
91
123
|
...props
|
|
92
124
|
},
|
|
93
125
|
children
|
|
94
|
-
);
|
|
126
|
+
));
|
|
95
127
|
};
|
|
96
128
|
var ModalTrigger = DialogPrimitive.Trigger;
|
|
97
129
|
var ModalClose = DialogPrimitive.Close;
|
|
98
130
|
var ModalPortal = DialogPrimitive.Portal;
|
|
131
|
+
var ModalHeaderCloseButton = () => /* @__PURE__ */ import_react.default.createElement(ModalClose, { asChild: true }, /* @__PURE__ */ import_react.default.createElement(
|
|
132
|
+
import_buttons.IconButton,
|
|
133
|
+
{
|
|
134
|
+
size: "small",
|
|
135
|
+
isBare: true,
|
|
136
|
+
icon: /* @__PURE__ */ import_react.default.createElement(import_icons.Close, { "aria-hidden": "true" }),
|
|
137
|
+
"aria-label": MODAL_CLOSE_LABEL
|
|
138
|
+
}
|
|
139
|
+
));
|
|
99
140
|
var ModalOverlay = (0, import_react.forwardRef)(
|
|
100
141
|
({ className, style, ...props }, ref) => /* @__PURE__ */ import_react.default.createElement(
|
|
101
142
|
DialogPrimitive.Overlay,
|
|
@@ -124,22 +165,20 @@ var ModalContent = (0, import_react.forwardRef)(
|
|
|
124
165
|
...restProps
|
|
125
166
|
}, ref) => {
|
|
126
167
|
const resolvedBackdropProps = backdropProps ?? backDropProps ?? {};
|
|
168
|
+
const { className: backdropClassName, style: backdropStyle, ...overlayProps } = resolvedBackdropProps;
|
|
127
169
|
return /* @__PURE__ */ import_react.default.createElement(ModalPortal, null, /* @__PURE__ */ import_react.default.createElement(
|
|
128
170
|
ModalOverlay,
|
|
129
171
|
{
|
|
130
|
-
...
|
|
131
|
-
className: (0, import_utils.cx)(
|
|
132
|
-
style: { ...
|
|
172
|
+
...overlayProps,
|
|
173
|
+
className: (0, import_utils.cx)(backdropClassName),
|
|
174
|
+
style: { ...backdropStyle }
|
|
133
175
|
}
|
|
134
176
|
), /* @__PURE__ */ import_react.default.createElement(
|
|
135
177
|
DialogPrimitive.Content,
|
|
136
178
|
{
|
|
137
179
|
ref,
|
|
138
180
|
className: (0, import_utils.cx)(
|
|
139
|
-
|
|
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",
|
|
181
|
+
MODAL_SURFACE_CLASSES,
|
|
143
182
|
getSizeClasses(size),
|
|
144
183
|
className
|
|
145
184
|
),
|
|
@@ -152,23 +191,25 @@ var ModalContent = (0, import_react.forwardRef)(
|
|
|
152
191
|
}
|
|
153
192
|
);
|
|
154
193
|
ModalContent.displayName = "ModalContent";
|
|
155
|
-
var ModalHeader = (0, import_react.forwardRef)(({ className, ...props }, ref) =>
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
className
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
)
|
|
194
|
+
var ModalHeader = (0, import_react.forwardRef)(({ className, children, ...props }, ref) => {
|
|
195
|
+
const { hasOnClose } = (0, import_react.useContext)(ModalContext);
|
|
196
|
+
return /* @__PURE__ */ import_react.default.createElement(
|
|
197
|
+
"div",
|
|
198
|
+
{
|
|
199
|
+
ref,
|
|
200
|
+
className: (0, import_utils.cx)(MODAL_HEADER_CLASSES, className),
|
|
201
|
+
...props
|
|
202
|
+
},
|
|
203
|
+
/* @__PURE__ */ import_react.default.createElement("div", { className: MODAL_HEADER_CONTENT_CLASSES }, children),
|
|
204
|
+
hasOnClose ? /* @__PURE__ */ import_react.default.createElement(ModalHeaderCloseButton, null) : null
|
|
205
|
+
);
|
|
206
|
+
});
|
|
166
207
|
ModalHeader.displayName = "ModalHeader";
|
|
167
208
|
var ModalBody = (0, import_react.forwardRef)(({ className, ...props }, ref) => /* @__PURE__ */ import_react.default.createElement(
|
|
168
209
|
"div",
|
|
169
210
|
{
|
|
170
211
|
ref,
|
|
171
|
-
className: (0, import_utils.cx)(
|
|
212
|
+
className: (0, import_utils.cx)(MODAL_BODY_CLASSES, className),
|
|
172
213
|
...props
|
|
173
214
|
}
|
|
174
215
|
));
|
|
@@ -177,10 +218,7 @@ var ModalFooter = (0, import_react.forwardRef)(({ className, ...props }, ref) =>
|
|
|
177
218
|
"div",
|
|
178
219
|
{
|
|
179
220
|
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
|
-
),
|
|
221
|
+
className: (0, import_utils.cx)(MODAL_FOOTER_CLASSES, className),
|
|
184
222
|
...props
|
|
185
223
|
}
|
|
186
224
|
));
|
|
@@ -189,7 +227,7 @@ var ModalTitle = (0, import_react.forwardRef)(({ className, ...props }, ref) =>
|
|
|
189
227
|
DialogPrimitive.Title,
|
|
190
228
|
{
|
|
191
229
|
ref,
|
|
192
|
-
className: (0, import_utils.cx)(
|
|
230
|
+
className: (0, import_utils.cx)(MODAL_TITLE_CLASSES, className),
|
|
193
231
|
...props
|
|
194
232
|
}
|
|
195
233
|
));
|
|
@@ -198,7 +236,7 @@ var ModalDescription = (0, import_react.forwardRef)(({ className, ...props }, re
|
|
|
198
236
|
DialogPrimitive.Description,
|
|
199
237
|
{
|
|
200
238
|
ref,
|
|
201
|
-
className: (0, import_utils.cx)(
|
|
239
|
+
className: (0, import_utils.cx)(MODAL_DESCRIPTION_CLASSES, className),
|
|
202
240
|
...props
|
|
203
241
|
}
|
|
204
242
|
));
|
package/dist/index.mjs
CHANGED
|
@@ -1,21 +1,55 @@
|
|
|
1
1
|
// src/elements/Modal.jsx
|
|
2
|
-
import React, {
|
|
2
|
+
import React, {
|
|
3
|
+
createContext,
|
|
4
|
+
forwardRef,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useMemo
|
|
8
|
+
} from "react";
|
|
3
9
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
4
10
|
import { cx } from "@edux-design/utils";
|
|
11
|
+
import { IconButton } from "@edux-design/buttons";
|
|
12
|
+
import { Close as Close2 } from "@edux-design/icons";
|
|
5
13
|
var MODAL_OVERLAY_Z_INDEX = 2147483645;
|
|
6
14
|
var MODAL_CONTENT_Z_INDEX = 2147483646;
|
|
15
|
+
var ModalContext = createContext({ hasOnClose: false });
|
|
16
|
+
var MODAL_SECTION_PADDING_X = "px-16";
|
|
17
|
+
var MODAL_SECTION_PADDING_Y = "py-16";
|
|
18
|
+
var MODAL_SURFACE_CLASSES = [
|
|
19
|
+
"fixed left-1/2 top-1/2 max-h-[92vh] -translate-x-1/2 -translate-y-1/2",
|
|
20
|
+
"bg-bg-base text-fg-base shadow-xl outline-none rounded-md border-2 border-border-subtle",
|
|
21
|
+
"flex flex-col overflow-hidden",
|
|
22
|
+
"data-[state=open]:animate-fade-in data-[state=closed]:animate-fade-out"
|
|
23
|
+
].join(" ");
|
|
24
|
+
var MODAL_HEADER_CLASSES = [
|
|
25
|
+
"flex justify-between gap-12",
|
|
26
|
+
MODAL_SECTION_PADDING_X,
|
|
27
|
+
MODAL_SECTION_PADDING_Y,
|
|
28
|
+
"border-b border-border-subtle"
|
|
29
|
+
].join(" ");
|
|
30
|
+
var MODAL_HEADER_CONTENT_CLASSES = "flex min-w-0 flex-1 flex-col gap-4";
|
|
31
|
+
var MODAL_BODY_CLASSES = [
|
|
32
|
+
"flex-1 overflow-auto",
|
|
33
|
+
MODAL_SECTION_PADDING_X,
|
|
34
|
+
MODAL_SECTION_PADDING_Y
|
|
35
|
+
].join(" ");
|
|
36
|
+
var MODAL_FOOTER_CLASSES = [
|
|
37
|
+
"flex items-center justify-end gap-8",
|
|
38
|
+
MODAL_SECTION_PADDING_X,
|
|
39
|
+
MODAL_SECTION_PADDING_Y,
|
|
40
|
+
"border-t border-border-subtle"
|
|
41
|
+
].join(" ");
|
|
42
|
+
var MODAL_TITLE_CLASSES = "text-lg font-semibold text-fg-base";
|
|
43
|
+
var MODAL_DESCRIPTION_CLASSES = "text-sm text-fg-subtle";
|
|
44
|
+
var MODAL_CLOSE_LABEL = "Close modal";
|
|
45
|
+
var MODAL_SIZE_CLASSES = {
|
|
46
|
+
small: "w-[min(92vw,420px)]",
|
|
47
|
+
medium: "w-[min(92vw,640px)]",
|
|
48
|
+
large: "w-[min(92vw,840px)]",
|
|
49
|
+
full: "w-[min(96vw,1200px)] h-[min(92vh,900px)]"
|
|
50
|
+
};
|
|
7
51
|
var getSizeClasses = (size) => {
|
|
8
|
-
|
|
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
|
-
}
|
|
52
|
+
return MODAL_SIZE_CLASSES[size] ?? MODAL_SIZE_CLASSES.medium;
|
|
19
53
|
};
|
|
20
54
|
var Modal = ({
|
|
21
55
|
open,
|
|
@@ -26,6 +60,10 @@ var Modal = ({
|
|
|
26
60
|
children,
|
|
27
61
|
...props
|
|
28
62
|
}) => {
|
|
63
|
+
const contextValue = useMemo(
|
|
64
|
+
() => ({ hasOnClose: typeof onClose === "function" }),
|
|
65
|
+
[onClose]
|
|
66
|
+
);
|
|
29
67
|
const handleOpenChange = useCallback(
|
|
30
68
|
(nextOpen) => {
|
|
31
69
|
onOpenChange == null ? void 0 : onOpenChange(nextOpen);
|
|
@@ -37,7 +75,7 @@ var Modal = ({
|
|
|
37
75
|
},
|
|
38
76
|
[onClose, onOpen, onOpenChange]
|
|
39
77
|
);
|
|
40
|
-
return /* @__PURE__ */ React.createElement(
|
|
78
|
+
return /* @__PURE__ */ React.createElement(ModalContext.Provider, { value: contextValue }, /* @__PURE__ */ React.createElement(
|
|
41
79
|
DialogPrimitive.Root,
|
|
42
80
|
{
|
|
43
81
|
open,
|
|
@@ -46,11 +84,20 @@ var Modal = ({
|
|
|
46
84
|
...props
|
|
47
85
|
},
|
|
48
86
|
children
|
|
49
|
-
);
|
|
87
|
+
));
|
|
50
88
|
};
|
|
51
89
|
var ModalTrigger = DialogPrimitive.Trigger;
|
|
52
90
|
var ModalClose = DialogPrimitive.Close;
|
|
53
91
|
var ModalPortal = DialogPrimitive.Portal;
|
|
92
|
+
var ModalHeaderCloseButton = () => /* @__PURE__ */ React.createElement(ModalClose, { asChild: true }, /* @__PURE__ */ React.createElement(
|
|
93
|
+
IconButton,
|
|
94
|
+
{
|
|
95
|
+
size: "small",
|
|
96
|
+
isBare: true,
|
|
97
|
+
icon: /* @__PURE__ */ React.createElement(Close2, { "aria-hidden": "true" }),
|
|
98
|
+
"aria-label": MODAL_CLOSE_LABEL
|
|
99
|
+
}
|
|
100
|
+
));
|
|
54
101
|
var ModalOverlay = forwardRef(
|
|
55
102
|
({ className, style, ...props }, ref) => /* @__PURE__ */ React.createElement(
|
|
56
103
|
DialogPrimitive.Overlay,
|
|
@@ -79,22 +126,20 @@ var ModalContent = forwardRef(
|
|
|
79
126
|
...restProps
|
|
80
127
|
}, ref) => {
|
|
81
128
|
const resolvedBackdropProps = backdropProps ?? backDropProps ?? {};
|
|
129
|
+
const { className: backdropClassName, style: backdropStyle, ...overlayProps } = resolvedBackdropProps;
|
|
82
130
|
return /* @__PURE__ */ React.createElement(ModalPortal, null, /* @__PURE__ */ React.createElement(
|
|
83
131
|
ModalOverlay,
|
|
84
132
|
{
|
|
85
|
-
...
|
|
86
|
-
className: cx(
|
|
87
|
-
style: { ...
|
|
133
|
+
...overlayProps,
|
|
134
|
+
className: cx(backdropClassName),
|
|
135
|
+
style: { ...backdropStyle }
|
|
88
136
|
}
|
|
89
137
|
), /* @__PURE__ */ React.createElement(
|
|
90
138
|
DialogPrimitive.Content,
|
|
91
139
|
{
|
|
92
140
|
ref,
|
|
93
141
|
className: cx(
|
|
94
|
-
|
|
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",
|
|
142
|
+
MODAL_SURFACE_CLASSES,
|
|
98
143
|
getSizeClasses(size),
|
|
99
144
|
className
|
|
100
145
|
),
|
|
@@ -107,23 +152,25 @@ var ModalContent = forwardRef(
|
|
|
107
152
|
}
|
|
108
153
|
);
|
|
109
154
|
ModalContent.displayName = "ModalContent";
|
|
110
|
-
var ModalHeader = forwardRef(({ className, ...props }, ref) =>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
className
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
)
|
|
155
|
+
var ModalHeader = forwardRef(({ className, children, ...props }, ref) => {
|
|
156
|
+
const { hasOnClose } = useContext(ModalContext);
|
|
157
|
+
return /* @__PURE__ */ React.createElement(
|
|
158
|
+
"div",
|
|
159
|
+
{
|
|
160
|
+
ref,
|
|
161
|
+
className: cx(MODAL_HEADER_CLASSES, className),
|
|
162
|
+
...props
|
|
163
|
+
},
|
|
164
|
+
/* @__PURE__ */ React.createElement("div", { className: MODAL_HEADER_CONTENT_CLASSES }, children),
|
|
165
|
+
hasOnClose ? /* @__PURE__ */ React.createElement(ModalHeaderCloseButton, null) : null
|
|
166
|
+
);
|
|
167
|
+
});
|
|
121
168
|
ModalHeader.displayName = "ModalHeader";
|
|
122
169
|
var ModalBody = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ React.createElement(
|
|
123
170
|
"div",
|
|
124
171
|
{
|
|
125
172
|
ref,
|
|
126
|
-
className: cx(
|
|
173
|
+
className: cx(MODAL_BODY_CLASSES, className),
|
|
127
174
|
...props
|
|
128
175
|
}
|
|
129
176
|
));
|
|
@@ -132,10 +179,7 @@ var ModalFooter = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ R
|
|
|
132
179
|
"div",
|
|
133
180
|
{
|
|
134
181
|
ref,
|
|
135
|
-
className: cx(
|
|
136
|
-
"flex items-center justify-end gap-8 px-20 py-16 border-t border-border-subtle",
|
|
137
|
-
className
|
|
138
|
-
),
|
|
182
|
+
className: cx(MODAL_FOOTER_CLASSES, className),
|
|
139
183
|
...props
|
|
140
184
|
}
|
|
141
185
|
));
|
|
@@ -144,7 +188,7 @@ var ModalTitle = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ Re
|
|
|
144
188
|
DialogPrimitive.Title,
|
|
145
189
|
{
|
|
146
190
|
ref,
|
|
147
|
-
className: cx(
|
|
191
|
+
className: cx(MODAL_TITLE_CLASSES, className),
|
|
148
192
|
...props
|
|
149
193
|
}
|
|
150
194
|
));
|
|
@@ -153,7 +197,7 @@ var ModalDescription = forwardRef(({ className, ...props }, ref) => /* @__PURE__
|
|
|
153
197
|
DialogPrimitive.Description,
|
|
154
198
|
{
|
|
155
199
|
ref,
|
|
156
|
-
className: cx(
|
|
200
|
+
className: cx(MODAL_DESCRIPTION_CLASSES, className),
|
|
157
201
|
...props
|
|
158
202
|
}
|
|
159
203
|
));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edux-design/modals",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"**/*.css"
|
|
@@ -15,11 +15,15 @@
|
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
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",
|
|
18
|
+
"build": "tsup src/index.js --format esm,cjs --external react,react-dom,@edux-design/utils,@edux-design/buttons,@edux-design/icons,@radix-ui/react-dialog",
|
|
19
|
+
"dev": "tsup src/index.js --watch --format esm,cjs --external react,react-dom,@edux-design/utils,@edux-design/buttons,@edux-design/icons,@radix-ui/react-dialog",
|
|
20
20
|
"generate:component": "turbo gen react-component",
|
|
21
21
|
"check-types": "tsc --noEmit"
|
|
22
22
|
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@edux-design/buttons": "*",
|
|
25
|
+
"@edux-design/icons": "*"
|
|
26
|
+
},
|
|
23
27
|
"peerDependencies": {
|
|
24
28
|
"react": ">=18",
|
|
25
29
|
"react-dom": ">=18",
|
|
@@ -12,6 +12,21 @@ import {
|
|
|
12
12
|
} from "../index";
|
|
13
13
|
import { Button } from "@edux-design/buttons";
|
|
14
14
|
|
|
15
|
+
const loremIpsumParagraphs = [
|
|
16
|
+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
|
|
17
|
+
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const BodyCopy = () => (
|
|
21
|
+
<div className="space-y-12">
|
|
22
|
+
{loremIpsumParagraphs.map((paragraph) => (
|
|
23
|
+
<p key={paragraph} className="text-sm text-fg-subtle">
|
|
24
|
+
{paragraph}
|
|
25
|
+
</p>
|
|
26
|
+
))}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
|
|
15
30
|
const meta = {
|
|
16
31
|
title: "Components/Modal",
|
|
17
32
|
component: Modal,
|
|
@@ -43,7 +58,7 @@ export const Basic = {
|
|
|
43
58
|
render: (args) => (
|
|
44
59
|
<Modal>
|
|
45
60
|
<ModalTrigger asChild>
|
|
46
|
-
<Button
|
|
61
|
+
<Button>Open Modal</Button>
|
|
47
62
|
</ModalTrigger>
|
|
48
63
|
<ModalContent {...args}>
|
|
49
64
|
<ModalHeader>
|
|
@@ -53,15 +68,13 @@ export const Basic = {
|
|
|
53
68
|
</ModalDescription>
|
|
54
69
|
</ModalHeader>
|
|
55
70
|
<ModalBody>
|
|
56
|
-
<
|
|
57
|
-
This modal supports title, description, body, and footer regions.
|
|
58
|
-
</p>
|
|
71
|
+
<BodyCopy />
|
|
59
72
|
</ModalBody>
|
|
60
73
|
<ModalFooter>
|
|
61
74
|
<ModalClose asChild>
|
|
62
|
-
<Button
|
|
75
|
+
<Button isSecondary>Cancel</Button>
|
|
63
76
|
</ModalClose>
|
|
64
|
-
<Button
|
|
77
|
+
<Button>Publish</Button>
|
|
65
78
|
</ModalFooter>
|
|
66
79
|
</ModalContent>
|
|
67
80
|
</Modal>
|
|
@@ -78,7 +91,7 @@ export const Controlled = {
|
|
|
78
91
|
return (
|
|
79
92
|
<Modal open={open} onOpenChange={setOpen}>
|
|
80
93
|
<ModalTrigger asChild>
|
|
81
|
-
<Button
|
|
94
|
+
<Button>
|
|
82
95
|
{open ? "Close Modal" : "Open Modal"}
|
|
83
96
|
</Button>
|
|
84
97
|
</ModalTrigger>
|
|
@@ -90,12 +103,10 @@ export const Controlled = {
|
|
|
90
103
|
</ModalDescription>
|
|
91
104
|
</ModalHeader>
|
|
92
105
|
<ModalBody>
|
|
93
|
-
<
|
|
94
|
-
Use this variant when the parent controls the workflow.
|
|
95
|
-
</p>
|
|
106
|
+
<BodyCopy />
|
|
96
107
|
</ModalBody>
|
|
97
108
|
<ModalFooter>
|
|
98
|
-
<Button
|
|
109
|
+
<Button isBare onClick={() => setOpen(false)}>
|
|
99
110
|
Dismiss
|
|
100
111
|
</Button>
|
|
101
112
|
</ModalFooter>
|
|
@@ -114,7 +125,7 @@ export const Sizes = {
|
|
|
114
125
|
{["small", "medium", "large", "full"].map((size) => (
|
|
115
126
|
<Modal key={size}>
|
|
116
127
|
<ModalTrigger asChild>
|
|
117
|
-
<Button
|
|
128
|
+
<Button isSecondary>Size: {size}</Button>
|
|
118
129
|
</ModalTrigger>
|
|
119
130
|
<ModalContent size={size}>
|
|
120
131
|
<ModalHeader>
|
|
@@ -124,10 +135,7 @@ export const Sizes = {
|
|
|
124
135
|
</ModalDescription>
|
|
125
136
|
</ModalHeader>
|
|
126
137
|
<ModalBody>
|
|
127
|
-
<
|
|
128
|
-
These presets use existing spacing, border, shadow, and
|
|
129
|
-
foreground/background tokens.
|
|
130
|
-
</p>
|
|
138
|
+
<BodyCopy />
|
|
131
139
|
</ModalBody>
|
|
132
140
|
</ModalContent>
|
|
133
141
|
</Modal>
|
|
@@ -140,7 +148,7 @@ export const CustomBackdrop = {
|
|
|
140
148
|
render: (args) => (
|
|
141
149
|
<Modal>
|
|
142
150
|
<ModalTrigger asChild>
|
|
143
|
-
<Button
|
|
151
|
+
<Button>Open With Backdrop</Button>
|
|
144
152
|
</ModalTrigger>
|
|
145
153
|
<ModalContent
|
|
146
154
|
{...args}
|
|
@@ -155,9 +163,7 @@ export const CustomBackdrop = {
|
|
|
155
163
|
</ModalDescription>
|
|
156
164
|
</ModalHeader>
|
|
157
165
|
<ModalBody>
|
|
158
|
-
<
|
|
159
|
-
Override color and opacity without changing the modal surface.
|
|
160
|
-
</p>
|
|
166
|
+
<BodyCopy />
|
|
161
167
|
</ModalBody>
|
|
162
168
|
</ModalContent>
|
|
163
169
|
</Modal>
|
|
@@ -166,3 +172,133 @@ export const CustomBackdrop = {
|
|
|
166
172
|
size: "medium",
|
|
167
173
|
},
|
|
168
174
|
};
|
|
175
|
+
|
|
176
|
+
export const FooterActions = {
|
|
177
|
+
render: (args) => (
|
|
178
|
+
<Modal>
|
|
179
|
+
<ModalTrigger asChild>
|
|
180
|
+
<Button isSecondary>Open Action Footer</Button>
|
|
181
|
+
</ModalTrigger>
|
|
182
|
+
<ModalContent {...args}>
|
|
183
|
+
<ModalHeader>
|
|
184
|
+
<ModalTitle>Save draft</ModalTitle>
|
|
185
|
+
<ModalDescription>
|
|
186
|
+
Footer actions can group secondary and primary outcomes.
|
|
187
|
+
</ModalDescription>
|
|
188
|
+
</ModalHeader>
|
|
189
|
+
<ModalBody>
|
|
190
|
+
<BodyCopy />
|
|
191
|
+
</ModalBody>
|
|
192
|
+
<ModalFooter>
|
|
193
|
+
<ModalClose asChild>
|
|
194
|
+
<Button isBare>Back</Button>
|
|
195
|
+
</ModalClose>
|
|
196
|
+
<Button isSecondary>Save Draft</Button>
|
|
197
|
+
<Button>Continue</Button>
|
|
198
|
+
</ModalFooter>
|
|
199
|
+
</ModalContent>
|
|
200
|
+
</Modal>
|
|
201
|
+
),
|
|
202
|
+
args: {
|
|
203
|
+
size: "medium",
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export const DestructiveFooter = {
|
|
208
|
+
render: (args) => (
|
|
209
|
+
<Modal>
|
|
210
|
+
<ModalTrigger asChild>
|
|
211
|
+
<Button>Open Destructive Footer</Button>
|
|
212
|
+
</ModalTrigger>
|
|
213
|
+
<ModalContent {...args}>
|
|
214
|
+
<ModalHeader>
|
|
215
|
+
<ModalTitle>Delete manuscript version</ModalTitle>
|
|
216
|
+
<ModalDescription>
|
|
217
|
+
Use a footer to separate destructive actions from cancellation.
|
|
218
|
+
</ModalDescription>
|
|
219
|
+
</ModalHeader>
|
|
220
|
+
<ModalBody>
|
|
221
|
+
<BodyCopy />
|
|
222
|
+
</ModalBody>
|
|
223
|
+
<ModalFooter>
|
|
224
|
+
<ModalClose asChild>
|
|
225
|
+
<Button isSecondary>Cancel</Button>
|
|
226
|
+
</ModalClose>
|
|
227
|
+
<Button isDanger>Delete Version</Button>
|
|
228
|
+
</ModalFooter>
|
|
229
|
+
</ModalContent>
|
|
230
|
+
</Modal>
|
|
231
|
+
),
|
|
232
|
+
args: {
|
|
233
|
+
size: "small",
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export const HeaderCloseButton = {
|
|
238
|
+
render: (args) => (
|
|
239
|
+
<Modal onClose={() => {}}>
|
|
240
|
+
<ModalTrigger asChild>
|
|
241
|
+
<Button isSecondary>Open Header Close</Button>
|
|
242
|
+
</ModalTrigger>
|
|
243
|
+
<ModalContent {...args}>
|
|
244
|
+
<ModalHeader>
|
|
245
|
+
<ModalTitle>Review publish settings</ModalTitle>
|
|
246
|
+
<ModalDescription>
|
|
247
|
+
Passing onClose to Modal automatically adds the header close action.
|
|
248
|
+
</ModalDescription>
|
|
249
|
+
</ModalHeader>
|
|
250
|
+
<ModalBody>
|
|
251
|
+
<BodyCopy />
|
|
252
|
+
</ModalBody>
|
|
253
|
+
<ModalFooter>
|
|
254
|
+
<ModalClose asChild>
|
|
255
|
+
<Button isSecondary>Cancel</Button>
|
|
256
|
+
</ModalClose>
|
|
257
|
+
<Button>Save</Button>
|
|
258
|
+
</ModalFooter>
|
|
259
|
+
</ModalContent>
|
|
260
|
+
</Modal>
|
|
261
|
+
),
|
|
262
|
+
args: {
|
|
263
|
+
size: "medium",
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
export const ControlledHeaderCloseButton = {
|
|
268
|
+
render: (args) => {
|
|
269
|
+
const [open, setOpen] = useState(false);
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<Modal
|
|
273
|
+
open={open}
|
|
274
|
+
onOpenChange={setOpen}
|
|
275
|
+
onClose={() => setOpen(false)}
|
|
276
|
+
>
|
|
277
|
+
<ModalTrigger asChild>
|
|
278
|
+
<Button>
|
|
279
|
+
{open ? "Modal Open" : "Open Controlled Header Close"}
|
|
280
|
+
</Button>
|
|
281
|
+
</ModalTrigger>
|
|
282
|
+
<ModalContent {...args}>
|
|
283
|
+
<ModalHeader>
|
|
284
|
+
<ModalTitle>Controlled header close</ModalTitle>
|
|
285
|
+
<ModalDescription>
|
|
286
|
+
The built-in close icon also works when modal state is controlled.
|
|
287
|
+
</ModalDescription>
|
|
288
|
+
</ModalHeader>
|
|
289
|
+
<ModalBody>
|
|
290
|
+
<BodyCopy />
|
|
291
|
+
</ModalBody>
|
|
292
|
+
<ModalFooter>
|
|
293
|
+
<Button isBare onClick={() => setOpen(false)}>
|
|
294
|
+
Dismiss
|
|
295
|
+
</Button>
|
|
296
|
+
</ModalFooter>
|
|
297
|
+
</ModalContent>
|
|
298
|
+
</Modal>
|
|
299
|
+
);
|
|
300
|
+
},
|
|
301
|
+
args: {
|
|
302
|
+
size: "medium",
|
|
303
|
+
},
|
|
304
|
+
};
|
package/src/elements/Modal.jsx
CHANGED
|
@@ -1,22 +1,56 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
forwardRef,
|
|
4
|
+
useCallback,
|
|
5
|
+
useContext,
|
|
6
|
+
useMemo,
|
|
7
|
+
} from "react";
|
|
2
8
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
3
9
|
import { cx } from "@edux-design/utils";
|
|
10
|
+
import { IconButton } from "@edux-design/buttons";
|
|
11
|
+
import { Close } from "@edux-design/icons";
|
|
4
12
|
|
|
5
13
|
const MODAL_OVERLAY_Z_INDEX = 2147483645;
|
|
6
14
|
const MODAL_CONTENT_Z_INDEX = 2147483646;
|
|
15
|
+
const ModalContext = createContext({ hasOnClose: false });
|
|
16
|
+
const MODAL_SECTION_PADDING_X = "px-16";
|
|
17
|
+
const MODAL_SECTION_PADDING_Y = "py-16";
|
|
18
|
+
const MODAL_SURFACE_CLASSES = [
|
|
19
|
+
"fixed left-1/2 top-1/2 max-h-[92vh] -translate-x-1/2 -translate-y-1/2",
|
|
20
|
+
"bg-bg-base text-fg-base shadow-xl outline-none rounded-md border-2 border-border-subtle",
|
|
21
|
+
"flex flex-col overflow-hidden",
|
|
22
|
+
"data-[state=open]:animate-fade-in data-[state=closed]:animate-fade-out",
|
|
23
|
+
].join(" ");
|
|
24
|
+
const MODAL_HEADER_CLASSES = [
|
|
25
|
+
"flex justify-between gap-12",
|
|
26
|
+
MODAL_SECTION_PADDING_X,
|
|
27
|
+
MODAL_SECTION_PADDING_Y,
|
|
28
|
+
"border-b border-border-subtle",
|
|
29
|
+
].join(" ");
|
|
30
|
+
const MODAL_HEADER_CONTENT_CLASSES = "flex min-w-0 flex-1 flex-col gap-4";
|
|
31
|
+
const MODAL_BODY_CLASSES = [
|
|
32
|
+
"flex-1 overflow-auto",
|
|
33
|
+
MODAL_SECTION_PADDING_X,
|
|
34
|
+
MODAL_SECTION_PADDING_Y,
|
|
35
|
+
].join(" ");
|
|
36
|
+
const MODAL_FOOTER_CLASSES = [
|
|
37
|
+
"flex items-center justify-end gap-8",
|
|
38
|
+
MODAL_SECTION_PADDING_X,
|
|
39
|
+
MODAL_SECTION_PADDING_Y,
|
|
40
|
+
"border-t border-border-subtle",
|
|
41
|
+
].join(" ");
|
|
42
|
+
const MODAL_TITLE_CLASSES = "text-lg font-semibold text-fg-base";
|
|
43
|
+
const MODAL_DESCRIPTION_CLASSES = "text-sm text-fg-subtle";
|
|
44
|
+
const MODAL_CLOSE_LABEL = "Close modal";
|
|
45
|
+
const MODAL_SIZE_CLASSES = {
|
|
46
|
+
small: "w-[min(92vw,420px)]",
|
|
47
|
+
medium: "w-[min(92vw,640px)]",
|
|
48
|
+
large: "w-[min(92vw,840px)]",
|
|
49
|
+
full: "w-[min(96vw,1200px)] h-[min(92vh,900px)]",
|
|
50
|
+
};
|
|
7
51
|
|
|
8
52
|
const getSizeClasses = (size) => {
|
|
9
|
-
|
|
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
|
-
}
|
|
53
|
+
return MODAL_SIZE_CLASSES[size] ?? MODAL_SIZE_CLASSES.medium;
|
|
20
54
|
};
|
|
21
55
|
|
|
22
56
|
export const Modal = ({
|
|
@@ -28,6 +62,11 @@ export const Modal = ({
|
|
|
28
62
|
children,
|
|
29
63
|
...props
|
|
30
64
|
}) => {
|
|
65
|
+
const contextValue = useMemo(
|
|
66
|
+
() => ({ hasOnClose: typeof onClose === "function" }),
|
|
67
|
+
[onClose]
|
|
68
|
+
);
|
|
69
|
+
|
|
31
70
|
const handleOpenChange = useCallback(
|
|
32
71
|
(nextOpen) => {
|
|
33
72
|
onOpenChange?.(nextOpen);
|
|
@@ -41,14 +80,16 @@ export const Modal = ({
|
|
|
41
80
|
);
|
|
42
81
|
|
|
43
82
|
return (
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
83
|
+
<ModalContext.Provider value={contextValue}>
|
|
84
|
+
<DialogPrimitive.Root
|
|
85
|
+
open={open}
|
|
86
|
+
defaultOpen={defaultOpen}
|
|
87
|
+
onOpenChange={handleOpenChange}
|
|
88
|
+
{...props}
|
|
89
|
+
>
|
|
90
|
+
{children}
|
|
91
|
+
</DialogPrimitive.Root>
|
|
92
|
+
</ModalContext.Provider>
|
|
52
93
|
);
|
|
53
94
|
};
|
|
54
95
|
|
|
@@ -56,6 +97,17 @@ export const ModalTrigger = DialogPrimitive.Trigger;
|
|
|
56
97
|
export const ModalClose = DialogPrimitive.Close;
|
|
57
98
|
export const ModalPortal = DialogPrimitive.Portal;
|
|
58
99
|
|
|
100
|
+
const ModalHeaderCloseButton = () => (
|
|
101
|
+
<ModalClose asChild>
|
|
102
|
+
<IconButton
|
|
103
|
+
size="small"
|
|
104
|
+
isBare
|
|
105
|
+
icon={<Close aria-hidden="true" />}
|
|
106
|
+
aria-label={MODAL_CLOSE_LABEL}
|
|
107
|
+
/>
|
|
108
|
+
</ModalClose>
|
|
109
|
+
);
|
|
110
|
+
|
|
59
111
|
export const ModalOverlay = forwardRef(
|
|
60
112
|
({ className, style, ...props }, ref) => (
|
|
61
113
|
<DialogPrimitive.Overlay
|
|
@@ -88,21 +140,20 @@ export const ModalContent = forwardRef(
|
|
|
88
140
|
ref
|
|
89
141
|
) => {
|
|
90
142
|
const resolvedBackdropProps = backdropProps ?? backDropProps ?? {};
|
|
143
|
+
const { className: backdropClassName, style: backdropStyle, ...overlayProps } =
|
|
144
|
+
resolvedBackdropProps;
|
|
91
145
|
|
|
92
146
|
return (
|
|
93
147
|
<ModalPortal>
|
|
94
148
|
<ModalOverlay
|
|
95
|
-
{...
|
|
96
|
-
className={cx(
|
|
97
|
-
style={{ ...
|
|
149
|
+
{...overlayProps}
|
|
150
|
+
className={cx(backdropClassName)}
|
|
151
|
+
style={{ ...backdropStyle }}
|
|
98
152
|
/>
|
|
99
153
|
<DialogPrimitive.Content
|
|
100
154
|
ref={ref}
|
|
101
155
|
className={cx(
|
|
102
|
-
|
|
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",
|
|
156
|
+
MODAL_SURFACE_CLASSES,
|
|
106
157
|
getSizeClasses(size),
|
|
107
158
|
className
|
|
108
159
|
)}
|
|
@@ -119,23 +170,27 @@ export const ModalContent = forwardRef(
|
|
|
119
170
|
|
|
120
171
|
ModalContent.displayName = "ModalContent";
|
|
121
172
|
|
|
122
|
-
export const ModalHeader = forwardRef(({ className, ...props }, ref) =>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
173
|
+
export const ModalHeader = forwardRef(({ className, children, ...props }, ref) => {
|
|
174
|
+
const { hasOnClose } = useContext(ModalContext);
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<div
|
|
178
|
+
ref={ref}
|
|
179
|
+
className={cx(MODAL_HEADER_CLASSES, className)}
|
|
180
|
+
{...props}
|
|
181
|
+
>
|
|
182
|
+
<div className={MODAL_HEADER_CONTENT_CLASSES}>{children}</div>
|
|
183
|
+
{hasOnClose ? <ModalHeaderCloseButton /> : null}
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
});
|
|
132
187
|
|
|
133
188
|
ModalHeader.displayName = "ModalHeader";
|
|
134
189
|
|
|
135
190
|
export const ModalBody = forwardRef(({ className, ...props }, ref) => (
|
|
136
191
|
<div
|
|
137
192
|
ref={ref}
|
|
138
|
-
className={cx(
|
|
193
|
+
className={cx(MODAL_BODY_CLASSES, className)}
|
|
139
194
|
{...props}
|
|
140
195
|
/>
|
|
141
196
|
));
|
|
@@ -145,10 +200,7 @@ ModalBody.displayName = "ModalBody";
|
|
|
145
200
|
export const ModalFooter = forwardRef(({ className, ...props }, ref) => (
|
|
146
201
|
<div
|
|
147
202
|
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
|
-
)}
|
|
203
|
+
className={cx(MODAL_FOOTER_CLASSES, className)}
|
|
152
204
|
{...props}
|
|
153
205
|
/>
|
|
154
206
|
));
|
|
@@ -158,7 +210,7 @@ ModalFooter.displayName = "ModalFooter";
|
|
|
158
210
|
export const ModalTitle = forwardRef(({ className, ...props }, ref) => (
|
|
159
211
|
<DialogPrimitive.Title
|
|
160
212
|
ref={ref}
|
|
161
|
-
className={cx(
|
|
213
|
+
className={cx(MODAL_TITLE_CLASSES, className)}
|
|
162
214
|
{...props}
|
|
163
215
|
/>
|
|
164
216
|
));
|
|
@@ -168,7 +220,7 @@ ModalTitle.displayName = "ModalTitle";
|
|
|
168
220
|
export const ModalDescription = forwardRef(({ className, ...props }, ref) => (
|
|
169
221
|
<DialogPrimitive.Description
|
|
170
222
|
ref={ref}
|
|
171
|
-
className={cx(
|
|
223
|
+
className={cx(MODAL_DESCRIPTION_CLASSES, className)}
|
|
172
224
|
{...props}
|
|
173
225
|
/>
|
|
174
226
|
));
|
|
@@ -13,9 +13,9 @@ import {
|
|
|
13
13
|
let lastRootProps;
|
|
14
14
|
|
|
15
15
|
vi.mock("@radix-ui/react-dialog", () => {
|
|
16
|
-
const Passthrough = ({ children, ...props }) =>
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const Passthrough = ({ children, ...props }) => <div {...props}>{children}</div>;
|
|
17
|
+
const AsChildPassthrough = ({ children, asChild, ...props }) =>
|
|
18
|
+
asChild ? children : <button type="button" {...props}>{children}</button>;
|
|
19
19
|
const Buttonish = ({ children, ...props }) => (
|
|
20
20
|
<button type="button" {...props}>
|
|
21
21
|
{children}
|
|
@@ -29,18 +29,18 @@ vi.mock("@radix-ui/react-dialog", () => {
|
|
|
29
29
|
<div ref={ref} data-testid="radix-overlay" {...props} />
|
|
30
30
|
));
|
|
31
31
|
Overlay.displayName = "RadixOverlay";
|
|
32
|
-
const Root = ({ children,
|
|
33
|
-
lastRootProps =
|
|
34
|
-
return <div
|
|
32
|
+
const Root = ({ children, open, defaultOpen, onOpenChange }) => {
|
|
33
|
+
lastRootProps = { open, defaultOpen, onOpenChange };
|
|
34
|
+
return <div>{children}</div>;
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
return {
|
|
38
38
|
Root,
|
|
39
39
|
Trigger: Buttonish,
|
|
40
|
-
Portal:
|
|
40
|
+
Portal: ({ children }) => <div>{children}</div>,
|
|
41
41
|
Overlay,
|
|
42
42
|
Content,
|
|
43
|
-
Close:
|
|
43
|
+
Close: AsChildPassthrough,
|
|
44
44
|
Title: Passthrough,
|
|
45
45
|
Description: Passthrough,
|
|
46
46
|
};
|
|
@@ -89,6 +89,34 @@ describe("ModalContent", () => {
|
|
|
89
89
|
expect(screen.getByText("Body")).toHaveClass("overflow-auto");
|
|
90
90
|
expect(screen.getByText("Footer")).toHaveClass("border-t");
|
|
91
91
|
});
|
|
92
|
+
|
|
93
|
+
it("adds a close icon button to the header when Modal receives onClose", () => {
|
|
94
|
+
render(
|
|
95
|
+
<Modal onClose={vi.fn()}>
|
|
96
|
+
<ModalContent>
|
|
97
|
+
<ModalHeader>
|
|
98
|
+
<ModalTitle>Header</ModalTitle>
|
|
99
|
+
</ModalHeader>
|
|
100
|
+
</ModalContent>
|
|
101
|
+
</Modal>
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(screen.getByLabelText("Close modal")).toBeInTheDocument();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("does not add a close icon button to the header without onClose", () => {
|
|
108
|
+
render(
|
|
109
|
+
<Modal>
|
|
110
|
+
<ModalContent>
|
|
111
|
+
<ModalHeader>
|
|
112
|
+
<ModalTitle>Header</ModalTitle>
|
|
113
|
+
</ModalHeader>
|
|
114
|
+
</ModalContent>
|
|
115
|
+
</Modal>
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
expect(screen.queryByLabelText("Close modal")).not.toBeInTheDocument();
|
|
119
|
+
});
|
|
92
120
|
});
|
|
93
121
|
|
|
94
122
|
describe("Modal", () => {
|