@yuno-payments/dashboard-design-system 0.0.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/.storybook/main.ts +20 -0
- package/.storybook/preview.ts +18 -0
- package/.storybook/vitest.setup.ts +7 -0
- package/README.md +69 -0
- package/components.json +21 -0
- package/eslint.config.js +26 -0
- package/index.html +13 -0
- package/package.json +57 -0
- package/public/vite.svg +1 -0
- package/src/App.css +42 -0
- package/src/App.tsx +11 -0
- package/src/assets/react.svg +1 -0
- package/src/components/atoms/button/button.stories.tsx +222 -0
- package/src/components/atoms/button/button.test.tsx +78 -0
- package/src/components/atoms/button/index.tsx +80 -0
- package/src/components/atoms/checkbox/checkbox.stories.tsx +314 -0
- package/src/components/atoms/checkbox/checkbox.test.tsx +278 -0
- package/src/components/atoms/checkbox/index.tsx +103 -0
- package/src/components/atoms/chip/chip.stories.tsx +317 -0
- package/src/components/atoms/chip/chip.test.tsx +300 -0
- package/src/components/atoms/chip/index.tsx +114 -0
- package/src/components/atoms/input/index.tsx +27 -0
- package/src/components/atoms/link/index.tsx +79 -0
- package/src/components/atoms/link/link.stories.tsx +159 -0
- package/src/components/atoms/link/link.test.tsx +176 -0
- package/src/components/atoms/radiobutton/index.tsx +103 -0
- package/src/components/atoms/radiobutton/radiobutton.stories.tsx +314 -0
- package/src/components/atoms/radiobutton/radiobutton.test.tsx +245 -0
- package/src/components/atoms/tag/index.tsx +196 -0
- package/src/components/atoms/tag/tag.stories.tsx +281 -0
- package/src/components/atoms/tag/tag.test.tsx +282 -0
- package/src/components/atoms/typography/index.tsx +62 -0
- package/src/components/atoms/typography/typography.stories.tsx +214 -0
- package/src/components/atoms/typography/typography.test.tsx +187 -0
- package/src/components/index.tsx +17 -0
- package/src/components/molecules/announcement/announcement.stories.tsx +277 -0
- package/src/components/molecules/announcement/announcement.test.tsx +354 -0
- package/src/components/molecules/announcement/index.tsx +200 -0
- package/src/components/molecules/notification-alert/index.tsx +293 -0
- package/src/components/molecules/notification-alert/notification-alert.stories.tsx +418 -0
- package/src/components/molecules/notification-alert/notification-alert.test.tsx +454 -0
- package/src/components/molecules/popover/index.tsx +175 -0
- package/src/components/molecules/popover/popover.stories.tsx +241 -0
- package/src/components/molecules/popover/popover.test.tsx +191 -0
- package/src/components/molecules/textfield/index.tsx +154 -0
- package/src/components/molecules/textfield/textfield.stories.tsx +168 -0
- package/src/components/molecules/textfield/textfield.test.tsx +157 -0
- package/src/components/molecules/tooltip/index.tsx +263 -0
- package/src/components/molecules/tooltip/tooltip.stories.tsx +363 -0
- package/src/components/molecules/tooltip/tooltip.test.tsx +468 -0
- package/src/components/organisms/dialog/dialog.stories.tsx +522 -0
- package/src/components/organisms/dialog/dialog.test.tsx +525 -0
- package/src/components/organisms/dialog/index.tsx +233 -0
- package/src/components/organisms/dropdown/dropdown.stories.tsx +529 -0
- package/src/components/organisms/dropdown/dropdown.test.tsx +390 -0
- package/src/components/organisms/dropdown/index.tsx +624 -0
- package/src/index.css +184 -0
- package/src/lib/color-utils.ts +94 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +10 -0
- package/src/stories/Colors.stories.tsx +107 -0
- package/src/stories/Shadows.stories.tsx +110 -0
- package/src/stories/Spacing.stories.tsx +121 -0
- package/src/stories/Typography.stories.tsx +197 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.app.json +33 -0
- package/tsconfig.json +13 -0
- package/tsconfig.node.json +25 -0
- package/vite.config.ts +43 -0
- package/vitest.config.ts +15 -0
- package/vitest.shims.d.ts +1 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default:
|
|
13
|
+
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
|
14
|
+
destructive:
|
|
15
|
+
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
16
|
+
outline:
|
|
17
|
+
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
|
18
|
+
secondary:
|
|
19
|
+
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
|
20
|
+
ghost:
|
|
21
|
+
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
22
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
23
|
+
},
|
|
24
|
+
size: {
|
|
25
|
+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
26
|
+
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
27
|
+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
28
|
+
icon: "size-9",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
defaultVariants: {
|
|
32
|
+
variant: "default",
|
|
33
|
+
size: "default",
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
export interface ButtonProps
|
|
39
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
40
|
+
VariantProps<typeof buttonVariants> {
|
|
41
|
+
asChild?: boolean
|
|
42
|
+
label?: React.ReactNode
|
|
43
|
+
startIcon?: React.ReactNode
|
|
44
|
+
endIcon?: React.ReactNode
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
48
|
+
(
|
|
49
|
+
{
|
|
50
|
+
className,
|
|
51
|
+
variant,
|
|
52
|
+
size,
|
|
53
|
+
asChild = false,
|
|
54
|
+
label,
|
|
55
|
+
startIcon,
|
|
56
|
+
endIcon,
|
|
57
|
+
children,
|
|
58
|
+
...props
|
|
59
|
+
},
|
|
60
|
+
ref
|
|
61
|
+
) => {
|
|
62
|
+
const Comp = asChild ? Slot : "button"
|
|
63
|
+
return (
|
|
64
|
+
<Comp
|
|
65
|
+
ref={ref}
|
|
66
|
+
data-slot="button"
|
|
67
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
68
|
+
{...props}
|
|
69
|
+
>
|
|
70
|
+
<>
|
|
71
|
+
{startIcon ? <span className="inline-flex">{startIcon}</span> : null}
|
|
72
|
+
<span>{children ?? label}</span>
|
|
73
|
+
{endIcon ? <span className="inline-flex">{endIcon}</span> : null}
|
|
74
|
+
</>
|
|
75
|
+
</Comp>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
Button.displayName = "Button"
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/* eslint-disable storybook/no-renderer-packages */
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react'
|
|
3
|
+
import { Checkbox } from './index'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Checkbox> = {
|
|
6
|
+
title: 'Atoms/Checkbox',
|
|
7
|
+
component: Checkbox,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'centered',
|
|
10
|
+
},
|
|
11
|
+
tags: ['autodocs'],
|
|
12
|
+
argTypes: {
|
|
13
|
+
size: {
|
|
14
|
+
control: 'select',
|
|
15
|
+
options: ['sm', 'md', 'lg'],
|
|
16
|
+
description: 'The size of the checkbox',
|
|
17
|
+
},
|
|
18
|
+
checked: {
|
|
19
|
+
control: 'boolean',
|
|
20
|
+
description: 'Whether the checkbox is checked',
|
|
21
|
+
},
|
|
22
|
+
disabled: {
|
|
23
|
+
control: 'boolean',
|
|
24
|
+
description: 'Whether the checkbox is disabled',
|
|
25
|
+
},
|
|
26
|
+
invalid: {
|
|
27
|
+
control: 'boolean',
|
|
28
|
+
description: 'Whether the checkbox is in an invalid state',
|
|
29
|
+
},
|
|
30
|
+
name: {
|
|
31
|
+
control: 'text',
|
|
32
|
+
description: 'The name attribute for the checkbox',
|
|
33
|
+
},
|
|
34
|
+
value: {
|
|
35
|
+
control: 'text',
|
|
36
|
+
description: 'The value attribute for the checkbox',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default meta
|
|
42
|
+
type Story = StoryObj<typeof meta>
|
|
43
|
+
|
|
44
|
+
export const Basic: Story = {
|
|
45
|
+
args: {
|
|
46
|
+
size: 'md',
|
|
47
|
+
checked: false,
|
|
48
|
+
disabled: false,
|
|
49
|
+
invalid: false,
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const Checked: Story = {
|
|
54
|
+
args: {
|
|
55
|
+
size: 'md',
|
|
56
|
+
checked: true,
|
|
57
|
+
disabled: false,
|
|
58
|
+
invalid: false,
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const Unchecked: Story = {
|
|
63
|
+
args: {
|
|
64
|
+
size: 'md',
|
|
65
|
+
checked: false,
|
|
66
|
+
disabled: false,
|
|
67
|
+
invalid: false,
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const Disabled: Story = {
|
|
72
|
+
args: {
|
|
73
|
+
size: 'md',
|
|
74
|
+
checked: false,
|
|
75
|
+
disabled: true,
|
|
76
|
+
invalid: false,
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const DisabledChecked: Story = {
|
|
81
|
+
args: {
|
|
82
|
+
size: 'md',
|
|
83
|
+
checked: true,
|
|
84
|
+
disabled: true,
|
|
85
|
+
invalid: false,
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const Invalid: Story = {
|
|
90
|
+
args: {
|
|
91
|
+
size: 'md',
|
|
92
|
+
checked: false,
|
|
93
|
+
disabled: false,
|
|
94
|
+
invalid: true,
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const InvalidChecked: Story = {
|
|
99
|
+
args: {
|
|
100
|
+
size: 'md',
|
|
101
|
+
checked: true,
|
|
102
|
+
disabled: false,
|
|
103
|
+
invalid: true,
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const AllSizes: Story = {
|
|
108
|
+
render: () => (
|
|
109
|
+
<div className="flex items-center gap-4">
|
|
110
|
+
<div className="flex flex-col items-center gap-2">
|
|
111
|
+
<Checkbox size="sm" checked={false} />
|
|
112
|
+
<span className="text-xs text-muted-foreground">Small</span>
|
|
113
|
+
</div>
|
|
114
|
+
<div className="flex flex-col items-center gap-2">
|
|
115
|
+
<Checkbox size="md" checked={false} />
|
|
116
|
+
<span className="text-xs text-muted-foreground">Medium</span>
|
|
117
|
+
</div>
|
|
118
|
+
<div className="flex flex-col items-center gap-2">
|
|
119
|
+
<Checkbox size="lg" checked={false} />
|
|
120
|
+
<span className="text-xs text-muted-foreground">Large</span>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
),
|
|
124
|
+
parameters: {
|
|
125
|
+
docs: {
|
|
126
|
+
description: {
|
|
127
|
+
story: 'Checkbox component in different sizes: small (14px), medium (16px), and large (20px).',
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const CheckboxGroup: Story = {
|
|
134
|
+
render: () => (
|
|
135
|
+
<div className="space-y-3">
|
|
136
|
+
<div className="flex items-center gap-2">
|
|
137
|
+
<Checkbox name="options" value="option1" checked={true} />
|
|
138
|
+
<label className="text-sm">Option 1 (Selected)</label>
|
|
139
|
+
</div>
|
|
140
|
+
<div className="flex items-center gap-2">
|
|
141
|
+
<Checkbox name="options" value="option2" checked={false} />
|
|
142
|
+
<label className="text-sm">Option 2</label>
|
|
143
|
+
</div>
|
|
144
|
+
<div className="flex items-center gap-2">
|
|
145
|
+
<Checkbox name="options" value="option3" checked={true} />
|
|
146
|
+
<label className="text-sm">Option 3 (Selected)</label>
|
|
147
|
+
</div>
|
|
148
|
+
<div className="flex items-center gap-2">
|
|
149
|
+
<Checkbox name="options" value="option4" checked={false} disabled />
|
|
150
|
+
<label className="text-sm text-muted-foreground">Option 4 (Disabled)</label>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
),
|
|
154
|
+
parameters: {
|
|
155
|
+
docs: {
|
|
156
|
+
description: {
|
|
157
|
+
story: 'Example of checkboxes used in a group with labels.',
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const AllStates: Story = {
|
|
164
|
+
render: () => (
|
|
165
|
+
<div className="space-y-4">
|
|
166
|
+
<div>
|
|
167
|
+
<h3 className="text-sm font-medium mb-2">Normal States</h3>
|
|
168
|
+
<div className="flex items-center gap-4">
|
|
169
|
+
<div className="flex items-center gap-2">
|
|
170
|
+
<Checkbox checked={false} />
|
|
171
|
+
<span className="text-sm">Unchecked</span>
|
|
172
|
+
</div>
|
|
173
|
+
<div className="flex items-center gap-2">
|
|
174
|
+
<Checkbox checked={true} />
|
|
175
|
+
<span className="text-sm">Checked</span>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div>
|
|
181
|
+
<h3 className="text-sm font-medium mb-2">Disabled States</h3>
|
|
182
|
+
<div className="flex items-center gap-4">
|
|
183
|
+
<div className="flex items-center gap-2">
|
|
184
|
+
<Checkbox checked={false} disabled />
|
|
185
|
+
<span className="text-sm text-muted-foreground">Disabled Unchecked</span>
|
|
186
|
+
</div>
|
|
187
|
+
<div className="flex items-center gap-2">
|
|
188
|
+
<Checkbox checked={true} disabled />
|
|
189
|
+
<span className="text-sm text-muted-foreground">Disabled Checked</span>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<div>
|
|
195
|
+
<h3 className="text-sm font-medium mb-2">Invalid States</h3>
|
|
196
|
+
<div className="flex items-center gap-4">
|
|
197
|
+
<div className="flex items-center gap-2">
|
|
198
|
+
<Checkbox checked={false} invalid />
|
|
199
|
+
<span className="text-sm">Invalid Unchecked</span>
|
|
200
|
+
</div>
|
|
201
|
+
<div className="flex items-center gap-2">
|
|
202
|
+
<Checkbox checked={true} invalid />
|
|
203
|
+
<span className="text-sm">Invalid Checked</span>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
),
|
|
209
|
+
parameters: {
|
|
210
|
+
docs: {
|
|
211
|
+
description: {
|
|
212
|
+
story: 'Comprehensive overview of all checkbox states including normal, disabled, and invalid variations.',
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export const Docs: Story = {
|
|
219
|
+
render: () => (
|
|
220
|
+
<div className="space-y-6">
|
|
221
|
+
<div>
|
|
222
|
+
<h2 className="text-lg font-semibold mb-3">Checkbox Component</h2>
|
|
223
|
+
<p className="text-sm text-muted-foreground mb-4">
|
|
224
|
+
Checkboxes allow users to select one or more options from a set. The Checkbox component
|
|
225
|
+
provides a custom-styled checkbox input that integrates seamlessly with Shadcn design
|
|
226
|
+
tokens and supports various sizes, states, and accessibility features.
|
|
227
|
+
</p>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<div>
|
|
231
|
+
<h3 className="text-base font-medium mb-2">Sizes</h3>
|
|
232
|
+
<div className="space-y-2">
|
|
233
|
+
<div className="flex items-center gap-2">
|
|
234
|
+
<Checkbox size="sm" checked={false} />
|
|
235
|
+
<span className="text-sm text-muted-foreground">Small (14px) - Compact layouts</span>
|
|
236
|
+
</div>
|
|
237
|
+
<div className="flex items-center gap-2">
|
|
238
|
+
<Checkbox size="md" checked={false} />
|
|
239
|
+
<span className="text-sm text-muted-foreground">Medium (16px) - Default size</span>
|
|
240
|
+
</div>
|
|
241
|
+
<div className="flex items-center gap-2">
|
|
242
|
+
<Checkbox size="lg" checked={false} />
|
|
243
|
+
<span className="text-sm text-muted-foreground">Large (20px) - Prominent selections</span>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<div>
|
|
249
|
+
<h3 className="text-base font-medium mb-2">States</h3>
|
|
250
|
+
<div className="grid grid-cols-2 gap-2">
|
|
251
|
+
<div className="flex items-center gap-2">
|
|
252
|
+
<Checkbox checked={false} />
|
|
253
|
+
<span className="text-xs text-muted-foreground">Unchecked</span>
|
|
254
|
+
</div>
|
|
255
|
+
<div className="flex items-center gap-2">
|
|
256
|
+
<Checkbox checked={true} />
|
|
257
|
+
<span className="text-xs text-muted-foreground">Checked</span>
|
|
258
|
+
</div>
|
|
259
|
+
<div className="flex items-center gap-2">
|
|
260
|
+
<Checkbox checked={false} disabled />
|
|
261
|
+
<span className="text-xs text-muted-foreground">Disabled</span>
|
|
262
|
+
</div>
|
|
263
|
+
<div className="flex items-center gap-2">
|
|
264
|
+
<Checkbox checked={false} invalid />
|
|
265
|
+
<span className="text-xs text-muted-foreground">Invalid</span>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<div>
|
|
271
|
+
<h3 className="text-base font-medium mb-2">Usage</h3>
|
|
272
|
+
<div className="space-y-2">
|
|
273
|
+
<p className="text-sm text-muted-foreground">
|
|
274
|
+
• Use checkboxes when users can select multiple options from a list
|
|
275
|
+
</p>
|
|
276
|
+
<p className="text-sm text-muted-foreground">
|
|
277
|
+
• For single selection, use radio buttons instead
|
|
278
|
+
</p>
|
|
279
|
+
<p className="text-sm text-muted-foreground">
|
|
280
|
+
• Always provide clear labels for accessibility
|
|
281
|
+
</p>
|
|
282
|
+
<p className="text-sm text-muted-foreground">
|
|
283
|
+
• Use the `invalid` prop to indicate validation errors
|
|
284
|
+
</p>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
<div>
|
|
289
|
+
<h3 className="text-base font-medium mb-2">Accessibility</h3>
|
|
290
|
+
<div className="space-y-2">
|
|
291
|
+
<p className="text-sm text-muted-foreground">
|
|
292
|
+
• Supports keyboard navigation (Tab, Space)
|
|
293
|
+
</p>
|
|
294
|
+
<p className="text-sm text-muted-foreground">
|
|
295
|
+
• Proper ARIA attributes for screen readers
|
|
296
|
+
</p>
|
|
297
|
+
<p className="text-sm text-muted-foreground">
|
|
298
|
+
• Focus indicators for keyboard users
|
|
299
|
+
</p>
|
|
300
|
+
<p className="text-sm text-muted-foreground">
|
|
301
|
+
• Semantic HTML input element for form integration
|
|
302
|
+
</p>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
),
|
|
307
|
+
parameters: {
|
|
308
|
+
docs: {
|
|
309
|
+
description: {
|
|
310
|
+
story: 'Complete documentation and usage guidelines for the Checkbox component.',
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { render } from '@testing-library/react'
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
3
|
+
import '@testing-library/jest-dom'
|
|
4
|
+
import { Checkbox } from './index'
|
|
5
|
+
|
|
6
|
+
function renderComponent(props = {}) {
|
|
7
|
+
const result = render(<Checkbox {...props} />)
|
|
8
|
+
const container = result.container
|
|
9
|
+
const root = container.firstChild as HTMLElement
|
|
10
|
+
return { container, root }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getByTestId(container: HTMLElement, testId: string) {
|
|
14
|
+
const el = container.querySelector(`[data-testid="${testId}"]`)
|
|
15
|
+
if (!el) throw new Error(`Element with data-testid="${testId}" not found`)
|
|
16
|
+
return el as HTMLElement
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('Checkbox', () => {
|
|
20
|
+
it('renders correctly', () => {
|
|
21
|
+
const { container } = renderComponent()
|
|
22
|
+
const checkbox = getByTestId(container, 'checkbox')
|
|
23
|
+
const input = getByTestId(container, 'checkbox-input')
|
|
24
|
+
|
|
25
|
+
expect(checkbox).toBeInTheDocument()
|
|
26
|
+
expect(input).toBeInTheDocument()
|
|
27
|
+
expect(input).toHaveAttribute('type', 'checkbox')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('renders with default size', () => {
|
|
31
|
+
const { container } = renderComponent()
|
|
32
|
+
const checkbox = getByTestId(container, 'checkbox')
|
|
33
|
+
|
|
34
|
+
expect(checkbox).toHaveClass('size-4')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('renders with small size', () => {
|
|
38
|
+
const { container } = renderComponent({ size: 'sm' })
|
|
39
|
+
const checkbox = getByTestId(container, 'checkbox')
|
|
40
|
+
|
|
41
|
+
expect(checkbox).toHaveClass('size-3.5')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('renders with medium size', () => {
|
|
45
|
+
const { container } = renderComponent({ size: 'md' })
|
|
46
|
+
const checkbox = getByTestId(container, 'checkbox')
|
|
47
|
+
|
|
48
|
+
expect(checkbox).toHaveClass('size-4')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('renders with large size', () => {
|
|
52
|
+
const { container } = renderComponent({ size: 'lg' })
|
|
53
|
+
const checkbox = getByTestId(container, 'checkbox')
|
|
54
|
+
|
|
55
|
+
expect(checkbox).toHaveClass('size-5')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('renders unchecked state', () => {
|
|
59
|
+
const { container } = renderComponent({ checked: false })
|
|
60
|
+
const input = getByTestId(container, 'checkbox-input')
|
|
61
|
+
|
|
62
|
+
expect(input).not.toBeChecked()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('renders checked state', () => {
|
|
66
|
+
const { container } = renderComponent({ checked: true })
|
|
67
|
+
const input = getByTestId(container, 'checkbox-input')
|
|
68
|
+
const checkbox = getByTestId(container, 'checkbox')
|
|
69
|
+
|
|
70
|
+
expect(input).toBeChecked()
|
|
71
|
+
expect(checkbox).toHaveClass('border-primary', 'bg-primary', 'text-primary-foreground')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('renders disabled state', () => {
|
|
75
|
+
const { container } = renderComponent({ disabled: true })
|
|
76
|
+
const input = getByTestId(container, 'checkbox-input')
|
|
77
|
+
const checkbox = getByTestId(container, 'checkbox')
|
|
78
|
+
|
|
79
|
+
expect(input).toBeDisabled()
|
|
80
|
+
expect(checkbox).toHaveClass('pointer-events-none', 'opacity-50')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('renders invalid state', () => {
|
|
84
|
+
const { container } = renderComponent({ invalid: true })
|
|
85
|
+
const checkbox = getByTestId(container, 'checkbox')
|
|
86
|
+
|
|
87
|
+
expect(checkbox).toHaveClass('border-destructive')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('renders invalid and checked state', () => {
|
|
91
|
+
const { container } = renderComponent({ invalid: true, checked: true })
|
|
92
|
+
const checkbox = getByTestId(container, 'checkbox')
|
|
93
|
+
|
|
94
|
+
expect(checkbox).toHaveClass('border-destructive')
|
|
95
|
+
expect(checkbox).toHaveClass('bg-destructive', 'text-destructive-foreground')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('forwards ref correctly', () => {
|
|
99
|
+
const ref = vi.fn()
|
|
100
|
+
renderComponent({ ref })
|
|
101
|
+
|
|
102
|
+
expect(ref).toHaveBeenCalled()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('applies custom className', () => {
|
|
106
|
+
const { container } = renderComponent({ className: 'custom-class' })
|
|
107
|
+
const checkbox = getByTestId(container, 'checkbox')
|
|
108
|
+
|
|
109
|
+
expect(checkbox).toHaveClass('custom-class')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('passes through HTML input attributes', () => {
|
|
113
|
+
const { container } = renderComponent({
|
|
114
|
+
name: 'test-checkbox',
|
|
115
|
+
value: 'test-value',
|
|
116
|
+
'data-custom': 'test'
|
|
117
|
+
})
|
|
118
|
+
const input = getByTestId(container, 'checkbox-input')
|
|
119
|
+
|
|
120
|
+
expect(input).toHaveAttribute('name', 'test-checkbox')
|
|
121
|
+
expect(input).toHaveAttribute('value', 'test-value')
|
|
122
|
+
expect(input).toHaveAttribute('data-custom', 'test')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('handles onChange event', () => {
|
|
126
|
+
const handleChange = vi.fn()
|
|
127
|
+
const { container } = renderComponent({ onChange: handleChange })
|
|
128
|
+
const input = getByTestId(container, 'checkbox-input')
|
|
129
|
+
|
|
130
|
+
input.click()
|
|
131
|
+
expect(handleChange).toHaveBeenCalled()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('does not trigger onChange when disabled', () => {
|
|
135
|
+
const handleChange = vi.fn()
|
|
136
|
+
const { container } = renderComponent({ onChange: handleChange, disabled: true })
|
|
137
|
+
const input = getByTestId(container, 'checkbox-input')
|
|
138
|
+
|
|
139
|
+
input.click()
|
|
140
|
+
expect(handleChange).not.toHaveBeenCalled()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('renders Square icon when unchecked', () => {
|
|
144
|
+
const { container } = renderComponent({ checked: false })
|
|
145
|
+
const checkbox = getByTestId(container, 'checkbox')
|
|
146
|
+
const squareIcon = checkbox.querySelector('svg')
|
|
147
|
+
|
|
148
|
+
expect(squareIcon).toBeInTheDocument()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('renders CheckSquare icon when checked', () => {
|
|
152
|
+
const { container } = renderComponent({ checked: true })
|
|
153
|
+
const checkbox = getByTestId(container, 'checkbox')
|
|
154
|
+
const checkSquareIcon = checkbox.querySelector('svg')
|
|
155
|
+
|
|
156
|
+
expect(checkSquareIcon).toBeInTheDocument()
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('applies correct icon size for small variant', () => {
|
|
160
|
+
const { container } = renderComponent({ size: 'sm' })
|
|
161
|
+
const checkbox = getByTestId(container, 'checkbox')
|
|
162
|
+
const icon = checkbox.querySelector('svg')
|
|
163
|
+
|
|
164
|
+
expect(icon).toHaveClass('size-3.5')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('applies correct icon size for medium variant', () => {
|
|
168
|
+
const { container } = renderComponent({ size: 'md' })
|
|
169
|
+
const checkbox = getByTestId(container, 'checkbox')
|
|
170
|
+
const icon = checkbox.querySelector('svg')
|
|
171
|
+
|
|
172
|
+
expect(icon).toHaveClass('size-4')
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('applies correct icon size for large variant', () => {
|
|
176
|
+
const { container } = renderComponent({ size: 'lg' })
|
|
177
|
+
const checkbox = getByTestId(container, 'checkbox')
|
|
178
|
+
const icon = checkbox.querySelector('svg')
|
|
179
|
+
|
|
180
|
+
expect(icon).toHaveClass('size-5')
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('applies destructive color to icon when invalid and unchecked', () => {
|
|
184
|
+
const { container } = renderComponent({ invalid: true, checked: false })
|
|
185
|
+
const checkbox = getByTestId(container, 'checkbox')
|
|
186
|
+
const icon = checkbox.querySelector('svg')
|
|
187
|
+
|
|
188
|
+
expect(icon).toHaveClass('text-destructive')
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('applies muted foreground color to icon when normal and unchecked', () => {
|
|
192
|
+
const { container } = renderComponent({ invalid: false, checked: false })
|
|
193
|
+
const checkbox = getByTestId(container, 'checkbox')
|
|
194
|
+
const icon = checkbox.querySelector('svg')
|
|
195
|
+
|
|
196
|
+
expect(icon).toHaveClass('text-muted-foreground')
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('applies primary foreground color to icon when checked', () => {
|
|
200
|
+
const { container } = renderComponent({ checked: true })
|
|
201
|
+
const checkbox = getByTestId(container, 'checkbox')
|
|
202
|
+
const icon = checkbox.querySelector('svg')
|
|
203
|
+
|
|
204
|
+
expect(icon).toHaveClass('text-primary-foreground')
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('applies destructive foreground color to icon when invalid and checked', () => {
|
|
208
|
+
const { container } = renderComponent({ invalid: true, checked: true })
|
|
209
|
+
const checkbox = getByTestId(container, 'checkbox')
|
|
210
|
+
const icon = checkbox.querySelector('svg')
|
|
211
|
+
|
|
212
|
+
expect(icon).toHaveClass('text-destructive-foreground')
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('has proper accessibility attributes', () => {
|
|
216
|
+
const { container } = renderComponent()
|
|
217
|
+
const input = getByTestId(container, 'checkbox-input')
|
|
218
|
+
|
|
219
|
+
expect(input).toHaveAttribute('type', 'checkbox')
|
|
220
|
+
expect(input).toHaveClass('sr-only')
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('supports focus states', () => {
|
|
224
|
+
const { container } = renderComponent()
|
|
225
|
+
const checkbox = getByTestId(container, 'checkbox')
|
|
226
|
+
|
|
227
|
+
expect(checkbox).toHaveClass('focus-visible:border-ring', 'focus-visible:ring-ring/50', 'focus-visible:ring-[3px]')
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('combines multiple states correctly', () => {
|
|
231
|
+
const { container } = renderComponent({
|
|
232
|
+
size: 'lg',
|
|
233
|
+
checked: true,
|
|
234
|
+
invalid: true,
|
|
235
|
+
className: 'custom-class'
|
|
236
|
+
})
|
|
237
|
+
const checkbox = getByTestId(container, 'checkbox')
|
|
238
|
+
const input = getByTestId(container, 'checkbox-input')
|
|
239
|
+
|
|
240
|
+
expect(checkbox).toHaveClass('size-5')
|
|
241
|
+
expect(checkbox).toHaveClass('border-destructive', 'bg-destructive', 'text-destructive-foreground')
|
|
242
|
+
expect(checkbox).toHaveClass('custom-class')
|
|
243
|
+
expect(input).toBeChecked()
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('renders with indeterminate state when checked is undefined', () => {
|
|
247
|
+
const { container } = renderComponent({ checked: undefined })
|
|
248
|
+
const input = getByTestId(container, 'checkbox-input') as HTMLInputElement
|
|
249
|
+
|
|
250
|
+
expect(input.checked).toBe(false)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('applies correct styling for all size variants', () => {
|
|
254
|
+
const sizes = ['sm', 'md', 'lg'] as const
|
|
255
|
+
const expectedClasses = ['size-3.5', 'size-4', 'size-5']
|
|
256
|
+
|
|
257
|
+
sizes.forEach((size, index) => {
|
|
258
|
+
const { container } = renderComponent({ size })
|
|
259
|
+
const checkbox = getByTestId(container, 'checkbox')
|
|
260
|
+
|
|
261
|
+
expect(checkbox).toHaveClass(expectedClasses[index])
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('maintains proper border styling across states', () => {
|
|
266
|
+
const { container: normalContainer } = renderComponent({ checked: false })
|
|
267
|
+
const normalCheckbox = getByTestId(normalContainer, 'checkbox')
|
|
268
|
+
expect(normalCheckbox).toHaveClass('border-input')
|
|
269
|
+
|
|
270
|
+
const { container: checkedContainer } = renderComponent({ checked: true })
|
|
271
|
+
const checkedCheckbox = getByTestId(checkedContainer, 'checkbox')
|
|
272
|
+
expect(checkedCheckbox).toHaveClass('border-primary')
|
|
273
|
+
|
|
274
|
+
const { container: invalidContainer } = renderComponent({ invalid: true })
|
|
275
|
+
const invalidCheckbox = getByTestId(invalidContainer, 'checkbox')
|
|
276
|
+
expect(invalidCheckbox).toHaveClass('border-destructive')
|
|
277
|
+
})
|
|
278
|
+
})
|