@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/ui",
3
- "version": "11.13.1",
3
+ "version": "11.14.0",
4
4
  "description": "Generic React UI components for Tangle products — primitives, chat, run, files, editor, markdown.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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
- <DialogPrimitive.Close
63
- className={cn(
64
- "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",
65
- focusRing,
66
- )}
67
- >
68
- <X className="h-4 w-4" />
69
- <span className="sr-only">Close</span>
70
- </DialogPrimitive.Close>
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
  );