@@ -3,7 +3,7 @@ import type { PropsWithChildren } from 'react';
3
3
import React , { useCallback } from 'react' ;
4
4
5
5
import type { LocalizationKey } from '../customizables' ;
6
- import { descriptors , Flex , Input } from '../customizables' ;
6
+ import { Box , descriptors , Flex , Input } from '../customizables' ;
7
7
import { useCardState } from '../elements/contexts' ;
8
8
import { useLoadingStatus } from '../hooks' ;
9
9
import type { PropsOfComponent } from '../styledSystem' ;
@@ -160,6 +160,7 @@ export const OTPResendButton = () => {
160
160
export const OTPCodeControl = React . forwardRef < { reset : any } > ( ( _ , ref ) => {
161
161
const [ disabled , setDisabled ] = React . useState ( false ) ;
162
162
const refs = React . useRef < Array < HTMLInputElement | null > > ( [ ] ) ;
163
+ const hiddenInputRef = React . useRef < HTMLInputElement > ( null ) ;
163
164
const firstClickRef = React . useRef ( false ) ;
164
165
165
166
const { otpControl, isLoading, isDisabled, centerAlign = true } = useOTPInputContext ( ) ;
@@ -169,6 +170,11 @@ export const OTPCodeControl = React.forwardRef<{ reset: any }>((_, ref) => {
169
170
reset : ( ) => {
170
171
setValues ( values . map ( ( ) => '' ) ) ;
171
172
setDisabled ( false ) ;
173
+
174
+ if ( hiddenInputRef . current ) {
175
+ hiddenInputRef . current . value = '' ;
176
+ }
177
+
172
178
setTimeout ( ( ) => focusInputAt ( 0 ) , 0 ) ;
173
179
} ,
174
180
} ) ) ;
@@ -183,6 +189,13 @@ export const OTPCodeControl = React.forwardRef<{ reset: any }>((_, ref) => {
183
189
}
184
190
} , [ feedback ] ) ;
185
191
192
+ // Update hidden input when values change
193
+ React . useEffect ( ( ) => {
194
+ if ( hiddenInputRef . current ) {
195
+ hiddenInputRef . current . value = values . join ( '' ) ;
196
+ }
197
+ } , [ values ] ) ;
198
+
186
199
const handleMultipleCharValue = ( { eventValue, inputPosition } : { eventValue : string ; inputPosition : number } ) => {
187
200
const eventValues = ( eventValue || '' ) . split ( '' ) ;
188
201
@@ -274,40 +287,91 @@ export const OTPCodeControl = React.forwardRef<{ reset: any }>((_, ref) => {
274
287
}
275
288
} ;
276
289
290
+ // Handle hidden input changes (for password manager autofill)
291
+ const handleHiddenInputChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
292
+ const value = e . target . value . replace ( / \D / g, '' ) . slice ( 0 , length ) ;
293
+ const newValues = value . split ( '' ) . concat ( Array . from ( { length : length - value . length } , ( ) => '' ) ) ;
294
+ setValues ( newValues ) ;
295
+
296
+ // Focus the appropriate visible input
297
+ if ( value . length > 0 ) {
298
+ focusInputAt ( Math . min ( value . length - 1 , length - 1 ) ) ;
299
+ }
300
+ } ;
301
+
277
302
const centerSx = centerAlign ? { justifyContent : 'center' , alignItems : 'center' } : { } ;
278
303
279
304
return (
280
- < Flex
281
- isLoading = { isLoading }
282
- hasError = { feedbackType === 'error' }
283
- elementDescriptor = { descriptors . otpCodeFieldInputs }
284
- gap = { 2 }
285
- sx = { t => ( { direction : 'ltr' , padding : t . space . $1 , marginLeft : `-${ t . space . $1 } ` , ...centerSx } ) }
305
+ < Box
306
+ elementDescriptor = { descriptors . otpCodeFieldInputContainer }
307
+ sx = { { position : 'relative' } }
286
308
>
287
- { values . map ( ( value , index : number ) => (
288
- < SingleCharInput
289
- elementDescriptor = { descriptors . otpCodeFieldInput }
290
- key = { index }
291
- value = { value }
292
- onClick = { handleOnClick ( index ) }
293
- onChange = { handleOnChange ( index ) }
294
- onKeyDown = { handleOnKeyDown ( index ) }
295
- onInput = { handleOnInput ( index ) }
296
- onPaste = { handleOnPaste ( index ) }
297
- id = { `digit-${ index } -field` }
298
- ref = { node => ( refs . current [ index ] = node ) }
299
- autoFocus = { index === 0 || undefined }
300
- autoComplete = 'one-time-code'
301
- aria-label = { `${ index === 0 ? 'Enter verification code. ' : '' } Digit ${ index + 1 } ` }
302
- isDisabled = { isDisabled || isLoading || disabled || feedbackType === 'success' }
303
- hasError = { feedbackType === 'error' }
304
- isSuccessfullyFilled = { feedbackType === 'success' }
305
- type = 'text'
306
- inputMode = 'numeric'
307
- name = { `codeInput-${ index } ` }
308
- />
309
- ) ) }
310
- </ Flex >
309
+ { /* Hidden input for password manager compatibility */ }
310
+ < Input
311
+ ref = { hiddenInputRef }
312
+ type = 'text'
313
+ autoComplete = 'one-time-code'
314
+ data-otp-hidden-input
315
+ inputMode = 'numeric'
316
+ pattern = { `[0-9]{${ length } }` }
317
+ minLength = { length }
318
+ maxLength = { length }
319
+ spellCheck = { false }
320
+ aria-hidden = 'true'
321
+ tabIndex = { - 1 }
322
+ onChange = { handleHiddenInputChange }
323
+ onFocus = { ( ) => {
324
+ // When password manager focuses the hidden input, focus the first visible input
325
+ focusInputAt ( 0 ) ;
326
+ } }
327
+ sx = { ( ) => ( {
328
+ ...common . visuallyHidden ( ) ,
329
+ left : '-9999px' ,
330
+ pointerEvents : 'none' ,
331
+ } ) }
332
+ />
333
+
334
+ < Flex
335
+ isLoading = { isLoading }
336
+ hasError = { feedbackType === 'error' }
337
+ elementDescriptor = { descriptors . otpCodeFieldInputs }
338
+ gap = { 2 }
339
+ sx = { t => ( { direction : 'ltr' , padding : t . space . $1 , marginLeft : `-${ t . space . $1 } ` , ...centerSx } ) }
340
+ role = 'group'
341
+ aria-label = 'Verification code input'
342
+ >
343
+ { values . map ( ( value : string , index : number ) => (
344
+ < SingleCharInput
345
+ elementDescriptor = { descriptors . otpCodeFieldInput }
346
+ // eslint-disable-next-line react/no-array-index-key
347
+ key = { index }
348
+ value = { value }
349
+ onClick = { handleOnClick ( index ) }
350
+ onChange = { handleOnChange ( index ) }
351
+ onKeyDown = { handleOnKeyDown ( index ) }
352
+ onInput = { handleOnInput ( index ) }
353
+ onPaste = { handleOnPaste ( index ) }
354
+ id = { `digit-${ index } -field` }
355
+ ref = { node => ( refs . current [ index ] = node ) }
356
+ // eslint-disable-next-line jsx-a11y/no-autofocus
357
+ autoFocus = { index === 0 || undefined }
358
+ autoComplete = 'off'
359
+ aria-label = { `${ index === 0 ? 'Enter verification code. ' : '' } Digit ${ index + 1 } ` }
360
+ isDisabled = { isDisabled || isLoading || disabled || feedbackType === 'success' }
361
+ hasError = { feedbackType === 'error' }
362
+ isSuccessfullyFilled = { feedbackType === 'success' }
363
+ type = 'text'
364
+ inputMode = 'numeric'
365
+ name = { `codeInput-${ index } ` }
366
+ data-otp-segment
367
+ data-1p-ignore
368
+ data-lpignore = 'true'
369
+ maxLength = { 1 }
370
+ pattern = '[0-9]'
371
+ />
372
+ ) ) }
373
+ </ Flex >
374
+ </ Box >
311
375
) ;
312
376
} ) ;
313
377
0 commit comments