@dowel-ui/react 0.1.0 → 0.1.1

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.
Files changed (120) hide show
  1. package/dist/components/ai-agent-status/ai-agent-status.d.ts +1 -1
  2. package/dist/components/ai-tool/ai-tool.d.ts +1 -1
  3. package/dist/components/alert/alert.d.ts +1 -1
  4. package/dist/components/avatar/avatar.d.ts +1 -1
  5. package/dist/components/badge/badge.d.ts +1 -1
  6. package/dist/components/button/button.d.ts +1 -1
  7. package/dist/components/progress/progress.d.ts +1 -1
  8. package/dist/components/spinner/spinner.d.ts +1 -1
  9. package/dist/components/toast/toast.d.ts +1 -1
  10. package/package.json +7 -4
  11. package/src/blocks/admin-users/admin-users.stories.tsx +0 -83
  12. package/src/blocks/admin-users/admin-users.test.tsx +0 -159
  13. package/src/blocks/ai-chat/ai-chat.stories.tsx +0 -149
  14. package/src/blocks/ai-chat/ai-chat.test.tsx +0 -166
  15. package/src/blocks/dashboard/dashboard.stories.tsx +0 -106
  16. package/src/blocks/dashboard/dashboard.test.tsx +0 -94
  17. package/src/blocks/forgot-password/forgot-password.stories.tsx +0 -41
  18. package/src/blocks/forgot-password/forgot-password.test.tsx +0 -62
  19. package/src/blocks/login/login.stories.tsx +0 -46
  20. package/src/blocks/login/login.test.tsx +0 -100
  21. package/src/blocks/pricing/pricing.stories.tsx +0 -62
  22. package/src/blocks/pricing/pricing.test.tsx +0 -101
  23. package/src/blocks/settings/settings.stories.tsx +0 -94
  24. package/src/blocks/settings/settings.test.tsx +0 -167
  25. package/src/blocks/signup/signup.stories.tsx +0 -27
  26. package/src/blocks/signup/signup.test.tsx +0 -97
  27. package/src/components/accordion/accordion.stories.tsx +0 -90
  28. package/src/components/accordion/accordion.test.tsx +0 -156
  29. package/src/components/activity-feed/activity-feed.stories.tsx +0 -163
  30. package/src/components/activity-feed/activity-feed.test.tsx +0 -125
  31. package/src/components/ai-agent-status/ai-agent-status.stories.tsx +0 -86
  32. package/src/components/ai-agent-status/ai-agent-status.test.tsx +0 -66
  33. package/src/components/ai-conversation/ai-conversation.stories.tsx +0 -126
  34. package/src/components/ai-conversation/ai-conversation.test.tsx +0 -144
  35. package/src/components/ai-message/ai-message.stories.tsx +0 -106
  36. package/src/components/ai-message/ai-message.test.tsx +0 -160
  37. package/src/components/ai-model-selector/ai-model-selector.stories.tsx +0 -66
  38. package/src/components/ai-model-selector/ai-model-selector.test.tsx +0 -120
  39. package/src/components/ai-prompt-input/ai-prompt-input.stories.tsx +0 -145
  40. package/src/components/ai-prompt-input/ai-prompt-input.test.tsx +0 -273
  41. package/src/components/ai-reasoning/ai-reasoning.stories.tsx +0 -60
  42. package/src/components/ai-reasoning/ai-reasoning.test.tsx +0 -95
  43. package/src/components/ai-response/ai-response.stories.tsx +0 -75
  44. package/src/components/ai-response/ai-response.test.tsx +0 -96
  45. package/src/components/ai-sources/ai-sources.stories.tsx +0 -93
  46. package/src/components/ai-sources/ai-sources.test.tsx +0 -102
  47. package/src/components/ai-token-usage/ai-token-usage.stories.tsx +0 -59
  48. package/src/components/ai-token-usage/ai-token-usage.test.tsx +0 -83
  49. package/src/components/ai-tool/ai-tool.stories.tsx +0 -96
  50. package/src/components/ai-tool/ai-tool.test.tsx +0 -137
  51. package/src/components/alert/alert.stories.tsx +0 -101
  52. package/src/components/alert/alert.test.tsx +0 -94
  53. package/src/components/avatar/avatar.stories.tsx +0 -58
  54. package/src/components/avatar/avatar.test.tsx +0 -64
  55. package/src/components/badge/badge.stories.tsx +0 -68
  56. package/src/components/badge/badge.test.tsx +0 -61
  57. package/src/components/button/button.stories.tsx +0 -128
  58. package/src/components/button/button.test.tsx +0 -224
  59. package/src/components/calendar/calendar.stories.tsx +0 -105
  60. package/src/components/calendar/calendar.test.tsx +0 -255
  61. package/src/components/card/card.stories.tsx +0 -61
  62. package/src/components/card/card.test.tsx +0 -66
  63. package/src/components/checkbox/checkbox.stories.tsx +0 -124
  64. package/src/components/checkbox/checkbox.test.tsx +0 -121
  65. package/src/components/code-block/code-block.stories.tsx +0 -62
  66. package/src/components/code-block/code-block.test.tsx +0 -139
  67. package/src/components/combobox/combobox.stories.tsx +0 -171
  68. package/src/components/combobox/combobox.test.tsx +0 -429
  69. package/src/components/command/command.stories.tsx +0 -152
  70. package/src/components/command/command.test.tsx +0 -380
  71. package/src/components/data-table/data-table.stories.tsx +0 -151
  72. package/src/components/data-table/data-table.test.tsx +0 -288
  73. package/src/components/date-picker/date-picker.stories.tsx +0 -103
  74. package/src/components/date-picker/date-picker.test.tsx +0 -205
  75. package/src/components/dialog/dialog.stories.tsx +0 -140
  76. package/src/components/dialog/dialog.test.tsx +0 -186
  77. package/src/components/drawer/drawer.stories.tsx +0 -111
  78. package/src/components/drawer/drawer.test.tsx +0 -187
  79. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +0 -128
  80. package/src/components/dropdown-menu/dropdown-menu.test.tsx +0 -321
  81. package/src/components/empty-state/empty-state.stories.tsx +0 -70
  82. package/src/components/empty-state/empty-state.test.tsx +0 -97
  83. package/src/components/form/form.stories.tsx +0 -144
  84. package/src/components/form/form.test.tsx +0 -203
  85. package/src/components/input/input.stories.tsx +0 -84
  86. package/src/components/input/input.test.tsx +0 -108
  87. package/src/components/label/label.stories.tsx +0 -51
  88. package/src/components/label/label.test.tsx +0 -55
  89. package/src/components/pagination/pagination.stories.tsx +0 -129
  90. package/src/components/pagination/pagination.test.tsx +0 -119
  91. package/src/components/popover/popover.stories.tsx +0 -84
  92. package/src/components/popover/popover.test.tsx +0 -163
  93. package/src/components/progress/progress.stories.tsx +0 -90
  94. package/src/components/progress/progress.test.tsx +0 -78
  95. package/src/components/radio-group/radio-group.stories.tsx +0 -102
  96. package/src/components/radio-group/radio-group.test.tsx +0 -152
  97. package/src/components/select/select.stories.tsx +0 -137
  98. package/src/components/select/select.test.tsx +0 -209
  99. package/src/components/separator/separator.stories.tsx +0 -42
  100. package/src/components/separator/separator.test.tsx +0 -46
  101. package/src/components/sheet/sheet.stories.tsx +0 -107
  102. package/src/components/sheet/sheet.test.tsx +0 -152
  103. package/src/components/skeleton/skeleton.stories.tsx +0 -46
  104. package/src/components/skeleton/skeleton.test.tsx +0 -57
  105. package/src/components/slider/slider.stories.tsx +0 -87
  106. package/src/components/slider/slider.test.tsx +0 -148
  107. package/src/components/spinner/spinner.stories.tsx +0 -47
  108. package/src/components/spinner/spinner.test.tsx +0 -41
  109. package/src/components/switch/switch.stories.tsx +0 -70
  110. package/src/components/switch/switch.test.tsx +0 -101
  111. package/src/components/table/table.stories.tsx +0 -131
  112. package/src/components/table/table.test.tsx +0 -153
  113. package/src/components/tabs/tabs.stories.tsx +0 -139
  114. package/src/components/tabs/tabs.test.tsx +0 -148
  115. package/src/components/toast/toast.stories.tsx +0 -167
  116. package/src/components/toast/toast.test.tsx +0 -306
  117. package/src/components/tooltip/tooltip.stories.tsx +0 -89
  118. package/src/components/tooltip/tooltip.test.tsx +0 -120
  119. package/src/lib/utils.test.ts +0 -32
  120. package/src/registry/meta.test.ts +0 -234
@@ -1,140 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react-vite";
2
-
3
- import { Button } from "@/components/button";
4
- import { Input } from "@/components/input";
5
- import { Label } from "@/components/label";
6
-
7
- import {
8
- Dialog,
9
- DialogClose,
10
- DialogContent,
11
- DialogDescription,
12
- DialogFooter,
13
- DialogHeader,
14
- DialogTitle,
15
- DialogTrigger,
16
- } from "./dialog";
17
-
18
- /**
19
- * Annotated rather than inferred with `satisfies`. These components are direct
20
- * re-exports of Radix primitives, and inferring the meta type makes the emitted
21
- * declaration reference Radix-internal prop types that are not nameable from
22
- * this path (TS2883).
23
- */
24
- const meta: Meta<typeof Dialog> = {
25
- title: "Overlays/Dialog",
26
- component: Dialog,
27
- parameters: { controls: { disable: true } },
28
- };
29
-
30
- export default meta;
31
- type Story = StoryObj<typeof Dialog>;
32
-
33
- export const Default: Story = {
34
- render: () => (
35
- <Dialog>
36
- <DialogTrigger asChild>
37
- <Button>Edit profile</Button>
38
- </DialogTrigger>
39
- <DialogContent>
40
- <DialogHeader>
41
- <DialogTitle>Edit profile</DialogTitle>
42
- <DialogDescription>
43
- Make changes to your profile. Save when you are done.
44
- </DialogDescription>
45
- </DialogHeader>
46
- <div className="grid gap-4">
47
- <div className="grid gap-2">
48
- <Label htmlFor="name">Name</Label>
49
- <Input id="name" defaultValue="Ada Lovelace" />
50
- </div>
51
- <div className="grid gap-2">
52
- <Label htmlFor="handle">Handle</Label>
53
- <Input id="handle" defaultValue="@ada" />
54
- </div>
55
- </div>
56
- <DialogFooter>
57
- <DialogClose asChild>
58
- <Button variant="outline">Cancel</Button>
59
- </DialogClose>
60
- <Button>Save changes</Button>
61
- </DialogFooter>
62
- </DialogContent>
63
- </Dialog>
64
- ),
65
- };
66
-
67
- /** A destructive confirmation: the description must state what is lost. */
68
- export const Confirmation: Story = {
69
- render: () => (
70
- <Dialog>
71
- <DialogTrigger asChild>
72
- <Button variant="destructive">Delete project</Button>
73
- </DialogTrigger>
74
- <DialogContent className="max-w-md">
75
- <DialogHeader>
76
- <DialogTitle>Delete acme-inc?</DialogTitle>
77
- <DialogDescription>
78
- This permanently deletes the project and all 42 of its deployments. This cannot be
79
- undone.
80
- </DialogDescription>
81
- </DialogHeader>
82
- <DialogFooter>
83
- <DialogClose asChild>
84
- <Button variant="outline">Cancel</Button>
85
- </DialogClose>
86
- <Button variant="destructive">Delete project</Button>
87
- </DialogFooter>
88
- </DialogContent>
89
- </Dialog>
90
- ),
91
- };
92
-
93
- /** Without the built-in close button, supply your own dismissal path. */
94
- export const WithoutCloseButton: Story = {
95
- render: () => (
96
- <Dialog>
97
- <DialogTrigger asChild>
98
- <Button variant="outline">Open</Button>
99
- </DialogTrigger>
100
- <DialogContent showCloseButton={false}>
101
- <DialogHeader>
102
- <DialogTitle>Terms of service</DialogTitle>
103
- <DialogDescription>Accept the terms to continue.</DialogDescription>
104
- </DialogHeader>
105
- <DialogFooter>
106
- <DialogClose asChild>
107
- <Button variant="outline">Decline</Button>
108
- </DialogClose>
109
- <DialogClose asChild>
110
- <Button>Accept</Button>
111
- </DialogClose>
112
- </DialogFooter>
113
- </DialogContent>
114
- </Dialog>
115
- ),
116
- };
117
-
118
- export const ScrollingContent: Story = {
119
- render: () => (
120
- <Dialog>
121
- <DialogTrigger asChild>
122
- <Button variant="outline">Read changelog</Button>
123
- </DialogTrigger>
124
- <DialogContent>
125
- <DialogHeader>
126
- <DialogTitle>Changelog</DialogTitle>
127
- <DialogDescription>Everything shipped this month.</DialogDescription>
128
- </DialogHeader>
129
- <div className="max-h-72 space-y-4 overflow-y-auto pr-2 text-sm text-muted-foreground">
130
- {Array.from({ length: 12 }, (_, index) => (
131
- <p key={index}>
132
- Release {String(index + 1)}: performance improvements, bug fixes and a handful of
133
- new keyboard shortcuts.
134
- </p>
135
- ))}
136
- </div>
137
- </DialogContent>
138
- </Dialog>
139
- ),
140
- };
@@ -1,186 +0,0 @@
1
- import { render, screen, waitFor } from "@testing-library/react";
2
- import userEvent from "@testing-library/user-event";
3
- import { useState } from "react";
4
- import { describe, expect, it, vi } from "vitest";
5
-
6
- import { expectNoA11yViolations } from "../../../test/a11y";
7
- import { Button } from "../button";
8
- import {
9
- Dialog,
10
- DialogClose,
11
- DialogContent,
12
- DialogDescription,
13
- DialogFooter,
14
- DialogHeader,
15
- DialogTitle,
16
- DialogTrigger,
17
- } from "./dialog";
18
-
19
- function Example({ showCloseButton = true }: { showCloseButton?: boolean }) {
20
- return (
21
- <Dialog>
22
- <DialogTrigger asChild>
23
- <Button>Open</Button>
24
- </DialogTrigger>
25
- <DialogContent showCloseButton={showCloseButton}>
26
- <DialogHeader>
27
- <DialogTitle>Delete project</DialogTitle>
28
- <DialogDescription>This cannot be undone.</DialogDescription>
29
- </DialogHeader>
30
- <DialogFooter>
31
- <DialogClose asChild>
32
- <Button variant="outline">Cancel</Button>
33
- </DialogClose>
34
- <Button variant="destructive">Delete</Button>
35
- </DialogFooter>
36
- </DialogContent>
37
- </Dialog>
38
- );
39
- }
40
-
41
- describe("Dialog", () => {
42
- it("is closed until the trigger is activated", async () => {
43
- const user = userEvent.setup();
44
- render(<Example />);
45
-
46
- expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
47
- await user.click(screen.getByRole("button", { name: "Open" }));
48
- expect(await screen.findByRole("dialog")).toBeInTheDocument();
49
- });
50
-
51
- it("is named and described by its title and description", async () => {
52
- const user = userEvent.setup();
53
- render(<Example />);
54
- await user.click(screen.getByRole("button", { name: "Open" }));
55
-
56
- const dialog = await screen.findByRole("dialog", { name: "Delete project" });
57
- expect(dialog).toHaveAccessibleDescription("This cannot be undone.");
58
- });
59
-
60
- it("opens from the keyboard", async () => {
61
- const user = userEvent.setup();
62
- render(<Example />);
63
-
64
- await user.tab();
65
- expect(screen.getByRole("button", { name: "Open" })).toHaveFocus();
66
- await user.keyboard("{Enter}");
67
- expect(await screen.findByRole("dialog")).toBeInTheDocument();
68
- });
69
-
70
- it("moves focus into the dialog when it opens", async () => {
71
- const user = userEvent.setup();
72
- render(<Example />);
73
- await user.click(screen.getByRole("button", { name: "Open" }));
74
-
75
- const dialog = await screen.findByRole("dialog");
76
- await waitFor(() => {
77
- expect(dialog).toContainElement(document.activeElement as HTMLElement);
78
- });
79
- });
80
-
81
- it("closes on Escape and restores focus to the trigger", async () => {
82
- const user = userEvent.setup();
83
- render(<Example />);
84
-
85
- const trigger = screen.getByRole("button", { name: "Open" });
86
- await user.click(trigger);
87
- await screen.findByRole("dialog");
88
-
89
- await user.keyboard("{Escape}");
90
- await waitFor(() => {
91
- expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
92
- });
93
- await waitFor(() => {
94
- expect(trigger).toHaveFocus();
95
- });
96
- });
97
-
98
- it("closes via the built-in close button", async () => {
99
- const user = userEvent.setup();
100
- render(<Example />);
101
- await user.click(screen.getByRole("button", { name: "Open" }));
102
- await screen.findByRole("dialog");
103
-
104
- await user.click(screen.getByRole("button", { name: "Close" }));
105
- await waitFor(() => {
106
- expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
107
- });
108
- });
109
-
110
- it("omits the close button when asked", async () => {
111
- const user = userEvent.setup();
112
- render(<Example showCloseButton={false} />);
113
- await user.click(screen.getByRole("button", { name: "Open" }));
114
- await screen.findByRole("dialog");
115
-
116
- expect(screen.queryByRole("button", { name: "Close" })).not.toBeInTheDocument();
117
- });
118
-
119
- it("closes via DialogClose", async () => {
120
- const user = userEvent.setup();
121
- render(<Example />);
122
- await user.click(screen.getByRole("button", { name: "Open" }));
123
- await screen.findByRole("dialog");
124
-
125
- await user.click(screen.getByRole("button", { name: "Cancel" }));
126
- await waitFor(() => {
127
- expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
128
- });
129
- });
130
-
131
- it("works as a controlled component", async () => {
132
- const onOpenChange = vi.fn();
133
- const user = userEvent.setup();
134
-
135
- function Controlled() {
136
- const [open, setOpen] = useState(false);
137
- return (
138
- <Dialog
139
- open={open}
140
- onOpenChange={(next) => {
141
- onOpenChange(next);
142
- setOpen(next);
143
- }}
144
- >
145
- <DialogTrigger asChild>
146
- <Button>Open</Button>
147
- </DialogTrigger>
148
- <DialogContent>
149
- <DialogTitle>Controlled</DialogTitle>
150
- </DialogContent>
151
- </Dialog>
152
- );
153
- }
154
-
155
- render(<Controlled />);
156
- await user.click(screen.getByRole("button", { name: "Open" }));
157
- expect(onOpenChange).toHaveBeenCalledWith(true);
158
- expect(await screen.findByRole("dialog")).toBeInTheDocument();
159
- });
160
-
161
- it("merges a consumer className on the content", async () => {
162
- const user = userEvent.setup();
163
- render(
164
- <Dialog>
165
- <DialogTrigger>Open</DialogTrigger>
166
- <DialogContent className="max-w-2xl">
167
- <DialogTitle>Wide</DialogTitle>
168
- </DialogContent>
169
- </Dialog>,
170
- );
171
-
172
- await user.click(screen.getByText("Open"));
173
- const dialog = await screen.findByRole("dialog");
174
- expect(dialog).toHaveClass("max-w-2xl");
175
- expect(dialog).not.toHaveClass("max-w-lg");
176
- });
177
-
178
- it("has no accessibility violations while open", async () => {
179
- const user = userEvent.setup();
180
- const { baseElement } = render(<Example />);
181
- await user.click(screen.getByRole("button", { name: "Open" }));
182
- await screen.findByRole("dialog");
183
-
184
- await expectNoA11yViolations(baseElement);
185
- });
186
- });
@@ -1,111 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react-vite";
2
-
3
- import { Button } from "@/components/button";
4
-
5
- import {
6
- Drawer,
7
- DrawerBody,
8
- DrawerCancel,
9
- DrawerContent,
10
- DrawerDescription,
11
- DrawerFooter,
12
- DrawerHeader,
13
- DrawerTitle,
14
- DrawerTrigger,
15
- } from "./drawer";
16
-
17
- const meta = {
18
- title: "Overlays/Drawer",
19
- component: Drawer,
20
- parameters: { controls: { disable: true } },
21
- } satisfies Meta<typeof Drawer>;
22
-
23
- export default meta;
24
- type Story = StoryObj<typeof meta>;
25
-
26
- /** Drag the handle downward to dismiss, or use Escape or Cancel. */
27
- export const Default: Story = {
28
- render: () => (
29
- <Drawer>
30
- <DrawerTrigger asChild>
31
- <Button variant="outline">Open drawer</Button>
32
- </DrawerTrigger>
33
- <DrawerContent>
34
- <DrawerHeader>
35
- <DrawerTitle>Filters</DrawerTitle>
36
- <DrawerDescription>Narrow down the deployment list.</DrawerDescription>
37
- </DrawerHeader>
38
- <DrawerBody className="space-y-2 text-sm text-muted-foreground">
39
- <p>Status, branch, environment and author filters would live here.</p>
40
- </DrawerBody>
41
- <DrawerFooter>
42
- <Button>Apply filters</Button>
43
- <DrawerCancel>Cancel</DrawerCancel>
44
- </DrawerFooter>
45
- </DrawerContent>
46
- </Drawer>
47
- ),
48
- };
49
-
50
- export const ScrollingContent: Story = {
51
- render: () => (
52
- <Drawer>
53
- <DrawerTrigger asChild>
54
- <Button variant="outline">Pick a region</Button>
55
- </DrawerTrigger>
56
- <DrawerContent>
57
- <DrawerHeader>
58
- <DrawerTitle>Region</DrawerTitle>
59
- <DrawerDescription>Where your workloads run.</DrawerDescription>
60
- </DrawerHeader>
61
- <DrawerBody className="max-h-72">
62
- <ul className="grid gap-1 text-sm">
63
- {[
64
- "us-east-1",
65
- "us-west-2",
66
- "eu-west-1",
67
- "eu-central-1",
68
- "ap-south-1",
69
- "ap-southeast-2",
70
- "sa-east-1",
71
- "af-south-1",
72
- ].map((region) => (
73
- <li key={region}>
74
- <button
75
- type="button"
76
- className="w-full rounded-md px-3 py-2 text-left hover:bg-accent hover:text-accent-foreground"
77
- >
78
- {region}
79
- </button>
80
- </li>
81
- ))}
82
- </ul>
83
- </DrawerBody>
84
- <DrawerFooter>
85
- <DrawerCancel>Cancel</DrawerCancel>
86
- </DrawerFooter>
87
- </DrawerContent>
88
- </Drawer>
89
- ),
90
- };
91
-
92
- /** Without a handle the drawer cannot be dragged; keep an explicit way out. */
93
- export const WithoutHandle: Story = {
94
- render: () => (
95
- <Drawer>
96
- <DrawerTrigger asChild>
97
- <Button variant="outline">Open</Button>
98
- </DrawerTrigger>
99
- <DrawerContent showHandle={false}>
100
- <DrawerHeader>
101
- <DrawerTitle>Confirm</DrawerTitle>
102
- <DrawerDescription>This drawer cannot be dragged away.</DrawerDescription>
103
- </DrawerHeader>
104
- <DrawerFooter>
105
- <Button variant="destructive">Delete</Button>
106
- <DrawerCancel>Cancel</DrawerCancel>
107
- </DrawerFooter>
108
- </DrawerContent>
109
- </Drawer>
110
- ),
111
- };
@@ -1,187 +0,0 @@
1
- import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2
- import userEvent from "@testing-library/user-event";
3
- import { describe, expect, it, vi } from "vitest";
4
-
5
- import { expectNoA11yViolations } from "../../../test/a11y";
6
- import { Button } from "../button";
7
- import {
8
- Drawer,
9
- DrawerBody,
10
- DrawerCancel,
11
- DrawerContent,
12
- DrawerDescription,
13
- DrawerFooter,
14
- DrawerHeader,
15
- DrawerTitle,
16
- DrawerTrigger,
17
- } from "./drawer";
18
-
19
- const DRAWER_HEIGHT = 400;
20
-
21
- function Example({ onOpenChange }: { onOpenChange?: (open: boolean) => void } = {}) {
22
- return (
23
- <Drawer onOpenChange={onOpenChange}>
24
- <DrawerTrigger asChild>
25
- <Button>Open</Button>
26
- </DrawerTrigger>
27
- <DrawerContent>
28
- <DrawerHeader>
29
- <DrawerTitle>Filters</DrawerTitle>
30
- <DrawerDescription>Narrow down the results.</DrawerDescription>
31
- </DrawerHeader>
32
- <DrawerBody>Body</DrawerBody>
33
- <DrawerFooter>
34
- <DrawerCancel>Cancel</DrawerCancel>
35
- </DrawerFooter>
36
- </DrawerContent>
37
- </Drawer>
38
- );
39
- }
40
-
41
- /** jsdom reports every box as zero-sized, so the drawer's height is stubbed to
42
- * exercise the distance-based dismiss rule. */
43
- function stubHeight(element: Element, height = DRAWER_HEIGHT) {
44
- vi.spyOn(element, "getBoundingClientRect").mockReturnValue({
45
- height,
46
- width: 320,
47
- top: 0,
48
- left: 0,
49
- right: 320,
50
- bottom: height,
51
- x: 0,
52
- y: 0,
53
- toJSON: () => ({}),
54
- });
55
- }
56
-
57
- async function open() {
58
- const user = userEvent.setup();
59
- render(<Example />);
60
- await user.click(screen.getByRole("button", { name: "Open" }));
61
- const dialog = await screen.findByRole("dialog");
62
- const handle = dialog.querySelector("[data-slot='drawer-handle']");
63
- if (!handle) throw new Error("drawer handle not rendered");
64
- stubHeight(dialog);
65
- return { user, dialog, handle };
66
- }
67
-
68
- function drag(handle: Element, distance: number) {
69
- fireEvent.pointerDown(handle, { button: 0, pointerId: 1, clientY: 0 });
70
- fireEvent.pointerMove(handle, { pointerId: 1, clientY: distance });
71
- fireEvent.pointerUp(handle, { pointerId: 1, clientY: distance });
72
- }
73
-
74
- describe("Drawer", () => {
75
- it("opens from its trigger", async () => {
76
- const user = userEvent.setup();
77
- render(<Example />);
78
-
79
- expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
80
- await user.click(screen.getByRole("button", { name: "Open" }));
81
- expect(await screen.findByRole("dialog", { name: "Filters" })).toBeInTheDocument();
82
- });
83
-
84
- it("hides the drag handle from assistive technology", async () => {
85
- const { handle } = await open();
86
- expect(handle).toHaveAttribute("aria-hidden", "true");
87
- });
88
-
89
- it("closes on Escape", async () => {
90
- const { user } = await open();
91
- await user.keyboard("{Escape}");
92
- await waitFor(() => {
93
- expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
94
- });
95
- });
96
-
97
- it("closes from DrawerCancel", async () => {
98
- const { user } = await open();
99
- await user.click(screen.getByRole("button", { name: "Cancel" }));
100
- await waitFor(() => {
101
- expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
102
- });
103
- });
104
-
105
- describe("drag to dismiss", () => {
106
- it("closes when dragged past a quarter of its height", async () => {
107
- const { handle } = await open();
108
-
109
- drag(handle, DRAWER_HEIGHT * 0.5);
110
- await waitFor(() => {
111
- expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
112
- });
113
- });
114
-
115
- it("stays open when dragged only a little", async () => {
116
- const { handle } = await open();
117
-
118
- // Below MIN_FLICK_DISTANCE, so neither the distance nor the velocity rule
119
- // fires. A tap on the handle must never dismiss the drawer.
120
- drag(handle, 8);
121
- expect(screen.getByRole("dialog")).toBeInTheDocument();
122
- });
123
-
124
- it("follows the pointer while dragging", async () => {
125
- const { dialog, handle } = await open();
126
-
127
- fireEvent.pointerDown(handle, { button: 0, pointerId: 1, clientY: 0 });
128
- fireEvent.pointerMove(handle, { pointerId: 1, clientY: 60 });
129
-
130
- expect(dialog).toHaveAttribute("data-dragging", "true");
131
- expect(dialog.style.transform).toBe("translate3d(0, 60px, 0)");
132
-
133
- fireEvent.pointerUp(handle, { pointerId: 1, clientY: 60 });
134
- });
135
-
136
- it("ignores upward drags so the panel stays anchored to the edge", async () => {
137
- const { dialog, handle } = await open();
138
-
139
- fireEvent.pointerDown(handle, { button: 0, pointerId: 1, clientY: 100 });
140
- fireEvent.pointerMove(handle, { pointerId: 1, clientY: 20 });
141
-
142
- expect(dialog.style.transform).toBe("");
143
-
144
- fireEvent.pointerUp(handle, { pointerId: 1, clientY: 20 });
145
- expect(screen.getByRole("dialog")).toBeInTheDocument();
146
- });
147
-
148
- it("ignores non-primary buttons", async () => {
149
- const { dialog, handle } = await open();
150
-
151
- fireEvent.pointerDown(handle, { button: 2, pointerId: 1, clientY: 0 });
152
- fireEvent.pointerMove(handle, { pointerId: 1, clientY: 300 });
153
-
154
- expect(dialog.style.transform).toBe("");
155
- });
156
- });
157
-
158
- it("reports open state changes", async () => {
159
- const onOpenChange = vi.fn();
160
- const user = userEvent.setup();
161
- render(<Example onOpenChange={onOpenChange} />);
162
-
163
- await user.click(screen.getByRole("button", { name: "Open" }));
164
- expect(onOpenChange).toHaveBeenCalledWith(true);
165
- });
166
-
167
- it("throws a useful error if content is rendered outside the root", () => {
168
- const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);
169
- expect(() =>
170
- render(
171
- <DrawerContent>
172
- <DrawerTitle>Orphan</DrawerTitle>
173
- </DrawerContent>,
174
- ),
175
- ).toThrow(/must be rendered inside <Drawer>/);
176
- consoleError.mockRestore();
177
- });
178
-
179
- it("has no accessibility violations while open", async () => {
180
- const user = userEvent.setup();
181
- const { baseElement } = render(<Example />);
182
- await user.click(screen.getByRole("button", { name: "Open" }));
183
- await screen.findByRole("dialog");
184
-
185
- await expectNoA11yViolations(baseElement);
186
- });
187
- });