@@ -48,6 +48,7 @@ export class InputOTP implements ComponentInterface {
4848
4949 @State ( ) private inputValues : string [ ] = [ ] ;
5050 @State ( ) hasFocus = false ;
51+ @State ( ) private previousInputValues : string [ ] = [ ] ;
5152
5253 /**
5354 * Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user.
@@ -336,6 +337,7 @@ export class InputOTP implements ComponentInterface {
336337 } ) ;
337338 // Update the value without emitting events
338339 this . value = this . inputValues . join ( '' ) ;
340+ this . previousInputValues = [ ...this . inputValues ] ;
339341 }
340342
341343 /**
@@ -525,19 +527,12 @@ export class InputOTP implements ComponentInterface {
525527 }
526528
527529 /**
528- * Handles keyboard navigation and input for the OTP component.
530+ * Handles keyboard navigation for the OTP component.
529531 *
530532 * Navigation:
531533 * - Backspace: Clears current input and moves to previous box if empty
532534 * - Arrow Left/Right: Moves focus between input boxes
533535 * - Tab: Allows normal tab navigation between components
534- *
535- * Input Behavior:
536- * - Validates input against the allowed pattern
537- * - When entering a key in a filled box:
538- * - Shifts existing values right if there is room
539- * - Updates the value of the input group
540- * - Prevents default behavior to avoid automatic focus shift
541536 */
542537 private onKeyDown = ( index : number ) => ( event : KeyboardEvent ) => {
543538 const { length } = this ;
@@ -595,79 +590,139 @@ export class InputOTP implements ComponentInterface {
595590 // Let all tab events proceed normally
596591 return ;
597592 }
598-
599- // If the input box contains a value and the key being
600- // entered is a valid key for the input box update the value
601- // and shift the values to the right if there is room.
602- if ( this . inputValues [ index ] && this . validKeyPattern . test ( event . key ) ) {
603- if ( ! this . inputValues [ length - 1 ] ) {
604- for ( let i = length - 1 ; i > index ; i -- ) {
605- this . inputValues [ i ] = this . inputValues [ i - 1 ] ;
606- this . inputRefs [ i ] . value = this . inputValues [ i ] || '' ;
607- }
608- }
609- this . inputValues [ index ] = event . key ;
610- this . inputRefs [ index ] . value = event . key ;
611- this . updateValue ( event ) ;
612-
613- // Prevent default to avoid the browser from
614- // automatically moving the focus to the next input
615- event . preventDefault ( ) ;
616- }
617593 } ;
618594
595+ /**
596+ * Processes all input scenarios for each input box.
597+ *
598+ * This function manages:
599+ * 1. Autofill handling
600+ * 2. Input validation
601+ * 3. Full selection replacement or typing in an empty box
602+ * 4. Inserting in the middle with available space (shifting)
603+ * 5. Single character replacement
604+ */
619605 private onInput = ( index : number ) => ( event : InputEvent ) => {
620606 const { length, validKeyPattern } = this ;
621- const value = ( event . target as HTMLInputElement ) . value ;
622-
623- // If the value is longer than 1 character (autofill), split it into
624- // characters and filter out invalid ones
625- if ( value . length > 1 ) {
607+ const input = event . target as HTMLInputElement ;
608+ const value = input . value ;
609+ const previousValue = this . previousInputValues [ index ] || '' ;
610+
611+ // 1. Autofill handling
612+ // If the length of the value increases by more than 1 from the previous
613+ // value, treat this as autofill. This is to prevent the case where the
614+ // user is typing a single character into an input box containing a value
615+ // as that will trigger this function with a value length of 2 characters.
616+ const isAutofill = value . length - previousValue . length > 1 ;
617+ if ( isAutofill ) {
618+ // Distribute valid characters across input boxes
626619 const validChars = value
627620 . split ( '' )
628621 . filter ( ( char ) => validKeyPattern . test ( char ) )
629622 . slice ( 0 , length ) ;
630-
631- // If there are no valid characters coming from the
632- // autofill, all input refs have to be cleared after the
633- // browser has finished the autofill behavior
634- if ( validChars . length === 0 ) {
635- requestAnimationFrame ( ( ) => {
636- this . inputRefs . forEach ( ( input ) => {
637- input . value = '' ;
638- } ) ;
639- } ) ;
623+ for ( let i = 0 ; i < length ; i ++ ) {
624+ this . inputValues [ i ] = validChars [ i ] || '' ;
625+ if ( this . inputRefs [ i ] != null ) {
626+ this . inputRefs [ i ] ! . value = validChars [ i ] || '' ;
627+ }
640628 }
641-
642- // Update the value of the input group and emit the input change event
643- this . value = validChars . join ( '' ) ;
644629 this . updateValue ( event ) ;
645-
646- // Focus the first empty input box or the last input box if all boxes
647- // are filled after a small delay to ensure the input boxes have been
648- // updated before moving the focus
630+ // Focus the next empty input or the last one
649631 setTimeout ( ( ) => {
650632 const nextIndex = validChars . length < length ? validChars . length : length - 1 ;
651- this . inputRefs [ nextIndex ] ?. focus ( ) ;
633+ if ( this . inputRefs [ nextIndex ] != null ) {
634+ this . inputRefs [ nextIndex ] ! . focus ( ) ;
635+ }
652636 } , 20 ) ;
637+ this . previousInputValues = [ ...this . inputValues ] ;
638+ return ;
639+ }
653640
641+ // 2. Input validation
642+ // If the character entered is invalid (does not match the pattern),
643+ // restore the previous value and exit
644+ if ( value . length > 0 && ! validKeyPattern . test ( value [ value . length - 1 ] ) ) {
645+ input . value = this . inputValues [ index ] || '' ;
646+ this . previousInputValues [ index ] = this . inputValues [ index ] || '' ;
647+ this . previousInputValues = [ ...this . inputValues ] ;
654648 return ;
655649 }
656650
657- // Only allow input if it matches the pattern
658- if ( value . length > 0 && ! validKeyPattern . test ( value ) ) {
659- this . inputRefs [ index ] . value = '' ;
660- this . inputValues [ index ] = '' ;
651+ // 3. Full selection replacement or typing in an empty box
652+ // If the user selects all text in the input box and types, or if the
653+ // input box is empty, replace only this input box. If the box is empty,
654+ // move to the next box, otherwise stay focused on this box.
655+ const isAllSelected = input . selectionStart === 0 && input . selectionEnd === value . length ;
656+ const isEmpty = ! this . inputValues [ index ] ;
657+ if ( isAllSelected || isEmpty ) {
658+ this . inputValues [ index ] = value ;
659+ input . value = value ;
660+ this . previousInputValues [ index ] = value ;
661+ this . updateValue ( event ) ;
662+ this . focusNext ( index ) ;
663+ this . previousInputValues = [ ...this . inputValues ] ;
661664 return ;
662665 }
663666
664- // For single character input, fill the current box
665- this . inputValues [ index ] = value ;
666- this . updateValue ( event ) ;
667+ // 4. Inserting in the middle with available space (shifting)
668+ // If typing in a filled input box and there are empty boxes at the end,
669+ // shift all values starting at the current box to the right, and insert
670+ // the new character at the current box.
671+ const hasAvailableBoxAtEnd = this . inputValues [ this . inputValues . length - 1 ] === '' ;
672+ if ( this . inputValues [ index ] && hasAvailableBoxAtEnd && value . length === 2 ) {
673+ // Get the inserted character (from event or by diffing value/previousValue)
674+ let newChar = ( event as InputEvent ) . data ;
675+ if ( ! newChar ) {
676+ newChar = value . split ( '' ) . find ( ( c , i ) => c !== previousValue [ i ] ) || value [ value . length - 1 ] ;
677+ }
678+ // Validate the new character before shifting
679+ if ( ! validKeyPattern . test ( newChar ) ) {
680+ input . value = this . inputValues [ index ] || '' ;
681+ this . previousInputValues [ index ] = this . inputValues [ index ] || '' ;
682+ this . previousInputValues = [ ...this . inputValues ] ;
683+ return ;
684+ }
685+ // Shift values right from the end to the insertion point
686+ for ( let i = this . inputValues . length - 1 ; i > index ; i -- ) {
687+ this . inputValues [ i ] = this . inputValues [ i - 1 ] ;
688+ if ( this . inputRefs [ i ] != null ) {
689+ this . inputRefs [ i ] ! . value = this . inputValues [ i ] || '' ;
690+ }
691+ }
692+ this . inputValues [ index ] = newChar ;
693+ if ( this . inputRefs [ index ] != null ) {
694+ this . inputRefs [ index ] ! . value = newChar ;
695+ }
696+ this . previousInputValues [ index ] = newChar ;
697+ this . updateValue ( event ) ;
698+ this . previousInputValues = [ ...this . inputValues ] ;
699+ return ;
700+ }
667701
668- if ( value . length > 0 ) {
669- this . focusNext ( index ) ;
702+ // 5. Single character replacement
703+ // Handles replacing a single character in a box containing a value based
704+ // on the cursor position. We need the cursor position to determine which
705+ // character was the last character typed. For example, if the user types "2"
706+ // in an input box with the cursor at the beginning of the value of "6",
707+ // the value will be "26", but we want to grab the "2" as the last character
708+ // typed.
709+ const cursorPos = input . selectionStart ?? value . length ;
710+ const newCharIndex = cursorPos - 1 ;
711+ const newChar = value [ newCharIndex ] ?? value [ 0 ] ;
712+
713+ // Check if the new character is valid before updating the value
714+ if ( ! validKeyPattern . test ( newChar ) ) {
715+ input . value = this . inputValues [ index ] || '' ;
716+ this . previousInputValues [ index ] = this . inputValues [ index ] || '' ;
717+ this . previousInputValues = [ ...this . inputValues ] ;
718+ return ;
670719 }
720+
721+ input . value = newChar ;
722+ this . inputValues [ index ] = newChar ;
723+ this . previousInputValues [ index ] = newChar ;
724+ this . updateValue ( event ) ;
725+ this . previousInputValues = [ ...this . inputValues ] ;
671726 } ;
672727
673728 /**
0 commit comments