Skip to content

Commit 2beb30b

Browse files
ntatoudivan-dalmet
andauthored
feat(v3): Checkbox, FieldCheckbox (#600)
* feat: single checkbox * feat: FieldCheckbox and stories + radio and checkbox improvements * test(FieldCheckbox): add usual tests * fix: typos * fix(Checkbox): disabled style --------- Co-authored-by: Ivan Dalmet <ivan-dalmet@users.noreply.github.com>
1 parent 658f9c9 commit 2beb30b

File tree

8 files changed

+566
-15
lines changed

8 files changed

+566
-15
lines changed
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { zodResolver } from '@hookform/resolvers/zod';
2+
import { Meta } from '@storybook/react-vite';
3+
import { CheckIcon } from 'lucide-react';
4+
import { useForm } from 'react-hook-form';
5+
import { z } from 'zod';
6+
7+
import { cn } from '@/lib/tailwind/utils';
8+
9+
import {
10+
Form,
11+
FormField,
12+
FormFieldController,
13+
FormFieldHelper,
14+
} from '@/components/form';
15+
import { onSubmit } from '@/components/form/docs.utils';
16+
import { FieldCheckbox } from '@/components/form/field-checkbox';
17+
import { Button } from '@/components/ui/button';
18+
19+
export default {
20+
title: 'Form/FieldCheckbox',
21+
component: FieldCheckbox,
22+
} satisfies Meta<typeof FieldCheckbox>;
23+
24+
const zFormSchema = () =>
25+
z.object({
26+
lovesBears: z.boolean().refine((val) => val === true, {
27+
message: 'Please say you love bears.',
28+
}),
29+
});
30+
31+
const formOptions = {
32+
mode: 'onBlur',
33+
resolver: zodResolver(zFormSchema()),
34+
defaultValues: {
35+
lovesBears: false,
36+
},
37+
} as const;
38+
39+
export const Default = () => {
40+
const form = useForm(formOptions);
41+
42+
return (
43+
<Form {...form} onSubmit={onSubmit}>
44+
<div className="flex flex-col gap-4">
45+
<FormField>
46+
<FormFieldController
47+
type="checkbox"
48+
control={form.control}
49+
name="lovesBears"
50+
>
51+
I love bears
52+
</FormFieldController>
53+
<FormFieldHelper>There is only one possible answer.</FormFieldHelper>
54+
</FormField>
55+
<div>
56+
<Button type="submit">Submit</Button>
57+
</div>
58+
</div>
59+
</Form>
60+
);
61+
};
62+
63+
export const DefaultValue = () => {
64+
const form = useForm({
65+
...formOptions,
66+
defaultValues: {
67+
lovesBears: true,
68+
},
69+
});
70+
71+
return (
72+
<Form {...form} onSubmit={onSubmit}>
73+
<div className="flex flex-col gap-4">
74+
<FormField>
75+
<FormFieldController
76+
type="checkbox"
77+
control={form.control}
78+
name="lovesBears"
79+
>
80+
I love bears
81+
</FormFieldController>
82+
<FormFieldHelper>There is only one possible answer.</FormFieldHelper>
83+
</FormField>
84+
<div>
85+
<Button type="submit">Submit</Button>
86+
</div>
87+
</div>
88+
</Form>
89+
);
90+
};
91+
92+
export const Disabled = () => {
93+
const form = useForm({
94+
...formOptions,
95+
defaultValues: {
96+
lovesBears: true,
97+
},
98+
});
99+
100+
return (
101+
<Form {...form} onSubmit={onSubmit}>
102+
<div className="flex flex-col gap-4">
103+
<FormField>
104+
<FormFieldController
105+
type="checkbox"
106+
control={form.control}
107+
name="lovesBears"
108+
disabled
109+
>
110+
I love bears
111+
</FormFieldController>
112+
<FormFieldHelper>There is only one possible answer.</FormFieldHelper>
113+
</FormField>
114+
<div>
115+
<Button type="submit">Submit</Button>
116+
</div>
117+
</div>
118+
</Form>
119+
);
120+
};
121+
122+
export const CustomCheckbox = () => {
123+
const form = useForm(formOptions);
124+
125+
return (
126+
<Form {...form} onSubmit={onSubmit}>
127+
<div className="flex flex-col gap-4">
128+
<FormField>
129+
<FormFieldController
130+
type="checkbox"
131+
name="lovesBears"
132+
control={form.control}
133+
labelProps={{
134+
className:
135+
'relative flex cursor-pointer items-center justify-between gap-4 rounded-lg border border-border p-4 transition-colors outline-none focus-within:ring-[3px] focus-within:ring-ring/50 hover:bg-muted/50 has-[&[data-checked]]:bg-primary/5',
136+
}}
137+
render={(props, { checked }) => {
138+
return (
139+
<div
140+
{...props}
141+
className="flex w-full items-center justify-between outline-none"
142+
>
143+
<div className="flex flex-col">
144+
<span className="font-medium">I love bears !</span>
145+
<FormFieldHelper>
146+
There is only one possible answer.
147+
</FormFieldHelper>
148+
</div>
149+
<div
150+
className={cn(
151+
'aspect-square rounded-full bg-primary p-1 opacity-0',
152+
{
153+
'opacity-100': checked,
154+
}
155+
)}
156+
>
157+
<CheckIcon className="h-4 w-4 text-primary-foreground" />
158+
</div>
159+
</div>
160+
);
161+
}}
162+
/>
163+
</FormField>
164+
<div>
165+
<Button type="submit">Submit</Button>
166+
</div>
167+
</div>
168+
</Form>
169+
);
170+
};
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { expect, test, vi } from 'vitest';
2+
import { axe } from 'vitest-axe';
3+
import { z } from 'zod';
4+
5+
import { render, screen, setupUser } from '@/tests/utils';
6+
7+
import { FormField, FormFieldController } from '..';
8+
import { FormMocked } from '../form-test-utils';
9+
10+
const zFormSchema = () =>
11+
z.object({
12+
lovesBears: z.boolean().refine((val) => val === true, {
13+
message: 'Please say you love bears.',
14+
}),
15+
});
16+
17+
test('should have no a11y violations', async () => {
18+
const mockedSubmit = vi.fn();
19+
20+
HTMLCanvasElement.prototype.getContext = vi.fn();
21+
22+
const { container } = render(
23+
<FormMocked
24+
schema={zFormSchema()}
25+
useFormOptions={{ defaultValues: { lovesBears: false } }}
26+
onSubmit={mockedSubmit}
27+
>
28+
{({ form }) => (
29+
<FormField>
30+
<FormFieldController
31+
type="checkbox"
32+
control={form.control}
33+
name="lovesBears"
34+
>
35+
I love bears
36+
</FormFieldController>
37+
</FormField>
38+
)}
39+
</FormMocked>
40+
);
41+
42+
const results = await axe(container);
43+
44+
expect(results).toHaveNoViolations();
45+
});
46+
47+
test('should select checkbox on button click', async () => {
48+
const user = setupUser();
49+
const mockedSubmit = vi.fn();
50+
51+
render(
52+
<FormMocked
53+
schema={zFormSchema()}
54+
useFormOptions={{ defaultValues: { lovesBears: false } }}
55+
onSubmit={mockedSubmit}
56+
>
57+
{({ form }) => (
58+
<FormField>
59+
<FormFieldController
60+
type="checkbox"
61+
control={form.control}
62+
name="lovesBears"
63+
>
64+
I love bears
65+
</FormFieldController>
66+
</FormField>
67+
)}
68+
</FormMocked>
69+
);
70+
71+
const checkbox = screen.getByRole('checkbox', { name: 'I love bears' });
72+
expect(checkbox).not.toBeChecked();
73+
74+
await user.click(checkbox);
75+
expect(checkbox).toBeChecked();
76+
77+
await user.click(screen.getByRole('button', { name: 'Submit' }));
78+
expect(mockedSubmit).toHaveBeenCalledWith({ lovesBears: true });
79+
});
80+
81+
test('should select checkbox on label click', async () => {
82+
const user = setupUser();
83+
const mockedSubmit = vi.fn();
84+
85+
render(
86+
<FormMocked
87+
schema={zFormSchema()}
88+
useFormOptions={{ defaultValues: { lovesBears: false } }}
89+
onSubmit={mockedSubmit}
90+
>
91+
{({ form }) => (
92+
<FormField>
93+
<FormFieldController
94+
type="checkbox"
95+
control={form.control}
96+
name="lovesBears"
97+
>
98+
I love bears
99+
</FormFieldController>
100+
</FormField>
101+
)}
102+
</FormMocked>
103+
);
104+
105+
const checkbox = screen.getByRole('checkbox', { name: 'I love bears' });
106+
const label = screen.getByText('I love bears');
107+
108+
expect(checkbox).not.toBeChecked();
109+
110+
// Test clicking the label specifically
111+
await user.click(label);
112+
expect(checkbox).toBeChecked();
113+
114+
await user.click(screen.getByRole('button', { name: 'Submit' }));
115+
expect(mockedSubmit).toHaveBeenCalledWith({ lovesBears: true });
116+
});
117+
118+
test('default value', async () => {
119+
const user = setupUser();
120+
const mockedSubmit = vi.fn();
121+
render(
122+
<FormMocked
123+
schema={zFormSchema()}
124+
useFormOptions={{ defaultValues: { lovesBears: true } }}
125+
onSubmit={mockedSubmit}
126+
>
127+
{({ form }) => (
128+
<FormField>
129+
<FormFieldController
130+
type="checkbox"
131+
control={form.control}
132+
name="lovesBears"
133+
>
134+
I love bears
135+
</FormFieldController>
136+
</FormField>
137+
)}
138+
</FormMocked>
139+
);
140+
141+
const checkbox = screen.getByRole('checkbox', { name: 'I love bears' });
142+
expect(checkbox).toBeChecked();
143+
144+
await user.click(screen.getByRole('button', { name: 'Submit' }));
145+
expect(mockedSubmit).toHaveBeenCalledWith({ lovesBears: true });
146+
});
147+
148+
test('disabled', async () => {
149+
const user = setupUser();
150+
const mockedSubmit = vi.fn();
151+
render(
152+
<FormMocked
153+
schema={z.object({ lovesBears: z.boolean() })}
154+
useFormOptions={{ defaultValues: { lovesBears: false } }}
155+
onSubmit={mockedSubmit}
156+
>
157+
{({ form }) => (
158+
<FormField>
159+
<FormFieldController
160+
type="checkbox"
161+
control={form.control}
162+
name="lovesBears"
163+
disabled
164+
>
165+
I love bears
166+
</FormFieldController>
167+
</FormField>
168+
)}
169+
</FormMocked>
170+
);
171+
172+
const checkbox = screen.getByRole('checkbox', { name: 'I love bears' });
173+
expect(checkbox).toBeDisabled();
174+
expect(checkbox).not.toBeChecked();
175+
176+
await user.click(checkbox);
177+
expect(checkbox).not.toBeChecked();
178+
await user.click(screen.getByRole('button', { name: 'Submit' }));
179+
expect(mockedSubmit).toHaveBeenCalledWith({ lovesBears: undefined });
180+
});

0 commit comments

Comments
 (0)