Skip to content

Commit bd601e9

Browse files
committed
feat: improve Radio and FieldRadio api
1 parent a67b69c commit bd601e9

File tree

4 files changed

+76
-32
lines changed

4 files changed

+76
-32
lines changed

app/components/form/field-radio-group/docs.stories.tsx

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { zodResolver } from '@hookform/resolvers/zod';
2-
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
2+
import { CheckIcon } from 'lucide-react';
33
import { useForm } from 'react-hook-form';
44
import { z } from 'zod';
55

@@ -9,6 +9,7 @@ import { zu } from '@/lib/zod/zod-utils';
99
import { FormFieldController } from '@/components/form';
1010
import { onSubmit } from '@/components/form/docs.utils';
1111
import { Button } from '@/components/ui/button';
12+
import { RadioItem } from '@/components/ui/radio-group';
1213

1314
import { Form, FormField, FormFieldHelper, FormFieldLabel } from '../';
1415

@@ -189,30 +190,31 @@ export const RenderOption = () => {
189190
type="radio-group"
190191
name="bear"
191192
options={options}
192-
renderRadio={(option, { field }) => {
193-
const _radioId = `radio-card-${option.value}`;
193+
renderRadio={({ radio, controller: { field } }) => {
194+
const radioId = `radio-card-${radio.value}`;
194195

195196
return (
196197
<label
197-
htmlFor={_radioId}
198+
htmlFor={radioId}
198199
className={cn(
199-
'relative flex cursor-pointer items-start gap-4 rounded-lg border p-4 transition-colors',
200-
'hover:bg-muted/50',
201-
'focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:outline-none',
202-
option.checked
200+
'relative flex cursor-pointer items-center justify-between gap-4 rounded-lg border p-4 transition-colors focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:outline-none hover:bg-muted/50',
201+
radio.checked
203202
? 'border-primary bg-primary/5'
204203
: 'border-border'
205204
)}
206205
>
207-
<RadioGroupPrimitive.Item
208-
id={_radioId}
209-
className="sr-only"
210-
value={option.value}
211-
disabled={option.disabled}
206+
<RadioItem
207+
id={radioId}
208+
className="peer sr-only"
209+
value={radio.value}
210+
disabled={radio.disabled}
212211
onBlur={field.onBlur}
213212
/>
214213
<div className="flex flex-col gap-1">
215-
<span className="font-medium">{option.label}</span>
214+
<span className="font-medium">{radio.label}</span>
215+
</div>
216+
<div className="rounded-full bg-primary p-1 opacity-0 peer-data-[state=checked]:opacity-100">
217+
<CheckIcon className="h-4 w-4 text-primary-foreground" />
216218
</div>
217219
</label>
218220
);

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ export type FieldRadioGroupProps<
3030
> = FieldProps<TFieldValues, TName> & {
3131
type: 'radio-group';
3232
options: Array<RadioOptionProps>;
33-
renderRadio?: (
34-
radioOptions: RadioOptionProps & { checked: boolean },
35-
fieldOptions: Parameters<
33+
renderRadio?: (options: {
34+
radio: RadioOptionProps & { checked: boolean };
35+
controller: Parameters<
3636
Pick<ControllerProps<TFieldValues, TName>, 'render'>['render']
37-
>[0]
38-
) => ReactNode;
37+
>[0];
38+
}) => ReactNode;
3939
containerProps?: ComponentProps<'div'>;
4040
} & RemoveFromType<
4141
Omit<
@@ -110,13 +110,13 @@ export const FieldRadioGroup = <
110110
{ui
111111
.match('render-radio', ({ renderRadio }) =>
112112
options.map((option) =>
113-
renderRadio(
114-
{
113+
renderRadio({
114+
radio: {
115115
...option,
116116
checked: option.value === field.value,
117117
},
118-
controllerRenderOptions
119-
)
118+
controller: controllerRenderOptions,
119+
})
120120
)
121121
)
122122
.match('default', () =>

app/components/ui/radio-group.stories.tsx

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { Meta } from '@storybook/react';
2+
import { CheckIcon } from 'lucide-react';
3+
import { useId } from 'react';
24

3-
import { Radio, RadioGroup } from '@/components/ui/radio-group';
5+
import { Radio, RadioGroup, RadioItem } from '@/components/ui/radio-group';
46

57
export default {
68
title: 'RadioGroup',
@@ -13,11 +15,12 @@ const astrobears = [
1315
] as const;
1416

1517
export const Default = () => {
18+
const radioGroupId = useId();
1619
return (
1720
<RadioGroup>
1821
{astrobears.map(({ value, label }) => {
1922
return (
20-
<Radio key={value} value={value} id={value}>
23+
<Radio key={`${radioGroupId}-${value}`} value={value}>
2124
{label}
2225
</Radio>
2326
);
@@ -27,11 +30,12 @@ export const Default = () => {
2730
};
2831

2932
export const DefaultValue = () => {
33+
const radioGroupId = useId();
3034
return (
3135
<RadioGroup defaultValue={astrobears[1].value}>
3236
{astrobears.map(({ value, label }) => {
3337
return (
34-
<Radio key={value} value={value} id={value}>
38+
<Radio key={`${radioGroupId}-${value}`} value={value}>
3539
{label}
3640
</Radio>
3741
);
@@ -41,11 +45,12 @@ export const DefaultValue = () => {
4145
};
4246

4347
export const Disabled = () => {
48+
const radioGroupId = useId();
4449
return (
4550
<RadioGroup defaultValue={astrobears[1].value} disabled>
4651
{astrobears.map(({ value, label }) => {
4752
return (
48-
<Radio key={value} value={value} id={value}>
53+
<Radio key={`${radioGroupId}-${value}`} value={value}>
4954
{label}
5055
</Radio>
5156
);
@@ -55,15 +60,42 @@ export const Disabled = () => {
5560
};
5661

5762
export const Row = () => {
63+
const radioGroupId = useId();
5864
return (
5965
<RadioGroup className="flex-row gap-4">
6066
{astrobears.map(({ value, label }) => {
6167
return (
62-
<Radio key={value} value={value} id={value}>
68+
<Radio key={`${radioGroupId}-${value}`} value={value}>
6369
{label}
6470
</Radio>
6571
);
6672
})}
6773
</RadioGroup>
6874
);
6975
};
76+
77+
export const WithCustomRadio = () => {
78+
const radioGroupId = useId();
79+
return (
80+
<RadioGroup>
81+
{astrobears.map(({ value, label }) => {
82+
const radioId = `${radioGroupId}-${value}`;
83+
return (
84+
<label
85+
key={radioId}
86+
htmlFor={radioId}
87+
className="relative flex cursor-pointer items-center justify-between gap-4 rounded-lg border border-border p-4 transition-colors focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:outline-none hover:bg-muted/50 has-[&[data-state=checked]]:border-primary has-[&[data-state=checked]]:bg-primary/5"
88+
>
89+
<RadioItem id={radioId} className="peer sr-only" value={value} />
90+
<div className="flex flex-col gap-1">
91+
<span className="font-medium">{label}</span>
92+
</div>
93+
<div className="rounded-full bg-primary p-1 opacity-0 peer-data-[state=checked]:opacity-100">
94+
<CheckIcon className="h-4 w-4 text-primary-foreground" />
95+
</div>
96+
</label>
97+
);
98+
})}
99+
</RadioGroup>
100+
);
101+
};

app/components/ui/radio-group.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ function Radio({
2626
className,
2727
value,
2828
children,
29+
id,
2930
...props
3031
}: RadioProps) {
31-
const _radioId = React.useId();
32+
const _id = React.useId();
33+
const radioId = id ?? _id;
3234

3335
return (
3436
<div
@@ -40,7 +42,7 @@ function Radio({
4042
'peer aspect-square h-4 w-4 cursor-pointer rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
4143
className
4244
)}
43-
id={_radioId}
45+
id={radioId}
4446
value={value}
4547
{...props}
4648
>
@@ -49,7 +51,7 @@ function Radio({
4951
</RadioGroupPrimitive.Indicator>
5052
</RadioGroupPrimitive.Item>
5153
<label
52-
htmlFor={_radioId}
54+
htmlFor={radioId}
5355
{...labelProps}
5456
className={cn(
5557
'cursor-pointer text-xs peer-disabled:cursor-not-allowed',
@@ -61,5 +63,13 @@ function Radio({
6163
</div>
6264
);
6365
}
64-
export { Radio, RadioGroup };
66+
67+
/**
68+
* Component holding the radio button.
69+
*
70+
* @see https://www.radix-ui.com/primitives/docs/components/radio-group
71+
*/
72+
const RadioItem = RadioGroupPrimitive.Item;
73+
74+
export { Radio, RadioGroup, RadioItem };
6575
export type { RadioGroupProps, RadioProps };

0 commit comments

Comments
 (0)