Skip to content

Commit abf7841

Browse files
committed
all api and tests working
1 parent 1ead8c9 commit abf7841

File tree

8 files changed

+160
-86
lines changed

8 files changed

+160
-86
lines changed

src/components/ui/TabNav/fragments/TabNavLink.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ const TabNavLink = ({ value, className = '', href = '#', children, disabled, asC
3939
asChild={asChild}
4040
aria-disabled={disabled}
4141
aria-selected={isActive}
42-
// @ts-expect-error
4342
disabled={disabled}
4443
{...disabled ? {} : { href }}
4544
>

src/core/primitives/Primitive/index.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@ const SUPPORTED_HTML_ELEMENTS = ['div', 'span', 'button', 'input', 'a', 'img', '
55
type SupportedElement = typeof SUPPORTED_HTML_ELEMENTS[number];
66

77
// Update type definitions to be more specific
8-
interface PrimitiveProps extends React.HTMLAttributes<HTMLElement> {
9-
asChild?: boolean;
10-
children?: React.ReactNode;
11-
}
8+
type PrimitiveProps =
9+
| (React.InputHTMLAttributes<HTMLInputElement> & { asChild?: boolean })
10+
| (React.HTMLAttributes<HTMLElement> & { asChild?: boolean, children?: React.ReactNode });
1211

1312
// Update component creation with proper typing
1413
const createPrimitiveComponent = (elementType: SupportedElement) => {

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ export const All = {
2626
console.log('data', data);
2727
},
2828
name: 'radio',
29-
value: 'radio1',
30-
checked: true,
29+
value: 'radio',
3130
required: true
3231
}
3332
};
Lines changed: 26 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,35 @@
1-
import React, { useState } from 'react';
2-
import Primitive from '~/core/primitives/Primitive';
1+
import React from 'react';
2+
import Primitive from '../Primitive';
33

4-
export type RadioPrimitiveProps = {
5-
onClick?: (data: any) => void;
6-
onChange?: (data: any) => void;
7-
onFocus?: (data: any) => void;
8-
checked?: boolean;
4+
type RadioPrimitiveProps = {
95
name: string;
106
value: string;
11-
disabled?: boolean
7+
id: string;
8+
onChange?: () => void;
9+
checked?: boolean;
10+
required?: boolean;
11+
disabled?: boolean;
1212
asChild?: boolean
13-
required?: boolean
14-
id?: string
15-
};
16-
17-
const RadioPrimitive = ({ name = '', value = '', checked = false, onClick, onChange, onFocus, disabled = false, asChild = false, required = false, id, ...props }:RadioPrimitiveProps) => {
18-
const [isChecked, setIsChecked] = useState(checked);
19-
const dataAttributes = {
20-
'data-checked': isChecked.toString()
21-
};
22-
23-
const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>) => {
24-
const isChecked = event.target.checked;
25-
console.log(isChecked);
26-
if (typeof onChange === 'function') {
27-
setIsChecked(isChecked);
28-
onChange({
29-
value,
30-
checked: isChecked
31-
});
32-
}
33-
};
13+
}
3414

15+
function RadioPrimitive({ name, value, id, checked, required, onChange, disabled, asChild, ...props }: RadioPrimitiveProps) {
3516
return (
36-
// @ts-ignore
37-
<Primitive.input id={id} type='radio' value={value} name={name} onFocus={onFocus} onChange={handleOnChange} disabled={disabled} asChild={asChild} required={required} tabIndex={-1} {...props} {...dataAttributes}/>
17+
<Primitive.input
18+
type="radio"
19+
checked={checked}
20+
name={name}
21+
tabIndex={-1}
22+
value={value}
23+
onChange={onChange}
24+
id={value}
25+
aria-disabled={disabled}
26+
disabled={disabled}
27+
required={required}
28+
aria-required={required}
29+
asChild={asChild}
30+
{...props}
31+
/>
3832
);
39-
};
33+
}
4034

4135
export default RadioPrimitive;

src/core/primitives/RadioGroup/RadioGroupPrimitive.stories.tsx

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ const RadioButton = (args: RadioButtonProps) => {
2626
};
2727
return (
2828
<SandboxEditor>
29-
<RadioGroupPrimitive.Root orientation='horizontal' dir='rtl' name='test'>
29+
<RadioGroupPrimitive.Root
30+
orientation='horizontal' dir='rtl'
31+
name='test'
32+
// defaultValue='css'
33+
>
3034
{options.map((option) => (
3135
<RadioGroupPrimitive.Item key={option.id} value={option.value}>
3236
{option.label}
@@ -65,30 +69,26 @@ export const InForm = () => {
6569

6670
return (
6771
<SandboxEditor>
68-
<form onSubmit={handleSubmit}>
69-
<RadioGroupPrimitive.Root
70-
orientation="horizontal"
71-
name="language"
72-
value={selected}
73-
onValueChange={handleChange}
74-
>
75-
{options.map((option) => (
76-
<RadioGroupPrimitive.Item key={option.id} value={option.value}>
77-
{option.label}
78-
</RadioGroupPrimitive.Item>
79-
))}
80-
</RadioGroupPrimitive.Root>
81-
<button type="submit" style={{ marginTop: 16 }}>Submit</button>
82-
{submitted && (
83-
<div style={{ marginTop: 12 }}>
84-
<strong>Submitted value:</strong> {submitted}
85-
</div>
86-
)}
87-
</form>
72+
<form onSubmit={handleSubmit}>
73+
<RadioGroupPrimitive.Root
74+
// orientation="horizontal"
75+
name="language"
76+
value={selected}
77+
onValueChange={handleChange}
78+
>
79+
{options.map((option) => (
80+
<RadioGroupPrimitive.Item key={option.id} value={option.value}>
81+
{option.label}
82+
</RadioGroupPrimitive.Item>
83+
))}
84+
</RadioGroupPrimitive.Root>
85+
<button type="submit" style={{ marginTop: 16 }}>Submit</button>
86+
{submitted && (
87+
<div style={{ marginTop: 12 }}>
88+
<strong>Submitted value:</strong> {submitted}
89+
</div>
90+
)}
91+
</form>
8892
</SandboxEditor>
8993
);
9094
};
91-
92-
93-
94-

src/core/primitives/RadioGroup/fragments/RadioGroupPrimitiveItem.tsx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,37 @@ import React, { PropsWithChildren, useContext } from 'react';
22
import RadioGroupContext from '../context/RadioGroupContext';
33
import RovingFocusGroup from '~/core/utils/RovingFocusGroup';
44
import RadioPrimitive from '~/core/primitives/Radio';
5-
import Primitive from '../../Primitive';
65

76
type RadioGroupPrimitiveItemProps = PropsWithChildren<{
87
value: string;
98
disabled?: boolean
10-
children?: React.ReactNode
9+
children?: React.ReactNode;
10+
required?: boolean
1111
}>;
1212

13-
const RadioGroupPrimitiveItem = ({ value, children, disabled, ...props }: RadioGroupPrimitiveItemProps) => {
13+
const RadioGroupPrimitiveItem = ({ value, children, disabled, required = false, ...props }: RadioGroupPrimitiveItemProps) => {
1414
const context = useContext(RadioGroupContext);
1515
if (!context) {
1616
throw new Error('RadioGroup.Item must be used within a RadioGroup.Root');
1717
}
18-
const { groupDisabled, name, selectedValue, setSelectedValue, } = context;
19-
18+
const { groupDisabled, name, selectedValue, setSelectedValue } = context;
2019

2120
return (
2221
<RovingFocusGroup.Item role='radio'>
2322
<div {...props} onFocus={() => setSelectedValue(value)}>
24-
<Primitive.input
25-
26-
type="radio"
23+
24+
<RadioPrimitive
2725
id={value}
2826
name={name}
2927
value={value}
30-
checked={selectedValue === value}
28+
checked = {selectedValue === value}
3129
disabled={groupDisabled || disabled}
32-
tabIndex={ -1}
30+
required={required}
3331
onChange={() => setSelectedValue(value)}
34-
onFocus={() => setSelectedValue(value)}
3532
/>
33+
3634
<label htmlFor={value}>{children}</label>
35+
3736
</div>
3837
</RovingFocusGroup.Item>
3938
);

src/core/primitives/RadioGroup/fragments/RadioGroupPrimitiveRoot.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import RadioGroupContext from '../context/RadioGroupContext';
44
import RovingFocusGroup from '~/core/utils/RovingFocusGroup';
55
import useControllableState from '~/core/hooks/useControllableState';
66

7-
87
type RadioGroupPrimitiveRootProps = PropsWithChildren<{
98
className?: string;
109
customRootClass?: string;
@@ -19,14 +18,12 @@ type RadioGroupPrimitiveRootProps = PropsWithChildren<{
1918
dir?: 'ltr' | 'rtl';
2019
}>;
2120

22-
const RadioGroupPrimitiveRoot = ({ value, defaultValue = '', onValueChange, children, disabled: groupDisabled = false, required = false, name = '', orientation = 'horizontal', loop = true, dir = 'ltr', ...props }: RadioGroupPrimitiveRootProps) => {
23-
21+
const RadioGroupPrimitiveRoot = ({ value, defaultValue = '', onValueChange, children, disabled: groupDisabled = false, required = false, name = '', orientation = 'horizontal', loop = false, dir = 'ltr', ...props }: RadioGroupPrimitiveRootProps) => {
2422
const [selectedValue, setSelectedValue] = useControllableState(
2523
value,
2624
defaultValue,
2725
onValueChange
2826
);
29-
3027

3128
const sendItems = {
3229
selectedValue,
@@ -36,16 +33,15 @@ const RadioGroupPrimitiveRoot = ({ value, defaultValue = '', onValueChange, chil
3633
};
3734

3835
return (
39-
<RovingFocusGroup.Root orientation={orientation} loop={loop} dir={dir}>
36+
<RovingFocusGroup.Root>
4037
<RadioGroupContext.Provider value={sendItems}>
41-
<RovingFocusGroup.Group>
42-
<Primitive.div {...props} aria-required={required} role='radiogroup' aria-disabled={groupDisabled}>
43-
38+
<RovingFocusGroup.Group>
39+
<Primitive.div {...props} aria-required={required} role='radiogroup' aria-disabled={groupDisabled}>
40+
4441
{children}
45-
4642

47-
</Primitive.div>
48-
</RovingFocusGroup.Group>
43+
</Primitive.div>
44+
</RovingFocusGroup.Group>
4945
</RadioGroupContext.Provider>
5046
</RovingFocusGroup.Root>
5147
)
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import React from 'react';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
import '@testing-library/jest-dom';
4+
import RadioGroupPrimitiveRoot from '../fragments/RadioGroupPrimitiveRoot';
5+
import RadioGroupPrimitiveItem from '../fragments/RadioGroupPrimitiveItem';
6+
7+
describe('RadioGroupPrimitive', () => {
8+
it('renders radio items and allows selection (uncontrolled)', () => {
9+
render(
10+
<RadioGroupPrimitiveRoot defaultValue="b" name="test-group">
11+
<RadioGroupPrimitiveItem value="a">Option A</RadioGroupPrimitiveItem>
12+
<RadioGroupPrimitiveItem value="b">Option B</RadioGroupPrimitiveItem>
13+
<RadioGroupPrimitiveItem value="c">Option C</RadioGroupPrimitiveItem>
14+
</RadioGroupPrimitiveRoot>
15+
);
16+
const radioA = screen.getByLabelText('Option A') as HTMLInputElement;
17+
const radioB = screen.getByLabelText('Option B') as HTMLInputElement;
18+
const radioC = screen.getByLabelText('Option C') as HTMLInputElement;
19+
expect(radioA.checked).toBe(false);
20+
expect(radioB.checked).toBe(true);
21+
expect(radioC.checked).toBe(false);
22+
fireEvent.click(radioA);
23+
expect(radioA.checked).toBe(true);
24+
expect(radioB.checked).toBe(false);
25+
expect(radioC.checked).toBe(false);
26+
});
27+
28+
it('calls onValueChange in controlled mode', () => {
29+
const handleChange = jest.fn();
30+
render(
31+
<RadioGroupPrimitiveRoot value="a" onValueChange={handleChange} name="test-group">
32+
<RadioGroupPrimitiveItem value="a">Option A</RadioGroupPrimitiveItem>
33+
<RadioGroupPrimitiveItem value="b">Option B</RadioGroupPrimitiveItem>
34+
</RadioGroupPrimitiveRoot>
35+
);
36+
const radioB = screen.getByLabelText('Option B') as HTMLInputElement;
37+
fireEvent.click(radioB);
38+
expect(handleChange).toHaveBeenCalledWith('b');
39+
});
40+
41+
it('disables all radios when group is disabled', () => {
42+
render(
43+
<RadioGroupPrimitiveRoot defaultValue="a" disabled name="test-group">
44+
<RadioGroupPrimitiveItem value="a">Option A</RadioGroupPrimitiveItem>
45+
<RadioGroupPrimitiveItem value="b">Option B</RadioGroupPrimitiveItem>
46+
</RadioGroupPrimitiveRoot>
47+
);
48+
const radioA = screen.getByLabelText('Option A') as HTMLInputElement;
49+
const radioB = screen.getByLabelText('Option B') as HTMLInputElement;
50+
expect(radioA).toBeDisabled();
51+
expect(radioB).toBeDisabled();
52+
});
53+
54+
it('disables individual radio when disabled', () => {
55+
render(
56+
<RadioGroupPrimitiveRoot defaultValue="a" name="test-group">
57+
<RadioGroupPrimitiveItem value="a" disabled>Option A</RadioGroupPrimitiveItem>
58+
<RadioGroupPrimitiveItem value="b">Option B</RadioGroupPrimitiveItem>
59+
</RadioGroupPrimitiveRoot>
60+
);
61+
const radioA = screen.getByLabelText('Option A') as HTMLInputElement;
62+
const radioB = screen.getByLabelText('Option B') as HTMLInputElement;
63+
expect(radioA).toBeDisabled();
64+
expect(radioB).not.toBeDisabled();
65+
});
66+
67+
it('sets required and name attributes', () => {
68+
render(
69+
<RadioGroupPrimitiveRoot defaultValue="a" required name="my-radio-group">
70+
<RadioGroupPrimitiveItem value="a">Option A</RadioGroupPrimitiveItem>
71+
</RadioGroupPrimitiveRoot>
72+
);
73+
const radioA = screen.getByLabelText('Option A') as HTMLInputElement;
74+
expect(radioA).toHaveAttribute('name', 'my-radio-group');
75+
// required is set on the group, not the input, but we can check the group
76+
const group = screen.getByRole('radiogroup');
77+
expect(group).toHaveAttribute('aria-required', 'true');
78+
});
79+
80+
it('throws error if RadioGroupPrimitiveItem is used outside RadioGroupPrimitiveRoot', () => {
81+
// Suppress error output for this test
82+
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
83+
expect(() => {
84+
render(<RadioGroupPrimitiveItem value="a">Option A</RadioGroupPrimitiveItem>);
85+
}).toThrow('RadioGroup.Item must be used within a RadioGroup.Root');
86+
spy.mockRestore();
87+
});
88+
});

0 commit comments

Comments
 (0)