Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .changeset/checkbox-input-forward-rest-props.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[fix] CheckboxInput now forwards rest props (including `data-testid`) to the underlying `<input>`. Previously the component used a closed destructuring list with no `...rest` capture, so any prop not explicitly named — `data-testid`, other `data-*` attributes, etc. — was silently dropped despite `CheckboxInputProps` (via `BaseProps`) typing them as valid. Every sibling input component (TextInput, Selector, Slider, Button, Badge) already forwards rest props; CheckboxInput was the sole outlier. Rest is spread before the component's own named attributes on the `<input>`, so it cannot override `checked`, `disabled`, `type`, or any other explicitly-set prop.

@let-sunny
75 changes: 71 additions & 4 deletions packages/core/src/CheckboxInput/CheckboxInput.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,59 @@ describe('CheckboxInput', () => {
expect(handleChange).not.toHaveBeenCalled();
});

it('forwards data-testid to the input', () => {
render(
<CheckboxInput
label="Accept terms"
value={false}
onChange={() => {}}
data-testid="accept-terms-checkbox"
/>,
);
expect(screen.getByTestId('accept-terms-checkbox')).toBe(
screen.getByRole('checkbox'),
);
});

it('forwards arbitrary data-* attributes to the input', () => {
render(
<CheckboxInput
label="Accept terms"
value={false}
onChange={() => {}}
data-tracking-id="checkbox-42"
/>,
);
expect(screen.getByRole('checkbox')).toHaveAttribute(
'data-tracking-id',
'checkbox-42',
);
});

it('does not let rest props override checked, disabled, or type', () => {
// Not part of CheckboxInputProps; spread (rather than named attributes)
// to sidestep JSX excess-property checks the same way a real caller's
// spread rest-props object would arrive untyped at runtime.
const foreignAttrs: Record<string, unknown> = {
checked: false,
disabled: false,
type: 'text',
};
render(
<CheckboxInput
{...foreignAttrs}
label="Accept terms"
value={true}
onChange={() => {}}
isDisabled
/>,
);
const checkbox = screen.getByRole('checkbox');
expect(checkbox).toBeChecked();
expect(checkbox).toBeDisabled();
expect(checkbox).toHaveAttribute('type', 'checkbox');
});

it('forwards ref correctly', () => {
const ref = vi.fn();
render(
Expand Down Expand Up @@ -396,7 +449,12 @@ describe('CheckboxInput', () => {
it('submits under htmlName when checked', () => {
const {container} = render(
<form>
<CheckboxInput label="Terms" htmlName="terms" value={true} onChange={() => {}} />
<CheckboxInput
label="Terms"
htmlName="terms"
value={true}
onChange={() => {}}
/>
</form>,
);
const data = new FormData(container.querySelector('form')!);
Expand All @@ -416,16 +474,25 @@ describe('CheckboxInput', () => {
/>
</form>,
);
expect([...new FormData(container.querySelector('form')!).keys()]).toEqual([]);
expect([
...new FormData(container.querySelector('form')!).keys(),
]).toEqual([]);
});

it('submits nothing when unchecked', () => {
const {container} = render(
<form>
<CheckboxInput label="Terms" htmlName="terms" value={false} onChange={() => {}} />
<CheckboxInput
label="Terms"
htmlName="terms"
value={false}
onChange={() => {}}
/>
</form>,
);
expect([...new FormData(container.querySelector('form')!).keys()]).toEqual([]);
expect([
...new FormData(container.querySelector('form')!).keys(),
]).toEqual([]);
});
});
});
2 changes: 2 additions & 0 deletions packages/core/src/CheckboxInput/CheckboxInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ export function CheckboxInput({
className,
style,
ref,
...rest
}: CheckboxInputProps) {
const id = useId();
const descriptionID = useId();
Expand Down Expand Up @@ -484,6 +485,7 @@ export function CheckboxInput({
)}>
<div {...stylex.props(styles.checkboxWrapper, wrapperSizeStyles[size])}>
<input
{...rest}
ref={mergeRefs(
ref,
indeterminateRef,
Expand Down
Loading