@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,129 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react-vite";
2
- import { useState } from "react";
3
-
4
- import {
5
- Pagination,
6
- PaginationContent,
7
- PaginationEllipsis,
8
- PaginationItem,
9
- PaginationLink,
10
- PaginationNext,
11
- PaginationPrevious,
12
- } from "./pagination";
13
-
14
- const meta = {
15
- title: "Navigation/Pagination",
16
- component: Pagination,
17
- parameters: { controls: { disable: true } },
18
- } satisfies Meta<typeof Pagination>;
19
-
20
- export default meta;
21
- type Story = StoryObj<typeof meta>;
22
-
23
- export const Default: Story = {
24
- render: () => (
25
- <Pagination>
26
- <PaginationContent>
27
- <PaginationItem>
28
- <PaginationPrevious href="#1" />
29
- </PaginationItem>
30
- <PaginationItem>
31
- <PaginationLink href="#1">1</PaginationLink>
32
- </PaginationItem>
33
- <PaginationItem>
34
- <PaginationLink href="#2" isActive>
35
- 2
36
- </PaginationLink>
37
- </PaginationItem>
38
- <PaginationItem>
39
- <PaginationLink href="#3">3</PaginationLink>
40
- </PaginationItem>
41
- <PaginationItem>
42
- <PaginationEllipsis />
43
- </PaginationItem>
44
- <PaginationItem>
45
- <PaginationLink href="#12">12</PaginationLink>
46
- </PaginationItem>
47
- <PaginationItem>
48
- <PaginationNext href="#3" />
49
- </PaginationItem>
50
- </PaginationContent>
51
- </Pagination>
52
- ),
53
- };
54
-
55
- export const Compact: Story = {
56
- render: () => (
57
- <Pagination>
58
- <PaginationContent>
59
- <PaginationItem>
60
- <PaginationPrevious href="#1" />
61
- </PaginationItem>
62
- <PaginationItem>
63
- <PaginationNext href="#3" />
64
- </PaginationItem>
65
- </PaginationContent>
66
- </Pagination>
67
- ),
68
- };
69
-
70
- /**
71
- * When paging only changes client state there is no URL to link to, so the
72
- * pages render as buttons instead — same styling, correct semantics.
73
- */
74
- export const AsButtons: Story = {
75
- render: function AsButtons() {
76
- const [page, setPage] = useState(2);
77
- const pages = [1, 2, 3, 4, 5];
78
-
79
- return (
80
- <Pagination>
81
- <PaginationContent>
82
- <PaginationItem>
83
- <PaginationLink asChild size="sm" className="gap-1 px-2.5">
84
- <button
85
- type="button"
86
- aria-label="Go to previous page"
87
- disabled={page === 1}
88
- onClick={() => {
89
- setPage((current) => Math.max(1, current - 1));
90
- }}
91
- >
92
- Previous
93
- </button>
94
- </PaginationLink>
95
- </PaginationItem>
96
- {pages.map((value) => (
97
- <PaginationItem key={value}>
98
- <PaginationLink asChild isActive={value === page}>
99
- <button
100
- type="button"
101
- aria-label={`Go to page ${String(value)}`}
102
- onClick={() => {
103
- setPage(value);
104
- }}
105
- >
106
- {value}
107
- </button>
108
- </PaginationLink>
109
- </PaginationItem>
110
- ))}
111
- <PaginationItem>
112
- <PaginationLink asChild size="sm" className="gap-1 px-2.5">
113
- <button
114
- type="button"
115
- aria-label="Go to next page"
116
- disabled={page === pages.length}
117
- onClick={() => {
118
- setPage((current) => Math.min(pages.length, current + 1));
119
- }}
120
- >
121
- Next
122
- </button>
123
- </PaginationLink>
124
- </PaginationItem>
125
- </PaginationContent>
126
- </Pagination>
127
- );
128
- },
129
- };
@@ -1,119 +0,0 @@
1
- import { render, screen } 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 {
7
- Pagination,
8
- PaginationContent,
9
- PaginationEllipsis,
10
- PaginationItem,
11
- PaginationLink,
12
- PaginationNext,
13
- PaginationPrevious,
14
- } from "./pagination";
15
-
16
- function Example({ current = 2 }: { current?: number } = {}) {
17
- return (
18
- <Pagination>
19
- <PaginationContent>
20
- <PaginationItem>
21
- <PaginationPrevious href="#1" />
22
- </PaginationItem>
23
- {[1, 2, 3].map((page) => (
24
- <PaginationItem key={page}>
25
- <PaginationLink href={`#${String(page)}`} isActive={page === current}>
26
- {page}
27
- </PaginationLink>
28
- </PaginationItem>
29
- ))}
30
- <PaginationItem>
31
- <PaginationEllipsis />
32
- </PaginationItem>
33
- <PaginationItem>
34
- <PaginationLink href="#12">12</PaginationLink>
35
- </PaginationItem>
36
- <PaginationItem>
37
- <PaginationNext href="#3" />
38
- </PaginationItem>
39
- </PaginationContent>
40
- </Pagination>
41
- );
42
- }
43
-
44
- describe("Pagination", () => {
45
- it("is a named navigation landmark", () => {
46
- render(<Example />);
47
- expect(screen.getByRole("navigation", { name: "Pagination" })).toBeInTheDocument();
48
- });
49
-
50
- it("marks the current page with aria-current", () => {
51
- render(<Example current={2} />);
52
-
53
- const current = screen.getByRole("link", { name: "2" });
54
- expect(current).toHaveAttribute("aria-current", "page");
55
- expect(screen.getByRole("link", { name: "1" })).not.toHaveAttribute("aria-current");
56
- });
57
-
58
- it("names the previous and next controls", () => {
59
- render(<Example />);
60
- expect(screen.getByRole("link", { name: "Go to previous page" })).toBeInTheDocument();
61
- expect(screen.getByRole("link", { name: "Go to next page" })).toBeInTheDocument();
62
- });
63
-
64
- it("renders pages as links, so they can be shared and opened in a tab", () => {
65
- render(<Example />);
66
- expect(screen.getByRole("link", { name: "2" })).toHaveAttribute("href", "#2");
67
- });
68
-
69
- it("hides the ellipsis but keeps a readable equivalent", () => {
70
- const { container } = render(<Example />);
71
- const ellipsis = container.querySelector("[data-slot='pagination-ellipsis']");
72
-
73
- expect(ellipsis).toHaveAttribute("aria-hidden", "true");
74
- expect(ellipsis).toHaveTextContent("More pages");
75
- });
76
-
77
- it("renders as buttons via asChild when paging is client state", async () => {
78
- const onClick = vi.fn();
79
- const user = userEvent.setup();
80
-
81
- render(
82
- <Pagination>
83
- <PaginationContent>
84
- <PaginationItem>
85
- <PaginationLink asChild isActive>
86
- <button type="button" onClick={onClick}>
87
- 1
88
- </button>
89
- </PaginationLink>
90
- </PaginationItem>
91
- </PaginationContent>
92
- </Pagination>,
93
- );
94
-
95
- const button = screen.getByRole("button", { name: "1" });
96
- expect(button).toHaveAttribute("aria-current", "page");
97
-
98
- await user.click(button);
99
- expect(onClick).toHaveBeenCalledTimes(1);
100
- });
101
-
102
- it("is reachable by keyboard", async () => {
103
- const user = userEvent.setup();
104
- render(<Example />);
105
-
106
- await user.tab();
107
- expect(screen.getByRole("link", { name: "Go to previous page" })).toHaveFocus();
108
- });
109
-
110
- it("styles the active page differently", () => {
111
- render(<Example current={3} />);
112
- expect(screen.getByRole("link", { name: "3" })).toHaveAttribute("data-active", "true");
113
- });
114
-
115
- it("has no accessibility violations", async () => {
116
- const { container } = render(<Example />);
117
- await expectNoA11yViolations(container);
118
- });
119
- });
@@ -1,84 +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 { Popover, PopoverArrow, PopoverContent, PopoverTrigger } from "./popover";
8
-
9
- const meta = {
10
- title: "Overlays/Popover",
11
- component: PopoverContent,
12
- args: { side: "bottom", align: "center" },
13
- argTypes: {
14
- side: { control: "inline-radio", options: ["top", "right", "bottom", "left"] },
15
- align: { control: "inline-radio", options: ["start", "center", "end"] },
16
- },
17
- } satisfies Meta<typeof PopoverContent>;
18
-
19
- export default meta;
20
- type Story = StoryObj<typeof meta>;
21
-
22
- export const Default: Story = {
23
- render: (args) => (
24
- <Popover>
25
- <PopoverTrigger asChild>
26
- <Button variant="outline">Dimensions</Button>
27
- </PopoverTrigger>
28
- <PopoverContent {...args} aria-labelledby="dimensions-heading">
29
- <div className="grid gap-3">
30
- <div className="space-y-1">
31
- <h4 id="dimensions-heading" className="text-sm font-medium">
32
- Dimensions
33
- </h4>
34
- <p className="text-xs text-muted-foreground">Set the layout for the layer.</p>
35
- </div>
36
- <div className="grid gap-2">
37
- <div className="grid grid-cols-3 items-center gap-2">
38
- <Label htmlFor="width">Width</Label>
39
- <Input id="width" inputSize="sm" defaultValue="100%" className="col-span-2" />
40
- </div>
41
- <div className="grid grid-cols-3 items-center gap-2">
42
- <Label htmlFor="height">Height</Label>
43
- <Input id="height" inputSize="sm" defaultValue="24px" className="col-span-2" />
44
- </div>
45
- </div>
46
- </div>
47
- </PopoverContent>
48
- </Popover>
49
- ),
50
- };
51
-
52
- /** Growth direction follows the anchor, so every side reads as one motion. */
53
- export const Sides: Story = {
54
- parameters: { controls: { disable: true } },
55
- render: () => (
56
- <div className="flex flex-wrap gap-3">
57
- {(["top", "right", "bottom", "left"] as const).map((side) => (
58
- <Popover key={side}>
59
- <PopoverTrigger asChild>
60
- <Button variant="outline">{side}</Button>
61
- </PopoverTrigger>
62
- <PopoverContent side={side} className="w-48" aria-label={`Opened from the ${side}`}>
63
- <p className="text-sm">Opened from the {side}.</p>
64
- </PopoverContent>
65
- </Popover>
66
- ))}
67
- </div>
68
- ),
69
- };
70
-
71
- export const WithArrow: Story = {
72
- parameters: { controls: { disable: true } },
73
- render: () => (
74
- <Popover>
75
- <PopoverTrigger asChild>
76
- <Button variant="outline">Show details</Button>
77
- </PopoverTrigger>
78
- <PopoverContent className="w-56" aria-label="Deployment details">
79
- <p className="text-sm">Deployed 4 minutes ago from main.</p>
80
- <PopoverArrow />
81
- </PopoverContent>
82
- </Popover>
83
- ),
84
- };
@@ -1,163 +0,0 @@
1
- import { 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 { Popover, PopoverArrow, PopoverClose, PopoverContent, PopoverTrigger } from "./popover";
8
-
9
- function Example() {
10
- return (
11
- <Popover>
12
- <PopoverTrigger asChild>
13
- <Button>Open settings</Button>
14
- </PopoverTrigger>
15
- <PopoverContent aria-labelledby="dimensions-heading">
16
- <p id="dimensions-heading">Dimensions</p>
17
- <PopoverClose>Done</PopoverClose>
18
- </PopoverContent>
19
- </Popover>
20
- );
21
- }
22
-
23
- describe("Popover", () => {
24
- it("is closed until the trigger is activated", async () => {
25
- const user = userEvent.setup();
26
- render(<Example />);
27
-
28
- expect(screen.queryByText("Dimensions")).not.toBeInTheDocument();
29
- await user.click(screen.getByRole("button", { name: "Open settings" }));
30
- expect(await screen.findByText("Dimensions")).toBeInTheDocument();
31
- });
32
-
33
- it("reports its state on the trigger", async () => {
34
- const user = userEvent.setup();
35
- render(<Example />);
36
-
37
- const trigger = screen.getByRole("button", { name: "Open settings" });
38
- expect(trigger).toHaveAttribute("aria-expanded", "false");
39
-
40
- await user.click(trigger);
41
- await waitFor(() => {
42
- expect(trigger).toHaveAttribute("aria-expanded", "true");
43
- });
44
- });
45
-
46
- it("opens from the keyboard", async () => {
47
- const user = userEvent.setup();
48
- render(<Example />);
49
-
50
- await user.tab();
51
- await user.keyboard("{Enter}");
52
- expect(await screen.findByText("Dimensions")).toBeInTheDocument();
53
- });
54
-
55
- it("closes on Escape and restores focus to the trigger", async () => {
56
- const user = userEvent.setup();
57
- render(<Example />);
58
-
59
- const trigger = screen.getByRole("button", { name: "Open settings" });
60
- await user.click(trigger);
61
- await screen.findByText("Dimensions");
62
-
63
- await user.keyboard("{Escape}");
64
- await waitFor(() => {
65
- expect(screen.queryByText("Dimensions")).not.toBeInTheDocument();
66
- });
67
- await waitFor(() => {
68
- expect(trigger).toHaveFocus();
69
- });
70
- });
71
-
72
- it("closes from PopoverClose", async () => {
73
- const user = userEvent.setup();
74
- render(<Example />);
75
- await user.click(screen.getByRole("button", { name: "Open settings" }));
76
- await screen.findByText("Dimensions");
77
-
78
- await user.click(screen.getByText("Done"));
79
- await waitFor(() => {
80
- expect(screen.queryByText("Dimensions")).not.toBeInTheDocument();
81
- });
82
- });
83
-
84
- it("merges a consumer className", async () => {
85
- const user = userEvent.setup();
86
- render(
87
- <Popover>
88
- <PopoverTrigger>Open</PopoverTrigger>
89
- <PopoverContent className="w-96" aria-label="Details" data-testid="content">
90
- Content
91
- </PopoverContent>
92
- </Popover>,
93
- );
94
-
95
- await user.click(screen.getByText("Open"));
96
- const content = await screen.findByTestId("content");
97
- expect(content).toHaveClass("w-96");
98
- expect(content).not.toHaveClass("w-72");
99
- });
100
-
101
- it("warns in development when the content has no accessible name", async () => {
102
- const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
103
- const user = userEvent.setup();
104
- render(
105
- <Popover>
106
- <PopoverTrigger>Open</PopoverTrigger>
107
- <PopoverContent>Unnamed</PopoverContent>
108
- </Popover>,
109
- );
110
-
111
- await user.click(screen.getByText("Open"));
112
- await screen.findByText("Unnamed");
113
-
114
- expect(warn).toHaveBeenCalledWith(expect.stringContaining("Missing accessible name"));
115
- warn.mockRestore();
116
- });
117
-
118
- it("does not warn when a name is supplied", async () => {
119
- const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
120
- const user = userEvent.setup();
121
- render(
122
- <Popover>
123
- <PopoverTrigger>Open</PopoverTrigger>
124
- <PopoverContent aria-label="Details">Named</PopoverContent>
125
- </Popover>,
126
- );
127
-
128
- await user.click(screen.getByText("Open"));
129
- await screen.findByText("Named");
130
-
131
- expect(warn).not.toHaveBeenCalled();
132
- warn.mockRestore();
133
- });
134
-
135
- it("renders an arrow that is hidden from assistive technology", async () => {
136
- const user = userEvent.setup();
137
- const { baseElement } = render(
138
- <Popover>
139
- <PopoverTrigger>Open</PopoverTrigger>
140
- <PopoverContent aria-label="Details">
141
- Details
142
- <PopoverArrow />
143
- </PopoverContent>
144
- </Popover>,
145
- );
146
-
147
- await user.click(screen.getByText("Open"));
148
- await screen.findByText("Details");
149
-
150
- const arrow = baseElement.querySelector("[data-slot='popover-arrow']");
151
- expect(arrow).toBeInTheDocument();
152
- expect(arrow).toHaveClass("fill-popover");
153
- });
154
-
155
- it("has no accessibility violations while open", async () => {
156
- const user = userEvent.setup();
157
- const { baseElement } = render(<Example />);
158
- await user.click(screen.getByRole("button", { name: "Open settings" }));
159
- await screen.findByText("Dimensions");
160
-
161
- await expectNoA11yViolations(baseElement);
162
- });
163
- });
@@ -1,90 +0,0 @@
1
- import type { Decorator, Meta, StoryObj } from "@storybook/react-vite";
2
- import { useEffect, useState } from "react";
3
-
4
- import { Progress } from "./progress";
5
-
6
- /** Named so its type is nameable in declaration output (TS2883). */
7
- const withFixedWidth: Decorator = (Story) => (
8
- <div className="w-72">
9
- <Story />
10
- </div>
11
- );
12
-
13
- const meta = {
14
- title: "Feedback/Progress",
15
- component: Progress,
16
- args: { value: 40, size: "md", tone: "primary", "aria-label": "Upload" },
17
- argTypes: {
18
- value: { control: { type: "range", min: 0, max: 100 } },
19
- size: { control: "inline-radio", options: ["sm", "md", "lg"] },
20
- tone: { control: "select", options: ["primary", "success", "warning", "destructive"] },
21
- },
22
- decorators: [withFixedWidth],
23
- } satisfies Meta<typeof Progress>;
24
-
25
- export default meta;
26
- type Story = StoryObj<typeof meta>;
27
-
28
- export const Default: Story = {};
29
-
30
- /** The number belongs on screen too — a bar alone is hard to read precisely. */
31
- export const WithLabel: Story = {
32
- parameters: { controls: { disable: true } },
33
- render: function WithLabel() {
34
- const [value, setValue] = useState(12);
35
-
36
- useEffect(() => {
37
- const timer = setInterval(() => {
38
- setValue((current) => (current >= 100 ? 0 : current + 4));
39
- }, 400);
40
- return () => {
41
- clearInterval(timer);
42
- };
43
- }, []);
44
-
45
- return (
46
- <div className="grid gap-2">
47
- <div className="flex items-center justify-between text-sm">
48
- <span id="upload-label">Uploading assets</span>
49
- <span className="text-muted-foreground tabular-nums">{value}%</span>
50
- </div>
51
- <Progress value={value} aria-labelledby="upload-label" />
52
- </div>
53
- );
54
- },
55
- };
56
-
57
- /** No known duration — announced as indeterminate, not as zero percent. */
58
- export const Indeterminate: Story = {
59
- parameters: { controls: { disable: true } },
60
- render: () => (
61
- <div className="grid gap-2">
62
- <span className="text-sm" id="indeterminate-label">
63
- Preparing your export…
64
- </span>
65
- <Progress aria-labelledby="indeterminate-label" />
66
- </div>
67
- ),
68
- };
69
-
70
- export const Tones: Story = {
71
- parameters: { controls: { disable: true } },
72
- render: () => (
73
- <div className="grid gap-4">
74
- {(["primary", "success", "warning", "destructive"] as const).map((tone) => (
75
- <Progress key={tone} tone={tone} value={65} aria-label={tone} />
76
- ))}
77
- </div>
78
- ),
79
- };
80
-
81
- export const Sizes: Story = {
82
- parameters: { controls: { disable: true } },
83
- render: () => (
84
- <div className="grid gap-4">
85
- {(["sm", "md", "lg"] as const).map((size) => (
86
- <Progress key={size} size={size} value={55} aria-label={size} />
87
- ))}
88
- </div>
89
- ),
90
- };
@@ -1,78 +0,0 @@
1
- import { render, screen } from "@testing-library/react";
2
- import { describe, expect, it } from "vitest";
3
-
4
- import { expectNoA11yViolations } from "../../../test/a11y";
5
- import { Progress } from "./progress";
6
-
7
- describe("Progress", () => {
8
- it("exposes a progressbar with its value", () => {
9
- render(<Progress value={40} aria-label="Upload" />);
10
-
11
- const bar = screen.getByRole("progressbar", { name: "Upload" });
12
- expect(bar).toHaveAttribute("aria-valuenow", "40");
13
- expect(bar).toHaveAttribute("aria-valuemax", "100");
14
- });
15
-
16
- it("reports an indeterminate state differently from zero", () => {
17
- render(<Progress aria-label="Working" />);
18
-
19
- const bar = screen.getByRole("progressbar");
20
- expect(bar).not.toHaveAttribute("aria-valuenow");
21
- expect(bar).toHaveAttribute("data-state", "indeterminate");
22
- });
23
-
24
- it("treats zero as a real value, not as unknown", () => {
25
- render(<Progress value={0} aria-label="Upload" />);
26
-
27
- const bar = screen.getByRole("progressbar");
28
- expect(bar).toHaveAttribute("aria-valuenow", "0");
29
- expect(bar).toHaveAttribute("data-state", "loading");
30
- });
31
-
32
- it("fills proportionally to the value", () => {
33
- const { container } = render(<Progress value={25} aria-label="Upload" />);
34
- const indicator = container.querySelector("[data-slot='progress-indicator']");
35
- expect(indicator).toHaveStyle({ transform: "translateX(-75%)" });
36
- });
37
-
38
- it("clamps a value outside the range", () => {
39
- const { container } = render(<Progress value={150} aria-label="Upload" max={100} />);
40
- const indicator = container.querySelector("[data-slot='progress-indicator']");
41
- expect(indicator).toHaveStyle({ transform: "translateX(-0%)" });
42
- });
43
-
44
- it.each([
45
- ["sm", "h-1"],
46
- ["md", "h-2"],
47
- ["lg", "h-3"],
48
- ] as const)("applies the %s size", (size, expectedClass) => {
49
- render(<Progress value={40} size={size} aria-label="Upload" />);
50
- expect(screen.getByRole("progressbar")).toHaveClass(expectedClass);
51
- });
52
-
53
- it.each([
54
- ["primary", "--progress-fill:var(--color-primary)"],
55
- ["success", "--progress-fill:var(--color-success)"],
56
- ["destructive", "--progress-fill:var(--color-destructive)"],
57
- ] as const)("applies the %s tone", (tone, expectedClass) => {
58
- render(<Progress value={40} tone={tone} aria-label="Upload" />);
59
- expect(screen.getByRole("progressbar").className).toContain(expectedClass);
60
- });
61
-
62
- it("lets a consumer className override a conflicting utility", () => {
63
- render(<Progress value={40} className="h-6" aria-label="Upload" />);
64
- const bar = screen.getByRole("progressbar");
65
- expect(bar).toHaveClass("h-6");
66
- expect(bar).not.toHaveClass("h-2");
67
- });
68
-
69
- it("has no accessibility violations", async () => {
70
- const { container } = render(
71
- <div>
72
- <span id="upload-label">Uploading</span>
73
- <Progress value={40} aria-labelledby="upload-label" />
74
- </div>,
75
- );
76
- await expectNoA11yViolations(container);
77
- });
78
- });