Skip to content

Commit cbaf95e

Browse files
committed
fix: radio sizz and examples
1 parent 23bc68f commit cbaf95e

File tree

4 files changed

+138
-86
lines changed

4 files changed

+138
-86
lines changed

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

Lines changed: 31 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +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 { Radio, RadioProps } from '@/components/ui/radio-group';
12+
import { Radio } from '@/components/ui/radio-group';
1313

1414
import { Form, FormField, FormFieldHelper, FormFieldLabel } from '../';
1515

@@ -177,49 +177,6 @@ export const WithDisabledOption = () => {
177177
};
178178

179179
export const WithCustomRadio = () => {
180-
// Let's say we have a custom radio component:
181-
// eslint-disable-next-line @eslint-react/no-nested-component-definitions
182-
const CardRadio = ({
183-
value,
184-
id,
185-
children,
186-
containerProps,
187-
...props
188-
}: RadioProps & { containerProps?: React.ComponentProps<'label'> }) => {
189-
return (
190-
<label
191-
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-checked]]:border-primary/90 has-[&[data-checked]]:bg-primary/5"
192-
{...containerProps}
193-
>
194-
<Radio
195-
value={value}
196-
id={id}
197-
noLabel
198-
render={(props, { checked }) => {
199-
return (
200-
<div
201-
{...props}
202-
className="flex w-full justify-between outline-none"
203-
>
204-
<div className="flex flex-col">
205-
<span className="font-medium">{children}</span>
206-
</div>
207-
<div
208-
className={cn('rounded-full bg-primary p-1 opacity-0', {
209-
'opacity-100': checked,
210-
})}
211-
>
212-
<CheckIcon className="h-4 w-4 text-primary-foreground" />
213-
</div>
214-
</div>
215-
);
216-
}}
217-
{...props}
218-
/>
219-
</label>
220-
);
221-
};
222-
223180
const form = useForm(formOptions);
224181

225182
return (
@@ -234,8 +191,36 @@ export const WithCustomRadio = () => {
234191
name="bear"
235192
options={options}
236193
renderOption={({ label, ...props }) => {
237-
// We can then customize the render of our field's radios
238-
return <CardRadio {...props}>{label}</CardRadio>;
194+
return (
195+
<Radio
196+
{...props}
197+
labelProps={{
198+
className:
199+
'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-[:checked]:border-transparent has-[:checked]:bg-primary has-[:checked]:text-primary-foreground',
200+
}}
201+
render={(props, { checked }) => {
202+
return (
203+
<button
204+
type="button"
205+
{...props}
206+
className="flex w-full justify-between outline-none"
207+
>
208+
<span className="font-medium">{label}</span>
209+
<span
210+
className={cn(
211+
'rounded-full bg-primary-foreground p-1 opacity-0',
212+
{
213+
'opacity-100': checked,
214+
}
215+
)}
216+
>
217+
<CheckIcon className="size-4 text-primary" />
218+
</span>
219+
</button>
220+
);
221+
}}
222+
/>
223+
);
239224
}}
240225
/>
241226
</FormField>

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export const FieldRadioGroup = <
4040
shouldUnregister,
4141
containerProps,
4242
options,
43+
size,
4344
renderOption,
4445
...rest
4546
} = props;
@@ -90,7 +91,7 @@ export const FieldRadioGroup = <
9091
}
9192

9293
return (
93-
<Radio key={radioId} {...field} {...option}>
94+
<Radio key={radioId} size={size} {...field} {...option}>
9495
{label}
9596
</Radio>
9697
);

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

Lines changed: 67 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,45 @@ export const Disabled = () => {
6161
);
6262
};
6363

64+
export const Sizes = () => {
65+
const radioGroupId = useId();
66+
return (
67+
<div className="flex flex-col gap-4">
68+
<RadioGroup defaultValue={astrobears[1].value}>
69+
{astrobears.map(({ value, label }) => {
70+
return (
71+
<Radio key={`${radioGroupId}-${value}`} value={value} size="sm">
72+
{label}
73+
</Radio>
74+
);
75+
})}
76+
</RadioGroup>
77+
<RadioGroup defaultValue={astrobears[1].value}>
78+
{astrobears.map(({ value, label }) => {
79+
return (
80+
<Radio
81+
key={`${radioGroupId}-${value}`}
82+
value={value}
83+
size="default"
84+
>
85+
{label}
86+
</Radio>
87+
);
88+
})}
89+
</RadioGroup>
90+
<RadioGroup defaultValue={astrobears[1].value}>
91+
{astrobears.map(({ value, label }) => {
92+
return (
93+
<Radio key={`${radioGroupId}-${value}`} value={value} size="lg">
94+
{label}
95+
</Radio>
96+
);
97+
})}
98+
</RadioGroup>
99+
</div>
100+
);
101+
};
102+
64103
export const Row = () => {
65104
const radioGroupId = useId();
66105
return (
@@ -77,40 +116,39 @@ export const Row = () => {
77116
};
78117

79118
export const WithCustomRadio = () => {
80-
const radioGroupId = useId();
81119
return (
82120
<RadioGroup>
83121
{astrobears.map(({ value, label }) => {
84-
const radioId = `${radioGroupId}-${value}`;
85122
return (
86-
<label
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-checked]]:border-primary/90 has-[&[data-checked]]:bg-primary/5"
88-
key={radioId}
89-
>
90-
<Radio
91-
value={value}
92-
noLabel
93-
render={(props, { checked }) => {
94-
return (
95-
<div
96-
{...props}
97-
className="flex w-full justify-between outline-none"
98-
>
99-
<div className="flex flex-col">
100-
<span className="font-medium">{label}</span>
101-
</div>
102-
<div
103-
className={cn('rounded-full bg-primary p-1 opacity-0', {
123+
<Radio
124+
key={value}
125+
value={value}
126+
labelProps={{
127+
className:
128+
'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-[:checked]:border-transparent has-[:checked]:bg-primary has-[:checked]:text-primary-foreground',
129+
}}
130+
render={(props, { checked }) => {
131+
return (
132+
<button
133+
type="button"
134+
{...props}
135+
className="flex w-full justify-between outline-none"
136+
>
137+
<span className="font-medium">{label}</span>
138+
<span
139+
className={cn(
140+
'rounded-full bg-primary-foreground p-1 opacity-0',
141+
{
104142
'opacity-100': checked,
105-
})}
106-
>
107-
<CheckIcon className="h-4 w-4 text-primary-foreground" />
108-
</div>
109-
</div>
110-
);
111-
}}
112-
/>
113-
</label>
143+
}
144+
)}
145+
>
146+
<CheckIcon className="size-4 text-primary" />
147+
</span>
148+
</button>
149+
);
150+
}}
151+
/>
114152
);
115153
})}
116154
</RadioGroup>

app/components/ui/radio-group.tsx

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,42 @@
11
import { Radio as RadioPrimitive } from '@base-ui-components/react/radio';
22
import { RadioGroup as RadioGroupPrimitive } from '@base-ui-components/react/radio-group';
3+
import { cva } from 'class-variance-authority';
34
import { Circle } from 'lucide-react';
45
import * as React from 'react';
56

67
import { cn } from '@/lib/tailwind/utils';
78

9+
const labelVariants = cva('flex items-center gap-2.5 text-primary', {
10+
variants: {
11+
size: {
12+
default: 'text-base',
13+
sm: 'gap-2 text-sm',
14+
lg: 'gap-3 text-lg',
15+
},
16+
},
17+
defaultVariants: {
18+
size: 'default',
19+
},
20+
});
21+
22+
const radioVariants = cva(
23+
'flex flex-none cursor-pointer items-center justify-center rounded-full outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-muted-foreground disabled:opacity-20 data-checked:bg-primary data-checked:text-primary-foreground data-indeterminate:border data-indeterminate:border-primary/20 data-unchecked:border data-unchecked:border-primary/20',
24+
{
25+
variants: {
26+
size: {
27+
default: 'size-5 [&_svg]:size-2.5',
28+
sm: 'size-4 [&_svg]:size-2',
29+
lg: 'size-6 [&_svg]:size-3',
30+
},
31+
},
32+
defaultVariants: {
33+
size: 'default',
34+
},
35+
}
36+
);
37+
838
export type RadioGroupProps = RadioGroupPrimitive.Props;
39+
940
export function RadioGroup({ className, ...rest }: RadioGroupProps) {
1041
return (
1142
<RadioGroupPrimitive
@@ -21,13 +52,15 @@ export type RadioProps = RadioPrimitive.Root.Props & {
2152
*/
2253
noLabel?: boolean;
2354
labelProps?: React.ComponentProps<'label'>;
55+
size?: 'default' | 'sm' | 'lg';
2456
};
2557

2658
export function Radio({
2759
children,
2860
className,
2961
noLabel,
3062
labelProps,
63+
size,
3164
...rest
3265
}: RadioProps) {
3366
const Comp = noLabel ? React.Fragment : 'label';
@@ -36,28 +69,23 @@ export function Radio({
3669
? {}
3770
: {
3871
...labelProps,
39-
className: cn('flex items-center gap-2 text-sm', labelProps?.className),
72+
className: cn(labelVariants({ size }), labelProps?.className),
4073
};
4174

4275
return (
4376
<Comp {...compProps}>
4477
<RadioPrimitive.Root
45-
className={cn(
46-
'peer size-4 cursor-pointer rounded-full border border-primary text-primary ring-offset-background',
47-
'focus:outline-none focus-visible:ring-[3px] focus-visible:ring-ring',
48-
'disabled:cursor-not-allowed disabled:opacity-50',
49-
className
50-
)}
78+
className={cn(radioVariants({ size }), className)}
5179
{...rest}
5280
>
5381
<RadioPrimitive.Indicator
5482
keepMounted={true}
5583
className={cn(
56-
'flex items-center justify-center transition-transform duration-150 ease-in-out',
57-
'data-checked:visible data-checked:scale-100 data-unchecked:invisible data-unchecked:scale-75'
84+
'flex transition-transform duration-150 ease-in-out',
85+
'data-checked:scale-100 data-unchecked:invisible data-unchecked:scale-75'
5886
)}
5987
>
60-
<Circle className="size-2.5 fill-current text-current" />
88+
<Circle className="fill-current" />
6189
</RadioPrimitive.Indicator>
6290
</RadioPrimitive.Root>
6391
{children}

0 commit comments

Comments
 (0)