From febd218450309e37e2f5afa94ff718fd3467ad1f Mon Sep 17 00:00:00 2001 From: Luke Deen Taylor Date: Wed, 13 Aug 2025 15:31:28 -0400 Subject: [PATCH 1/4] Fix iOS Chrome autocomplete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iOS chrome autocomplete only fires an `input` event up to the `maxLength`, so we need the input that “supports autocomplete” to accept the full OTP length. Later, in the `input` handler, the PASTE action will be dispatched. --- .../one-time-password-field/src/one-time-password-field.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/one-time-password-field/src/one-time-password-field.tsx b/packages/react/one-time-password-field/src/one-time-password-field.tsx index 791d80a7f..d411ee160 100644 --- a/packages/react/one-time-password-field/src/one-time-password-field.tsx +++ b/packages/react/one-time-password-field/src/one-time-password-field.tsx @@ -647,7 +647,7 @@ const OneTimePasswordFieldInput = React.forwardRef< data-protonpass-ignore={supportsAutoComplete ? undefined : 'true'} data-bwignore={supportsAutoComplete ? undefined : 'true'} inputMode={validation?.inputMode} - maxLength={1} + maxLength={supportsAutoComplete ? collection.size : 1} pattern={validation?.pattern} readOnly={context.readOnly} value={char} From 808aa5994324c2e60790da63b520eaa888890287 Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Wed, 13 Aug 2025 15:43:44 -0400 Subject: [PATCH 2/4] add changeset --- .changeset/shaky-carrots-invite.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/shaky-carrots-invite.md diff --git a/.changeset/shaky-carrots-invite.md b/.changeset/shaky-carrots-invite.md new file mode 100644 index 000000000..d88e4962c --- /dev/null +++ b/.changeset/shaky-carrots-invite.md @@ -0,0 +1,5 @@ +--- +'@radix-ui/react-one-time-password-field': patch +--- + +Fix iOS Chrome autocomplete (#3641) From cfe974ecd302621254efaf4abc628dd80409ecf3 Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Wed, 13 Aug 2025 16:04:41 -0400 Subject: [PATCH 3/4] =?UTF-8?q?Make=20sure=20iOS=20chrome=20autocomplete?= =?UTF-8?q?=20doesn=E2=80=99t=20focus=20the=20wrong=20input?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - iOS Chrome fires onChange for the first input after firing `onInput` with the full code - If unchecked, the onChange handler will result in the _second_ input being pressed, after the PASTE action handler already focused the _final_ input --- .../src/one-time-password-field.tsx | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/react/one-time-password-field/src/one-time-password-field.tsx b/packages/react/one-time-password-field/src/one-time-password-field.tsx index d411ee160..1ea5acc55 100644 --- a/packages/react/one-time-password-field/src/one-time-password-field.tsx +++ b/packages/react/one-time-password-field/src/one-time-password-field.tsx @@ -612,7 +612,9 @@ const OneTimePasswordFieldInput = React.forwardRef< const keyboardActionTimeoutRef = React.useRef(null); React.useEffect(() => { return () => { - window.clearTimeout(keyboardActionTimeoutRef.current!); + if (keyboardActionTimeoutRef.current) { + window.clearTimeout(keyboardActionTimeoutRef.current); + } }; }, []); @@ -664,11 +666,9 @@ const OneTimePasswordFieldInput = React.forwardRef< // In this case the value will be cleared, but we don't want to // set it directly because the user may want to prevent default // behavior in the onChange handler. The userActionRef will - // is set temporarily so the change handler can behave correctly + // be set temporarily so the change handler can behave correctly // in response to the action. - userActionRef.current = { - type: 'cut', - }; + userActionRef.current = { type: 'cut' }; // Set a short timeout to clear the action tracker after the change // handler has had time to complete. keyboardActionTimeoutRef.current = window.setTimeout(() => { @@ -684,7 +684,11 @@ const OneTimePasswordFieldInput = React.forwardRef< // additional input. Handle this the same as if a user were // pasting a value. event.preventDefault(); + userActionRef.current = { type: 'paste' }; dispatch({ type: 'PASTE', value }); + keyboardActionTimeoutRef.current = window.setTimeout(() => { + userActionRef.current = null; + }, 10); } })} onChange={composeEventHandlers(props.onChange, (event) => { @@ -696,11 +700,16 @@ const OneTimePasswordFieldInput = React.forwardRef< if (action) { switch (action.type) { case 'cut': - // TODO: do we want to assume the user wantt to clear the + // TODO: do we want to assume the user wants to clear the // entire value here and copy the code to the clipboard instead // of just the value of the given input? dispatch({ type: 'CLEAR_CHAR', index, reason: 'Cut' }); return; + case 'paste': + // the PASTE handler will already set the value and focus the final + // input; we want to skip focusing the wrong element if the browser fires + // onChange for the first input. This sometimes happens during autocomplete. + return; case 'keydown': { if (action.key === 'Char') { // update resulting from a keydown event that set a value @@ -718,6 +727,7 @@ const OneTimePasswordFieldInput = React.forwardRef< return; } default: + action satisfies never; return; } } @@ -929,7 +939,8 @@ type KeyboardActionDetails = metaKey: boolean; ctrlKey: boolean; } - | { type: 'cut' }; + | { type: 'cut' } + | { type: 'paste' }; type UpdateAction = | { From ba7a89b9f6a287721348e680a61934d767d96aa9 Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Wed, 13 Aug 2025 16:23:21 -0400 Subject: [PATCH 4/4] rename type in KeyboardActionDetails to clarify purpose --- .../one-time-password-field/src/one-time-password-field.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react/one-time-password-field/src/one-time-password-field.tsx b/packages/react/one-time-password-field/src/one-time-password-field.tsx index 1ea5acc55..ad7c4b596 100644 --- a/packages/react/one-time-password-field/src/one-time-password-field.tsx +++ b/packages/react/one-time-password-field/src/one-time-password-field.tsx @@ -684,7 +684,7 @@ const OneTimePasswordFieldInput = React.forwardRef< // additional input. Handle this the same as if a user were // pasting a value. event.preventDefault(); - userActionRef.current = { type: 'paste' }; + userActionRef.current = { type: 'autocomplete-paste' }; dispatch({ type: 'PASTE', value }); keyboardActionTimeoutRef.current = window.setTimeout(() => { userActionRef.current = null; @@ -705,7 +705,7 @@ const OneTimePasswordFieldInput = React.forwardRef< // of just the value of the given input? dispatch({ type: 'CLEAR_CHAR', index, reason: 'Cut' }); return; - case 'paste': + case 'autocomplete-paste': // the PASTE handler will already set the value and focus the final // input; we want to skip focusing the wrong element if the browser fires // onChange for the first input. This sometimes happens during autocomplete. @@ -940,7 +940,7 @@ type KeyboardActionDetails = ctrlKey: boolean; } | { type: 'cut' } - | { type: 'paste' }; + | { type: 'autocomplete-paste' }; type UpdateAction = | {