@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.
- package/CHANGELOG.md +18 -1
- package/README.md +14 -0
- package/dist/index.d.mts +78 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.js +258 -67
- package/dist/index.mjs +264 -68
- package/docs/confirmation.md +117 -0
- package/package.json +11 -18
- package/src/demos/Modal.stories.jsx +0 -304
- package/src/elements/Modal.jsx +0 -228
- package/src/elements/Modal.test.jsx +0 -139
- package/src/index.js +0 -13
package/dist/index.mjs
CHANGED
|
@@ -1,21 +1,56 @@
|
|
|
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";
|
|
13
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
14
|
var MODAL_OVERLAY_Z_INDEX = 2147483645;
|
|
6
15
|
var MODAL_CONTENT_Z_INDEX = 2147483646;
|
|
16
|
+
var ModalContext = createContext({ hasOnClose: false });
|
|
17
|
+
var MODAL_SECTION_PADDING_X = "px-16";
|
|
18
|
+
var MODAL_SECTION_PADDING_Y = "py-16";
|
|
19
|
+
var MODAL_SURFACE_CLASSES = [
|
|
20
|
+
"fixed left-1/2 top-1/2 max-h-[92vh] -translate-x-1/2 -translate-y-1/2",
|
|
21
|
+
"bg-bg-base text-fg-base shadow-xl outline-none rounded-md border-2 border-border-subtle",
|
|
22
|
+
"flex flex-col overflow-hidden",
|
|
23
|
+
"data-[state=open]:animate-fade-in data-[state=closed]:animate-fade-out"
|
|
24
|
+
].join(" ");
|
|
25
|
+
var MODAL_HEADER_CLASSES = [
|
|
26
|
+
"flex justify-between gap-12",
|
|
27
|
+
MODAL_SECTION_PADDING_X,
|
|
28
|
+
MODAL_SECTION_PADDING_Y,
|
|
29
|
+
"border-b border-border-subtle"
|
|
30
|
+
].join(" ");
|
|
31
|
+
var MODAL_HEADER_CONTENT_CLASSES = "flex min-w-0 flex-1 flex-col gap-4";
|
|
32
|
+
var MODAL_BODY_CLASSES = [
|
|
33
|
+
"flex-1 overflow-auto",
|
|
34
|
+
MODAL_SECTION_PADDING_X,
|
|
35
|
+
MODAL_SECTION_PADDING_Y
|
|
36
|
+
].join(" ");
|
|
37
|
+
var MODAL_FOOTER_CLASSES = [
|
|
38
|
+
"flex items-center justify-end gap-8",
|
|
39
|
+
MODAL_SECTION_PADDING_X,
|
|
40
|
+
MODAL_SECTION_PADDING_Y,
|
|
41
|
+
"border-t border-border-subtle"
|
|
42
|
+
].join(" ");
|
|
43
|
+
var MODAL_TITLE_CLASSES = "text-lg font-semibold text-fg-base";
|
|
44
|
+
var MODAL_DESCRIPTION_CLASSES = "text-sm text-fg-subtle";
|
|
45
|
+
var MODAL_CLOSE_LABEL = "Close modal";
|
|
46
|
+
var MODAL_SIZE_CLASSES = {
|
|
47
|
+
small: "w-[min(92vw,420px)]",
|
|
48
|
+
medium: "w-[min(92vw,640px)]",
|
|
49
|
+
large: "w-[min(92vw,840px)]",
|
|
50
|
+
full: "w-[min(96vw,1200px)] h-[min(92vh,900px)]"
|
|
51
|
+
};
|
|
7
52
|
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
|
-
}
|
|
53
|
+
return MODAL_SIZE_CLASSES[size] ?? MODAL_SIZE_CLASSES.medium;
|
|
19
54
|
};
|
|
20
55
|
var Modal = ({
|
|
21
56
|
open,
|
|
@@ -26,33 +61,46 @@ var Modal = ({
|
|
|
26
61
|
children,
|
|
27
62
|
...props
|
|
28
63
|
}) => {
|
|
64
|
+
const contextValue = useMemo(
|
|
65
|
+
() => ({ hasOnClose: typeof onClose === "function" }),
|
|
66
|
+
[onClose]
|
|
67
|
+
);
|
|
29
68
|
const handleOpenChange = useCallback(
|
|
30
69
|
(nextOpen) => {
|
|
31
|
-
onOpenChange
|
|
70
|
+
onOpenChange?.(nextOpen);
|
|
32
71
|
if (nextOpen) {
|
|
33
|
-
onOpen
|
|
72
|
+
onOpen?.();
|
|
34
73
|
return;
|
|
35
74
|
}
|
|
36
|
-
onClose
|
|
75
|
+
onClose?.();
|
|
37
76
|
},
|
|
38
77
|
[onClose, onOpen, onOpenChange]
|
|
39
78
|
);
|
|
40
|
-
return /* @__PURE__ */
|
|
79
|
+
return /* @__PURE__ */ jsx(ModalContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
|
|
41
80
|
DialogPrimitive.Root,
|
|
42
81
|
{
|
|
43
82
|
open,
|
|
44
83
|
defaultOpen,
|
|
45
84
|
onOpenChange: handleOpenChange,
|
|
46
|
-
...props
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
);
|
|
85
|
+
...props,
|
|
86
|
+
children
|
|
87
|
+
}
|
|
88
|
+
) });
|
|
50
89
|
};
|
|
51
90
|
var ModalTrigger = DialogPrimitive.Trigger;
|
|
52
91
|
var ModalClose = DialogPrimitive.Close;
|
|
53
92
|
var ModalPortal = DialogPrimitive.Portal;
|
|
93
|
+
var ModalHeaderCloseButton = () => /* @__PURE__ */ jsx(ModalClose, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
94
|
+
IconButton,
|
|
95
|
+
{
|
|
96
|
+
size: "small",
|
|
97
|
+
isBare: true,
|
|
98
|
+
icon: /* @__PURE__ */ jsx(Close2, { "aria-hidden": "true" }),
|
|
99
|
+
"aria-label": MODAL_CLOSE_LABEL
|
|
100
|
+
}
|
|
101
|
+
) });
|
|
54
102
|
var ModalOverlay = forwardRef(
|
|
55
|
-
({ className, style, ...props }, ref) => /* @__PURE__ */
|
|
103
|
+
({ className, style, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
56
104
|
DialogPrimitive.Overlay,
|
|
57
105
|
{
|
|
58
106
|
ref,
|
|
@@ -76,89 +124,237 @@ var ModalContent = forwardRef(
|
|
|
76
124
|
backdropProps,
|
|
77
125
|
backDropProps,
|
|
78
126
|
contentProps = {},
|
|
127
|
+
portalProps = {},
|
|
79
128
|
...restProps
|
|
80
129
|
}, ref) => {
|
|
81
130
|
const resolvedBackdropProps = backdropProps ?? backDropProps ?? {};
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
131
|
+
const { className: backdropClassName, style: backdropStyle, ...overlayProps } = resolvedBackdropProps;
|
|
132
|
+
return /* @__PURE__ */ jsxs(ModalPortal, { ...portalProps, children: [
|
|
133
|
+
/* @__PURE__ */ jsx(
|
|
134
|
+
ModalOverlay,
|
|
135
|
+
{
|
|
136
|
+
...overlayProps,
|
|
137
|
+
className: cx(backdropClassName),
|
|
138
|
+
style: { ...backdropStyle }
|
|
139
|
+
}
|
|
140
|
+
),
|
|
141
|
+
/* @__PURE__ */ jsx(
|
|
142
|
+
DialogPrimitive.Content,
|
|
143
|
+
{
|
|
144
|
+
ref,
|
|
145
|
+
className: cx(
|
|
146
|
+
MODAL_SURFACE_CLASSES,
|
|
147
|
+
getSizeClasses(size),
|
|
148
|
+
className
|
|
149
|
+
),
|
|
150
|
+
style: { zIndex: MODAL_CONTENT_Z_INDEX, ...style },
|
|
151
|
+
...contentProps,
|
|
152
|
+
...restProps,
|
|
153
|
+
children
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
] });
|
|
107
157
|
}
|
|
108
158
|
);
|
|
109
159
|
ModalContent.displayName = "ModalContent";
|
|
110
|
-
var ModalHeader = forwardRef(({ className, ...props }, ref) =>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
className
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
)
|
|
160
|
+
var ModalHeader = forwardRef(({ className, children, ...props }, ref) => {
|
|
161
|
+
const { hasOnClose } = useContext(ModalContext);
|
|
162
|
+
return /* @__PURE__ */ jsxs(
|
|
163
|
+
"div",
|
|
164
|
+
{
|
|
165
|
+
ref,
|
|
166
|
+
className: cx(MODAL_HEADER_CLASSES, className),
|
|
167
|
+
...props,
|
|
168
|
+
children: [
|
|
169
|
+
/* @__PURE__ */ jsx("div", { className: MODAL_HEADER_CONTENT_CLASSES, children }),
|
|
170
|
+
hasOnClose ? /* @__PURE__ */ jsx(ModalHeaderCloseButton, {}) : null
|
|
171
|
+
]
|
|
172
|
+
}
|
|
173
|
+
);
|
|
174
|
+
});
|
|
121
175
|
ModalHeader.displayName = "ModalHeader";
|
|
122
|
-
var ModalBody = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */
|
|
176
|
+
var ModalBody = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
123
177
|
"div",
|
|
124
178
|
{
|
|
125
179
|
ref,
|
|
126
|
-
className: cx(
|
|
180
|
+
className: cx(MODAL_BODY_CLASSES, className),
|
|
127
181
|
...props
|
|
128
182
|
}
|
|
129
183
|
));
|
|
130
184
|
ModalBody.displayName = "ModalBody";
|
|
131
|
-
var ModalFooter = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */
|
|
185
|
+
var ModalFooter = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
132
186
|
"div",
|
|
133
187
|
{
|
|
134
188
|
ref,
|
|
135
|
-
className: cx(
|
|
136
|
-
"flex items-center justify-end gap-8 px-20 py-16 border-t border-border-subtle",
|
|
137
|
-
className
|
|
138
|
-
),
|
|
189
|
+
className: cx(MODAL_FOOTER_CLASSES, className),
|
|
139
190
|
...props
|
|
140
191
|
}
|
|
141
192
|
));
|
|
142
193
|
ModalFooter.displayName = "ModalFooter";
|
|
143
|
-
var ModalTitle = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */
|
|
194
|
+
var ModalTitle = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
144
195
|
DialogPrimitive.Title,
|
|
145
196
|
{
|
|
146
197
|
ref,
|
|
147
|
-
className: cx(
|
|
198
|
+
className: cx(MODAL_TITLE_CLASSES, className),
|
|
148
199
|
...props
|
|
149
200
|
}
|
|
150
201
|
));
|
|
151
202
|
ModalTitle.displayName = "ModalTitle";
|
|
152
|
-
var ModalDescription = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */
|
|
203
|
+
var ModalDescription = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
153
204
|
DialogPrimitive.Description,
|
|
154
205
|
{
|
|
155
206
|
ref,
|
|
156
|
-
className: cx(
|
|
207
|
+
className: cx(MODAL_DESCRIPTION_CLASSES, className),
|
|
157
208
|
...props
|
|
158
209
|
}
|
|
159
210
|
));
|
|
160
211
|
ModalDescription.displayName = "ModalDescription";
|
|
212
|
+
|
|
213
|
+
// src/elements/ConfirmationDialog.tsx
|
|
214
|
+
import { forwardRef as forwardRef2, useId, useRef, useState } from "react";
|
|
215
|
+
import { Button } from "@edux-design/buttons";
|
|
216
|
+
import { cx as cx2 } from "@edux-design/utils";
|
|
217
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
218
|
+
var ConfirmationDialog = forwardRef2(function ConfirmationDialog2({
|
|
219
|
+
open,
|
|
220
|
+
defaultOpen = false,
|
|
221
|
+
onOpenChange,
|
|
222
|
+
modal = true,
|
|
223
|
+
title,
|
|
224
|
+
description,
|
|
225
|
+
children,
|
|
226
|
+
trigger,
|
|
227
|
+
onConfirm,
|
|
228
|
+
onCancel,
|
|
229
|
+
confirmLabel = "Confirm",
|
|
230
|
+
cancelLabel = "Cancel",
|
|
231
|
+
pendingLabel = "Working\u2026",
|
|
232
|
+
pending = false,
|
|
233
|
+
error,
|
|
234
|
+
confirmDisabled = false,
|
|
235
|
+
cancelDisabled = false,
|
|
236
|
+
dismissible = true,
|
|
237
|
+
returnFocusRef,
|
|
238
|
+
isDanger = false,
|
|
239
|
+
allowDismissWhilePending = false,
|
|
240
|
+
size = "small",
|
|
241
|
+
contentProps = {},
|
|
242
|
+
backdropProps,
|
|
243
|
+
portalProps,
|
|
244
|
+
confirmButtonProps = {},
|
|
245
|
+
cancelButtonProps = {}
|
|
246
|
+
}, ref) {
|
|
247
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
248
|
+
const resolvedOpen = open ?? uncontrolledOpen;
|
|
249
|
+
const cancelRef = useRef(null);
|
|
250
|
+
const previousFocusRef = useRef(null);
|
|
251
|
+
const statusId = useId();
|
|
252
|
+
const locked = !dismissible || pending && !allowDismissWhilePending;
|
|
253
|
+
const hasBody = children != null || Boolean(error) || pending;
|
|
254
|
+
const {
|
|
255
|
+
onClick: onCancelClick,
|
|
256
|
+
ref: actionRef,
|
|
257
|
+
...cancelProps
|
|
258
|
+
} = cancelButtonProps;
|
|
259
|
+
const { onClick: onConfirmClick, ...confirmProps } = confirmButtonProps;
|
|
260
|
+
const requestOpen = (next) => {
|
|
261
|
+
if (!next && locked) return;
|
|
262
|
+
if (open === void 0) setUncontrolledOpen(next);
|
|
263
|
+
onOpenChange?.(next);
|
|
264
|
+
};
|
|
265
|
+
return /* @__PURE__ */ jsxs2(Modal, { open: resolvedOpen, onOpenChange: requestOpen, modal, children: [
|
|
266
|
+
trigger && /* @__PURE__ */ jsx2(ModalTrigger, { asChild: true, children: trigger }),
|
|
267
|
+
/* @__PURE__ */ jsxs2(
|
|
268
|
+
ModalContent,
|
|
269
|
+
{
|
|
270
|
+
...contentProps,
|
|
271
|
+
ref,
|
|
272
|
+
size,
|
|
273
|
+
backdropProps,
|
|
274
|
+
portalProps,
|
|
275
|
+
...!description && !contentProps["aria-describedby"] ? { "aria-describedby": void 0 } : {},
|
|
276
|
+
"aria-busy": pending || void 0,
|
|
277
|
+
className: cx2("motion-reduce:animate-none", contentProps.className),
|
|
278
|
+
onOpenAutoFocus: (event) => {
|
|
279
|
+
previousFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
280
|
+
contentProps.onOpenAutoFocus?.(event);
|
|
281
|
+
if (!event.defaultPrevented) {
|
|
282
|
+
event.preventDefault();
|
|
283
|
+
if (!locked && !cancelDisabled) cancelRef.current?.focus();
|
|
284
|
+
else if (event.target instanceof HTMLElement) event.target.focus();
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
onCloseAutoFocus: (event) => {
|
|
288
|
+
contentProps.onCloseAutoFocus?.(event);
|
|
289
|
+
const target = returnFocusRef?.current ?? (!trigger ? previousFocusRef.current : null);
|
|
290
|
+
if (!event.defaultPrevented && target?.isConnected) {
|
|
291
|
+
event.preventDefault();
|
|
292
|
+
target.focus();
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
onEscapeKeyDown: (event) => {
|
|
296
|
+
contentProps.onEscapeKeyDown?.(event);
|
|
297
|
+
if (locked) event.preventDefault();
|
|
298
|
+
},
|
|
299
|
+
onInteractOutside: (event) => {
|
|
300
|
+
contentProps.onInteractOutside?.(event);
|
|
301
|
+
if (locked) event.preventDefault();
|
|
302
|
+
},
|
|
303
|
+
children: [
|
|
304
|
+
/* @__PURE__ */ jsxs2(ModalHeader, { className: !hasBody ? "border-b-0" : void 0, children: [
|
|
305
|
+
/* @__PURE__ */ jsx2(ModalTitle, { children: title }),
|
|
306
|
+
description && /* @__PURE__ */ jsx2(ModalDescription, { children: description })
|
|
307
|
+
] }),
|
|
308
|
+
hasBody && /* @__PURE__ */ jsxs2(ModalBody, { className: "min-h-0 text-sm text-fg-base", children: [
|
|
309
|
+
children,
|
|
310
|
+
error && /* @__PURE__ */ jsx2("div", { role: "alert", className: "mt-8 text-fg-danger-emphasis", children: error }),
|
|
311
|
+
pending && /* @__PURE__ */ jsx2("p", { id: statusId, role: "status", className: "sr-only", children: pendingLabel })
|
|
312
|
+
] }),
|
|
313
|
+
/* @__PURE__ */ jsxs2(ModalFooter, { className: "flex-wrap", children: [
|
|
314
|
+
/* @__PURE__ */ jsx2(
|
|
315
|
+
Button,
|
|
316
|
+
{
|
|
317
|
+
...cancelProps,
|
|
318
|
+
ref: (node) => {
|
|
319
|
+
cancelRef.current = node;
|
|
320
|
+
if (typeof actionRef === "function") actionRef(node);
|
|
321
|
+
else if (actionRef) actionRef.current = node;
|
|
322
|
+
},
|
|
323
|
+
type: "button",
|
|
324
|
+
size: "small",
|
|
325
|
+
isSecondary: true,
|
|
326
|
+
disabled: locked || cancelDisabled,
|
|
327
|
+
onClick: (event) => {
|
|
328
|
+
onCancelClick?.(event);
|
|
329
|
+
if (!event.defaultPrevented) onCancel?.(event);
|
|
330
|
+
if (!event.defaultPrevented) requestOpen(false);
|
|
331
|
+
},
|
|
332
|
+
children: cancelLabel
|
|
333
|
+
}
|
|
334
|
+
),
|
|
335
|
+
/* @__PURE__ */ jsx2(
|
|
336
|
+
Button,
|
|
337
|
+
{
|
|
338
|
+
...confirmProps,
|
|
339
|
+
type: "button",
|
|
340
|
+
size: "small",
|
|
341
|
+
isDanger,
|
|
342
|
+
disabled: pending || confirmDisabled,
|
|
343
|
+
onClick: (event) => {
|
|
344
|
+
onConfirmClick?.(event);
|
|
345
|
+
if (!event.defaultPrevented) onConfirm(event);
|
|
346
|
+
},
|
|
347
|
+
children: pending ? pendingLabel : confirmLabel
|
|
348
|
+
}
|
|
349
|
+
)
|
|
350
|
+
] })
|
|
351
|
+
]
|
|
352
|
+
}
|
|
353
|
+
)
|
|
354
|
+
] });
|
|
355
|
+
});
|
|
161
356
|
export {
|
|
357
|
+
ConfirmationDialog,
|
|
162
358
|
Modal,
|
|
163
359
|
ModalBody,
|
|
164
360
|
ModalClose,
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# ConfirmationDialog
|
|
2
|
+
|
|
3
|
+
Import `ConfirmationDialog` and `ConfirmationDialogProps` from `@edux-design/modals`.
|
|
4
|
+
It composes the existing Modal root, surface, title/description and sections, retaining
|
|
5
|
+
its focus trap, overlay, size presets and high overlay stacking order.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
const [open, setOpen] = useState(false);
|
|
9
|
+
const [pending, setPending] = useState(false);
|
|
10
|
+
const [error, setError] = useState("");
|
|
11
|
+
|
|
12
|
+
<ConfirmationDialog
|
|
13
|
+
open={open}
|
|
14
|
+
onOpenChange={setOpen}
|
|
15
|
+
title="Remove this upload?"
|
|
16
|
+
description="This permanently removes the unpublished font file."
|
|
17
|
+
trigger={<Button>Review removal</Button>}
|
|
18
|
+
confirmLabel="Remove upload"
|
|
19
|
+
pendingLabel="Removing…"
|
|
20
|
+
isDanger
|
|
21
|
+
pending={pending}
|
|
22
|
+
error={error}
|
|
23
|
+
onConfirm={async () => {
|
|
24
|
+
setPending(true);
|
|
25
|
+
setError("");
|
|
26
|
+
try {
|
|
27
|
+
if (await removeUpload()) setOpen(false);
|
|
28
|
+
else setError("The upload could not be removed. Please try again.");
|
|
29
|
+
} catch {
|
|
30
|
+
setError("The upload could not be removed. Please try again.");
|
|
31
|
+
} finally {
|
|
32
|
+
setPending(false);
|
|
33
|
+
}
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
<p>{filename}</p>
|
|
37
|
+
</ConfirmationDialog>;
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`trigger` is optional for an editor/API-opened request. A supplied trigger is a
|
|
41
|
+
single ref-forwarding element composed with `ModalTrigger asChild`, preserving its
|
|
42
|
+
existing handlers and ref. Triggerless dialogs remember the previously focused
|
|
43
|
+
connected element; supplied triggers use Radix return focus. Use an explicit
|
|
44
|
+
`returnFocusRef` when deleting that origin, and the preventable close-autofocus hook
|
|
45
|
+
when the host intentionally focuses the editor.
|
|
46
|
+
|
|
47
|
+
`size` defaults to `small`; `medium`, `large` and `full` retain existing Modal bounds.
|
|
48
|
+
`backdropProps` forwards the overlay's native attributes/classes.
|
|
49
|
+
`portalProps` forwards Radix Dialog Portal options, including a theme-scoped container.
|
|
50
|
+
The additive `ModalContent portalProps` capability is also public for ordinary Modal
|
|
51
|
+
consumers; default Modal portal behaviour is unchanged.
|
|
52
|
+
|
|
53
|
+
Both confirmations leave mutation policy in the host. They never await, interpret,
|
|
54
|
+
catch, or auto-close after `onConfirm`: synchronous commands, promises, false results,
|
|
55
|
+
blocking document errors and retriable service errors can keep their own lifecycle.
|
|
56
|
+
An async handler must catch its own rejection and update its own `pending`/`error`.
|
|
57
|
+
Nothing schedules asynchronous component state after the callback.
|
|
58
|
+
|
|
59
|
+
`open` is authoritative when supplied. Omit it and use `defaultOpen` for an
|
|
60
|
+
uncontrolled presentation; `onOpenChange(next)` reports user requests in either mode.
|
|
61
|
+
Confirm does not close even in uncontrolled mode: use controlled state when success
|
|
62
|
+
must close the presentation. `onCancel(event)` only reports the Cancel button;
|
|
63
|
+
Escape and outside dismissal report through `onOpenChange`, without pretending to
|
|
64
|
+
be a Cancel activation.
|
|
65
|
+
|
|
66
|
+
Actions use Edux `Button`, default to `type="button"`, and wrap on narrow surfaces.
|
|
67
|
+
The default confirm action is neutral. Set `isDanger` for destructive actions.
|
|
68
|
+
`confirmDisabled` and `cancelDisabled` independently disable their respective
|
|
69
|
+
buttons. An error renders a `role="alert"`; an error alone does not disable retry.
|
|
70
|
+
For a stale editor/document error, the host also sets `confirmDisabled`.
|
|
71
|
+
|
|
72
|
+
While `pending`, both actions are disabled, the surface has `aria-busy`, and
|
|
73
|
+
`pendingLabel` (default “Working…”) replaces the confirm text and is exposed as a
|
|
74
|
+
status. Escape, outside pointer/focus dismissal, Cancel, and trigger toggle requests
|
|
75
|
+
cannot close the presentation. `allowDismissWhilePending` explicitly unlocks user
|
|
76
|
+
cancellation while confirm remains disabled; use it only when the host permits that
|
|
77
|
+
policy. `dismissible={false}` locks user dismissal regardless of pending. A controlled
|
|
78
|
+
host can always set `open={false}`, for example after confirmed success. There is no
|
|
79
|
+
implicit header X action to bypass these rules.
|
|
80
|
+
|
|
81
|
+
Safe initial focus goes to Cancel. If Cancel is unavailable, focus stays on the
|
|
82
|
+
surface rather than choosing the destructive action. Native `contentProps`
|
|
83
|
+
`onOpenAutoFocus`/`onCloseAutoFocus` can call `event.preventDefault()` to replace the
|
|
84
|
+
focus policy; these hooks run first. `returnFocusRef` supplies an explicit return
|
|
85
|
+
target when the originating row might disappear. For an editor command that places
|
|
86
|
+
its own selection/focus, prevent close autofocus so automatic restoration cannot
|
|
87
|
+
undo that command. A missing or disconnected return target is not focused.
|
|
88
|
+
|
|
89
|
+
`contentProps` forwards native attributes, style/className, keyboard/pointer events,
|
|
90
|
+
and Radix focus/dismissal hooks. The surface ref is an `HTMLDivElement`.
|
|
91
|
+
`confirmButtonProps`/`cancelButtonProps` forward native button attributes, refs and
|
|
92
|
+
mouse/keyboard handlers. Their click handlers run before `onConfirm`/`onCancel`;
|
|
93
|
+
`preventDefault()` vetoes the corresponding action, and `stopPropagation()` remains
|
|
94
|
+
available for editable/sortable row adapters. Forced action type, disabled state and
|
|
95
|
+
pending guards take precedence. Caller dismissal hooks run first, then the internal
|
|
96
|
+
lock always prevents dismissal while locked; a handler cannot accidentally undo it.
|
|
97
|
+
|
|
98
|
+
Each surface is a labelled `dialog`, with a meaningful description when provided.
|
|
99
|
+
Confirmation is a decision, so it uses ordinary buttons rather than menu navigation.
|
|
100
|
+
It does not automatically claim urgent `alertdialog` semantics. A caller may set
|
|
101
|
+
`contentProps.role="alertdialog"` when that semantic is appropriate. Error messages
|
|
102
|
+
have alert semantics separately. IDs remain unique across instances.
|
|
103
|
+
|
|
104
|
+
Use current semantic surface/foreground/border/action/danger tokens. There are no
|
|
105
|
+
new tokens or package-owned palettes, and theme switching does not change geometry.
|
|
106
|
+
Pass `portalProps.container` when a brand/mode scope is below the document root;
|
|
107
|
+
put `brand--blue`/`brand--teal` and `theme--light`/`theme--dark` on that same scope.
|
|
108
|
+
Default portals inherit the application root theme. Native reduced-motion rules
|
|
109
|
+
apply; the compositions also disable their surface animation for reduced motion.
|
|
110
|
+
The primitives keep collision/viewport bounds, content scrolls, and footer actions
|
|
111
|
+
wrap. Domain checks, permissions, editor resolver registration, persistence, source
|
|
112
|
+
warnings, server mutation and cancellation remain local.
|
|
113
|
+
|
|
114
|
+
Storybook: `Modals/ConfirmationDialog` includes destructive and neutral actions,
|
|
115
|
+
bodyless content, warnings, async pending/error/retry, blocking errors, narrow content,
|
|
116
|
+
and light/dark blue/teal scopes. Component tests exercise real Radix focus and dismissal,
|
|
117
|
+
not mocked primitives. Public type/runtime fixtures live under `test/`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edux-design/modals",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"**/*.css"
|
|
@@ -8,18 +8,11 @@
|
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
11
12
|
"import": "./dist/index.mjs",
|
|
12
|
-
"require": "./dist/index.js"
|
|
13
|
-
"types": "./dist/index.d.ts"
|
|
13
|
+
"require": "./dist/index.js"
|
|
14
14
|
}
|
|
15
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,@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
|
-
"generate:component": "turbo gen react-component",
|
|
21
|
-
"check-types": "tsc --noEmit"
|
|
22
|
-
},
|
|
23
16
|
"dependencies": {
|
|
24
17
|
"@edux-design/buttons": "*",
|
|
25
18
|
"@edux-design/icons": "*"
|
|
@@ -30,12 +23,12 @@
|
|
|
30
23
|
"@edux-design/utils": "*",
|
|
31
24
|
"@radix-ui/react-dialog": "1.1.5"
|
|
32
25
|
},
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"docs",
|
|
29
|
+
"README.md",
|
|
30
|
+
"CHANGELOG.md",
|
|
31
|
+
"LICENSE",
|
|
32
|
+
"LICENSE.md"
|
|
33
|
+
]
|
|
41
34
|
}
|