@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.
- package/dist/components/ai-agent-status/ai-agent-status.d.ts +1 -1
- package/dist/components/ai-tool/ai-tool.d.ts +1 -1
- package/dist/components/alert/alert.d.ts +1 -1
- package/dist/components/avatar/avatar.d.ts +1 -1
- package/dist/components/badge/badge.d.ts +1 -1
- package/dist/components/button/button.d.ts +1 -1
- package/dist/components/progress/progress.d.ts +1 -1
- package/dist/components/spinner/spinner.d.ts +1 -1
- package/dist/components/toast/toast.d.ts +1 -1
- package/package.json +7 -4
- package/src/blocks/admin-users/admin-users.stories.tsx +0 -83
- package/src/blocks/admin-users/admin-users.test.tsx +0 -159
- package/src/blocks/ai-chat/ai-chat.stories.tsx +0 -149
- package/src/blocks/ai-chat/ai-chat.test.tsx +0 -166
- package/src/blocks/dashboard/dashboard.stories.tsx +0 -106
- package/src/blocks/dashboard/dashboard.test.tsx +0 -94
- package/src/blocks/forgot-password/forgot-password.stories.tsx +0 -41
- package/src/blocks/forgot-password/forgot-password.test.tsx +0 -62
- package/src/blocks/login/login.stories.tsx +0 -46
- package/src/blocks/login/login.test.tsx +0 -100
- package/src/blocks/pricing/pricing.stories.tsx +0 -62
- package/src/blocks/pricing/pricing.test.tsx +0 -101
- package/src/blocks/settings/settings.stories.tsx +0 -94
- package/src/blocks/settings/settings.test.tsx +0 -167
- package/src/blocks/signup/signup.stories.tsx +0 -27
- package/src/blocks/signup/signup.test.tsx +0 -97
- package/src/components/accordion/accordion.stories.tsx +0 -90
- package/src/components/accordion/accordion.test.tsx +0 -156
- package/src/components/activity-feed/activity-feed.stories.tsx +0 -163
- package/src/components/activity-feed/activity-feed.test.tsx +0 -125
- package/src/components/ai-agent-status/ai-agent-status.stories.tsx +0 -86
- package/src/components/ai-agent-status/ai-agent-status.test.tsx +0 -66
- package/src/components/ai-conversation/ai-conversation.stories.tsx +0 -126
- package/src/components/ai-conversation/ai-conversation.test.tsx +0 -144
- package/src/components/ai-message/ai-message.stories.tsx +0 -106
- package/src/components/ai-message/ai-message.test.tsx +0 -160
- package/src/components/ai-model-selector/ai-model-selector.stories.tsx +0 -66
- package/src/components/ai-model-selector/ai-model-selector.test.tsx +0 -120
- package/src/components/ai-prompt-input/ai-prompt-input.stories.tsx +0 -145
- package/src/components/ai-prompt-input/ai-prompt-input.test.tsx +0 -273
- package/src/components/ai-reasoning/ai-reasoning.stories.tsx +0 -60
- package/src/components/ai-reasoning/ai-reasoning.test.tsx +0 -95
- package/src/components/ai-response/ai-response.stories.tsx +0 -75
- package/src/components/ai-response/ai-response.test.tsx +0 -96
- package/src/components/ai-sources/ai-sources.stories.tsx +0 -93
- package/src/components/ai-sources/ai-sources.test.tsx +0 -102
- package/src/components/ai-token-usage/ai-token-usage.stories.tsx +0 -59
- package/src/components/ai-token-usage/ai-token-usage.test.tsx +0 -83
- package/src/components/ai-tool/ai-tool.stories.tsx +0 -96
- package/src/components/ai-tool/ai-tool.test.tsx +0 -137
- package/src/components/alert/alert.stories.tsx +0 -101
- package/src/components/alert/alert.test.tsx +0 -94
- package/src/components/avatar/avatar.stories.tsx +0 -58
- package/src/components/avatar/avatar.test.tsx +0 -64
- package/src/components/badge/badge.stories.tsx +0 -68
- package/src/components/badge/badge.test.tsx +0 -61
- package/src/components/button/button.stories.tsx +0 -128
- package/src/components/button/button.test.tsx +0 -224
- package/src/components/calendar/calendar.stories.tsx +0 -105
- package/src/components/calendar/calendar.test.tsx +0 -255
- package/src/components/card/card.stories.tsx +0 -61
- package/src/components/card/card.test.tsx +0 -66
- package/src/components/checkbox/checkbox.stories.tsx +0 -124
- package/src/components/checkbox/checkbox.test.tsx +0 -121
- package/src/components/code-block/code-block.stories.tsx +0 -62
- package/src/components/code-block/code-block.test.tsx +0 -139
- package/src/components/combobox/combobox.stories.tsx +0 -171
- package/src/components/combobox/combobox.test.tsx +0 -429
- package/src/components/command/command.stories.tsx +0 -152
- package/src/components/command/command.test.tsx +0 -380
- package/src/components/data-table/data-table.stories.tsx +0 -151
- package/src/components/data-table/data-table.test.tsx +0 -288
- package/src/components/date-picker/date-picker.stories.tsx +0 -103
- package/src/components/date-picker/date-picker.test.tsx +0 -205
- package/src/components/dialog/dialog.stories.tsx +0 -140
- package/src/components/dialog/dialog.test.tsx +0 -186
- package/src/components/drawer/drawer.stories.tsx +0 -111
- package/src/components/drawer/drawer.test.tsx +0 -187
- package/src/components/dropdown-menu/dropdown-menu.stories.tsx +0 -128
- package/src/components/dropdown-menu/dropdown-menu.test.tsx +0 -321
- package/src/components/empty-state/empty-state.stories.tsx +0 -70
- package/src/components/empty-state/empty-state.test.tsx +0 -97
- package/src/components/form/form.stories.tsx +0 -144
- package/src/components/form/form.test.tsx +0 -203
- package/src/components/input/input.stories.tsx +0 -84
- package/src/components/input/input.test.tsx +0 -108
- package/src/components/label/label.stories.tsx +0 -51
- package/src/components/label/label.test.tsx +0 -55
- package/src/components/pagination/pagination.stories.tsx +0 -129
- package/src/components/pagination/pagination.test.tsx +0 -119
- package/src/components/popover/popover.stories.tsx +0 -84
- package/src/components/popover/popover.test.tsx +0 -163
- package/src/components/progress/progress.stories.tsx +0 -90
- package/src/components/progress/progress.test.tsx +0 -78
- package/src/components/radio-group/radio-group.stories.tsx +0 -102
- package/src/components/radio-group/radio-group.test.tsx +0 -152
- package/src/components/select/select.stories.tsx +0 -137
- package/src/components/select/select.test.tsx +0 -209
- package/src/components/separator/separator.stories.tsx +0 -42
- package/src/components/separator/separator.test.tsx +0 -46
- package/src/components/sheet/sheet.stories.tsx +0 -107
- package/src/components/sheet/sheet.test.tsx +0 -152
- package/src/components/skeleton/skeleton.stories.tsx +0 -46
- package/src/components/skeleton/skeleton.test.tsx +0 -57
- package/src/components/slider/slider.stories.tsx +0 -87
- package/src/components/slider/slider.test.tsx +0 -148
- package/src/components/spinner/spinner.stories.tsx +0 -47
- package/src/components/spinner/spinner.test.tsx +0 -41
- package/src/components/switch/switch.stories.tsx +0 -70
- package/src/components/switch/switch.test.tsx +0 -101
- package/src/components/table/table.stories.tsx +0 -131
- package/src/components/table/table.test.tsx +0 -153
- package/src/components/tabs/tabs.stories.tsx +0 -139
- package/src/components/tabs/tabs.test.tsx +0 -148
- package/src/components/toast/toast.stories.tsx +0 -167
- package/src/components/toast/toast.test.tsx +0 -306
- package/src/components/tooltip/tooltip.stories.tsx +0 -89
- package/src/components/tooltip/tooltip.test.tsx +0 -120
- package/src/lib/utils.test.ts +0 -32
- package/src/registry/meta.test.ts +0 -234
|
@@ -1,107 +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
|
-
Sheet,
|
|
9
|
-
SheetClose,
|
|
10
|
-
SheetContent,
|
|
11
|
-
SheetDescription,
|
|
12
|
-
SheetFooter,
|
|
13
|
-
SheetHeader,
|
|
14
|
-
SheetTitle,
|
|
15
|
-
SheetTrigger,
|
|
16
|
-
} from "./sheet";
|
|
17
|
-
|
|
18
|
-
const meta = {
|
|
19
|
-
title: "Overlays/Sheet",
|
|
20
|
-
component: SheetContent,
|
|
21
|
-
args: { side: "right" },
|
|
22
|
-
argTypes: {
|
|
23
|
-
side: { control: "inline-radio", options: ["top", "right", "bottom", "left"] },
|
|
24
|
-
},
|
|
25
|
-
} satisfies Meta<typeof SheetContent>;
|
|
26
|
-
|
|
27
|
-
export default meta;
|
|
28
|
-
type Story = StoryObj<typeof meta>;
|
|
29
|
-
|
|
30
|
-
export const Default: Story = {
|
|
31
|
-
render: (args) => (
|
|
32
|
-
<Sheet>
|
|
33
|
-
<SheetTrigger asChild>
|
|
34
|
-
<Button variant="outline">Open sheet</Button>
|
|
35
|
-
</SheetTrigger>
|
|
36
|
-
<SheetContent {...args}>
|
|
37
|
-
<SheetHeader>
|
|
38
|
-
<SheetTitle>Edit profile</SheetTitle>
|
|
39
|
-
<SheetDescription>Changes are saved when you close the sheet.</SheetDescription>
|
|
40
|
-
</SheetHeader>
|
|
41
|
-
<div className="grid gap-4">
|
|
42
|
-
<div className="grid gap-2">
|
|
43
|
-
<Label htmlFor="sheet-name">Name</Label>
|
|
44
|
-
<Input id="sheet-name" defaultValue="Ada Lovelace" />
|
|
45
|
-
</div>
|
|
46
|
-
</div>
|
|
47
|
-
<SheetFooter>
|
|
48
|
-
<SheetClose asChild>
|
|
49
|
-
<Button>Done</Button>
|
|
50
|
-
</SheetClose>
|
|
51
|
-
</SheetFooter>
|
|
52
|
-
</SheetContent>
|
|
53
|
-
</Sheet>
|
|
54
|
-
),
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export const Sides: Story = {
|
|
58
|
-
parameters: { controls: { disable: true } },
|
|
59
|
-
render: () => (
|
|
60
|
-
<div className="flex flex-wrap gap-3">
|
|
61
|
-
{(["top", "right", "bottom", "left"] as const).map((side) => (
|
|
62
|
-
<Sheet key={side}>
|
|
63
|
-
<SheetTrigger asChild>
|
|
64
|
-
<Button variant="outline">{side}</Button>
|
|
65
|
-
</SheetTrigger>
|
|
66
|
-
<SheetContent side={side}>
|
|
67
|
-
<SheetHeader>
|
|
68
|
-
<SheetTitle>From the {side}</SheetTitle>
|
|
69
|
-
<SheetDescription>Sheets can enter from any edge.</SheetDescription>
|
|
70
|
-
</SheetHeader>
|
|
71
|
-
</SheetContent>
|
|
72
|
-
</Sheet>
|
|
73
|
-
))}
|
|
74
|
-
</div>
|
|
75
|
-
),
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
/** A navigation sheet carries a nav landmark; placement alone conveys nothing. */
|
|
79
|
-
export const Navigation: Story = {
|
|
80
|
-
parameters: { controls: { disable: true } },
|
|
81
|
-
render: () => (
|
|
82
|
-
<Sheet>
|
|
83
|
-
<SheetTrigger asChild>
|
|
84
|
-
<Button variant="outline">Menu</Button>
|
|
85
|
-
</SheetTrigger>
|
|
86
|
-
<SheetContent side="left">
|
|
87
|
-
<SheetHeader>
|
|
88
|
-
<SheetTitle>Navigation</SheetTitle>
|
|
89
|
-
</SheetHeader>
|
|
90
|
-
<nav aria-label="Main">
|
|
91
|
-
<ul className="grid gap-1 text-sm">
|
|
92
|
-
{["Overview", "Projects", "Deployments", "Settings"].map((item) => (
|
|
93
|
-
<li key={item}>
|
|
94
|
-
<a
|
|
95
|
-
href={`#${item.toLowerCase()}`}
|
|
96
|
-
className="block rounded-md px-3 py-2 hover:bg-accent hover:text-accent-foreground"
|
|
97
|
-
>
|
|
98
|
-
{item}
|
|
99
|
-
</a>
|
|
100
|
-
</li>
|
|
101
|
-
))}
|
|
102
|
-
</ul>
|
|
103
|
-
</nav>
|
|
104
|
-
</SheetContent>
|
|
105
|
-
</Sheet>
|
|
106
|
-
),
|
|
107
|
-
};
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import { render, screen, waitFor } from "@testing-library/react";
|
|
2
|
-
import userEvent from "@testing-library/user-event";
|
|
3
|
-
import { describe, expect, it } from "vitest";
|
|
4
|
-
|
|
5
|
-
import { expectNoA11yViolations } from "../../../test/a11y";
|
|
6
|
-
import { Button } from "../button";
|
|
7
|
-
import {
|
|
8
|
-
Sheet,
|
|
9
|
-
SheetClose,
|
|
10
|
-
SheetContent,
|
|
11
|
-
SheetDescription,
|
|
12
|
-
SheetFooter,
|
|
13
|
-
SheetHeader,
|
|
14
|
-
SheetTitle,
|
|
15
|
-
SheetTrigger,
|
|
16
|
-
type SheetContentProps,
|
|
17
|
-
} from "./sheet";
|
|
18
|
-
|
|
19
|
-
function Example({ side }: { side?: SheetContentProps["side"] } = {}) {
|
|
20
|
-
return (
|
|
21
|
-
<Sheet>
|
|
22
|
-
<SheetTrigger asChild>
|
|
23
|
-
<Button>Open</Button>
|
|
24
|
-
</SheetTrigger>
|
|
25
|
-
<SheetContent side={side}>
|
|
26
|
-
<SheetHeader>
|
|
27
|
-
<SheetTitle>Edit profile</SheetTitle>
|
|
28
|
-
<SheetDescription>Changes are saved immediately.</SheetDescription>
|
|
29
|
-
</SheetHeader>
|
|
30
|
-
<SheetFooter>
|
|
31
|
-
<SheetClose asChild>
|
|
32
|
-
<Button variant="outline">Done</Button>
|
|
33
|
-
</SheetClose>
|
|
34
|
-
</SheetFooter>
|
|
35
|
-
</SheetContent>
|
|
36
|
-
</Sheet>
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
describe("Sheet", () => {
|
|
41
|
-
it("opens from its trigger and is named by its title", async () => {
|
|
42
|
-
const user = userEvent.setup();
|
|
43
|
-
render(<Example />);
|
|
44
|
-
|
|
45
|
-
await user.click(screen.getByRole("button", { name: "Open" }));
|
|
46
|
-
expect(await screen.findByRole("dialog", { name: "Edit profile" })).toBeInTheDocument();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it.each([
|
|
50
|
-
["top", "[--slide-y:-100%]"],
|
|
51
|
-
["bottom", "[--slide-y:100%]"],
|
|
52
|
-
["left", "[--slide-x:-100%]"],
|
|
53
|
-
["right", "[--slide-x:100%]"],
|
|
54
|
-
] as const)("slides in from the %s", async (side, expectedClass) => {
|
|
55
|
-
const user = userEvent.setup();
|
|
56
|
-
render(<Example side={side} />);
|
|
57
|
-
|
|
58
|
-
await user.click(screen.getByRole("button", { name: "Open" }));
|
|
59
|
-
expect(await screen.findByRole("dialog")).toHaveClass(expectedClass);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("defaults to the right edge", async () => {
|
|
63
|
-
const user = userEvent.setup();
|
|
64
|
-
render(<Example />);
|
|
65
|
-
|
|
66
|
-
await user.click(screen.getByRole("button", { name: "Open" }));
|
|
67
|
-
expect(await screen.findByRole("dialog")).toHaveClass("[--slide-x:100%]");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("closes on Escape and restores focus", async () => {
|
|
71
|
-
const user = userEvent.setup();
|
|
72
|
-
render(<Example />);
|
|
73
|
-
|
|
74
|
-
const trigger = screen.getByRole("button", { name: "Open" });
|
|
75
|
-
await user.click(trigger);
|
|
76
|
-
await screen.findByRole("dialog");
|
|
77
|
-
|
|
78
|
-
await user.keyboard("{Escape}");
|
|
79
|
-
await waitFor(() => {
|
|
80
|
-
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
81
|
-
});
|
|
82
|
-
await waitFor(() => {
|
|
83
|
-
expect(trigger).toHaveFocus();
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("closes from SheetClose", async () => {
|
|
88
|
-
const user = userEvent.setup();
|
|
89
|
-
render(<Example />);
|
|
90
|
-
await user.click(screen.getByRole("button", { name: "Open" }));
|
|
91
|
-
await screen.findByRole("dialog");
|
|
92
|
-
|
|
93
|
-
await user.click(screen.getByRole("button", { name: "Done" }));
|
|
94
|
-
await waitFor(() => {
|
|
95
|
-
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("omits the close button when asked", async () => {
|
|
100
|
-
const user = userEvent.setup();
|
|
101
|
-
render(
|
|
102
|
-
<Sheet>
|
|
103
|
-
<SheetTrigger>Open</SheetTrigger>
|
|
104
|
-
<SheetContent showCloseButton={false}>
|
|
105
|
-
<SheetTitle>No close button</SheetTitle>
|
|
106
|
-
</SheetContent>
|
|
107
|
-
</Sheet>,
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
await user.click(screen.getByText("Open"));
|
|
111
|
-
await screen.findByRole("dialog");
|
|
112
|
-
expect(screen.queryByRole("button", { name: "Close" })).not.toBeInTheDocument();
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("closes from the built-in close button", async () => {
|
|
116
|
-
const user = userEvent.setup();
|
|
117
|
-
render(<Example />);
|
|
118
|
-
await user.click(screen.getByRole("button", { name: "Open" }));
|
|
119
|
-
await screen.findByRole("dialog");
|
|
120
|
-
|
|
121
|
-
await user.click(screen.getByRole("button", { name: "Close" }));
|
|
122
|
-
await waitFor(() => {
|
|
123
|
-
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("lets a consumer className override the width", async () => {
|
|
128
|
-
const user = userEvent.setup();
|
|
129
|
-
render(
|
|
130
|
-
<Sheet>
|
|
131
|
-
<SheetTrigger>Open</SheetTrigger>
|
|
132
|
-
<SheetContent className="max-w-2xl">
|
|
133
|
-
<SheetTitle>Wide</SheetTitle>
|
|
134
|
-
</SheetContent>
|
|
135
|
-
</Sheet>,
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
await user.click(screen.getByText("Open"));
|
|
139
|
-
const dialog = await screen.findByRole("dialog");
|
|
140
|
-
expect(dialog).toHaveClass("max-w-2xl");
|
|
141
|
-
expect(dialog).not.toHaveClass("max-w-sm");
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("has no accessibility violations while open", async () => {
|
|
145
|
-
const user = userEvent.setup();
|
|
146
|
-
const { baseElement } = render(<Example />);
|
|
147
|
-
await user.click(screen.getByRole("button", { name: "Open" }));
|
|
148
|
-
await screen.findByRole("dialog");
|
|
149
|
-
|
|
150
|
-
await expectNoA11yViolations(baseElement);
|
|
151
|
-
});
|
|
152
|
-
});
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
|
|
3
|
-
import { Card, CardContent, CardHeader } from "@/components/card";
|
|
4
|
-
|
|
5
|
-
import { Skeleton } from "./skeleton";
|
|
6
|
-
|
|
7
|
-
const meta = {
|
|
8
|
-
title: "Feedback/Skeleton",
|
|
9
|
-
component: Skeleton,
|
|
10
|
-
parameters: { controls: { disable: true } },
|
|
11
|
-
} satisfies Meta<typeof Skeleton>;
|
|
12
|
-
|
|
13
|
-
export default meta;
|
|
14
|
-
type Story = StoryObj<typeof meta>;
|
|
15
|
-
|
|
16
|
-
export const Default: Story = {
|
|
17
|
-
render: () => <Skeleton className="h-4 w-48" />,
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export const Shapes: Story = {
|
|
21
|
-
render: () => (
|
|
22
|
-
<div className="flex items-center gap-4">
|
|
23
|
-
<Skeleton className="size-10 rounded-full" />
|
|
24
|
-
<div className="space-y-2">
|
|
25
|
-
<Skeleton className="h-4 w-40" />
|
|
26
|
-
<Skeleton className="h-4 w-24" />
|
|
27
|
-
</div>
|
|
28
|
-
</div>
|
|
29
|
-
),
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
/** aria-busy belongs on the region that owns the data, not on each placeholder. */
|
|
33
|
-
export const LoadingCard: Story = {
|
|
34
|
-
render: () => (
|
|
35
|
-
<Card className="w-80" aria-busy="true" aria-label="Loading project">
|
|
36
|
-
<CardHeader className="gap-3">
|
|
37
|
-
<Skeleton className="h-5 w-40" />
|
|
38
|
-
<Skeleton className="h-4 w-56" />
|
|
39
|
-
</CardHeader>
|
|
40
|
-
<CardContent className="space-y-2">
|
|
41
|
-
<Skeleton className="h-4 w-full" />
|
|
42
|
-
<Skeleton className="h-4 w-4/5" />
|
|
43
|
-
</CardContent>
|
|
44
|
-
</Card>
|
|
45
|
-
),
|
|
46
|
-
};
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { render } from "@testing-library/react";
|
|
2
|
-
import { renderToStaticMarkup } from "react-dom/server";
|
|
3
|
-
import { describe, expect, it } from "vitest";
|
|
4
|
-
|
|
5
|
-
import { expectNoA11yViolations } from "../../../test/a11y";
|
|
6
|
-
import { Skeleton } from "./skeleton";
|
|
7
|
-
|
|
8
|
-
describe("Skeleton", () => {
|
|
9
|
-
it("is hidden from assistive technology", () => {
|
|
10
|
-
const { container } = render(<Skeleton />);
|
|
11
|
-
expect(container.querySelector("[data-slot='skeleton']")).toHaveAttribute(
|
|
12
|
-
"aria-hidden",
|
|
13
|
-
"true",
|
|
14
|
-
);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("animates", () => {
|
|
18
|
-
const { container } = render(<Skeleton />);
|
|
19
|
-
expect(container.firstElementChild).toHaveClass("animate-pulse-soft");
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("takes its shape from the consumer className", () => {
|
|
23
|
-
const { container } = render(<Skeleton className="h-4 w-32 rounded-full" />);
|
|
24
|
-
const skeleton = container.firstElementChild;
|
|
25
|
-
expect(skeleton).toHaveClass("h-4", "w-32", "rounded-full");
|
|
26
|
-
expect(skeleton).not.toHaveClass("rounded-md");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("survives HTML parsing inside a paragraph", () => {
|
|
30
|
-
// A skeleton stands in for content, so it lands inside paragraphs, labels
|
|
31
|
-
// and headings — all of which accept phrasing content only. A `div` there
|
|
32
|
-
// is invalid, and the parser closes the paragraph early to correct it, so
|
|
33
|
-
// the server HTML and the client tree disagree and hydration fails.
|
|
34
|
-
//
|
|
35
|
-
// Rendering through the real parser is what makes this catchable: React
|
|
36
|
-
// builds the DOM with appendChild and never applies parsing rules, so
|
|
37
|
-
// `render` alone reproduces neither the correction nor the failure.
|
|
38
|
-
const host = document.createElement("div");
|
|
39
|
-
host.innerHTML = renderToStaticMarkup(
|
|
40
|
-
<p>
|
|
41
|
-
<Skeleton className="h-7 w-24" />
|
|
42
|
-
</p>,
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
const skeleton = host.querySelector("[data-slot='skeleton']");
|
|
46
|
-
expect(skeleton?.parentElement?.tagName).toBe("P");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("has no accessibility violations", async () => {
|
|
50
|
-
const { container } = render(
|
|
51
|
-
<div aria-busy="true">
|
|
52
|
-
<Skeleton className="h-4 w-32" />
|
|
53
|
-
</div>,
|
|
54
|
-
);
|
|
55
|
-
await expectNoA11yViolations(container);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import type { Decorator, Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
import { useState } from "react";
|
|
3
|
-
|
|
4
|
-
import { Label } from "@/components/label";
|
|
5
|
-
|
|
6
|
-
import { Slider } from "./slider";
|
|
7
|
-
|
|
8
|
-
/** Named so its type is nameable in declaration output (TS2883). */
|
|
9
|
-
const withFixedWidth: Decorator = (Story) => (
|
|
10
|
-
<div className="w-72">
|
|
11
|
-
<Story />
|
|
12
|
-
</div>
|
|
13
|
-
);
|
|
14
|
-
|
|
15
|
-
const meta = {
|
|
16
|
-
title: "Forms/Slider",
|
|
17
|
-
component: Slider,
|
|
18
|
-
args: { defaultValue: [40], min: 0, max: 100, step: 1, "aria-label": "Volume" },
|
|
19
|
-
argTypes: {
|
|
20
|
-
step: { control: { type: "number", min: 1, max: 25 } },
|
|
21
|
-
disabled: { control: "boolean" },
|
|
22
|
-
},
|
|
23
|
-
decorators: [withFixedWidth],
|
|
24
|
-
} satisfies Meta<typeof Slider>;
|
|
25
|
-
|
|
26
|
-
export default meta;
|
|
27
|
-
type Story = StoryObj<typeof meta>;
|
|
28
|
-
|
|
29
|
-
export const Default: Story = {};
|
|
30
|
-
|
|
31
|
-
/** A slider is imprecise; showing the value makes it usable. */
|
|
32
|
-
export const WithValue: Story = {
|
|
33
|
-
parameters: { controls: { disable: true } },
|
|
34
|
-
render: function WithValue() {
|
|
35
|
-
const [value, setValue] = useState([40]);
|
|
36
|
-
return (
|
|
37
|
-
<div className="grid gap-3">
|
|
38
|
-
<div className="flex items-center justify-between">
|
|
39
|
-
<Label htmlFor="quality">Quality</Label>
|
|
40
|
-
<span className="text-sm text-muted-foreground tabular-nums">{value[0]}%</span>
|
|
41
|
-
</div>
|
|
42
|
-
<Slider id="quality" aria-label="Quality" value={value} onValueChange={setValue} />
|
|
43
|
-
</div>
|
|
44
|
-
);
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
/** Each thumb is its own slider control, so each one gets its own name. */
|
|
49
|
-
export const Range: Story = {
|
|
50
|
-
parameters: { controls: { disable: true } },
|
|
51
|
-
render: function Range() {
|
|
52
|
-
const [range, setRange] = useState([20, 80]);
|
|
53
|
-
return (
|
|
54
|
-
<div className="grid gap-3">
|
|
55
|
-
<div className="flex items-center justify-between">
|
|
56
|
-
<Label>Price range</Label>
|
|
57
|
-
<span className="text-sm text-muted-foreground tabular-nums">
|
|
58
|
-
${range[0]} – ${range[1]}
|
|
59
|
-
</span>
|
|
60
|
-
</div>
|
|
61
|
-
<Slider
|
|
62
|
-
value={range}
|
|
63
|
-
onValueChange={setRange}
|
|
64
|
-
thumbLabels={["Minimum price", "Maximum price"]}
|
|
65
|
-
thumbValueTexts={[`$${String(range[0])}`, `$${String(range[1])}`]}
|
|
66
|
-
/>
|
|
67
|
-
</div>
|
|
68
|
-
);
|
|
69
|
-
},
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
export const Steps: Story = {
|
|
73
|
-
args: { defaultValue: [50], step: 10 },
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
export const Disabled: Story = {
|
|
77
|
-
args: { disabled: true },
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
export const Vertical: Story = {
|
|
81
|
-
parameters: { controls: { disable: true } },
|
|
82
|
-
render: () => (
|
|
83
|
-
<div className="flex h-44 justify-center">
|
|
84
|
-
<Slider orientation="vertical" defaultValue={[60]} aria-label="Volume" />
|
|
85
|
-
</div>
|
|
86
|
-
),
|
|
87
|
-
};
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { render, screen } 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 { Slider } from "./slider";
|
|
8
|
-
|
|
9
|
-
describe("Slider", () => {
|
|
10
|
-
it("renders a single thumb by default", () => {
|
|
11
|
-
render(<Slider aria-label="Volume" />);
|
|
12
|
-
expect(screen.getAllByRole("slider")).toHaveLength(1);
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("renders one thumb per value", () => {
|
|
16
|
-
render(<Slider aria-label="Price range" defaultValue={[20, 80]} />);
|
|
17
|
-
expect(screen.getAllByRole("slider")).toHaveLength(2);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("exposes its value and bounds", () => {
|
|
21
|
-
render(<Slider aria-label="Volume" defaultValue={[40]} min={0} max={100} />);
|
|
22
|
-
|
|
23
|
-
const thumb = screen.getByRole("slider");
|
|
24
|
-
expect(thumb).toHaveAttribute("aria-valuenow", "40");
|
|
25
|
-
expect(thumb).toHaveAttribute("aria-valuemin", "0");
|
|
26
|
-
expect(thumb).toHaveAttribute("aria-valuemax", "100");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("increments with the arrow keys", async () => {
|
|
30
|
-
const user = userEvent.setup();
|
|
31
|
-
render(<Slider aria-label="Volume" defaultValue={[40]} />);
|
|
32
|
-
|
|
33
|
-
await user.tab();
|
|
34
|
-
const thumb = screen.getByRole("slider");
|
|
35
|
-
expect(thumb).toHaveFocus();
|
|
36
|
-
|
|
37
|
-
await user.keyboard("{ArrowRight}");
|
|
38
|
-
expect(thumb).toHaveAttribute("aria-valuenow", "41");
|
|
39
|
-
|
|
40
|
-
await user.keyboard("{ArrowLeft}");
|
|
41
|
-
expect(thumb).toHaveAttribute("aria-valuenow", "40");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("jumps to the bounds with Home and End", async () => {
|
|
45
|
-
const user = userEvent.setup();
|
|
46
|
-
render(<Slider aria-label="Volume" defaultValue={[40]} min={10} max={90} />);
|
|
47
|
-
|
|
48
|
-
await user.tab();
|
|
49
|
-
await user.keyboard("{Home}");
|
|
50
|
-
expect(screen.getByRole("slider")).toHaveAttribute("aria-valuenow", "10");
|
|
51
|
-
|
|
52
|
-
await user.keyboard("{End}");
|
|
53
|
-
expect(screen.getByRole("slider")).toHaveAttribute("aria-valuenow", "90");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("honours the step", async () => {
|
|
57
|
-
const user = userEvent.setup();
|
|
58
|
-
render(<Slider aria-label="Volume" defaultValue={[40]} step={10} />);
|
|
59
|
-
|
|
60
|
-
await user.tab();
|
|
61
|
-
await user.keyboard("{ArrowRight}");
|
|
62
|
-
expect(screen.getByRole("slider")).toHaveAttribute("aria-valuenow", "50");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("reports changes", async () => {
|
|
66
|
-
const onValueChange = vi.fn();
|
|
67
|
-
const user = userEvent.setup();
|
|
68
|
-
render(<Slider aria-label="Volume" defaultValue={[40]} onValueChange={onValueChange} />);
|
|
69
|
-
|
|
70
|
-
await user.tab();
|
|
71
|
-
await user.keyboard("{ArrowRight}");
|
|
72
|
-
expect(onValueChange).toHaveBeenCalledWith([41]);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("works controlled", async () => {
|
|
76
|
-
const user = userEvent.setup();
|
|
77
|
-
|
|
78
|
-
function Controlled() {
|
|
79
|
-
const [value, setValue] = useState([30]);
|
|
80
|
-
return <Slider aria-label="Volume" value={value} onValueChange={setValue} />;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
render(<Controlled />);
|
|
84
|
-
await user.tab();
|
|
85
|
-
await user.keyboard("{ArrowRight}");
|
|
86
|
-
expect(screen.getByRole("slider")).toHaveAttribute("aria-valuenow", "31");
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("does not respond while disabled", async () => {
|
|
90
|
-
const onValueChange = vi.fn();
|
|
91
|
-
const user = userEvent.setup();
|
|
92
|
-
render(
|
|
93
|
-
<Slider aria-label="Volume" defaultValue={[40]} disabled onValueChange={onValueChange} />,
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
await user.tab();
|
|
97
|
-
await user.keyboard("{ArrowRight}");
|
|
98
|
-
expect(onValueChange).not.toHaveBeenCalled();
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("supports vertical orientation", () => {
|
|
102
|
-
render(<Slider aria-label="Volume" orientation="vertical" data-testid="slider" />);
|
|
103
|
-
expect(screen.getByTestId("slider")).toHaveAttribute("data-orientation", "vertical");
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("forwards aria-valuetext onto the thumb", () => {
|
|
107
|
-
render(
|
|
108
|
-
<Slider aria-label="Duration" defaultValue={[90]} aria-valuetext="1 minute 30 seconds" />,
|
|
109
|
-
);
|
|
110
|
-
expect(screen.getByRole("slider")).toHaveAttribute("aria-valuetext", "1 minute 30 seconds");
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("names each thumb of a range slider separately", () => {
|
|
114
|
-
render(
|
|
115
|
-
<Slider
|
|
116
|
-
defaultValue={[20, 80]}
|
|
117
|
-
thumbLabels={["Minimum price", "Maximum price"]}
|
|
118
|
-
thumbValueTexts={["$20", "$80"]}
|
|
119
|
-
/>,
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
const min = screen.getByRole("slider", { name: "Minimum price" });
|
|
123
|
-
const max = screen.getByRole("slider", { name: "Maximum price" });
|
|
124
|
-
expect(min).toHaveAttribute("aria-valuetext", "$20");
|
|
125
|
-
expect(max).toHaveAttribute("aria-valuetext", "$80");
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("warns in development when a thumb would be unnamed", () => {
|
|
129
|
-
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
130
|
-
render(<Slider defaultValue={[40]} />);
|
|
131
|
-
|
|
132
|
-
expect(warn).toHaveBeenCalledWith(expect.stringContaining("no accessible name"));
|
|
133
|
-
warn.mockRestore();
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("does not warn when every thumb is named", () => {
|
|
137
|
-
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
138
|
-
render(<Slider defaultValue={[20, 80]} thumbLabels={["Minimum", "Maximum"]} />);
|
|
139
|
-
|
|
140
|
-
expect(warn).not.toHaveBeenCalled();
|
|
141
|
-
warn.mockRestore();
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("has no accessibility violations", async () => {
|
|
145
|
-
const { container } = render(<Slider aria-label="Volume" defaultValue={[40]} />);
|
|
146
|
-
await expectNoA11yViolations(container);
|
|
147
|
-
});
|
|
148
|
-
});
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
|
|
3
|
-
import { Button } from "@/components/button";
|
|
4
|
-
|
|
5
|
-
import { Spinner } from "./spinner";
|
|
6
|
-
|
|
7
|
-
const meta = {
|
|
8
|
-
title: "Feedback/Spinner",
|
|
9
|
-
component: Spinner,
|
|
10
|
-
args: { size: "md" },
|
|
11
|
-
argTypes: { size: { control: "select", options: ["xs", "sm", "md", "lg", "xl"] } },
|
|
12
|
-
} satisfies Meta<typeof Spinner>;
|
|
13
|
-
|
|
14
|
-
export default meta;
|
|
15
|
-
type Story = StoryObj<typeof meta>;
|
|
16
|
-
|
|
17
|
-
export const Default: Story = {
|
|
18
|
-
args: { label: "Loading" },
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export const Sizes: Story = {
|
|
22
|
-
parameters: { controls: { disable: true } },
|
|
23
|
-
render: () => (
|
|
24
|
-
<div className="flex items-center gap-4">
|
|
25
|
-
{(["xs", "sm", "md", "lg", "xl"] as const).map((size) => (
|
|
26
|
-
<Spinner key={size} size={size} />
|
|
27
|
-
))}
|
|
28
|
-
</div>
|
|
29
|
-
),
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export const InheritsColour: Story = {
|
|
33
|
-
parameters: { controls: { disable: true } },
|
|
34
|
-
render: () => (
|
|
35
|
-
<div className="flex items-center gap-4">
|
|
36
|
-
<Spinner className="text-primary" />
|
|
37
|
-
<Spinner className="text-destructive" />
|
|
38
|
-
<Spinner className="text-muted-foreground" />
|
|
39
|
-
</div>
|
|
40
|
-
),
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
/** Inside a control that already reports its own busy state, omit `label`. */
|
|
44
|
-
export const InsideAButton: Story = {
|
|
45
|
-
parameters: { controls: { disable: true } },
|
|
46
|
-
render: () => <Button loading>Saving changes</Button>,
|
|
47
|
-
};
|
|
@@ -1,41 +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 { Spinner } from "./spinner";
|
|
6
|
-
|
|
7
|
-
describe("Spinner", () => {
|
|
8
|
-
it("is hidden from assistive technology by default", () => {
|
|
9
|
-
const { container } = render(<Spinner />);
|
|
10
|
-
expect(container.querySelector("svg")).toHaveAttribute("aria-hidden", "true");
|
|
11
|
-
expect(screen.queryByRole("status")).not.toBeInTheDocument();
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("announces itself when given a label", () => {
|
|
15
|
-
render(<Spinner label="Loading results" />);
|
|
16
|
-
expect(screen.getByRole("status")).toHaveTextContent("Loading results");
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it.each([
|
|
20
|
-
["xs", "size-3"],
|
|
21
|
-
["sm", "size-3.5"],
|
|
22
|
-
["md", "size-4"],
|
|
23
|
-
["lg", "size-5"],
|
|
24
|
-
["xl", "size-6"],
|
|
25
|
-
] as const)("applies the %s size", (size, expectedClass) => {
|
|
26
|
-
const { container } = render(<Spinner size={size} />);
|
|
27
|
-
expect(container.querySelector("svg")).toHaveClass(expectedClass);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("lets a consumer className override the size", () => {
|
|
31
|
-
const { container } = render(<Spinner className="size-10" />);
|
|
32
|
-
const svg = container.querySelector("svg");
|
|
33
|
-
expect(svg).toHaveClass("size-10");
|
|
34
|
-
expect(svg).not.toHaveClass("size-4");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("has no accessibility violations", async () => {
|
|
38
|
-
const { container } = render(<Spinner label="Loading" />);
|
|
39
|
-
await expectNoA11yViolations(container);
|
|
40
|
-
});
|
|
41
|
-
});
|