Skip to content

Commit ecd616f

Browse files
authored
radio themed (#1205)
1 parent fd4ec12 commit ecd616f

File tree

14 files changed

+752
-495
lines changed

14 files changed

+752
-495
lines changed

src/components/ui/Radio/Radio.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
'use client';
2+
import React from 'react';
3+
import RadioPrimitive, { RadioPrimitiveProps } from '~/core/primitives/Radio';
4+
5+
import clsx from 'clsx';
6+
import { customClassSwitcher } from '~/core';
7+
8+
import { useCreateDataAttribute, useComposeAttributes, useCreateDataAccentColorAttribute } from '~/core/hooks/createDataAttribute';
9+
10+
const COMPONENT_NAME = 'Radio';
11+
12+
export type RadioProps = RadioPrimitiveProps & {
13+
customRootClass?: string;
14+
className?: string;
15+
size?: string;
16+
color?: string;
17+
variant?: string;
18+
};
19+
20+
function Radio({ name, value, id, checked = false, required, onChange, disabled, asChild, className, customRootClass, variant = '', size = '', color = '', ...props }: RadioProps) {
21+
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
22+
const [isChecked, setIsChecked] = React.useState(checked);
23+
24+
const dataAttributes = useCreateDataAttribute('button', { variant, size });
25+
const accentAttributes = useCreateDataAccentColorAttribute(color);
26+
const composedAttributes = useComposeAttributes(dataAttributes(), accentAttributes());
27+
28+
const handleChange = () => {
29+
if (onChange) {
30+
onChange();
31+
}
32+
setIsChecked(!isChecked);
33+
};
34+
return (
35+
<RadioPrimitive
36+
name={name}
37+
id={id}
38+
value={value}
39+
checked={isChecked}
40+
required={required}
41+
onChange={handleChange}
42+
disabled={disabled}
43+
asChild={asChild}
44+
className={clsx(rootClass, className)}
45+
data-checked={isChecked}
46+
{...composedAttributes()}
47+
{...props}
48+
/>
49+
50+
);
51+
}
52+
53+
export default Radio;
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import React from 'react';
2+
import Radio from '../Radio';
3+
import SandboxEditor from '~/components/tools/SandboxEditor/SandboxEditor';
4+
5+
export default {
6+
title: 'WIP/Radio',
7+
component: Radio,
8+
render: (args: React.ComponentProps<typeof Radio>) => (
9+
<SandboxEditor>
10+
<form>
11+
<Radio {...args} />
12+
<label htmlFor={args.id || 'radio'} style={{ marginLeft: 8 }}>
13+
Radio 1
14+
</label>
15+
</form>
16+
</SandboxEditor>
17+
)
18+
};
19+
20+
export const Default = {
21+
args: {
22+
name: 'radio',
23+
value: 'radio',
24+
id: 'radio',
25+
required: true,
26+
onChange: () => {
27+
// action handler
28+
}
29+
}
30+
};
31+
32+
export const Disabled = {
33+
args: {
34+
disabled: true,
35+
name: 'radio',
36+
value: 'radio1',
37+
id: 'radio1',
38+
onChange: () => {
39+
// action handler
40+
}
41+
}
42+
};
43+
44+
export const Variants = {
45+
render: (args: React.ComponentProps<typeof Radio>) => (
46+
<SandboxEditor>
47+
<div style={{ display: 'flex', gap: 16 }}>
48+
<div>
49+
<Radio {...args} variant="outline" id="radio-outline" />
50+
<label htmlFor="radio-outline" style={{ marginLeft: 8 }}>Outline</label>
51+
</div>
52+
<div>
53+
<Radio {...args} variant="solid" id="radio-solid" />
54+
<label htmlFor="radio-solid" style={{ marginLeft: 8 }}>Solid</label>
55+
</div>
56+
</div>
57+
</SandboxEditor>
58+
),
59+
args: {
60+
name: 'radio-variant',
61+
value: 'radio',
62+
required: false,
63+
onChange: () => {}
64+
}
65+
};
66+
67+
export const Sizes = {
68+
render: (args: React.ComponentProps<typeof Radio>) => (
69+
<SandboxEditor>
70+
<div style={{ display: 'flex', gap: 16 }}>
71+
<div>
72+
<Radio {...args} size="small" id="radio-small" />
73+
<label htmlFor="radio-small" style={{ marginLeft: 8 }}>Small</label>
74+
</div>
75+
<div>
76+
<Radio {...args} size="medium" id="radio-medium" />
77+
<label htmlFor="radio-medium" style={{ marginLeft: 8 }}>Medium</label>
78+
</div>
79+
<div>
80+
<Radio {...args} size="large" id="radio-large" />
81+
<label htmlFor="radio-large" style={{ marginLeft: 8 }}>Large</label>
82+
</div>
83+
</div>
84+
</SandboxEditor>
85+
),
86+
args: {
87+
name: 'radio-size',
88+
value: 'radio',
89+
required: false,
90+
onChange: () => {}
91+
}
92+
};
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import React from 'react';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
import Radio from '../Radio';
4+
5+
describe('Radio', () => {
6+
const baseProps = {
7+
name: 'test-radio',
8+
value: 'option1',
9+
id: 'radio1'
10+
};
11+
12+
it('renders with required props', () => {
13+
render(<Radio {...baseProps} />);
14+
const radio = screen.getByRole('radio');
15+
expect(radio).toBeInTheDocument();
16+
expect(radio).toHaveAttribute('name', 'test-radio');
17+
expect(radio).toHaveAttribute('value', 'option1');
18+
expect(radio).toHaveAttribute('id', 'radio1');
19+
});
20+
21+
it('applies checked, required, and disabled props', () => {
22+
render(
23+
<Radio {...baseProps} checked required disabled />
24+
);
25+
const radio = screen.getByRole('radio');
26+
expect(radio).toBeChecked();
27+
expect(radio).toBeRequired();
28+
expect(radio).toBeDisabled();
29+
expect(radio).toHaveAttribute('aria-disabled', 'true');
30+
expect(radio).toHaveAttribute('aria-required', 'true');
31+
});
32+
33+
it('toggles checked state on click', () => {
34+
render(<Radio {...baseProps} />);
35+
const radio = screen.getByRole('radio');
36+
expect(radio).not.toBeChecked();
37+
fireEvent.click(radio);
38+
expect(radio).toBeChecked();
39+
});
40+
41+
it('calls onChange when clicked', () => {
42+
const handleChange = jest.fn();
43+
render(
44+
<Radio {...baseProps} onChange={handleChange} />
45+
);
46+
const radio = screen.getByRole('radio');
47+
fireEvent.click(radio);
48+
expect(handleChange).toHaveBeenCalled();
49+
});
50+
51+
it('applies custom class names', () => {
52+
render(
53+
<Radio {...baseProps} className="custom-class" customRootClass="root-class" />
54+
);
55+
const radio = screen.getByRole('radio');
56+
expect(radio.className).toMatch(/custom-class/);
57+
expect(radio.className).toMatch(/root-class/);
58+
});
59+
60+
it('applies data attributes for variant, size, and color', () => {
61+
render(
62+
<Radio {...baseProps} variant="filled" size="lg" color="red" />
63+
);
64+
const radio = screen.getByRole('radio');
65+
expect(radio).toHaveAttribute('data-button-variant', 'filled');
66+
expect(radio).toHaveAttribute('data-button-size', 'lg');
67+
expect(radio).toHaveAttribute('data-rad-ui-accent-color', 'red');
68+
});
69+
});

src/core/primitives/Dialog/fragments/DialogPrimitiveRoot.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export type DialogPrimitiveRootProps = {
1313

1414
const COMPONENT_NAME = 'DialogPrimitive';
1515

16-
const DialogPrimitiveRoot = ({ children, open=false, onOpenChange = () => {}, onClickOutside = () => {}, className, ...props } : DialogPrimitiveRootProps) => {
16+
const DialogPrimitiveRoot = ({ children, open = false, onOpenChange = () => {}, onClickOutside = () => {}, className, ...props } : DialogPrimitiveRootProps) => {
1717
const [isOpen, setIsOpen] = useState(open);
1818
const handleOpenChange = (open: boolean) => {
1919
setIsOpen(open);

src/core/primitives/Radio/RadioPrimitive.stories.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const All = {
2727
},
2828
name: 'radio',
2929
value: 'radio',
30+
id: 'radio',
3031
required: true
3132
}
3233
};

src/core/primitives/Radio/index.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import React from 'react';
22
import Primitive from '../Primitive';
33

4-
type RadioPrimitiveProps = {
4+
export type RadioPrimitiveProps = {
55
name: string;
66
value: string;
77
id: string;
88
onChange?: () => void;
99
checked?: boolean;
1010
required?: boolean;
1111
disabled?: boolean;
12-
asChild?: boolean
12+
asChild?: boolean;
13+
className?: string;
1314
}
1415

15-
function RadioPrimitive({ name, value, id, checked, required, onChange, disabled, asChild, ...props }: RadioPrimitiveProps) {
16+
function RadioPrimitive({ name, value, id, checked, required, onChange, disabled, asChild, className, ...props }: RadioPrimitiveProps) {
1617
return (
1718
<Primitive.input
1819
type="radio"
@@ -21,12 +22,13 @@ function RadioPrimitive({ name, value, id, checked, required, onChange, disabled
2122
tabIndex={-1}
2223
value={value}
2324
onChange={onChange}
24-
id={value}
25+
id={id}
2526
aria-disabled={disabled}
2627
disabled={disabled}
2728
required={required}
2829
aria-required={required}
2930
asChild={asChild}
31+
className={className}
3032
{...props}
3133
/>
3234
);

src/core/primitives/Radio/tests/Radio.test.tsx

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,46 +3,46 @@ import { render, screen, fireEvent } from '@testing-library/react';
33
import RadioPrimitive from '../index';
44

55
describe('RadioPrimitive', () => {
6-
const baseProps = {
7-
name: 'test-radio',
8-
value: 'option1',
9-
id: 'radio1',
10-
};
6+
const baseProps = {
7+
name: 'test-radio',
8+
value: 'option1',
9+
id: 'radio1'
10+
};
1111

12-
it('renders with required props', () => {
13-
render(<RadioPrimitive {...baseProps} />);
14-
const radio = screen.getByRole('radio');
15-
expect(radio).toBeInTheDocument();
16-
expect(radio).toHaveAttribute('name', 'test-radio');
17-
expect(radio).toHaveAttribute('value', 'option1');
18-
expect(radio).toHaveAttribute('id', 'option1');
19-
});
12+
it('renders with required props', () => {
13+
render(<RadioPrimitive {...baseProps} />);
14+
const radio = screen.getByRole('radio');
15+
expect(radio).toBeInTheDocument();
16+
expect(radio).toHaveAttribute('name', 'test-radio');
17+
expect(radio).toHaveAttribute('value', 'option1');
18+
expect(radio).toHaveAttribute('id', 'radio1');
19+
});
2020

21-
it('applies checked, required, and disabled props', () => {
22-
render(
23-
<RadioPrimitive {...baseProps} checked required disabled />
24-
);
25-
const radio = screen.getByRole('radio');
26-
expect(radio).toBeChecked();
27-
expect(radio).toBeRequired();
28-
expect(radio).toBeDisabled();
29-
expect(radio).toHaveAttribute('aria-disabled', 'true');
30-
expect(radio).toHaveAttribute('aria-required', 'true');
31-
});
21+
it('applies checked, required, and disabled props', () => {
22+
render(
23+
<RadioPrimitive {...baseProps} checked required disabled />
24+
);
25+
const radio = screen.getByRole('radio');
26+
expect(radio).toBeChecked();
27+
expect(radio).toBeRequired();
28+
expect(radio).toBeDisabled();
29+
expect(radio).toHaveAttribute('aria-disabled', 'true');
30+
expect(radio).toHaveAttribute('aria-required', 'true');
31+
});
3232

33-
it('calls onChange when clicked', () => {
34-
const handleChange = jest.fn();
35-
render(
36-
<RadioPrimitive {...baseProps} onChange={handleChange} />
37-
);
38-
const radio = screen.getByRole('radio');
39-
fireEvent.click(radio);
40-
expect(handleChange).toHaveBeenCalled();
41-
});
33+
it('calls onChange when clicked', () => {
34+
const handleChange = jest.fn();
35+
render(
36+
<RadioPrimitive {...baseProps} onChange={handleChange} />
37+
);
38+
const radio = screen.getByRole('radio');
39+
fireEvent.click(radio);
40+
expect(handleChange).toHaveBeenCalled();
41+
});
4242

43-
it('supports asChild prop (renders without error)', () => {
44-
render(<RadioPrimitive {...baseProps} asChild />);
45-
const radio = screen.getByRole('radio');
46-
expect(radio).toBeInTheDocument();
47-
});
43+
it('supports asChild prop (renders without error)', () => {
44+
render(<RadioPrimitive {...baseProps} asChild />);
45+
const radio = screen.getByRole('radio');
46+
expect(radio).toBeInTheDocument();
47+
});
4848
});

src/core/primitives/RadioGroup/RadioGroupPrimitive.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,4 @@ const RadioGroupPrimitive = () => {
1616
RadioGroupPrimitive.Root = RadioGroupPrimitiveRoot;
1717
RadioGroupPrimitive.Item = RadioGroupPrimitiveItem;
1818

19-
2019
export default RadioGroupPrimitive;

0 commit comments

Comments
 (0)