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
+ ),
);
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;
}