@tangle-network/ui 11.13.1 → 11.14.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
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @tangle-network/ui
|
|
2
2
|
|
|
3
|
+
## 11.14.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 999438e: Add DialogContent hideCloseButton with a false default. It hides only the built-in close button, preserving Radix dismissal handlers, explicit close actions, variants and refs. Callers that require an explicit choice must still guard Escape/outside events and provide an accessible cancel or completion action.
|
|
8
|
+
|
|
3
9
|
## 11.13.1
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
|
@@ -8,6 +8,12 @@ declare const DialogClose: React.ForwardRefExoticComponent<DialogPrimitive.Dialo
|
|
|
8
8
|
declare const DialogOverlay: React.ForwardRefExoticComponent<Omit<DialogPrimitive.DialogOverlayProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
9
9
|
declare const DialogContent: React.ForwardRefExoticComponent<Omit<DialogPrimitive.DialogContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & {
|
|
10
10
|
variant?: "default" | "sandbox";
|
|
11
|
+
/**
|
|
12
|
+
* Hide only the built-in close button. Escape/outside dismissal still follows
|
|
13
|
+
* Radix's handlers; prevent those events explicitly when a choice is required
|
|
14
|
+
* and provide an accessible way to finish or cancel the operation.
|
|
15
|
+
*/
|
|
16
|
+
hideCloseButton?: boolean;
|
|
11
17
|
} & React.RefAttributes<HTMLDivElement>>;
|
|
12
18
|
declare function DialogHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>): React.JSX.Element;
|
|
13
19
|
declare namespace DialogHeader {
|
|
@@ -15,7 +15,7 @@ const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => /* @__P
|
|
|
15
15
|
...props
|
|
16
16
|
}));
|
|
17
17
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
|
18
|
-
const DialogContent = React.forwardRef(({ className, children, variant = "default", ...props }, ref) => {
|
|
18
|
+
const DialogContent = React.forwardRef(({ className, children, variant = "default", hideCloseButton = false, ...props }, ref) => {
|
|
19
19
|
return /* @__PURE__ */ jsxs(DialogPortal, { children: [/* @__PURE__ */ jsx(DialogOverlay, {}), /* @__PURE__ */ jsxs(DialogPrimitive.Content, {
|
|
20
20
|
ref,
|
|
21
21
|
className: cn("fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%]", "gap-4 rounded-lg border bg-card p-6 shadow-lg duration-200", "data-[state=closed]:animate-out data-[state=open]:animate-in", "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95", "data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]", "data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]", {
|
|
@@ -23,7 +23,7 @@ const DialogContent = React.forwardRef(({ className, children, variant = "defaul
|
|
|
23
23
|
sandbox: "border-[var(--border-accent)] shadow-[var(--shadow-accent)]"
|
|
24
24
|
}[variant], className),
|
|
25
25
|
...props,
|
|
26
|
-
children: [children, /* @__PURE__ */ jsxs(DialogPrimitive.Close, {
|
|
26
|
+
children: [children, !hideCloseButton && /* @__PURE__ */ jsxs(DialogPrimitive.Close, {
|
|
27
27
|
className: cn("absolute top-4 right-4 rounded-sm opacity-70 transition-[opacity,box-shadow] hover:opacity-100 focus-visible:opacity-100 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground", focusRing),
|
|
28
28
|
children: [/* @__PURE__ */ jsx(X, { className: "h-4 w-4" }), /* @__PURE__ */ jsx("span", {
|
|
29
29
|
className: "sr-only",
|
package/package.json
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { Button } from "./button";
|
|
4
|
+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "./dialog";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Primitives/Dialog/Dismissal",
|
|
8
|
+
component: DialogContent,
|
|
9
|
+
parameters: { layout: "centered" },
|
|
10
|
+
} satisfies Meta<typeof DialogContent>;
|
|
11
|
+
export default meta;
|
|
12
|
+
type Story = StoryObj<typeof meta>;
|
|
13
|
+
|
|
14
|
+
function ExplicitChoiceExample() {
|
|
15
|
+
const [open, setOpen] = useState(false);
|
|
16
|
+
const [choice, setChoice] = useState("No choice recorded.");
|
|
17
|
+
const finish = (value: string) => {
|
|
18
|
+
setChoice(value);
|
|
19
|
+
setOpen(false);
|
|
20
|
+
};
|
|
21
|
+
return (
|
|
22
|
+
<div className="space-y-4">
|
|
23
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
24
|
+
<DialogTrigger asChild><Button>Review operation</Button></DialogTrigger>
|
|
25
|
+
<DialogContent
|
|
26
|
+
hideCloseButton
|
|
27
|
+
onEscapeKeyDown={(event) => event.preventDefault()}
|
|
28
|
+
onInteractOutside={(event) => event.preventDefault()}
|
|
29
|
+
>
|
|
30
|
+
<DialogHeader>
|
|
31
|
+
<DialogTitle>Continue this operation?</DialogTitle>
|
|
32
|
+
<DialogDescription>
|
|
33
|
+
This example requires an explicit choice. Hiding the close button
|
|
34
|
+
alone does not block Escape or outside dismissal; these handlers do.
|
|
35
|
+
</DialogDescription>
|
|
36
|
+
</DialogHeader>
|
|
37
|
+
<DialogFooter>
|
|
38
|
+
<Button variant="outline" onClick={() => finish("Cancelled in this example.")}>Cancel</Button>
|
|
39
|
+
<Button onClick={() => finish("Confirmed in this example.")}>Confirm</Button>
|
|
40
|
+
</DialogFooter>
|
|
41
|
+
</DialogContent>
|
|
42
|
+
</Dialog>
|
|
43
|
+
<output className="block text-sm text-muted-foreground" aria-live="polite">{choice}</output>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const ExplicitChoice: Story = { render: () => <ExplicitChoiceExample /> };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createRef, type ComponentProps } from "react";
|
|
2
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogTitle } from "./dialog";
|
|
6
|
+
|
|
7
|
+
function Example(props: ComponentProps<typeof DialogContent>) {
|
|
8
|
+
return (
|
|
9
|
+
<Dialog defaultOpen>
|
|
10
|
+
<DialogContent {...props}>
|
|
11
|
+
<DialogTitle>Confirm operation</DialogTitle>
|
|
12
|
+
<DialogDescription>Choose whether to continue.</DialogDescription>
|
|
13
|
+
<DialogClose asChild><button type="button">Cancel operation</button></DialogClose>
|
|
14
|
+
</DialogContent>
|
|
15
|
+
</Dialog>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("DialogContent close-button visibility", () => {
|
|
20
|
+
it("retains the default accessible close button and its dismissal behavior", async () => {
|
|
21
|
+
const user = userEvent.setup();
|
|
22
|
+
render(<Example />);
|
|
23
|
+
expect(screen.getByRole("dialog", { name: "Confirm operation" })).toBeInTheDocument();
|
|
24
|
+
await user.click(screen.getByRole("button", { name: "Close", exact: true }));
|
|
25
|
+
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it.each(["default", "sandbox"] as const)("hides only the built-in button for the %s variant", async (variant) => {
|
|
29
|
+
const user = userEvent.setup();
|
|
30
|
+
render(<Example hideCloseButton variant={variant} />);
|
|
31
|
+
expect(screen.queryByRole("button", { name: "Close", exact: true })).toBeNull();
|
|
32
|
+
expect(screen.getByRole("dialog")).not.toHaveAttribute("hideCloseButton");
|
|
33
|
+
expect(screen.getByRole("dialog")).toHaveAccessibleDescription("Choose whether to continue.");
|
|
34
|
+
await user.click(screen.getByRole("button", { name: "Cancel operation" }));
|
|
35
|
+
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("restores the same default button when visibility changes", () => {
|
|
39
|
+
const { rerender } = render(<Example hideCloseButton />);
|
|
40
|
+
expect(screen.queryByRole("button", { name: "Close", exact: true })).toBeNull();
|
|
41
|
+
rerender(<Example hideCloseButton={false} />);
|
|
42
|
+
expect(screen.getByRole("button", { name: "Close", exact: true })).toBeInTheDocument();
|
|
43
|
+
rerender(<Example hideCloseButton />);
|
|
44
|
+
expect(screen.queryByRole("button", { name: "Close", exact: true })).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("does not silently disable Escape when the close button is hidden", async () => {
|
|
48
|
+
const user = userEvent.setup();
|
|
49
|
+
render(<Example hideCloseButton />);
|
|
50
|
+
await user.keyboard("{Escape}");
|
|
51
|
+
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("preserves the caller's Escape guard and an explicit keyboard-operable exit", async () => {
|
|
55
|
+
const user = userEvent.setup();
|
|
56
|
+
const guard = vi.fn((event: KeyboardEvent) => event.preventDefault());
|
|
57
|
+
render(<Example hideCloseButton onEscapeKeyDown={guard} />);
|
|
58
|
+
await user.keyboard("{Escape}");
|
|
59
|
+
expect(guard).toHaveBeenCalledOnce();
|
|
60
|
+
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
|
61
|
+
screen.getByRole("button", { name: "Cancel operation" }).focus();
|
|
62
|
+
await user.keyboard("{Enter}");
|
|
63
|
+
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("forwards content refs, native attributes and class overrides", () => {
|
|
67
|
+
const ref = createRef<HTMLDivElement>();
|
|
68
|
+
render(<Example ref={ref} hideCloseButton id="operation-dialog" data-example="retained" className="max-w-xl" />);
|
|
69
|
+
const dialog = screen.getByRole("dialog");
|
|
70
|
+
expect(ref.current).toBe(dialog);
|
|
71
|
+
expect(dialog).toHaveAttribute("id", "operation-dialog");
|
|
72
|
+
expect(dialog).toHaveAttribute("data-example", "retained");
|
|
73
|
+
expect(dialog).toHaveClass("max-w-xl");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -33,8 +33,14 @@ const DialogContent = React.forwardRef<
|
|
|
33
33
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
34
34
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
|
35
35
|
variant?: "default" | "sandbox";
|
|
36
|
+
/**
|
|
37
|
+
* Hide only the built-in close button. Escape/outside dismissal still follows
|
|
38
|
+
* Radix's handlers; prevent those events explicitly when a choice is required
|
|
39
|
+
* and provide an accessible way to finish or cancel the operation.
|
|
40
|
+
*/
|
|
41
|
+
hideCloseButton?: boolean;
|
|
36
42
|
}
|
|
37
|
-
>(({ className, children, variant = "default", ...props }, ref) => {
|
|
43
|
+
>(({ className, children, variant = "default", hideCloseButton = false, ...props }, ref) => {
|
|
38
44
|
const variants = {
|
|
39
45
|
default: "border-border",
|
|
40
46
|
sandbox: "border-[var(--border-accent)] shadow-[var(--shadow-accent)]",
|
|
@@ -59,15 +65,17 @@ const DialogContent = React.forwardRef<
|
|
|
59
65
|
{...props}
|
|
60
66
|
>
|
|
61
67
|
{children}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
{!hideCloseButton && (
|
|
69
|
+
<DialogPrimitive.Close
|
|
70
|
+
className={cn(
|
|
71
|
+
"absolute top-4 right-4 rounded-sm opacity-70 transition-[opacity,box-shadow] hover:opacity-100 focus-visible:opacity-100 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground",
|
|
72
|
+
focusRing,
|
|
73
|
+
)}
|
|
74
|
+
>
|
|
75
|
+
<X className="h-4 w-4" />
|
|
76
|
+
<span className="sr-only">Close</span>
|
|
77
|
+
</DialogPrimitive.Close>
|
|
78
|
+
)}
|
|
71
79
|
</DialogPrimitive.Content>
|
|
72
80
|
</DialogPortal>
|
|
73
81
|
);
|