diff --git a/packages/react/src/otp-field/root/OTPFieldRoot.test.tsx b/packages/react/src/otp-field/root/OTPFieldRoot.test.tsx index 4a83f528292..a2b9a14f38d 100644 --- a/packages/react/src/otp-field/root/OTPFieldRoot.test.tsx +++ b/packages/react/src/otp-field/root/OTPFieldRoot.test.tsx @@ -900,62 +900,60 @@ describe('', () => { expect(onValueComplete.mock.calls[0]?.[1].reason).toBe(REASONS.inputChange); }); - it('ignores hidden-input autofill when the field is readonly', async () => { + it.each([ + { lockState: 'readOnly', label: 'inside Field', withField: true }, + { lockState: 'disabled', label: 'inside Field', withField: true }, + { lockState: 'readOnly', label: 'outside Field', withField: false }, + { lockState: 'disabled', label: 'outside Field', withField: false }, + ] as const)('ignores hidden-input autofill when $lockState $label', async ({ + lockState, + withField, + }) => { const onValueChange = vi.fn(); const onValueInvalid = vi.fn(); const onValueComplete = vi.fn(); - - await render( + const otpField = ( , + /> ); - const hiddenInput = document.querySelector('input[name="otp"]'); - - expect(hiddenInput).not.toBeNull(); - - fireEvent.change(hiddenInput!, { target: { value: '12a34b56' } }); - - const inputs = screen.getAllByRole('textbox'); - - expect(inputs.map((input) => input.value)).toEqual(['', '', '', '', '', '']); - expect(onValueChange).not.toHaveBeenCalled(); - expect(onValueInvalid).not.toHaveBeenCalled(); - expect(onValueComplete).not.toHaveBeenCalled(); - }); - - it('ignores hidden-input autofill when the field is disabled', async () => { - const onValueChange = vi.fn(); - const onValueInvalid = vi.fn(); - const onValueComplete = vi.fn(); - await render( - , + withField ? ( +
+ + {otpField} + + +
+ ) : ( + otpField + ), ); const hiddenInput = document.querySelector('input[name="otp"]'); expect(hiddenInput).not.toBeNull(); - fireEvent.change(hiddenInput!, { target: { value: '12a34b56' } }); + if (withField) { + expect(screen.getByTestId('error')).toHaveTextContent('test'); + } - const inputs = screen.getAllByRole('textbox'); + fireEvent.change(hiddenInput!, { target: { value: '12a34b56' } }); - expect(inputs.map((input) => input.value)).toEqual(['', '', '', '', '', '']); + expect(getValues()).toBe(''); expect(onValueChange).not.toHaveBeenCalled(); expect(onValueInvalid).not.toHaveBeenCalled(); expect(onValueComplete).not.toHaveBeenCalled(); + + if (withField) { + expect(screen.getByTestId('error')).toHaveTextContent('test'); + } }); describe('prop: autoSubmit', () => { diff --git a/packages/react/src/otp-field/root/OTPFieldRoot.tsx b/packages/react/src/otp-field/root/OTPFieldRoot.tsx index 713433d67c1..113e8440fd1 100644 --- a/packages/react/src/otp-field/root/OTPFieldRoot.tsx +++ b/packages/react/src/otp-field/root/OTPFieldRoot.tsx @@ -19,7 +19,7 @@ import { useAriaLabelledBy } from '../../internals/labelable-provider/useAriaLab import { useLabelableId } from '../../internals/labelable-provider/useLabelableId'; import { useRenderElement } from '../../internals/useRenderElement'; import { useValueChanged } from '../../internals/useValueChanged'; -import type { BaseUIComponentProps } from '../../internals/types'; +import type { BaseUIComponentProps, BaseUIEvent } from '../../internals/types'; import { createChangeEventDetails, createGenericEventDetails, @@ -406,8 +406,10 @@ export const OTPFieldRoot = React.forwardRef(function OTPFieldRoot( onFocus() { focusInput(0); }, - onChange(event) { + onChange(event: BaseUIEvent>) { if (event.nativeEvent.defaultPrevented || disabled || readOnly) { + // Outside Field.Root, the event is not wrapped by mergeProps. + event.preventBaseUIHandler?.(); return; }