1- import * as React from 'react' ;
2- import * as S from './styles' ;
1+ import React , {
2+ ChangeEvent ,
3+ createRef ,
4+ Fragment ,
5+ KeyboardEvent ,
6+ useEffect ,
7+ useMemo ,
8+ useState ,
9+ } from 'react' ;
310
4- const KEY_CODE = {
5- BACKSPACE : 8 ,
6- ARROW_LEFT : 37 ,
7- ARROW_RIGHT : 39 ,
8- DELETE : 46 ,
9- } ;
11+ import * as S from './styles' ;
1012
1113export interface ReactInputVerificationCodeProps {
1214 autoFocus ?: boolean ;
@@ -20,204 +22,191 @@ export interface ReactInputVerificationCodeProps {
2022}
2123
2224const ReactInputVerificationCode = ( {
23- autoFocus = false ,
25+ autoFocus = true ,
2426 length = 4 ,
2527 onChange = ( ) => { } ,
2628 onCompleted = ( ) => { } ,
2729 placeholder = '·' ,
28- value : pValue ,
30+ // value: pValue,
2931 dataCy = 'verification-code' ,
30- type = 'text' ,
31- } : ReactInputVerificationCodeProps ) => {
32- const emptyValue = new Array ( length ) . fill ( placeholder ) ;
32+ } : // type = 'text',
33+ ReactInputVerificationCodeProps ) => {
34+ const [ values , setValues ] = useState ( new Array ( length ) . fill ( placeholder ) ) ;
3335
34- const [ activeIndex , setActiveIndex ] = React . useState < number > ( - 1 ) ;
35- const [ value , setValue ] = React . useState < string [ ] > (
36- pValue ? pValue . split ( '' ) : emptyValue
37- ) ;
36+ const [ focusedIndex , setFocusedIndex ] = useState < number > ( - 1 ) ;
3837
39- const codeInputRef = React . createRef < HTMLInputElement > ( ) ;
40- const itemsRef = React . useMemo (
41- ( ) =>
42- new Array ( length ) . fill ( null ) . map ( ( ) => React . createRef < HTMLDivElement > ( ) ) ,
38+ // const codeInputRef = createRef<HTMLInputElement>();
39+
40+ const inputsRefs = useMemo (
41+ ( ) => new Array ( length ) . fill ( null ) . map ( ( ) => createRef < HTMLInputElement > ( ) ) ,
4342 [ length ]
4443 ) ;
4544
46- const isCodeRegex = new RegExp ( `^[0-9]{${ length } }$` ) ;
47-
48- const getItem = ( index : number ) => itemsRef [ index ] ?. current ;
49- const focusItem = ( index : number ) : void => getItem ( index ) ?. focus ( ) ;
50- const blurItem = ( index : number ) : void => getItem ( index ) ?. blur ( ) ;
51-
52- const onItemFocus = ( index : number ) => ( ) => {
53- setActiveIndex ( index ) ;
54- if ( codeInputRef . current ) codeInputRef . current . focus ( ) ;
55- } ;
56-
57- const onInputKeyUp = ( { key, keyCode } : React . KeyboardEvent ) => {
58- const newValue = [ ...value ] ;
59- const nextIndex = activeIndex + 1 ;
60- const prevIndex = activeIndex - 1 ;
45+ // handle mobile autocompletion
46+ // const onInputChange = (e: ChangeEvent<HTMLInputElement>) => {
47+ // const { value: changeValue } = e.target;
48+ // const isCode = isCodeRegex.test(changeValue);
6149
62- const codeInput = codeInputRef . current ;
63- const currentItem = getItem ( activeIndex ) ;
50+ // if (!isCode) return;
6451
65- const isLast = nextIndex === length ;
66- const isDeleting =
67- keyCode === KEY_CODE . DELETE || keyCode === KEY_CODE . BACKSPACE ;
52+ // setValue(changeValue.split('')) ;
53+ // blurItem(activeIndex);
54+ // } ;
6855
69- // keep items focus in sync
70- onItemFocus ( activeIndex ) ;
56+ const setValue = ( value : string , index : number ) => {
57+ const nextValues = [ ...values ] ;
58+ nextValues [ index ] = value ;
7159
72- // on delete, replace the current value
73- // and focus on the previous item
74- if ( isDeleting ) {
75- newValue [ activeIndex ] = placeholder ;
76- setValue ( newValue ) ;
60+ setValues ( nextValues ) ;
7761
78- if ( activeIndex > 0 ) {
79- setActiveIndex ( prevIndex ) ;
80- focusItem ( prevIndex ) ;
81- }
62+ const stringifiedValues = nextValues . join ( '' ) ;
63+ const isCompleted = ! stringifiedValues . includes ( placeholder ) ;
8264
65+ if ( isCompleted ) {
66+ onCompleted ( stringifiedValues ) ;
8367 return ;
8468 }
8569
86- // if the key pressed is not a number
87- // don't do anything
88- if ( Number . isNaN ( + key ) ) return ;
70+ onChange ( stringifiedValues ) ;
71+ } ;
8972
90- // reset the current value
91- // and set the new one
92- if ( codeInput ) codeInput . value = '' ;
93- newValue [ activeIndex ] = key ;
94- setValue ( newValue ) ;
73+ const focusInput = ( index : number ) => {
74+ const input = inputsRefs [ index ] ?. current ;
9575
96- if ( ! isLast ) {
97- setActiveIndex ( nextIndex ) ;
98- focusItem ( nextIndex ) ;
99- return ;
76+ if ( input ) {
77+ requestAnimationFrame ( ( ) => {
78+ input . focus ( ) ;
79+ } ) ;
10080 }
101-
102- if ( codeInput ) codeInput . blur ( ) ;
103- if ( currentItem ) currentItem . blur ( ) ;
104-
105- setActiveIndex ( - 1 ) ;
10681 } ;
10782
108- // handle mobile autocompletion
109- const onInputChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
110- const { value : changeValue } = e . target ;
111- const isCode = isCodeRegex . test ( changeValue ) ;
83+ const blurInput = ( index : number ) => {
84+ const input = inputsRefs [ index ] ?. current ;
11285
113- if ( ! isCode ) return ;
114-
115- setValue ( changeValue . split ( '' ) ) ;
116- blurItem ( activeIndex ) ;
86+ if ( input ) {
87+ requestAnimationFrame ( ( ) => {
88+ input . blur ( ) ;
89+ } ) ;
90+ }
11791 } ;
11892
119- const onInputBlur = ( ) => {
120- // https://github.com/ugogo/react-input-verification-code/issues/1
121- if ( activeIndex === - 1 ) return ;
93+ const onInputFocus = ( index : number ) => {
94+ const input = inputsRefs [ index ] ?. current ;
12295
123- blurItem ( activeIndex ) ;
124- setActiveIndex ( - 1 ) ;
96+ if ( input ) {
97+ setFocusedIndex ( index ) ;
98+ input . select ( ) ;
99+ }
125100 } ;
126101
127- // autoFocus
128- React . useEffect ( ( ) => {
129- if ( autoFocus && itemsRef [ 0 ] . current ) {
130- itemsRef [ 0 ] . current . focus ( ) ;
102+ const onInputChange = (
103+ event : ChangeEvent < HTMLInputElement > ,
104+ index : number
105+ ) => {
106+ const eventValue = event . target . value ;
107+ /**
108+ * ensure we only display 1 character in the input
109+ * by clearing the already setted value
110+ */
111+ const value = eventValue . replace ( values [ index ] , '' ) ;
112+
113+ setValue ( value , index ) ;
114+
115+ /**
116+ * if the input is the last of the list
117+ * blur it, otherwise focus the next one
118+ */
119+ if ( index === length - 1 ) {
120+ blurInput ( index ) ;
121+ return ;
131122 }
132- } , [ ] ) ;
133123
134- // handle pasting
135- React . useEffect ( ( ) => {
136- const codeInput = codeInputRef . current ;
137- if ( ! codeInput ) return ;
124+ focusInput ( index + 1 ) ;
125+ } ;
126+
127+ const onInputKeyDown = ( event : KeyboardEvent , index : number ) => {
128+ const eventKey = event . key ;
138129
139- const onPaste = ( e : ClipboardEvent ) => {
140- e . preventDefault ( ) ;
130+ if ( eventKey === 'Backspace' || eventKey === 'Delete' ) {
131+ /**
132+ * prevent trigger a change event
133+ * `onInputChange` won't be called
134+ */
135+ event . preventDefault ( ) ;
141136
142- const pastedString = e . clipboardData ?. getData ( 'text' ) ;
143- if ( ! pastedString ) return ;
137+ setValue ( placeholder , focusedIndex ) ;
138+ focusInput ( index - 1 ) ;
144139
145- const isNumber = / ^ \d + $ / . test ( pastedString ) ;
146- if ( isNumber ) setValue ( pastedString . split ( '' ) . slice ( 0 , length ) ) ;
147- } ;
140+ return ;
141+ }
148142
149- codeInput . addEventListener ( 'paste' , onPaste ) ;
150- return ( ) => codeInput . removeEventListener ( 'paste' , onPaste ) ;
151- } , [ ] ) ;
143+ /**
144+ * since the value won't change, `onInputChange` won't be called
145+ * only focus the next input
146+ */
147+ if ( eventKey === values [ index ] ) {
148+ focusInput ( index + 1 ) ;
149+ }
150+ } ;
152151
153- React . useEffect ( ( ) => {
154- const stringValue = value . join ( '' ) ;
155- const isCompleted = stringValue . length === length ;
152+ /**
153+ * autoFocus
154+ */
155+ useEffect ( ( ) => {
156+ if ( autoFocus ) {
157+ focusInput ( 0 ) ;
158+ }
159+ } , [ inputsRefs ] ) ;
156160
157- if ( isCompleted && stringValue !== emptyValue . join ( '' ) )
158- onCompleted ( stringValue ) ;
159- onChange ( stringValue ) ;
160- } , [ value , length ] ) ;
161+ // handle pasting
162+ // useEffect(() => {
163+ // const codeInput = codeInputRef.current ;
164+ // if (!codeInput) return ;
161165
162- React . useEffect ( ( ) => {
163- if ( typeof pValue !== 'string' ) return ;
166+ // const onPaste = (e: ClipboardEvent ) => {
167+ // e.preventDefault() ;
164168
165- // avoid infinite loop
166- if ( pValue === '' && value . join ( '' ) === emptyValue . join ( '' ) ) return ;
169+ // const pastedString = e.clipboardData?.getData('text');
170+ // if (!pastedString ) return;
167171
168- // keep internal and external states in sync
169- if ( pValue !== value . join ( '' ) ) setValue ( pValue . split ( '' ) ) ;
170- } , [ pValue ] ) ;
172+ // const isNumber = /^\d+$/.test(pastedString);
173+ // if (isNumber) setValue(pastedString .split('').slice(0, length ));
174+ // } ;
171175
172- const renderItemText = ( itemValue : string ) => {
173- if ( itemValue === placeholder ) return placeholder ;
174- return type === 'password' ? placeholder : itemValue ;
175- } ;
176+ // codeInput.addEventListener('paste', onPaste);
177+ // return () => codeInput.removeEventListener('paste', onPaste);
178+ // }, []);
176179
177180 return (
178- < React . Fragment >
181+ < Fragment >
179182 < S . GlobalStyle />
180183
181- < S . Container
182- className = 'ReactInputVerificationCode__container'
183- // needed for styling
184- itemsCount = { length }
185- >
186- < S . Input
187- ref = { codeInputRef }
184+ < S . Container className = 'ReactInputVerificationCode__container' >
185+ { /* <S.Input
186+ // ref={codeInputRef}
188187 className='ReactInputVerificationCode__input'
189188 autoComplete='one-time-code'
190189 type='text'
191190 inputMode='decimal'
192191 id='one-time-code'
193- // use onKeyUp rather than onChange for a better control
194- // onChange is still needed to handle the autocompletion
195- // when receiving a code by SMS
196- onChange = { onInputChange }
197- onKeyUp = { onInputKeyUp }
198- onBlur = { onInputBlur }
199- // needed for styling
200192 activeIndex={activeIndex}
201193 data-cy={`${dataCy}-otc-input`}
202- />
194+ /> */ }
203195
204- { itemsRef . map ( ( ref , i ) => (
196+ { inputsRefs . map ( ( ref , i ) => (
205197 < S . Item
206198 key = { i }
207199 ref = { ref }
208- role = 'button'
209- tabIndex = { 0 }
210- className = { `ReactInputVerificationCode__item ${
211- value [ i ] !== placeholder ? 'is-filled' : ''
212- } ${ i === activeIndex ? 'is-active' : '' } `}
213- onFocus = { onItemFocus ( i ) }
200+ className = 'ReactInputVerificationCode__item'
214201 data-cy = { `${ dataCy } -${ i } -item` }
215- >
216- { renderItemText ( value [ i ] ) }
217- </ S . Item >
202+ value = { values [ i ] }
203+ onChange = { ( event ) => onInputChange ( event , i ) }
204+ onFocus = { ( ) => onInputFocus ( i ) }
205+ onKeyDown = { ( event ) => onInputKeyDown ( event , i ) }
206+ />
218207 ) ) }
219208 </ S . Container >
220- </ React . Fragment >
209+ </ Fragment >
221210 ) ;
222211} ;
223212
0 commit comments