Skip to content

Commit ecbe5ec

Browse files
committed
feat: FieldCheckbox and stories + radio and checkbox improvements
1 parent 4e19271 commit ecbe5ec

File tree

8 files changed

+292
-21
lines changed

8 files changed

+292
-21
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/FieldCheckboxGroup',
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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { test } from 'vitest';
2+
3+
test('should have no a11y violations', () => {});
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { ComponentProps } from 'react';
2+
import { Controller, FieldPath, FieldValues } from 'react-hook-form';
3+
4+
import { cn } from '@/lib/tailwind/utils';
5+
6+
import { FormFieldError } from '@/components/form';
7+
import { useFormField } from '@/components/form/form-field';
8+
import { FieldProps } from '@/components/form/form-field-controller';
9+
import { Checkbox } from '@/components/ui/checkbox';
10+
11+
export type FieldCheckboxProps<
12+
TFieldValues extends FieldValues = FieldValues,
13+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
14+
> = FieldProps<
15+
TFieldValues,
16+
TName,
17+
{
18+
type: 'checkbox';
19+
containerProps?: ComponentProps<'div'>;
20+
} & ComponentProps<typeof Checkbox>
21+
>;
22+
23+
export const FieldCheckbox = <
24+
TFieldValues extends FieldValues = FieldValues,
25+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
26+
>(
27+
props: FieldCheckboxProps<TFieldValues, TName>
28+
) => {
29+
const {
30+
name,
31+
control,
32+
disabled,
33+
defaultValue,
34+
type,
35+
shouldUnregister,
36+
containerProps,
37+
...rest
38+
} = props;
39+
const ctx = useFormField();
40+
return (
41+
<Controller
42+
name={name}
43+
control={control}
44+
disabled={disabled}
45+
defaultValue={defaultValue}
46+
shouldUnregister={shouldUnregister}
47+
render={({ field: { onChange, value, ...field }, fieldState }) => {
48+
return (
49+
<div
50+
{...containerProps}
51+
className={cn(
52+
'flex flex-1 flex-col gap-1',
53+
containerProps?.className
54+
)}
55+
>
56+
<Checkbox
57+
id={ctx.id}
58+
aria-invalid={fieldState.error ? true : undefined}
59+
aria-describedby={
60+
!fieldState.error
61+
? `${ctx.descriptionId}`
62+
: `${ctx.descriptionId} ${ctx.errorId}`
63+
}
64+
checked={value}
65+
onCheckedChange={onChange}
66+
{...rest}
67+
{...field}
68+
/>
69+
<FormFieldError />
70+
</div>
71+
);
72+
}}
73+
/>
74+
);
75+
};

app/components/form/field-radio-group/index.tsx

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,7 @@ import { cn } from '@/lib/tailwind/utils';
66
import { FormFieldError } from '@/components/form';
77
import { useFormField } from '@/components/form/form-field';
88
import { FieldProps } from '@/components/form/form-field-controller';
9-
import {
10-
Radio,
11-
RadioGroup,
12-
RadioGroupProps,
13-
RadioProps,
14-
} from '@/components/ui/radio-group';
9+
import { Radio, RadioGroup, RadioProps } from '@/components/ui/radio-group';
1510

1611
type RadioOption = Omit<RadioProps, 'children' | 'render'> & {
1712
label: string;
@@ -20,13 +15,16 @@ type RadioOption = Omit<RadioProps, 'children' | 'render'> & {
2015
export type FieldRadioGroupProps<
2116
TFieldValues extends FieldValues = FieldValues,
2217
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
23-
> = FieldProps<TFieldValues, TName> & {
24-
type: 'radio-group';
25-
options: Array<RadioOption>;
26-
renderOption?: (props: RadioOption) => React.JSX.Element;
27-
} & Omit<RadioGroupProps, 'id' | 'aria-invalid' | 'aria-describedby'> & {
18+
> = FieldProps<
19+
TFieldValues,
20+
TName,
21+
{
22+
type: 'radio-group';
23+
options: Array<RadioOption>;
24+
renderOption?: (props: RadioOption) => React.JSX.Element;
2825
containerProps?: React.ComponentProps<'div'>;
29-
};
26+
} & React.ComponentProps<typeof RadioGroup>
27+
>;
3028

3129
export const FieldRadioGroup = <
3230
TFieldValues extends FieldValues = FieldValues,

app/components/form/form-field-controller.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import {
66
FieldValues,
77
} from 'react-hook-form';
88

9+
import {
10+
FieldCheckbox,
11+
FieldCheckboxProps,
12+
} from '@/components/form/field-checkbox';
913
import { FieldNumber, FieldNumberProps } from '@/components/form/field-number';
1014

1115
import { FieldDate, FieldDateProps } from './field-date';
@@ -49,7 +53,8 @@ export type FormFieldControllerProps<
4953
| FieldDateProps<TFieldValues, TName>
5054
| FieldTextProps<TFieldValues, TName>
5155
| FieldOtpProps<TFieldValues, TName>
52-
| FieldRadioGroupProps<TFieldValues, TName>;
56+
| FieldRadioGroupProps<TFieldValues, TName>
57+
| FieldCheckboxProps<TFieldValues, TName>;
5358

5459
export const FormFieldController = <
5560
TFieldValues extends FieldValues = FieldValues,
@@ -88,6 +93,9 @@ export const FormFieldController = <
8893

8994
case 'radio-group':
9095
return <FieldRadioGroup {...props} />;
96+
97+
case 'checkbox':
98+
return <FieldCheckbox {...props} />;
9199
// -- ADD NEW FIELD COMPONENT HERE --
92100
}
93101
};

app/components/ui/checkbox.stories.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,7 @@ export const CustomCheckbox = () => {
3333
className="flex w-full justify-between outline-none"
3434
>
3535
<div className="flex flex-col">
36-
<span className="font-medium">
37-
I love bear (and customization) !{' '}
38-
</span>
36+
<span className="font-medium">I love bears</span>
3937
</div>
4038
<div
4139
className={cn('rounded-full bg-primary p-1 opacity-0', {

app/components/ui/checkbox.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,38 @@ import React from 'react';
44

55
import { cn } from '@/lib/tailwind/utils';
66

7-
export type CheckboxProps = CheckboxPrimitive.Root.Props & {
7+
export type CheckboxProps = Omit<CheckboxPrimitive.Root.Props, 'type'> & {
8+
/**
9+
* By default, the radio is wrapped in a `<label>`. Set to `false` if you do not want it.
10+
*/
811
noLabel?: boolean;
12+
labelProps?: React.ComponentProps<'label'>;
913
};
1014

1115
export function Checkbox({
1216
children,
1317
className,
1418
noLabel,
19+
labelProps,
1520
...props
1621
}: CheckboxProps) {
1722
const Comp = noLabel ? React.Fragment : 'label';
1823

1924
const compProps = noLabel
2025
? {}
2126
: {
22-
className: 'flex items-center gap-2 text-base text-primary',
27+
...labelProps,
28+
className: cn(
29+
'flex items-center gap-2 text-base text-primary',
30+
labelProps?.className
31+
),
2332
};
2433

2534
return (
2635
<Comp {...compProps}>
2736
<CheckboxPrimitive.Root
2837
className={cn(
29-
'flex size-5 items-center justify-center rounded-sm outline-none',
38+
'flex size-5 cursor-pointer items-center justify-center rounded-sm outline-none',
3039
'focus-visible:ring-[3px] focus-visible:ring-ring/50',
3140
'data-checked:bg-primary data-unchecked:border data-unchecked:border-primary/50',
3241
className

app/components/ui/radio-group.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,25 @@ export type RadioProps = RadioPrimitive.Root.Props & {
2020
* By default, the radio is wrapped in a `<label>`. Set to `false` if you do not want it.
2121
*/
2222
noLabel?: boolean;
23+
labelProps?: React.ComponentProps<'label'>;
2324
};
24-
export function Radio({ children, className, noLabel, ...rest }: RadioProps) {
25+
26+
export function Radio({
27+
children,
28+
className,
29+
noLabel,
30+
labelProps,
31+
...rest
32+
}: RadioProps) {
2533
const Comp = noLabel ? React.Fragment : 'label';
2634

2735
const compProps = noLabel
2836
? {}
2937
: {
30-
className: 'flex items-center gap-2 text-sm',
38+
...labelProps,
39+
className: cn('flex items-center gap-2 text-sm', labelProps?.className),
3140
};
41+
3242
return (
3343
<Comp {...compProps}>
3444
<RadioPrimitive.Root

0 commit comments

Comments
 (0)