Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 112 additions & 40 deletions apps/www/src/content/docs/components/radio/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,20 @@
export const preview = {
type: 'code',
code: `
<Radio.Group defaultValue="2">
<Flex direction="column" gap="small">
<Flex gap="small" align="center">
<Radio value="1" id="P1" />
<label htmlFor="P1">Option One</label>
</Flex>
<Flex gap="small" align="center">
<Radio value="2" id="P2" />
<label htmlFor="P2">Option Two</label>
</Flex>
<Flex gap="small" align="center">
<Radio value="3" id="P3" disabled/>
<label htmlFor="P3">Option Three</label>
</Flex>
</Flex>
</Radio.Group>`
<Radio.Group defaultValue="2">
<Flex gap="small" align="center">
<Radio value="1" id="P1" />
<label htmlFor="P1">Option One</label>
</Flex>
<Flex gap="small" align="center">
<Radio value="2" id="P2" />
<label htmlFor="P2">Option Two</label>
</Flex>
<Flex gap="small" align="center">
<Radio value="3" id="P3" disabled />
<label htmlFor="P3">Option Three</label>
</Flex>
</Radio.Group>`
};

export const stateDemo = {
Expand Down Expand Up @@ -47,44 +45,118 @@ export const stateDemo = {
]
};

export const sizeDemo = {
type: 'code',
tabs: [
{
name: 'Large (default)',
code: `
<Radio.Group defaultValue="1">
<Flex gap="small" align="center">
<Radio value="1" size="large" id="sl1" />
<label htmlFor="sl1">Large Option One</label>
</Flex>
<Flex gap="small" align="center">
<Radio value="2" size="large" id="sl2" />
<label htmlFor="sl2">Large Option Two</label>
</Flex>
</Radio.Group>`
},
{
name: 'Small',
code: `
<Radio.Group defaultValue="1">
<Flex gap="small" align="center">
<Radio value="1" size="small" id="ss1" />
<label htmlFor="ss1">Small Option One</label>
</Flex>
<Flex gap="small" align="center">
<Radio value="2" size="small" id="ss2" />
<label htmlFor="ss2">Small Option Two</label>
</Flex>
</Radio.Group>`
}
]
};

export const orientationDemo = {
type: 'code',
tabs: [
{
name: 'Vertical (default)',
code: `
<Radio.Group defaultValue="1" orientation="vertical">
<Flex gap="small" align="center">
<Radio value="1" id="ov1" />
<label htmlFor="ov1">Option One</label>
</Flex>
<Flex gap="small" align="center">
<Radio value="2" id="ov2" />
<label htmlFor="ov2">Option Two</label>
</Flex>
<Flex gap="small" align="center">
<Radio value="3" id="ov3" />
<label htmlFor="ov3">Option Three</label>
</Flex>
</Radio.Group>`
},
{
name: 'Horizontal',
code: `
<Radio.Group defaultValue="1" orientation="horizontal">
<Flex gap="small" align="center">
<Radio value="1" id="oh1" />
<label htmlFor="oh1">Option One</label>
</Flex>
<Flex gap="small" align="center">
<Radio value="2" id="oh2" />
<label htmlFor="oh2">Option Two</label>
</Flex>
<Flex gap="small" align="center">
<Radio value="3" id="oh3" />
<label htmlFor="oh3">Option Three</label>
</Flex>
</Radio.Group>`
}
]
};

export const labelDemo = {
type: 'code',
code: `
<Radio.Group defaultValue="1">
<Flex gap="small" align="center">
<Radio value="1" id="L1" />
<label htmlFor="L1">Option One</label>
</Flex>
<Flex gap="small" align="center">
<Radio value="2" id="L2" />
<label htmlFor="L2">Option Two</label>
</Flex>
<Flex gap="small" align="center">
<Radio value="3" id="L3" />
<label htmlFor="L3">Option Three</label>
</Flex>
</Radio.Group>`
<Radio.Group defaultValue="1">
<Flex gap="small" align="center">
<Radio value="1" id="L1" />
<label htmlFor="L1">Option One</label>
</Flex>
<Flex gap="small" align="center">
<Radio value="2" id="L2" />
<label htmlFor="L2">Option Two</label>
</Flex>
<Flex gap="small" align="center">
<Radio value="3" id="L3" />
<label htmlFor="L3">Option Three</label>
</Flex>
</Radio.Group>`
};

export const formDemo = {
type: 'code',
code: `
<form onSubmit={(e) => {
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.target);
alert(JSON.stringify(Object.fromEntries(formData)));
}}>
<Flex direction="column" gap="medium">
<Radio.Group name="plan" defaultValue="monthly">
<Flex direction="column" gap="small">
<Flex gap="small" align="center">
<Radio value="monthly" id="mp" />
<label htmlFor="mp">Monthly Plan</label>
</Flex>
<Flex gap="small" align="center">
<Radio value="yearly" id="yp" />
<label htmlFor="yp">Yearly Plan</label>
</Flex>
<Flex gap="small" align="center">
<Radio value="monthly" id="mp" />
<label htmlFor="mp">Monthly Plan</label>
</Flex>
<Flex gap="small" align="center">
<Radio value="yearly" id="yp" />
<label htmlFor="yp">Yearly Plan</label>
</Flex>
</Radio.Group>
<Button type="submit" width="100%">Submit</Button>
Expand Down
14 changes: 13 additions & 1 deletion apps/www/src/content/docs/components/radio/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: A radio group component for selecting a single option from a list o
source: packages/raystack/components/radio
---

import { preview, stateDemo, labelDemo, formDemo } from "./demo.ts";
import { preview, stateDemo, sizeDemo, orientationDemo, labelDemo, formDemo } from "./demo.ts";

<Demo data={preview} />

Expand Down Expand Up @@ -43,6 +43,18 @@ Radio buttons support different states to indicate interactivity and selection.

<Demo data={stateDemo} />

### Size Variants

The Radio component comes in two sizes: large (default) and small.

<Demo data={sizeDemo} />

### Orientation

The Radio.Group supports vertical (default) and horizontal orientation to control the layout direction of its items.

<Demo data={orientationDemo} />

### With Labels

Radio buttons should always be accompanied by labels for accessibility and usability.
Expand Down
12 changes: 12 additions & 0 deletions apps/www/src/content/docs/components/radio/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ export interface RadioGroupProps {
/** When true, prevents user interaction with the radio group. */
disabled?: boolean;

/**
* The layout orientation of the radio group.
* @default "vertical"
*/
orientation?: 'vertical' | 'horizontal';

/** The name of the radio group when submitted as a form field. */
name?: string;

Expand All @@ -35,6 +41,12 @@ export interface RadioProps {
/** When true, prevents user interaction with this radio item. */
disabled?: boolean;

/**
* The size of the radio button.
* @default "large"
*/
size?: 'large' | 'small';

/** The unique identifier for the radio item. */
id?: string;

Expand Down
72 changes: 72 additions & 0 deletions packages/raystack/components/radio/__tests__/radio.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vitest';
import { Radio } from '../radio';
import styles from '../radio.module.css';

describe('Radio', () => {
describe('Basic Rendering', () => {
Expand Down Expand Up @@ -258,4 +259,75 @@ describe('Radio', () => {
expect(radio).toHaveAttribute('data-disabled');
});
});

describe('Sizes', () => {
const sizes = ['small', 'large'] as const;
sizes.forEach(size => {
it(`renders ${size} size`, () => {
render(
<Radio.Group>
<Radio value='option1' size={size} />
</Radio.Group>
);
const radio = screen.getByRole('radio');
expect(radio).toHaveClass(styles[size]);
});
});

it('renders large size by default', () => {
render(
<Radio.Group>
<Radio value='option1' />
</Radio.Group>
);
const radio = screen.getByRole('radio');
expect(radio).toHaveClass(styles.large);
});

it('applies radioitem base class with size variant', () => {
render(
<Radio.Group>
<Radio value='option1' size='small' />
</Radio.Group>
);
const radio = screen.getByRole('radio');
expect(radio).toHaveClass(styles.radioitem);
expect(radio).toHaveClass(styles.small);
});
});

describe('Orientation', () => {
it('applies vertical layout by default', () => {
render(
<Radio.Group>
<Radio value='option1' />
</Radio.Group>
);
const group = screen.getByRole('radiogroup');
expect(group).toHaveClass(styles.group);
expect(group).not.toHaveClass(styles['group-horizontal']);
});

it('applies vertical layout when orientation is vertical', () => {
render(
<Radio.Group orientation='vertical'>
<Radio value='option1' />
</Radio.Group>
);
const group = screen.getByRole('radiogroup');
expect(group).toHaveClass(styles.group);
expect(group).not.toHaveClass(styles['group-horizontal']);
});

it('applies horizontal layout when orientation is horizontal', () => {
render(
<Radio.Group orientation='horizontal'>
<Radio value='option1' />
</Radio.Group>
);
const group = screen.getByRole('radiogroup');
expect(group).toHaveClass(styles.group);
expect(group).toHaveClass(styles['group-horizontal']);
});
});
});
37 changes: 30 additions & 7 deletions packages/raystack/components/radio/radio.module.css
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
.radio {
/* Group layout — uses flex to arrange radio items */
.group {
display: flex;
flex-direction: column;
gap: var(--rs-space-3);
}

.group-horizontal {
flex-direction: row;
}

/* Radio button — uses inline-flex to center the indicator dot */
.radioitem {
all: unset;
width: var(--rs-space-5);
height: var(--rs-space-5);
min-width: var(--rs-space-5);
min-height: var(--rs-space-5);
border-radius: var(--rs-radius-full);
border: 1px solid var(--rs-color-border-base-tertiary);
background: var(--rs-color-background-base-primary);
Expand All @@ -19,6 +22,20 @@
box-sizing: border-box;
}

.large {
width: var(--rs-space-5);
height: var(--rs-space-5);
min-width: var(--rs-space-5);
min-height: var(--rs-space-5);
}

.small {
width: var(--rs-space-4);
height: var(--rs-space-4);
min-width: var(--rs-space-4);
min-height: var(--rs-space-4);
}

.radioitem:hover {
border-color: var(--rs-color-border-base-tertiary-hover);
background: var(--rs-color-background-base-primary-hover);
Expand Down Expand Up @@ -58,12 +75,18 @@
.indicator::after {
content: "";
display: block;
width: var(--rs-radius-3);
height: var(--rs-radius-3);
width: var(--rs-space-2);
height: var(--rs-space-2);
border-radius: var(--rs-radius-full);
background: var(--rs-color-foreground-base-emphasis);
}

/* Scale indicator dot for small size */
.small .indicator::after {
width: var(--rs-space-1);
height: var(--rs-space-1);
}

.radioitem[data-disabled] .indicator::after {
background: var(--rs-color-foreground-base-emphasis);
}
Loading
Loading