[checkbox][switch][slider] Fix extra validate fn calls (#4911)

This commit is contained in:
Albert Yu authored and GitHub committed 2026-05-27 18:09:17 +10:00
1 parent e18d788327
commit 2b68bf3efc
6 files changed
+80 -6

No files matched your search

@@ -1202,6 +1202,21 @@ describe('<Checkbox.Root />', () => {
expect(button).toHaveAttribute('aria-invalid', 'true');
});
it('validates once when changed by the user', async () => {
const validate = vi.fn();
const { user } = await render(
<Field.Root validationMode="onChange" validate={validate}>
<Checkbox.Root />
</Field.Root>,
);
await user.click(screen.getByRole('checkbox'));
expect(validate).toHaveBeenCalledTimes(1);
expect(validate.mock.lastCall?.[0]).toBe(true);
});
it('revalidates when a controlled value changes externally', async () => {
const validateSpy = vi.fn((value: unknown) => ((value as boolean) ? 'error' : null));
@@ -259,10 +259,7 @@ export const CheckboxRoot = React.forwardRef(function CheckboxRoot(
? { value: (groupContext ? checked && valueProp : valueProp) || '' }
: EMPTY_OBJECT,
getDescriptionProps,
(props) =>
groupContext
? validation.getValidationProps(disabled, props)
: validation.getInputValidationProps(disabled, props),
(props) => validation.getValidationProps(disabled, props),
);
const computedChecked = isGroupedWithParent ? Boolean(groupChecked) : checked;
@@ -2459,6 +2459,53 @@ describe.skipIf(typeof Touch === 'undefined')('<Slider.Root />', () => {
expect(input).toHaveAttribute('aria-invalid', 'true');
});
it('validates once when changed by the user', async () => {
const validate = vi.fn();
const { user } = await render(
<Field.Root validationMode="onChange" validate={validate}>
<Slider.Root defaultValue={0}>
<Slider.Control>
<Slider.Thumb />
</Slider.Control>
</Slider.Root>
</Field.Root>,
);
await user.keyboard('[Tab]');
expect(screen.getByRole('slider')).toHaveFocus();
await user.keyboard(`[${ARROW_RIGHT}]`);
await flushMicrotasks();
expect(validate).toHaveBeenCalledTimes(1);
expect(validate.mock.lastCall?.[0]).toBe(1);
});
it('validates once with an array value for range sliders when changed by the user', async () => {
const validate = vi.fn();
const { user } = await render(
<Field.Root validationMode="onChange" validate={validate}>
<Slider.Root defaultValue={[0, 5]}>
<Slider.Control>
<Slider.Thumb index={0} />
<Slider.Thumb index={1} />
</Slider.Control>
</Slider.Root>
</Field.Root>,
);
await user.keyboard('[Tab]');
expect(screen.getAllByRole('slider')[0]).toHaveFocus();
await user.keyboard(`[${ARROW_RIGHT}]`);
await flushMicrotasks();
expect(validate).toHaveBeenCalledTimes(1);
expect(validate.mock.lastCall?.[0]).toEqual([1, 5]);
});
it('revalidates when the controlled value changes externally', async () => {
const validateSpy = vi.fn((value: unknown) => (Number(value) === 5 ? 'error' : null));
@@ -452,7 +452,7 @@ export const SliderThumb = React.forwardRef(function SliderThumb(
type: 'range',
value: thumbValue ?? '',
},
(props) => validation.getInputValidationProps(disabled, props),
(props) => validation.getValidationProps(disabled, props),
);
const mergedInputRef = useMergedRefs(inputRef, validation.inputRef, inputRefProp);
@@ -831,6 +831,21 @@ describe('<Switch.Root />', () => {
expect(button).toHaveAttribute('aria-invalid', 'true');
});
it('validates once when changed by the user', async () => {
const validate = vi.fn();
const { user } = await render(
<Field.Root validationMode="onChange" validate={validate}>
<Switch.Root />
</Field.Root>,
);
await user.click(screen.getByRole('switch'));
expect(validate).toHaveBeenCalledTimes(1);
expect(validate.mock.lastCall?.[0]).toBe(true);
});
it('revalidates when a controlled value changes externally', async () => {
const validateSpy = vi.fn((value: unknown) => ((value as boolean) ? 'error' : null));
@@ -211,7 +211,7 @@ export const SwitchRoot = React.forwardRef(function SwitchRoot(
switchRef.current?.focus();
},
},
(props) => validation.getInputValidationProps(disabled, props),
(props) => validation.getValidationProps(disabled, props),
// React <19 sets an empty value if `undefined` is passed explicitly
// To avoid this, we only set the value if it's defined
value !== undefined ? { value } : EMPTY_OBJECT,