@edux-design/modals 0.1.2 → 0.2.0

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.
@@ -1,304 +0,0 @@
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 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
-
30
- const meta = {
31
- title: "Components/Modal",
32
- component: Modal,
33
- parameters: {
34
- layout: "centered",
35
- docs: {
36
- description: {
37
- component: `
38
- Centered **Modal** built on Radix Dialog primitives.
39
-
40
- Use for confirmations, forms, and focused blocking workflows.
41
- `,
42
- },
43
- },
44
- },
45
- argTypes: {
46
- size: {
47
- control: "select",
48
- options: ["small", "medium", "large", "full"],
49
- description: "Width preset for the modal content.",
50
- table: { defaultValue: { summary: "medium" } },
51
- },
52
- },
53
- };
54
-
55
- export default meta;
56
-
57
- export const Basic = {
58
- render: (args) => (
59
- <Modal>
60
- <ModalTrigger asChild>
61
- <Button>Open Modal</Button>
62
- </ModalTrigger>
63
- <ModalContent {...args}>
64
- <ModalHeader>
65
- <ModalTitle>Publish changes</ModalTitle>
66
- <ModalDescription>
67
- Review your changes before making them visible to learners.
68
- </ModalDescription>
69
- </ModalHeader>
70
- <ModalBody>
71
- <BodyCopy />
72
- </ModalBody>
73
- <ModalFooter>
74
- <ModalClose asChild>
75
- <Button isSecondary>Cancel</Button>
76
- </ModalClose>
77
- <Button>Publish</Button>
78
- </ModalFooter>
79
- </ModalContent>
80
- </Modal>
81
- ),
82
- args: {
83
- size: "medium",
84
- },
85
- };
86
-
87
- export const Controlled = {
88
- render: (args) => {
89
- const [open, setOpen] = useState(false);
90
-
91
- return (
92
- <Modal open={open} onOpenChange={setOpen}>
93
- <ModalTrigger asChild>
94
- <Button>
95
- {open ? "Close Modal" : "Open Modal"}
96
- </Button>
97
- </ModalTrigger>
98
- <ModalContent {...args}>
99
- <ModalHeader>
100
- <ModalTitle>Controlled modal</ModalTitle>
101
- <ModalDescription>
102
- State is managed externally via open and onOpenChange.
103
- </ModalDescription>
104
- </ModalHeader>
105
- <ModalBody>
106
- <BodyCopy />
107
- </ModalBody>
108
- <ModalFooter>
109
- <Button isBare onClick={() => setOpen(false)}>
110
- Dismiss
111
- </Button>
112
- </ModalFooter>
113
- </ModalContent>
114
- </Modal>
115
- );
116
- },
117
- args: {
118
- size: "medium",
119
- },
120
- };
121
-
122
- export const Sizes = {
123
- render: () => (
124
- <div className="flex flex-wrap gap-12">
125
- {["small", "medium", "large", "full"].map((size) => (
126
- <Modal key={size}>
127
- <ModalTrigger asChild>
128
- <Button isSecondary>Size: {size}</Button>
129
- </ModalTrigger>
130
- <ModalContent size={size}>
131
- <ModalHeader>
132
- <ModalTitle>{size} modal</ModalTitle>
133
- <ModalDescription>
134
- Demonstrates the {size} content width preset.
135
- </ModalDescription>
136
- </ModalHeader>
137
- <ModalBody>
138
- <BodyCopy />
139
- </ModalBody>
140
- </ModalContent>
141
- </Modal>
142
- ))}
143
- </div>
144
- ),
145
- };
146
-
147
- export const CustomBackdrop = {
148
- render: (args) => (
149
- <Modal>
150
- <ModalTrigger asChild>
151
- <Button>Open With Backdrop</Button>
152
- </ModalTrigger>
153
- <ModalContent
154
- {...args}
155
- backdropProps={{
156
- className: "bg-bg-primary-subtle opacity-80",
157
- }}
158
- >
159
- <ModalHeader>
160
- <ModalTitle>Custom backdrop</ModalTitle>
161
- <ModalDescription>
162
- Backdrop styles can be customized via backdropProps.
163
- </ModalDescription>
164
- </ModalHeader>
165
- <ModalBody>
166
- <BodyCopy />
167
- </ModalBody>
168
- </ModalContent>
169
- </Modal>
170
- ),
171
- args: {
172
- size: "medium",
173
- },
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
- };
@@ -1,228 +0,0 @@
1
- import React, {
2
- createContext,
3
- forwardRef,
4
- useCallback,
5
- useContext,
6
- useMemo,
7
- } from "react";
8
- import * as DialogPrimitive from "@radix-ui/react-dialog";
9
- import { cx } from "@edux-design/utils";
10
- import { IconButton } from "@edux-design/buttons";
11
- import { Close } from "@edux-design/icons";
12
-
13
- const MODAL_OVERLAY_Z_INDEX = 2147483645;
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
- };
51
-
52
- const getSizeClasses = (size) => {
53
- return MODAL_SIZE_CLASSES[size] ?? MODAL_SIZE_CLASSES.medium;
54
- };
55
-
56
- export const Modal = ({
57
- open,
58
- defaultOpen,
59
- onOpenChange,
60
- onOpen,
61
- onClose,
62
- children,
63
- ...props
64
- }) => {
65
- const contextValue = useMemo(
66
- () => ({ hasOnClose: typeof onClose === "function" }),
67
- [onClose]
68
- );
69
-
70
- const handleOpenChange = useCallback(
71
- (nextOpen) => {
72
- onOpenChange?.(nextOpen);
73
- if (nextOpen) {
74
- onOpen?.();
75
- return;
76
- }
77
- onClose?.();
78
- },
79
- [onClose, onOpen, onOpenChange]
80
- );
81
-
82
- return (
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>
93
- );
94
- };
95
-
96
- export const ModalTrigger = DialogPrimitive.Trigger;
97
- export const ModalClose = DialogPrimitive.Close;
98
- export const ModalPortal = DialogPrimitive.Portal;
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
-
111
- export const ModalOverlay = forwardRef(
112
- ({ className, style, ...props }, ref) => (
113
- <DialogPrimitive.Overlay
114
- ref={ref}
115
- className={cx(
116
- "fixed inset-0 bg-bg-focus-overlay opacity-70",
117
- "data-[state=open]:animate-drawer-fade-in data-[state=closed]:animate-drawer-fade-out",
118
- className
119
- )}
120
- style={{ zIndex: MODAL_OVERLAY_Z_INDEX, ...style }}
121
- {...props}
122
- />
123
- )
124
- );
125
-
126
- ModalOverlay.displayName = "ModalOverlay";
127
-
128
- export const ModalContent = forwardRef(
129
- (
130
- {
131
- children,
132
- size = "medium",
133
- className,
134
- style,
135
- backdropProps,
136
- backDropProps,
137
- contentProps = {},
138
- ...restProps
139
- },
140
- ref
141
- ) => {
142
- const resolvedBackdropProps = backdropProps ?? backDropProps ?? {};
143
- const { className: backdropClassName, style: backdropStyle, ...overlayProps } =
144
- resolvedBackdropProps;
145
-
146
- return (
147
- <ModalPortal>
148
- <ModalOverlay
149
- {...overlayProps}
150
- className={cx(backdropClassName)}
151
- style={{ ...backdropStyle }}
152
- />
153
- <DialogPrimitive.Content
154
- ref={ref}
155
- className={cx(
156
- MODAL_SURFACE_CLASSES,
157
- getSizeClasses(size),
158
- className
159
- )}
160
- style={{ zIndex: MODAL_CONTENT_Z_INDEX, ...style }}
161
- {...contentProps}
162
- {...restProps}
163
- >
164
- {children}
165
- </DialogPrimitive.Content>
166
- </ModalPortal>
167
- );
168
- }
169
- );
170
-
171
- ModalContent.displayName = "ModalContent";
172
-
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
- });
187
-
188
- ModalHeader.displayName = "ModalHeader";
189
-
190
- export const ModalBody = forwardRef(({ className, ...props }, ref) => (
191
- <div
192
- ref={ref}
193
- className={cx(MODAL_BODY_CLASSES, className)}
194
- {...props}
195
- />
196
- ));
197
-
198
- ModalBody.displayName = "ModalBody";
199
-
200
- export const ModalFooter = forwardRef(({ className, ...props }, ref) => (
201
- <div
202
- ref={ref}
203
- className={cx(MODAL_FOOTER_CLASSES, className)}
204
- {...props}
205
- />
206
- ));
207
-
208
- ModalFooter.displayName = "ModalFooter";
209
-
210
- export const ModalTitle = forwardRef(({ className, ...props }, ref) => (
211
- <DialogPrimitive.Title
212
- ref={ref}
213
- className={cx(MODAL_TITLE_CLASSES, className)}
214
- {...props}
215
- />
216
- ));
217
-
218
- ModalTitle.displayName = "ModalTitle";
219
-
220
- export const ModalDescription = forwardRef(({ className, ...props }, ref) => (
221
- <DialogPrimitive.Description
222
- ref={ref}
223
- className={cx(MODAL_DESCRIPTION_CLASSES, className)}
224
- {...props}
225
- />
226
- ));
227
-
228
- ModalDescription.displayName = "ModalDescription";
@@ -1,139 +0,0 @@
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 }) => <div {...props}>{children}</div>;
17
- const AsChildPassthrough = ({ children, asChild, ...props }) =>
18
- asChild ? children : <button type="button" {...props}>{children}</button>;
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, open, defaultOpen, onOpenChange }) => {
33
- lastRootProps = { open, defaultOpen, onOpenChange };
34
- return <div>{children}</div>;
35
- };
36
-
37
- return {
38
- Root,
39
- Trigger: Buttonish,
40
- Portal: ({ children }) => <div>{children}</div>,
41
- Overlay,
42
- Content,
43
- Close: AsChildPassthrough,
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
- 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
- });
120
- });
121
-
122
- describe("Modal", () => {
123
- it("fires onOpen and onClose from onOpenChange", () => {
124
- const onOpen = vi.fn();
125
- const onClose = vi.fn();
126
-
127
- render(
128
- <Modal onOpen={onOpen} onClose={onClose}>
129
- <ModalContent>Body</ModalContent>
130
- </Modal>
131
- );
132
-
133
- lastRootProps.onOpenChange(true);
134
- lastRootProps.onOpenChange(false);
135
-
136
- expect(onOpen).toHaveBeenCalledTimes(1);
137
- expect(onClose).toHaveBeenCalledTimes(1);
138
- });
139
- });
package/src/index.js DELETED
@@ -1,13 +0,0 @@
1
- export {
2
- Modal,
3
- ModalTrigger,
4
- ModalClose,
5
- ModalPortal,
6
- ModalOverlay,
7
- ModalContent,
8
- ModalHeader,
9
- ModalBody,
10
- ModalFooter,
11
- ModalTitle,
12
- ModalDescription,
13
- } from "./elements/Modal";