@@ -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,34 +590,32 @@ 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 ) )
@@ -639,8 +632,10 @@ export class InputOTP implements ComponentInterface {
639632 } ) ;
640633 }
641634
642- // Update the value of the input group and emit the input change event
643- this . value = validChars . join ( '' ) ;
635+ for ( let i = 0 ; i < length ; i ++ ) {
636+ this . inputValues [ i ] = validChars [ i ] || '' ;
637+ this . inputRefs [ i ] . value = validChars [ i ] || '' ;
638+ }
644639 this . updateValue ( event ) ;
645640
646641 // Focus the first empty input box or the last input box if all boxes
@@ -651,23 +646,85 @@ export class InputOTP implements ComponentInterface {
651646 this . inputRefs [ nextIndex ] ?. focus ( ) ;
652647 } , 20 ) ;
653648
649+ this . previousInputValues = [ ...this . inputValues ] ;
654650 return ;
655651 }
656652
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 ] = '' ;
653+ // 2. Input validation
654+ // If the character entered is invalid (does not match the pattern),
655+ // restore the previous value and exit
656+ if ( value . length > 0 && ! validKeyPattern . test ( value [ value . length - 1 ] ) ) {
657+ input . value = this . inputValues [ index ] || '' ;
658+ this . previousInputValues = [ ...this . inputValues ] ;
661659 return ;
662660 }
663661
664- // For single character input, fill the current box
665- this . inputValues [ index ] = value ;
666- this . updateValue ( event ) ;
667-
668- if ( value . length > 0 ) {
662+ // 3. Full selection replacement or typing in an empty box
663+ // If the user selects all text in the input box and types, or if the
664+ // input box is empty, replace only this input box. If the box is empty,
665+ // move to the next box, otherwise stay focused on this box.
666+ const isAllSelected = input . selectionStart === 0 && input . selectionEnd === value . length ;
667+ const isEmpty = ! this . inputValues [ index ] ;
668+ if ( isAllSelected || isEmpty ) {
669+ this . inputValues [ index ] = value ;
670+ input . value = value ;
671+ this . updateValue ( event ) ;
669672 this . focusNext ( index ) ;
673+ this . previousInputValues = [ ...this . inputValues ] ;
674+ return ;
670675 }
676+
677+ // 4. Inserting in the middle with available space (shifting)
678+ // If typing in a filled input box and there are empty boxes at the end,
679+ // shift all values starting at the current box to the right, and insert
680+ // the new character at the current box.
681+ const hasAvailableBoxAtEnd = this . inputValues [ this . inputValues . length - 1 ] === '' ;
682+ if ( this . inputValues [ index ] && hasAvailableBoxAtEnd && value . length === 2 ) {
683+ // Get the inserted character (from event or by diffing value/previousValue)
684+ let newChar = ( event as InputEvent ) . data ;
685+ if ( ! newChar ) {
686+ newChar = value . split ( '' ) . find ( ( c , i ) => c !== previousValue [ i ] ) || value [ value . length - 1 ] ;
687+ }
688+ // Validate the new character before shifting
689+ if ( ! validKeyPattern . test ( newChar ) ) {
690+ input . value = this . inputValues [ index ] || '' ;
691+ this . previousInputValues = [ ...this . inputValues ] ;
692+ return ;
693+ }
694+ // Shift values right from the end to the insertion point
695+ for ( let i = this . inputValues . length - 1 ; i > index ; i -- ) {
696+ this . inputValues [ i ] = this . inputValues [ i - 1 ] ;
697+ this . inputRefs [ i ] . value = this . inputValues [ i ] || '' ;
698+ }
699+ this . inputValues [ index ] = newChar ;
700+ this . inputRefs [ index ] . value = newChar ;
701+ this . updateValue ( event ) ;
702+ this . previousInputValues = [ ...this . inputValues ] ;
703+ return ;
704+ }
705+
706+ // 5. Single character replacement
707+ // Handles replacing a single character in a box containing a value based
708+ // on the cursor position. We need the cursor position to determine which
709+ // character was the last character typed. For example, if the user types "2"
710+ // in an input box with the cursor at the beginning of the value of "6",
711+ // the value will be "26", but we want to grab the "2" as the last character
712+ // typed.
713+ const cursorPos = input . selectionStart ?? value . length ;
714+ const newCharIndex = cursorPos - 1 ;
715+ const newChar = value [ newCharIndex ] ?? value [ 0 ] ;
716+
717+ // Check if the new character is valid before updating the value
718+ if ( ! validKeyPattern . test ( newChar ) ) {
719+ input . value = this . inputValues [ index ] || '' ;
720+ this . previousInputValues = [ ...this . inputValues ] ;
721+ return ;
722+ }
723+
724+ this . inputValues [ index ] = newChar ;
725+ input . value = newChar ;
726+ this . updateValue ( event ) ;
727+ this . previousInputValues = [ ...this . inputValues ] ;
671728 } ;
672729
673730 /**
@@ -711,12 +768,8 @@ export class InputOTP implements ComponentInterface {
711768
712769 // Focus the next empty input after pasting
713770 // If all boxes are filled, focus the last input
714- const nextEmptyIndex = validChars . length ;
715- if ( nextEmptyIndex < length ) {
716- inputRefs [ nextEmptyIndex ] ?. focus ( ) ;
717- } else {
718- inputRefs [ length - 1 ] ?. focus ( ) ;
719- }
771+ const nextEmptyIndex = validChars . length < length ? validChars . length : length - 1 ;
772+ inputRefs [ nextEmptyIndex ] ?. focus ( ) ;
720773 } ;
721774
722775 /**
0 commit comments