Skip to content

Commit 186d08f

Browse files
committed
feat(wip/FieldCheckboxGroup): CheckboxGroup, stories and small style adjustments
1 parent a85be28 commit 186d08f

File tree

8 files changed

+439
-24
lines changed

8 files changed

+439
-24
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { zodResolver } from '@hookform/resolvers/zod';
2+
import { Meta } from '@storybook/react-vite';
3+
import { useForm } from 'react-hook-form';
4+
import { z } from 'zod';
5+
6+
import { zu } from '@/lib/zod/zod-utils';
7+
8+
import { FormFieldController } from '@/components/form';
9+
import { onSubmit } from '@/components/form/docs.utils';
10+
import { FieldCheckboxGroup } from '@/components/form/field-checkbox-group';
11+
import { Button } from '@/components/ui/button';
12+
13+
import { Form, FormField, FormFieldHelper, FormFieldLabel } from '../';
14+
15+
export default {
16+
title: 'Form/FieldCheckboxGroup',
17+
component: FieldCheckboxGroup,
18+
} satisfies Meta<typeof FieldCheckboxGroup>;
19+
20+
const zFormSchema = () =>
21+
z.object({
22+
bears: zu.array.nonEmpty(
23+
z.string().array(),
24+
'Please select your favorite bearstronaut'
25+
),
26+
});
27+
28+
const formOptions = {
29+
mode: 'onBlur',
30+
resolver: zodResolver(zFormSchema()),
31+
defaultValues: {
32+
bears: [] as string[],
33+
},
34+
} as const;
35+
36+
const astrobears = [
37+
{ value: 'bearstrong', label: 'Bearstrong' },
38+
{ value: 'pawdrin', label: 'Buzz Pawdrin' },
39+
{ value: 'grizzlyrin', label: 'Yuri Grizzlyrin' },
40+
];
41+
42+
export const Default = () => {
43+
const form = useForm(formOptions);
44+
45+
return (
46+
<Form {...form} onSubmit={onSubmit}>
47+
<div className="flex flex-col gap-4">
48+
<FormField>
49+
<FormFieldLabel>Bearstronaut</FormFieldLabel>
50+
<FormFieldHelper>Select your favorite bearstronaut</FormFieldHelper>
51+
<FormFieldController
52+
type="checkbox-group"
53+
control={form.control}
54+
name="bears"
55+
options={astrobears}
56+
/>
57+
</FormField>
58+
<div>
59+
<Button type="submit">Submit</Button>
60+
</div>
61+
</div>
62+
</Form>
63+
);
64+
};
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { ComponentProps, ReactNode } 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, CheckboxProps } from '@/components/ui/checkbox';
10+
import { CheckboxGroup } from '@/components/ui/checkbox-group';
11+
12+
type CheckboxOption = Omit<CheckboxProps, 'children' | 'render'> & {
13+
label: ReactNode;
14+
};
15+
export type FieldCheckboxGroupProps<
16+
TFIeldValues extends FieldValues = FieldValues,
17+
TName extends FieldPath<TFIeldValues> = FieldPath<TFIeldValues>,
18+
> = FieldProps<
19+
TFIeldValues,
20+
TName,
21+
{
22+
type: 'checkbox-group';
23+
options: Array<CheckboxOption>;
24+
containerProps?: ComponentProps<'div'>;
25+
} & ComponentProps<typeof CheckboxGroup>
26+
>;
27+
28+
export const FieldCheckboxGroup = <
29+
TFIeldValues extends FieldValues = FieldValues,
30+
TName extends FieldPath<TFIeldValues> = FieldPath<TFIeldValues>,
31+
>(
32+
props: FieldCheckboxGroupProps<TFIeldValues, TName>
33+
) => {
34+
const {
35+
type,
36+
name,
37+
control,
38+
defaultValue,
39+
disabled,
40+
shouldUnregister,
41+
containerProps,
42+
options,
43+
...rest
44+
} = props;
45+
46+
const ctx = useFormField();
47+
48+
return (
49+
<Controller
50+
name={name}
51+
control={control}
52+
defaultValue={defaultValue}
53+
disabled={disabled}
54+
shouldUnregister={shouldUnregister}
55+
render={({ field: { value, onChange, ...field }, fieldState }) => {
56+
const isInvalid = fieldState.error ? true : undefined;
57+
return (
58+
<div
59+
{...containerProps}
60+
className={cn('', containerProps?.className)}
61+
>
62+
<CheckboxGroup
63+
id={ctx.id}
64+
aria-invalid={isInvalid}
65+
aria-labelledby={ctx.labelId}
66+
aria-describedby={
67+
!fieldState.error
68+
? `${ctx.descriptionId}`
69+
: `${ctx.descriptionId} ${ctx.errorId}`
70+
}
71+
value={value}
72+
onValueChange={onChange}
73+
{...rest}
74+
>
75+
{options.map(({ label, ...option }) => (
76+
<Checkbox
77+
key={option.value}
78+
{...option}
79+
aria-invalid={isInvalid}
80+
{...field}
81+
>
82+
{label}
83+
</Checkbox>
84+
))}
85+
</CheckboxGroup>
86+
<FormFieldError />
87+
</div>
88+
);
89+
}}
90+
/>
91+
);
92+
};

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export const FieldRadioGroup = <
5353
defaultValue={defaultValue}
5454
shouldUnregister={shouldUnregister}
5555
render={({ field: { onChange, value, ...field }, fieldState }) => {
56+
const isInvalid = fieldState.error ? true : undefined;
5657
return (
5758
<div
5859
{...containerProps}
@@ -63,7 +64,7 @@ export const FieldRadioGroup = <
6364
>
6465
<RadioGroup
6566
id={ctx.id}
66-
aria-invalid={fieldState.error ? true : undefined}
67+
aria-invalid={isInvalid}
6768
aria-labelledby={ctx.labelId}
6869
aria-describedby={
6970
!fieldState.error
@@ -82,6 +83,7 @@ export const FieldRadioGroup = <
8283
<React.Fragment key={radioId}>
8384
{renderOption({
8485
label,
86+
'aria-invalid': isInvalid,
8587
...field,
8688
...option,
8789
})}
@@ -90,7 +92,12 @@ export const FieldRadioGroup = <
9092
}
9193

9294
return (
93-
<Radio key={radioId} {...field} {...option}>
95+
<Radio
96+
key={radioId}
97+
aria-invalid={isInvalid}
98+
{...field}
99+
{...option}
100+
>
94101
{label}
95102
</Radio>
96103
);

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import {
1010
FieldCheckbox,
1111
FieldCheckboxProps,
1212
} from '@/components/form/field-checkbox';
13+
import {
14+
FieldCheckboxGroup,
15+
FieldCheckboxGroupProps,
16+
} from '@/components/form/field-checkbox-group';
1317
import { FieldNumber, FieldNumberProps } from '@/components/form/field-number';
1418

1519
import { FieldDate, FieldDateProps } from './field-date';
@@ -54,7 +58,8 @@ export type FormFieldControllerProps<
5458
| FieldTextProps<TFieldValues, TName>
5559
| FieldOtpProps<TFieldValues, TName>
5660
| FieldRadioGroupProps<TFieldValues, TName>
57-
| FieldCheckboxProps<TFieldValues, TName>;
61+
| FieldCheckboxProps<TFieldValues, TName>
62+
| FieldCheckboxGroupProps<TFieldValues, TName>;
5863

5964
export const FormFieldController = <
6065
TFieldValues extends FieldValues = FieldValues,
@@ -96,6 +101,9 @@ export const FormFieldController = <
96101

97102
case 'checkbox':
98103
return <FieldCheckbox {...props} />;
104+
105+
case 'checkbox-group':
106+
return <FieldCheckboxGroup {...props} />;
99107
// -- ADD NEW FIELD COMPONENT HERE --
100108
}
101109
};
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import { Meta } from '@storybook/react-vite';
2+
import { useState } from 'react';
3+
4+
import { Checkbox } from '@/components/ui/checkbox';
5+
import { CheckboxGroup } from '@/components/ui/checkbox-group';
6+
7+
export default {
8+
title: 'CheckboxGroup',
9+
component: CheckboxGroup,
10+
} satisfies Meta<typeof CheckboxGroup>;
11+
12+
const astrobears = [
13+
{ value: 'bearstrong', label: 'Bearstrong', disabled: false },
14+
{ value: 'pawdrin', label: 'Buzz Pawdrin', disabled: false },
15+
{ value: 'grizzlyrin', label: 'Yuri Grizzlyrin', disabled: true },
16+
] as const;
17+
18+
export const Default = () => {
19+
return (
20+
<CheckboxGroup>
21+
{astrobears.map((option) => (
22+
<Checkbox key={option.value} value={option.value}>
23+
{option.label}
24+
</Checkbox>
25+
))}
26+
</CheckboxGroup>
27+
);
28+
};
29+
30+
export const DefaultValue = () => {
31+
return (
32+
<CheckboxGroup defaultValue={['bearstrong']}>
33+
{astrobears.map((option) => (
34+
<Checkbox key={option.value} value={option.value}>
35+
{option.label}
36+
</Checkbox>
37+
))}
38+
</CheckboxGroup>
39+
);
40+
};
41+
42+
export const Disabled = () => {
43+
return (
44+
<CheckboxGroup disabled>
45+
{astrobears.map((option) => (
46+
<Checkbox key={option.value} value={option.value}>
47+
{option.label}
48+
</Checkbox>
49+
))}
50+
</CheckboxGroup>
51+
);
52+
};
53+
54+
export const DisabledOption = () => {
55+
return (
56+
<CheckboxGroup>
57+
{astrobears.map((option) => (
58+
<Checkbox
59+
key={option.value}
60+
value={option.value}
61+
disabled={option.disabled}
62+
>
63+
{option.label}
64+
</Checkbox>
65+
))}
66+
</CheckboxGroup>
67+
);
68+
};
69+
70+
const nestedBears = [
71+
{
72+
label: 'Bear 1',
73+
value: 'bear-1',
74+
children: null,
75+
},
76+
{
77+
label: 'Bear 2',
78+
value: 'bear-2',
79+
children: null,
80+
},
81+
{
82+
label: 'Bear 3',
83+
value: 'bear-3',
84+
children: [
85+
{
86+
label: 'Little bear 1',
87+
value: 'little-bear-1',
88+
},
89+
{
90+
label: 'Little bear 2',
91+
value: 'little-bear-2',
92+
},
93+
{
94+
label: 'Little bear 3',
95+
value: 'little-bear-3',
96+
},
97+
],
98+
},
99+
] as const;
100+
101+
const bears = nestedBears.map((bear) => bear.value);
102+
const littleBears = nestedBears[2].children.map((bear) => bear.value);
103+
export const WithNestedGroups = () => {
104+
const [bearsValue, setBearsValue] = useState<string[]>([]);
105+
const [littleBearsValue, setLittleBearsValue] = useState<string[]>([]);
106+
107+
return (
108+
<CheckboxGroup
109+
value={bearsValue}
110+
onValueChange={(value) => {
111+
if (value.includes('bear-3')) {
112+
setLittleBearsValue(littleBears);
113+
} else if (littleBearsValue.length === littleBears.length) {
114+
setLittleBearsValue([]);
115+
}
116+
setBearsValue(value);
117+
}}
118+
allValues={bears}
119+
defaultValue={[]}
120+
>
121+
<Checkbox parent>Astrobears</Checkbox>
122+
<div className="pl-4">
123+
{nestedBears.map((option) => {
124+
if (!option.children) {
125+
return (
126+
<Checkbox key={option.value} value={option.value}>
127+
{option.label}
128+
</Checkbox>
129+
);
130+
}
131+
132+
return (
133+
<CheckboxGroup
134+
key={option.value}
135+
value={littleBearsValue}
136+
onValueChange={(value) => {
137+
if (value.length === littleBears.length) {
138+
setBearsValue((prev) =>
139+
Array.from(new Set([...prev, 'bear-3']))
140+
);
141+
} else {
142+
setBearsValue((prev) => prev.filter((v) => v !== 'bear-3'));
143+
}
144+
setLittleBearsValue(value);
145+
}}
146+
allValues={option.children.map((bear) => bear.value)}
147+
defaultValue={[]}
148+
>
149+
<Checkbox parent>{option.label}</Checkbox>
150+
<div className="pl-4">
151+
{option.children.map((nestedOption) => {
152+
return (
153+
<Checkbox
154+
key={nestedOption.value}
155+
value={nestedOption.value}
156+
>
157+
{nestedOption.label}
158+
</Checkbox>
159+
);
160+
})}
161+
</div>
162+
</CheckboxGroup>
163+
);
164+
})}
165+
</div>
166+
</CheckboxGroup>
167+
);
168+
};

0 commit comments

Comments
 (0)