Skip to content

Commit fb9d540

Browse files
committed
chore: rewrite, simplify code
1 parent a7174c3 commit fb9d540

File tree

4 files changed

+149
-181
lines changed

4 files changed

+149
-181
lines changed

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@
9191
"pin-code",
9292
"authentication-code",
9393
"input",
94-
"autocompletion"
94+
"autocompletion",
95+
"otp",
96+
"otp-code",
97+
"one-time-password"
9598
]
9699
}

playground/src/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import ReactInputVerificationCode from 'react-input-verification-code';
55
import './index.css';
66

77
ReactDOM.render(
8-
<ReactInputVerificationCode autoFocus onChange={console.log} />,
8+
<ReactInputVerificationCode
9+
autoFocus
10+
onChange={(e) => console.log('onChange', e)}
11+
onCompleted={(e) => console.log('onCompleted', e)}
12+
/>,
913
document.getElementById('root')
1014
);

src/index.tsx

Lines changed: 135 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
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

1113
export interface ReactInputVerificationCodeProps {
1214
autoFocus?: boolean;
@@ -20,204 +22,191 @@ export interface ReactInputVerificationCodeProps {
2022
}
2123

2224
const 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

Comments
 (0)