@vvedantb/ui 0.5.3 → 0.6.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/README.md +165 -7
- package/package.json +1 -1
- package/shell/src/AppShell.tsx +130 -43
- package/shell/src/AppSidebar.tsx +27 -119
- package/shell/src/Sidebar.tsx +78 -0
- package/shell/src/SidebarAccountMenu.tsx +169 -77
- package/shell/src/SidebarChrome.tsx +295 -0
- package/shell/src/SidebarHeader.tsx +6 -7
- package/shell/src/SidebarPanel.tsx +95 -33
- package/shell/src/SidebarRail.tsx +74 -28
- package/shell/src/SignOutDialog.tsx +24 -51
- package/shell/src/app-shell-classes.ts +6 -0
- package/shell/src/index.ts +9 -1
- package/src/composer/input-area/InputAreaEditor.tsx +52 -13
- package/src/index.ts +9 -1
- package/src/ui/confirm-dialog-parts.tsx +318 -0
- package/src/ui/confirm-dialog.tsx +65 -106
- package/src/ui/overlay-a11y.ts +5 -3
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { IconLoader2 } from "@tabler/icons-react";
|
|
3
|
+
import { confirmFieldLabelClass } from "../../shell/src/app-shell-classes";
|
|
4
|
+
import { cn } from "../utils/cn";
|
|
5
|
+
import {
|
|
6
|
+
AlertDialogAction,
|
|
7
|
+
AlertDialogCancel,
|
|
8
|
+
AlertDialogContent,
|
|
9
|
+
} from "./alert-dialog";
|
|
10
|
+
import { Button, type ButtonProps } from "./button";
|
|
11
|
+
import {
|
|
12
|
+
Dialog,
|
|
13
|
+
DialogContent,
|
|
14
|
+
DialogDescription,
|
|
15
|
+
DialogFooter,
|
|
16
|
+
DialogHeader,
|
|
17
|
+
DialogTitle,
|
|
18
|
+
} from "./dialog";
|
|
19
|
+
import { Input } from "./input";
|
|
20
|
+
|
|
21
|
+
export type ConfirmDialogSize = "default" | "sm" | "md";
|
|
22
|
+
|
|
23
|
+
type ConfirmDialogContextValue = {
|
|
24
|
+
size: ConfirmDialogSize;
|
|
25
|
+
alert: boolean;
|
|
26
|
+
pending: boolean;
|
|
27
|
+
confirmPhrase: string | undefined;
|
|
28
|
+
typed: string;
|
|
29
|
+
setTyped: (value: string) => void;
|
|
30
|
+
phraseMatches: boolean;
|
|
31
|
+
/** Calls the Root's `onOpenChange(false)` unless `pending`. */
|
|
32
|
+
requestClose: () => void;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const ConfirmDialogContext = React.createContext<ConfirmDialogContextValue>({
|
|
36
|
+
size: "default",
|
|
37
|
+
alert: true,
|
|
38
|
+
pending: false,
|
|
39
|
+
confirmPhrase: undefined,
|
|
40
|
+
typed: "",
|
|
41
|
+
setTyped: () => {},
|
|
42
|
+
phraseMatches: true,
|
|
43
|
+
requestClose: () => {},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/** State of the nearest `ConfirmDialog.Root`, for custom parts. */
|
|
47
|
+
export function useConfirmDialog(): ConfirmDialogContextValue {
|
|
48
|
+
return React.useContext(ConfirmDialogContext);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type ConfirmDialogRootProps = React.ComponentProps<typeof Dialog> & {
|
|
52
|
+
/** Layout slot for the other parts. Default `"default"`. */
|
|
53
|
+
size?: ConfirmDialogSize;
|
|
54
|
+
/** Blocking alert dialog: no outside dismiss, no close button, Cancel focused. Default `size === "default"`. */
|
|
55
|
+
alert?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* While `true`, close requests (Escape, outside click, close button, Cancel)
|
|
58
|
+
* are swallowed, Cancel is disabled and Action shows its pending state.
|
|
59
|
+
* Needs a controlled `open`.
|
|
60
|
+
*/
|
|
61
|
+
pending?: boolean;
|
|
62
|
+
/** Type-to-confirm phrase for `ConfirmDialog.Field`. Action stays disabled until it matches (trimmed, case-insensitive). */
|
|
63
|
+
confirmPhrase?: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function Root({
|
|
67
|
+
size = "default",
|
|
68
|
+
alert = size === "default",
|
|
69
|
+
pending = false,
|
|
70
|
+
confirmPhrase,
|
|
71
|
+
open,
|
|
72
|
+
onOpenChange,
|
|
73
|
+
children,
|
|
74
|
+
...props
|
|
75
|
+
}: ConfirmDialogRootProps) {
|
|
76
|
+
const [typed, setTyped] = React.useState("");
|
|
77
|
+
const [prevOpen, setPrevOpen] = React.useState(open);
|
|
78
|
+
if (open !== prevOpen) {
|
|
79
|
+
setPrevOpen(open);
|
|
80
|
+
if (open) setTyped("");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const handleOpenChange = (next: boolean) => {
|
|
84
|
+
if (!next && pending) return;
|
|
85
|
+
if (next) setTyped("");
|
|
86
|
+
onOpenChange?.(next);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const phraseMatches =
|
|
90
|
+
confirmPhrase === undefined ||
|
|
91
|
+
typed.trim().toLowerCase() === confirmPhrase.trim().toLowerCase();
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<ConfirmDialogContext.Provider
|
|
95
|
+
value={{
|
|
96
|
+
size,
|
|
97
|
+
alert,
|
|
98
|
+
pending,
|
|
99
|
+
confirmPhrase,
|
|
100
|
+
typed,
|
|
101
|
+
setTyped,
|
|
102
|
+
phraseMatches,
|
|
103
|
+
requestClose: () => handleOpenChange(false),
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
|
|
107
|
+
{children}
|
|
108
|
+
</Dialog>
|
|
109
|
+
</ConfirmDialogContext.Provider>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
Root.displayName = "ConfirmDialog.Root";
|
|
113
|
+
|
|
114
|
+
export type ConfirmDialogContentProps = React.ComponentProps<typeof DialogContent>;
|
|
115
|
+
|
|
116
|
+
/** Alert or plain dialog content (from Root `alert`); `sm` and `md` cap the width. */
|
|
117
|
+
function Content({ className, ...props }: ConfirmDialogContentProps) {
|
|
118
|
+
const { size, alert } = useConfirmDialog();
|
|
119
|
+
const Comp = alert ? AlertDialogContent : DialogContent;
|
|
120
|
+
return <Comp {...props} className={cn(size !== "default" && "sm:max-w-96", className)} />;
|
|
121
|
+
}
|
|
122
|
+
Content.displayName = "ConfirmDialog.Content";
|
|
123
|
+
|
|
124
|
+
export type ConfirmDialogHeaderProps = React.HTMLAttributes<HTMLDivElement>;
|
|
125
|
+
|
|
126
|
+
function Header({ className, ...props }: ConfirmDialogHeaderProps) {
|
|
127
|
+
const { size } = useConfirmDialog();
|
|
128
|
+
return <DialogHeader {...props} className={cn(size === "md" && "pb-4", className)} />;
|
|
129
|
+
}
|
|
130
|
+
Header.displayName = "ConfirmDialog.Header";
|
|
131
|
+
|
|
132
|
+
export type ConfirmDialogTitleProps = React.ComponentProps<typeof DialogTitle>;
|
|
133
|
+
|
|
134
|
+
function Title({ className, ...props }: ConfirmDialogTitleProps) {
|
|
135
|
+
const { size } = useConfirmDialog();
|
|
136
|
+
return <DialogTitle {...props} className={cn(size === "sm" && "mb-2 text-sm font-medium", className)} />;
|
|
137
|
+
}
|
|
138
|
+
Title.displayName = "ConfirmDialog.Title";
|
|
139
|
+
|
|
140
|
+
export type ConfirmDialogDescriptionProps = React.ComponentProps<typeof DialogDescription>;
|
|
141
|
+
|
|
142
|
+
function Description({ className, ...props }: ConfirmDialogDescriptionProps) {
|
|
143
|
+
const { size } = useConfirmDialog();
|
|
144
|
+
return (
|
|
145
|
+
<DialogDescription
|
|
146
|
+
{...props}
|
|
147
|
+
className={cn(
|
|
148
|
+
size === "sm" && "mb-4 text-sm text-muted-foreground",
|
|
149
|
+
size === "md" && "py-6 text-muted-foreground",
|
|
150
|
+
className,
|
|
151
|
+
)}
|
|
152
|
+
/>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
// Matched by `DialogContent` when it checks an alert dialog for a description.
|
|
156
|
+
Description.displayName = "ConfirmDialog.Description";
|
|
157
|
+
|
|
158
|
+
export type ConfirmDialogBodyProps = React.ComponentProps<"div">;
|
|
159
|
+
|
|
160
|
+
/** Plain `<div>` for extra content. No default classes. */
|
|
161
|
+
function Body(props: ConfirmDialogBodyProps) {
|
|
162
|
+
return <div {...props} />;
|
|
163
|
+
}
|
|
164
|
+
Body.displayName = "ConfirmDialog.Body";
|
|
165
|
+
|
|
166
|
+
export type ConfirmDialogFooterProps = React.HTMLAttributes<HTMLDivElement>;
|
|
167
|
+
|
|
168
|
+
function Footer({ className, ...props }: ConfirmDialogFooterProps) {
|
|
169
|
+
const { size } = useConfirmDialog();
|
|
170
|
+
if (size === "sm") return <div {...props} className={cn("flex justify-end gap-2", className)} />;
|
|
171
|
+
return <DialogFooter {...props} className={cn(size === "md" && "pt-4", className)} />;
|
|
172
|
+
}
|
|
173
|
+
Footer.displayName = "ConfirmDialog.Footer";
|
|
174
|
+
|
|
175
|
+
export type ConfirmDialogCancelProps = ButtonProps;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Alert: a `DialogClose` outline button. Otherwise a ghost button that runs
|
|
179
|
+
* `onClick` and then, unless it called `preventDefault()`, the Root's
|
|
180
|
+
* `onOpenChange(false)`. Disabled while pending.
|
|
181
|
+
*/
|
|
182
|
+
function Cancel({ children = "Cancel", variant, size, disabled, onClick, ...props }: ConfirmDialogCancelProps) {
|
|
183
|
+
const dialog = useConfirmDialog();
|
|
184
|
+
if (dialog.alert) {
|
|
185
|
+
return (
|
|
186
|
+
<AlertDialogCancel
|
|
187
|
+
variant={variant}
|
|
188
|
+
size={size}
|
|
189
|
+
{...props}
|
|
190
|
+
onClick={onClick}
|
|
191
|
+
disabled={disabled || dialog.pending}
|
|
192
|
+
>
|
|
193
|
+
{children}
|
|
194
|
+
</AlertDialogCancel>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return (
|
|
198
|
+
<Button
|
|
199
|
+
variant={variant ?? "ghost"}
|
|
200
|
+
size={size ?? (dialog.size === "sm" ? "sm" : undefined)}
|
|
201
|
+
{...props}
|
|
202
|
+
onClick={(event) => {
|
|
203
|
+
onClick?.(event);
|
|
204
|
+
if (!event.defaultPrevented) dialog.requestClose();
|
|
205
|
+
}}
|
|
206
|
+
disabled={disabled || dialog.pending}
|
|
207
|
+
>
|
|
208
|
+
{children}
|
|
209
|
+
</Button>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
Cancel.displayName = "ConfirmDialog.Cancel";
|
|
213
|
+
|
|
214
|
+
export type ConfirmDialogActionProps = ButtonProps & {
|
|
215
|
+
/** Default from Root `pending`. Disables the button. */
|
|
216
|
+
pending?: boolean;
|
|
217
|
+
/** Shown with a spinner while pending. Without it the children stay. */
|
|
218
|
+
pendingLabel?: React.ReactNode;
|
|
219
|
+
/** Spinner size in px. Default `14`. */
|
|
220
|
+
spinnerSize?: number;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/** Confirm button. Does not close the dialog. Disabled while pending or until the confirm phrase matches. */
|
|
224
|
+
function Action({
|
|
225
|
+
pending: pendingProp,
|
|
226
|
+
pendingLabel,
|
|
227
|
+
spinnerSize = 14,
|
|
228
|
+
size,
|
|
229
|
+
disabled,
|
|
230
|
+
children,
|
|
231
|
+
...props
|
|
232
|
+
}: ConfirmDialogActionProps) {
|
|
233
|
+
const dialog = useConfirmDialog();
|
|
234
|
+
const pending = pendingProp ?? dialog.pending;
|
|
235
|
+
const content =
|
|
236
|
+
pending && pendingLabel !== undefined ? (
|
|
237
|
+
<>
|
|
238
|
+
<IconLoader2 size={spinnerSize} className="animate-spin" />
|
|
239
|
+
{pendingLabel}
|
|
240
|
+
</>
|
|
241
|
+
) : (
|
|
242
|
+
children
|
|
243
|
+
);
|
|
244
|
+
const isDisabled = disabled || pending || !dialog.phraseMatches;
|
|
245
|
+
if (dialog.alert) {
|
|
246
|
+
return (
|
|
247
|
+
<AlertDialogAction size={size} {...props} disabled={isDisabled}>
|
|
248
|
+
{content}
|
|
249
|
+
</AlertDialogAction>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
return (
|
|
253
|
+
<Button size={size ?? (dialog.size === "sm" ? "sm" : undefined)} {...props} disabled={isDisabled}>
|
|
254
|
+
{content}
|
|
255
|
+
</Button>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
Action.displayName = "ConfirmDialog.Action";
|
|
259
|
+
|
|
260
|
+
export type ConfirmDialogFieldProps = {
|
|
261
|
+
/** Replaces the whole label content. Default "Type <phrase> to confirm". */
|
|
262
|
+
label?: React.ReactNode;
|
|
263
|
+
/** Enter in the input, only while the phrase matches and not pending. */
|
|
264
|
+
onEnter?: () => void;
|
|
265
|
+
className?: string;
|
|
266
|
+
/** Spread onto the `Input`; `id` also sets the label `htmlFor`. */
|
|
267
|
+
inputProps?: Omit<React.ComponentProps<typeof Input>, "value" | "defaultValue" | "onChange">;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/** Type-to-confirm input bound to the Root `confirmPhrase`. */
|
|
271
|
+
function Field({ label, onEnter, className, inputProps }: ConfirmDialogFieldProps) {
|
|
272
|
+
const { confirmPhrase = "", typed, setTyped, phraseMatches, pending } = useConfirmDialog();
|
|
273
|
+
const autoId = React.useId();
|
|
274
|
+
const { id = autoId, onKeyDown, ...rest } = inputProps ?? {};
|
|
275
|
+
return (
|
|
276
|
+
<div className={cn("space-y-2", className)}>
|
|
277
|
+
<label htmlFor={id} className={confirmFieldLabelClass}>
|
|
278
|
+
{label ?? (
|
|
279
|
+
<>
|
|
280
|
+
Type{" "}
|
|
281
|
+
<span className="font-mono text-foreground">{confirmPhrase}</span>{" "}
|
|
282
|
+
to confirm
|
|
283
|
+
</>
|
|
284
|
+
)}
|
|
285
|
+
</label>
|
|
286
|
+
<Input
|
|
287
|
+
id={id}
|
|
288
|
+
autoComplete="off"
|
|
289
|
+
autoFocus
|
|
290
|
+
value={typed}
|
|
291
|
+
onChange={(event) => setTyped(event.target.value)}
|
|
292
|
+
disabled={pending}
|
|
293
|
+
placeholder={confirmPhrase}
|
|
294
|
+
{...rest}
|
|
295
|
+
onKeyDown={(event) => {
|
|
296
|
+
onKeyDown?.(event);
|
|
297
|
+
if (event.key !== "Enter" || !phraseMatches || pending) return;
|
|
298
|
+
event.preventDefault();
|
|
299
|
+
onEnter?.();
|
|
300
|
+
}}
|
|
301
|
+
/>
|
|
302
|
+
</div>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
Field.displayName = "ConfirmDialog.Field";
|
|
306
|
+
|
|
307
|
+
export const ConfirmDialogParts = {
|
|
308
|
+
Root,
|
|
309
|
+
Content,
|
|
310
|
+
Header,
|
|
311
|
+
Title,
|
|
312
|
+
Description,
|
|
313
|
+
Body,
|
|
314
|
+
Footer,
|
|
315
|
+
Cancel,
|
|
316
|
+
Action,
|
|
317
|
+
Field,
|
|
318
|
+
};
|
|
@@ -1,34 +1,39 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import {
|
|
3
|
-
AlertDialog,
|
|
4
|
-
AlertDialogAction,
|
|
5
|
-
AlertDialogCancel,
|
|
6
|
-
AlertDialogContent,
|
|
7
|
-
AlertDialogDescription,
|
|
8
|
-
AlertDialogFooter,
|
|
9
|
-
AlertDialogHeader,
|
|
10
|
-
AlertDialogTitle,
|
|
11
|
-
} from "./alert-dialog";
|
|
12
|
-
import { Button } from "./button";
|
|
13
|
-
import {
|
|
14
|
-
Dialog,
|
|
15
|
-
DialogContent,
|
|
16
|
-
DialogDescription,
|
|
17
|
-
DialogFooter,
|
|
18
|
-
DialogHeader,
|
|
19
|
-
DialogTitle,
|
|
20
|
-
} from "./dialog";
|
|
2
|
+
import { ConfirmDialogParts as Parts, type ConfirmDialogRootProps } from "./confirm-dialog-parts";
|
|
21
3
|
|
|
22
|
-
export
|
|
4
|
+
export { useConfirmDialog } from "./confirm-dialog-parts";
|
|
5
|
+
export type {
|
|
6
|
+
ConfirmDialogActionProps,
|
|
7
|
+
ConfirmDialogBodyProps,
|
|
8
|
+
ConfirmDialogCancelProps,
|
|
9
|
+
ConfirmDialogContentProps,
|
|
10
|
+
ConfirmDialogDescriptionProps,
|
|
11
|
+
ConfirmDialogFieldProps,
|
|
12
|
+
ConfirmDialogFooterProps,
|
|
13
|
+
ConfirmDialogHeaderProps,
|
|
14
|
+
ConfirmDialogRootProps,
|
|
15
|
+
ConfirmDialogSize,
|
|
16
|
+
ConfirmDialogTitleProps,
|
|
17
|
+
} from "./confirm-dialog-parts";
|
|
18
|
+
|
|
19
|
+
export type ConfirmDialogProps = Omit<
|
|
20
|
+
ConfirmDialogRootProps,
|
|
21
|
+
"size" | "alert" | "pending" | "confirmPhrase"
|
|
22
|
+
> & {
|
|
23
23
|
title: React.ReactNode;
|
|
24
24
|
description?: React.ReactNode;
|
|
25
|
+
/** Legacy (still supported): compose `ConfirmDialog.Cancel` instead. */
|
|
25
26
|
cancelLabel?: React.ReactNode;
|
|
27
|
+
/** Legacy (still supported): compose `ConfirmDialog.Action` instead. */
|
|
26
28
|
confirmLabel?: React.ReactNode;
|
|
27
29
|
onCancel?: () => void;
|
|
28
30
|
onConfirm?: () => void;
|
|
31
|
+
/** Legacy (still supported): pass `variant` to `ConfirmDialog.Action` instead. */
|
|
29
32
|
variant?: "default" | "destructive";
|
|
30
33
|
confirmDisabled?: boolean;
|
|
31
34
|
/**
|
|
35
|
+
* Legacy (still supported): use `ConfirmDialog.Root size` with the parts.
|
|
36
|
+
*
|
|
32
37
|
* `"default"`: blocking alert dialog that closes on either button.
|
|
33
38
|
* `"sm"`: compact dismissible dialog (outside click, Escape and the close
|
|
34
39
|
* button close it). Confirm does not close it; the caller controls `open`.
|
|
@@ -38,7 +43,7 @@ export type ConfirmDialogProps = React.ComponentProps<typeof AlertDialog> & {
|
|
|
38
43
|
size?: "default" | "sm" | "md";
|
|
39
44
|
};
|
|
40
45
|
|
|
41
|
-
function
|
|
46
|
+
function LegacyConfirmDialog({
|
|
42
47
|
title,
|
|
43
48
|
description,
|
|
44
49
|
cancelLabel = "Cancel",
|
|
@@ -51,95 +56,49 @@ function ConfirmDialog({
|
|
|
51
56
|
children,
|
|
52
57
|
...props
|
|
53
58
|
}: ConfirmDialogProps) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
{children}
|
|
58
|
-
<DialogContent className="sm:max-w-96">
|
|
59
|
-
<DialogTitle className="mb-2 text-sm font-medium">{title}</DialogTitle>
|
|
60
|
-
{description != null ? (
|
|
61
|
-
<DialogDescription className="mb-4 text-sm text-muted-foreground">
|
|
62
|
-
{description}
|
|
63
|
-
</DialogDescription>
|
|
64
|
-
) : null}
|
|
65
|
-
<div className="flex justify-end gap-2">
|
|
66
|
-
<Button
|
|
67
|
-
variant="ghost"
|
|
68
|
-
size="sm"
|
|
69
|
-
onClick={() => {
|
|
70
|
-
onCancel?.();
|
|
71
|
-
props.onOpenChange?.(false);
|
|
72
|
-
}}
|
|
73
|
-
>
|
|
74
|
-
{cancelLabel}
|
|
75
|
-
</Button>
|
|
76
|
-
<Button
|
|
77
|
-
variant={variant}
|
|
78
|
-
size="sm"
|
|
79
|
-
disabled={confirmDisabled}
|
|
80
|
-
onClick={onConfirm}
|
|
81
|
-
>
|
|
82
|
-
{confirmLabel}
|
|
83
|
-
</Button>
|
|
84
|
-
</div>
|
|
85
|
-
</DialogContent>
|
|
86
|
-
</Dialog>
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (size === "md") {
|
|
91
|
-
return (
|
|
92
|
-
<Dialog {...props}>
|
|
93
|
-
{children}
|
|
94
|
-
<DialogContent className="sm:max-w-96">
|
|
95
|
-
<DialogHeader className="pb-4">
|
|
96
|
-
<DialogTitle>{title}</DialogTitle>
|
|
97
|
-
</DialogHeader>
|
|
98
|
-
{description != null ? (
|
|
99
|
-
<DialogDescription className="py-6 text-muted-foreground">{description}</DialogDescription>
|
|
100
|
-
) : null}
|
|
101
|
-
<DialogFooter className="pt-4">
|
|
102
|
-
<Button
|
|
103
|
-
variant="ghost"
|
|
104
|
-
onClick={() => {
|
|
105
|
-
onCancel?.();
|
|
106
|
-
props.onOpenChange?.(false);
|
|
107
|
-
}}
|
|
108
|
-
>
|
|
109
|
-
{cancelLabel}
|
|
110
|
-
</Button>
|
|
111
|
-
<Button variant={variant} disabled={confirmDisabled} onClick={onConfirm}>
|
|
112
|
-
{confirmLabel}
|
|
113
|
-
</Button>
|
|
114
|
-
</DialogFooter>
|
|
115
|
-
</DialogContent>
|
|
116
|
-
</Dialog>
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
59
|
+
const titleNode = <Parts.Title>{title}</Parts.Title>;
|
|
60
|
+
const descriptionNode =
|
|
61
|
+
description != null ? <Parts.Description>{description}</Parts.Description> : null;
|
|
120
62
|
return (
|
|
121
|
-
<
|
|
63
|
+
<Parts.Root {...props} size={size}>
|
|
122
64
|
{children}
|
|
123
|
-
<
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
65
|
+
<Parts.Content>
|
|
66
|
+
{size === "sm" ? (
|
|
67
|
+
<>
|
|
68
|
+
{titleNode}
|
|
69
|
+
{descriptionNode}
|
|
70
|
+
</>
|
|
71
|
+
) : size === "md" ? (
|
|
72
|
+
<>
|
|
73
|
+
<Parts.Header>{titleNode}</Parts.Header>
|
|
74
|
+
{descriptionNode}
|
|
75
|
+
</>
|
|
76
|
+
) : (
|
|
77
|
+
<Parts.Header>
|
|
78
|
+
{titleNode}
|
|
79
|
+
{descriptionNode}
|
|
80
|
+
</Parts.Header>
|
|
81
|
+
)}
|
|
82
|
+
<Parts.Footer>
|
|
83
|
+
{/* The alert cancel passes the click event on; sm and md call onCancel bare. */}
|
|
84
|
+
<Parts.Cancel onClick={size === "default" ? onCancel : () => onCancel?.()}>
|
|
85
|
+
{cancelLabel}
|
|
86
|
+
</Parts.Cancel>
|
|
87
|
+
<Parts.Action variant={variant} disabled={confirmDisabled} onClick={onConfirm}>
|
|
137
88
|
{confirmLabel}
|
|
138
|
-
</
|
|
139
|
-
</
|
|
140
|
-
</
|
|
141
|
-
</
|
|
89
|
+
</Parts.Action>
|
|
90
|
+
</Parts.Footer>
|
|
91
|
+
</Parts.Content>
|
|
92
|
+
</Parts.Root>
|
|
142
93
|
);
|
|
143
94
|
}
|
|
95
|
+
LegacyConfirmDialog.displayName = "ConfirmDialog";
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Confirm dialog. Call it with props (the legacy prop-driven API), or compose
|
|
99
|
+
* the parts: `ConfirmDialog.Root`, `.Content`, `.Header`, `.Title`,
|
|
100
|
+
* `.Description`, `.Body`, `.Footer`, `.Cancel`, `.Action` and `.Field`.
|
|
101
|
+
*/
|
|
102
|
+
const ConfirmDialog = Object.assign(LegacyConfirmDialog, Parts);
|
|
144
103
|
|
|
145
104
|
export { ConfirmDialog };
|
package/src/ui/overlay-a11y.ts
CHANGED
|
@@ -143,11 +143,13 @@ export function treeHasDialogDescription(
|
|
|
143
143
|
if (!React.isValidElement(child)) return false;
|
|
144
144
|
const type = child.type;
|
|
145
145
|
if (type === descriptionType) return true;
|
|
146
|
+
// Wrappers named like `ConfirmDialog.Description` count too.
|
|
146
147
|
const displayName =
|
|
147
|
-
typeof type === "object" && type !== null
|
|
148
|
-
|
|
148
|
+
(typeof type === "function" || (typeof type === "object" && type !== null)) &&
|
|
149
|
+
"displayName" in type
|
|
150
|
+
? type.displayName
|
|
149
151
|
: undefined;
|
|
150
|
-
if (displayName === "
|
|
152
|
+
if (typeof displayName === "string" && /Dialog\.?Description$/.test(displayName)) return true;
|
|
151
153
|
const props = child.props as { children?: React.ReactNode };
|
|
152
154
|
return treeHasDialogDescription(props.children, descriptionType);
|
|
153
155
|
});
|