Skip to content
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_radio_group_a11y.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"packages/components/package.json": "Patch"
},
"note": "RadioGroup is a radiogroup with an optional label, its radios share a name (generated when not given) so arrow keys move between them, and it keeps the typed values of its options: false, 0 and '' stay selectable and onChange receives the option's own value rather than a string. Button-variant radios are visually hidden instead of display:none, so they stay focusable and show a focus ring, and the default radio's focus outline is valid CSS and shown on :focus-visible. Checkbox uses an id it is given for its labels as well as its input, where the labels used to point at a generated id",
"date": "2026-10-01T00:00:00.000Z"
}
37 changes: 36 additions & 1 deletion packages/components/src/__tests__/forward-ref.browser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,44 @@ import { describe, expect, it } from 'bun:test'
import { render } from 'bun-test-env-dom'
import { createRef } from 'react'

import { Button, Input, Radio, Textarea } from '../index'
import { Button, Checkbox, Input, Radio, Textarea } from '../index'

describe('forwardRef', () => {
it('links both labels to the forwarded checkbox when a custom id is given', () => {
// Given
const ref = createRef<HTMLInputElement>()

// When
const { container } = render(
<Checkbox ref={ref} id="custom-checkbox">
Checkbox
</Checkbox>,
)

// Then
expect(ref.current?.id).toBe('custom-checkbox')
const labels = container.querySelectorAll('label')
expect(labels).toHaveLength(2)
for (const label of labels) {
expect(label.htmlFor).toBe(ref.current?.id)
}
})

it('hands its ref to the native radio when the button variant is used', () => {
// Given
const ref = createRef<HTMLInputElement>()

// When
render(
<Radio ref={ref} variant="button">
Radio
</Radio>,
)

// Then
expect(ref.current?.type).toBe('radio')
})

it('hands each component ref to its form element', () => {
const button = createRef<HTMLButtonElement>()
const input = createRef<HTMLInputElement>()
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,35 @@ import { Checkbox } from '../index'

describe('Checkbox', () => {
it('should render basic checkbox', () => {
const { container } = render(<Checkbox>Test Checkbox</Checkbox>)
const { container } = render(
<Checkbox id="checkbox">Test Checkbox</Checkbox>,
)
expect(container).toMatchSnapshot()
})

it('should render checked checkbox', () => {
const { container } = render(<Checkbox checked>Test Checkbox</Checkbox>)
const { container } = render(
<Checkbox checked id="checkbox">
Test Checkbox
</Checkbox>,
)
expect(container).toMatchSnapshot()
expect(container.querySelector('input')).toBeChecked()
})

it('should render disabled checkbox', () => {
const { container } = render(<Checkbox disabled>Test Checkbox</Checkbox>)
const { container } = render(
<Checkbox disabled id="checkbox">
Test Checkbox
</Checkbox>,
)
expect(container).toMatchSnapshot()
expect(container.querySelector('input')).toBeDisabled()
})

it('should render disabled and checked checkbox', () => {
const { container } = render(
<Checkbox checked disabled>
<Checkbox checked disabled id="checkbox">
Test Checkbox
</Checkbox>,
)
Expand All @@ -35,7 +45,7 @@ describe('Checkbox', () => {

it('should render checkbox with custom child', () => {
const { container } = render(
<Checkbox>
<Checkbox id="checkbox">
<div>Custom Child</div>
</Checkbox>,
)
Expand All @@ -46,7 +56,7 @@ describe('Checkbox', () => {
it('should call onChange with true when checkbox is clicked and unchecked', async () => {
const onChange = mock()
const { container } = render(
<Checkbox checked={false} onChange={onChange}>
<Checkbox checked={false} id="checkbox" onChange={onChange}>
Test Checkbox
</Checkbox>,
)
Expand All @@ -65,7 +75,7 @@ describe('Checkbox', () => {
it('should call onChange with false when checkbox is clicked and checked', async () => {
const onChange = mock()
const { container } = render(
<Checkbox checked={true} onChange={onChange}>
<Checkbox checked={true} id="checkbox" onChange={onChange}>
Test Checkbox
</Checkbox>,
)
Expand All @@ -83,7 +93,7 @@ describe('Checkbox', () => {
it('should not call onChange when disabled is true', async () => {
const onChange = mock()
const { container } = render(
<Checkbox disabled onChange={onChange}>
<Checkbox disabled id="checkbox" onChange={onChange}>
Test Checkbox
</Checkbox>,
)
Expand All @@ -98,7 +108,9 @@ describe('Checkbox', () => {
})

it('should not call onChange when onChange prop is not provided', async () => {
const { container } = render(<Checkbox>Test Checkbox</Checkbox>)
const { container } = render(
<Checkbox id="checkbox">Test Checkbox</Checkbox>,
)

const input = container.querySelector('input')

Expand All @@ -114,7 +126,7 @@ describe('Checkbox', () => {
it('should not call onChange when both disabled and onChange are provided', async () => {
const onChange = mock()
const { container } = render(
<Checkbox checked={false} disabled onChange={onChange}>
<Checkbox checked={false} disabled id="checkbox" onChange={onChange}>
Test Checkbox
</Checkbox>,
)
Expand All @@ -131,7 +143,7 @@ describe('Checkbox', () => {
it('should handle label click and trigger onChange', async () => {
const onChange = mock()
const { container } = render(
<Checkbox checked={false} onChange={onChange}>
<Checkbox checked={false} id="checkbox" onChange={onChange}>
Test Checkbox
</Checkbox>,
)
Expand All @@ -149,7 +161,7 @@ describe('Checkbox', () => {
it('should not trigger onChange on label click when disabled', async () => {
const onChange = mock()
const { container } = render(
<Checkbox disabled onChange={onChange}>
<Checkbox disabled id="checkbox" onChange={onChange}>
Test Checkbox
</Checkbox>,
)
Expand All @@ -166,7 +178,7 @@ describe('Checkbox', () => {
it('should pass correct event target checked value to onChange', async () => {
const onChange = mock()
const { container } = render(
<Checkbox checked={false} onChange={onChange}>
<Checkbox checked={false} id="checkbox" onChange={onChange}>
Test Checkbox
</Checkbox>,
)
Expand All @@ -182,23 +194,31 @@ describe('Checkbox', () => {
})

it('should have proper accessibility attributes', () => {
const { container } = render(<Checkbox>Test Checkbox</Checkbox>)
const { container } = render(
<Checkbox id="checkbox">Test Checkbox</Checkbox>,
)

const input = container.querySelector('input')

expect(input).toHaveAttribute('type', 'checkbox')
})

it('should display CheckIcon when checked', () => {
const { container } = render(<Checkbox checked>Test Checkbox</Checkbox>)
const { container } = render(
<Checkbox checked id="checkbox">
Test Checkbox
</Checkbox>,
)

const checkIcon = container.querySelector('svg')
expect(checkIcon).toBeInTheDocument()
})

it('should not display CheckIcon when unchecked', () => {
const { container } = render(
<Checkbox checked={false}>Test Checkbox</Checkbox>,
<Checkbox checked={false} id="checkbox">
Test Checkbox
</Checkbox>,
)

const checkIcon = container.querySelector('svg')
Expand All @@ -209,6 +229,7 @@ describe('Checkbox', () => {
const { container } = render(
<Checkbox
data-testid="custom-checkbox"
id="checkbox"
name="test-name"
value="test-value"
>
Expand All @@ -232,7 +253,9 @@ describe('Checkbox', () => {
}

const { container } = render(
<Checkbox colors={customColors}>Test Checkbox</Checkbox>,
<Checkbox colors={customColors} id="checkbox">
Test Checkbox
</Checkbox>,
)

expect(container).toMatchSnapshot()
Expand All @@ -245,7 +268,9 @@ describe('Checkbox', () => {
}

const { container } = render(
<Checkbox colors={partialColors}>Test Checkbox</Checkbox>,
<Checkbox colors={partialColors} id="checkbox">
Test Checkbox
</Checkbox>,
)

expect(container).toMatchSnapshot()
Expand All @@ -261,7 +286,7 @@ describe('Checkbox', () => {
}

const { container } = render(
<Checkbox checked colors={customColors}>
<Checkbox checked colors={customColors} id="checkbox">
Test Checkbox
</Checkbox>,
)
Expand All @@ -279,7 +304,7 @@ describe('Checkbox', () => {
}

const { container } = render(
<Checkbox colors={customColors} disabled>
<Checkbox colors={customColors} disabled id="checkbox">
Test Checkbox
</Checkbox>,
)
Expand All @@ -293,7 +318,9 @@ describe('Checkbox', () => {
}

const { container } = render(
<Checkbox colors={customColors}>Test Checkbox</Checkbox>,
<Checkbox colors={customColors} id="checkbox">
Test Checkbox
</Checkbox>,
)

const input = container.querySelector('input')
Expand All @@ -308,7 +335,9 @@ describe('Checkbox', () => {
}

const { container } = render(
<Checkbox colors={customColors}>Test Checkbox</Checkbox>,
<Checkbox colors={customColors} id="checkbox">
Test Checkbox
</Checkbox>,
)

const input = container.querySelector('input')
Expand All @@ -323,7 +352,9 @@ describe('Checkbox', () => {
}

const { container } = render(
<Checkbox colors={customColors}>Test Checkbox</Checkbox>,
<Checkbox colors={customColors} id="checkbox">
Test Checkbox
</Checkbox>,
)

const input = container.querySelector('span')
Expand All @@ -338,7 +369,9 @@ describe('Checkbox', () => {
}

const { container } = render(
<Checkbox colors={customColors}>Test Checkbox</Checkbox>,
<Checkbox colors={customColors} id="checkbox">
Test Checkbox
</Checkbox>,
)

const input = container.querySelector('input')
Expand All @@ -353,7 +386,7 @@ describe('Checkbox', () => {
}

const { container } = render(
<Checkbox checked colors={customColors}>
<Checkbox checked colors={customColors} id="checkbox">
Test Checkbox
</Checkbox>,
)
Expand All @@ -374,7 +407,9 @@ describe('Checkbox', () => {
}

const { container } = render(
<Checkbox colors={customColors}>Test Checkbox</Checkbox>,
<Checkbox colors={customColors} id="checkbox">
Test Checkbox
</Checkbox>,
)

const input = container.querySelector('input')
Expand All @@ -386,7 +421,9 @@ describe('Checkbox', () => {
})

it('should not apply CSS variables when colors prop is not provided', () => {
const { container } = render(<Checkbox>Test Checkbox</Checkbox>)
const { container } = render(
<Checkbox id="checkbox">Test Checkbox</Checkbox>,
)

const input = container.querySelector('input')
// CSS 변수가 undefined로 설정되지 않아야 함
Expand All @@ -408,7 +445,12 @@ describe('Checkbox', () => {
}

const { container } = render(
<Checkbox checked={false} colors={customColors} onChange={onChange}>
<Checkbox
checked={false}
colors={customColors}
id="checkbox"
onChange={onChange}
>
Test Checkbox
</Checkbox>,
)
Expand Down
3 changes: 2 additions & 1 deletion packages/components/src/components/Checkbox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
}
}

export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(

Check warning on line 23 in packages/components/src/components/Checkbox/index.tsx

View workflow job for this annotation

GitHub Actions / publish

In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead
function Checkbox(
{
children,
Expand All @@ -33,7 +33,8 @@
},
ref,
) {
const generateId = useId()
const generatedId = useId()
const generateId = props.id ?? generatedId
const [innerChecked, setInnerChecked] = useState(defaultChecked)
const finalChecked = checked ?? innerChecked

Expand Down
Loading
Loading