Skip to content

Commit 42eb6c2

Browse files
author
ugogo
committed
build component
1 parent b9133bb commit 42eb6c2

File tree

2 files changed

+169
-6
lines changed

2 files changed

+169
-6
lines changed

src/index.css

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
.ReactVerificationCode__container {
2+
display: flex;
3+
position: relative;
4+
width: calc(
5+
var(--itemWidth) * var(--itemsCount) + var(--itemSpacing) *
6+
(var(--itemsCount) - 1)
7+
);
8+
justify-content: space-between;
9+
}
10+
11+
.ReactVerificationCode__input,
12+
.ReactVerificationCode__item {
13+
width: var(--itemWidth);
14+
height: var(--itemHeight);
15+
padding: 0;
16+
border-radius: 4px;
17+
font-size: 1.5rem;
18+
font-weight: 800;
19+
line-height: var(--itemHeight);
20+
text-align: center;
21+
}
22+
23+
.ReactVerificationCode__item {
24+
transition: box-shadow 0.2s ease-out;
25+
box-shadow: 0 0 0 1px inset #ccc;
26+
}
27+
28+
.ReactVerificationCode__item.is-active {
29+
box-shadow: 0 0 0 2px inset #888;
30+
}
31+
32+
.ReactVerificationCode__input {
33+
position: absolute;
34+
top: 0;
35+
left: calc(
36+
var(--activeIndex) * var(--itemWidth) + var(--itemSpacing) *
37+
var(--activeIndex)
38+
);
39+
opacity: 0;
40+
}

src/index.tsx

Lines changed: 129 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,133 @@
11
import * as React from 'react';
2-
import styles from './styles.module.css';
2+
import './index.css';
33

4-
interface Props {
5-
text: string;
6-
}
4+
const KEY_CODE = {
5+
BACKSPACE: 8,
6+
ARROW_LEFT: 37,
7+
ARROW_RIGHT: 39,
8+
DELETE: 46,
9+
};
10+
11+
export default ({ length = 4, placeholder = '·' }) => {
12+
const [activeIndex, setActiveIndex] = React.useState<number>(-1);
13+
const [value, setValue] = React.useState<string[]>(
14+
new Array(length).fill(placeholder)
15+
);
16+
17+
const codeInputRef = React.createRef<HTMLInputElement>();
18+
const itemsRef = React.useMemo(
19+
() => [...new Array(length)].map(() => React.createRef<HTMLDivElement>()),
20+
[length]
21+
);
22+
23+
const isCodeRegex = new RegExp(`^[0-9]{${length}}$`);
24+
25+
const getItem = (index: number) => itemsRef[index].current!;
26+
const focusItem = (index: number): void => getItem(index).focus();
27+
const blurItem = (index: number): void => getItem(index).blur();
28+
29+
const onItemFocus = (index: number) => () => {
30+
setActiveIndex(index);
31+
if (codeInputRef.current) codeInputRef.current.focus();
32+
};
33+
34+
const onItemKeyUp = ({ key, keyCode }: React.KeyboardEvent) => {
35+
const newValue = [...value];
36+
const nextIndex = activeIndex + 1;
37+
const prevIndex = activeIndex - 1;
38+
39+
const isLast = nextIndex === length;
40+
const isDeleting =
41+
keyCode === KEY_CODE.DELETE || keyCode === KEY_CODE.BACKSPACE;
42+
43+
// keep items focus in sync
44+
onItemFocus(activeIndex);
45+
46+
// on delete, replace the current value
47+
// and focus on the previous item
48+
if (isDeleting) {
49+
newValue[activeIndex] = placeholder;
50+
setValue(newValue);
51+
52+
if (activeIndex > 0) {
53+
setActiveIndex(prevIndex);
54+
focusItem(prevIndex);
55+
}
56+
57+
return;
58+
}
59+
60+
// if the key pressed is not a number
61+
// don't do anything
62+
if (Number.isNaN(+key)) return;
63+
64+
// reset the current value
65+
// and set the new one
66+
if (codeInputRef.current) codeInputRef.current.value = '';
67+
newValue[activeIndex] = key;
68+
setValue(newValue);
69+
70+
if (!isLast) {
71+
setActiveIndex(nextIndex);
72+
focusItem(nextIndex);
73+
return;
74+
}
75+
76+
if (codeInputRef.current) codeInputRef.current.blur();
77+
getItem(activeIndex).blur();
78+
setActiveIndex(-1);
79+
};
80+
81+
const onItemChange = (e: React.ChangeEvent<HTMLInputElement>) => {
82+
const { value: changeValue } = e.target;
83+
const isCode = isCodeRegex.test(changeValue);
84+
85+
if (!isCode) return;
86+
setValue(changeValue.split(''));
87+
blurItem(activeIndex);
88+
};
89+
90+
return (
91+
<div
92+
className='ReactVerificationCode__container'
93+
style={
94+
{
95+
'--activeIndex': activeIndex,
96+
'--itemsCount': length,
97+
'--itemWidth': '4.5rem',
98+
'--itemHeight': '5rem',
99+
'--itemSpacing': '1rem',
100+
} as React.CSSProperties
101+
}
102+
>
103+
<input
104+
ref={codeInputRef}
105+
className='ReactVerificationCode__input'
106+
autoComplete='one-time-code'
107+
type='text'
108+
inputMode='decimal'
109+
id='one-time-code'
110+
// use onKeyUp rather than onChange for a better control
111+
// onChange is still needed to handle the autocompletion
112+
// when receiving a code by SMS
113+
onChange={onItemChange}
114+
onKeyUp={onItemKeyUp}
115+
/>
7116

8-
export const ExampleComponent = ({ text }: Props) => {
9-
return <div className={styles.test}>Example Component: {text}</div>;
117+
{itemsRef.map((ref, i) => (
118+
<div
119+
key={i}
120+
ref={ref}
121+
role='button'
122+
tabIndex={0}
123+
className={`ReactVerificationCode__item ${
124+
activeIndex === i ? 'is-active' : ''
125+
}`}
126+
onFocus={onItemFocus(i)}
127+
>
128+
{value[i] || placeholder}
129+
</div>
130+
))}
131+
</div>
132+
);
10133
};

0 commit comments

Comments
 (0)