Skip to content

Commit 53a6225

Browse files
committed
chore: update API
1 parent 186aa99 commit 53a6225

File tree

4 files changed

+380
-301
lines changed

4 files changed

+380
-301
lines changed

src/Input.tsx

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import React, {
2+
ChangeEvent,
3+
forwardRef,
4+
KeyboardEvent,
5+
RefObject,
6+
} from 'react';
7+
import { useInputVerificationCode } from './context';
8+
9+
import './index.css';
10+
11+
interface InputProps {
12+
index?: number;
13+
isLast?: boolean;
14+
value?: string;
15+
}
16+
17+
const Input = forwardRef<HTMLInputElement, InputProps>(
18+
({ index, isLast, value }, ref) => {
19+
const {
20+
focusInput,
21+
handleValuesChange,
22+
selectInputContent,
23+
setInputValue,
24+
validate,
25+
} = useInputVerificationCode();
26+
27+
// passed by the context
28+
if (
29+
handleValuesChange === undefined ||
30+
index === undefined ||
31+
isLast === undefined ||
32+
value === undefined
33+
) {
34+
return null;
35+
}
36+
37+
const blurInput = () => {
38+
const input = (ref as RefObject<HTMLInputElement>).current;
39+
40+
if (input) {
41+
input.blur();
42+
}
43+
};
44+
45+
const onInputFocus = () => {
46+
selectInputContent(index);
47+
};
48+
49+
const onInputKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
50+
const eventKey = event.key;
51+
52+
console.log({ event, eventKey, index });
53+
54+
/**
55+
* handle "delete and go back" events
56+
*/
57+
if (eventKey === 'Backspace' || eventKey === 'Delete') {
58+
setInputValue('', index);
59+
focusInput(index - 1);
60+
61+
return;
62+
}
63+
64+
/**
65+
* if the eventKey is not valid, don't go any further
66+
* and select the content of the input for a better UX
67+
*/
68+
if (!validate(eventKey)) {
69+
selectInputContent(index);
70+
return;
71+
}
72+
73+
setInputValue(eventKey, index);
74+
75+
/**
76+
* if the input is the last of the list
77+
* blur it, otherwise focus the next one
78+
*/
79+
if (isLast) {
80+
blurInput();
81+
return;
82+
}
83+
84+
focusInput(index + 1);
85+
};
86+
87+
const onInputChange = (event: ChangeEvent<HTMLInputElement>) => {
88+
const eventValue = event.target.value;
89+
90+
console.log('onInputChange', { event, eventValue });
91+
92+
/**
93+
* handle OTP and pasted codes
94+
*/
95+
if (eventValue.length > 1) {
96+
handleValuesChange(eventValue);
97+
}
98+
};
99+
100+
return (
101+
<input
102+
autoComplete='one-time-code'
103+
className='ReactInputVerificationCode-item'
104+
onChange={onInputChange}
105+
onFocus={onInputFocus}
106+
onKeyDown={onInputKeyDown}
107+
ref={ref}
108+
value={value}
109+
/>
110+
);
111+
}
112+
);
113+
114+
export default Input;

src/context.tsx

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import React, {
2+
Children,
3+
cloneElement,
4+
createContext,
5+
createRef,
6+
ReactElement,
7+
useCallback,
8+
useContext,
9+
useEffect,
10+
useMemo,
11+
useState,
12+
} from 'react';
13+
14+
const Context = createContext<{
15+
focusInput: (index: number) => void;
16+
handleValuesChange?: (values: string) => void;
17+
selectInputContent: (index: number) => void;
18+
setInputValue: (value: string, index: number) => void;
19+
validate: (data: string) => boolean;
20+
}>({
21+
focusInput: () => null,
22+
handleValuesChange: () => null,
23+
selectInputContent: () => null,
24+
setInputValue: () => null,
25+
validate: () => true,
26+
});
27+
28+
export interface ProviderProps {
29+
autoFocus?: boolean;
30+
children: ReactElement[];
31+
defaultValue?: string;
32+
onChange?: (data: string) => void;
33+
onCompleted?: (data: string) => void;
34+
type?: 'alphanumeric' | 'number';
35+
}
36+
37+
export default function Provider({
38+
children,
39+
autoFocus,
40+
defaultValue = '',
41+
onChange = () => null,
42+
onCompleted = () => null,
43+
type = 'number',
44+
}: ProviderProps) {
45+
/**
46+
* generate a new array, map through it
47+
* and replace with the value when possible
48+
*/
49+
const fillValues = useCallback(
50+
(value: string) =>
51+
new Array(children.length).fill('').map((_, index) => value[index] ?? ''),
52+
[children.length]
53+
);
54+
55+
const [values, setValues] = useState(fillValues(defaultValue));
56+
57+
const refs = useMemo(
58+
() =>
59+
new Array(children.length)
60+
.fill(null)
61+
.map(() => createRef<HTMLInputElement>()),
62+
[children.length]
63+
);
64+
65+
const focusInput = useCallback(
66+
(index: number) => {
67+
const input = refs[index]?.current;
68+
69+
if (input) {
70+
requestAnimationFrame(() => {
71+
input.focus();
72+
});
73+
}
74+
},
75+
[refs]
76+
);
77+
78+
const handleValuesChange = useCallback(
79+
(input: string) => {
80+
setValues(fillValues(input));
81+
82+
const isCompleted = input.length === length;
83+
84+
if (isCompleted) {
85+
onCompleted(input);
86+
// blurInput();
87+
}
88+
89+
focusInput(input.length);
90+
},
91+
[fillValues, focusInput, onCompleted]
92+
);
93+
94+
const selectInputContent = useCallback(
95+
(index: number) => {
96+
const input = refs[index]?.current;
97+
98+
if (input) {
99+
requestAnimationFrame(() => {
100+
input.select();
101+
});
102+
}
103+
},
104+
[refs]
105+
);
106+
107+
const setInputValue = useCallback(
108+
(value: string, index: number) => {
109+
const nextValues = [...values];
110+
nextValues[index] = value;
111+
112+
setValues(nextValues);
113+
114+
const stringifiedValues = nextValues.join('');
115+
const isLast = index === children.length - 1;
116+
console.log(index, children.length, index === children.length - 1);
117+
118+
if (isLast) {
119+
onCompleted(stringifiedValues);
120+
return;
121+
}
122+
123+
onChange(stringifiedValues);
124+
},
125+
[children.length, onChange, onCompleted, values]
126+
);
127+
128+
const validate = useCallback(
129+
(input: string) => {
130+
if (type === 'number') {
131+
return /^\d/.test(input);
132+
}
133+
134+
if (type === 'alphanumeric') {
135+
return /^[a-zA-Z0-9]/.test(input);
136+
}
137+
138+
return true;
139+
},
140+
[type]
141+
);
142+
143+
const memoizedValue = useMemo(
144+
() => ({
145+
focusInput,
146+
handleValuesChange,
147+
selectInputContent,
148+
setInputValue,
149+
validate,
150+
}),
151+
[
152+
focusInput,
153+
handleValuesChange,
154+
selectInputContent,
155+
setInputValue,
156+
validate,
157+
]
158+
);
159+
160+
/**
161+
* autoFocus
162+
*/
163+
useEffect(() => {
164+
if (autoFocus) {
165+
focusInput(0);
166+
}
167+
}, [autoFocus, focusInput]);
168+
169+
return (
170+
<Context.Provider value={memoizedValue}>
171+
<>{JSON.stringify(values, null, 2)}</>
172+
173+
{Children.map(children, (child, index) =>
174+
cloneElement(child, {
175+
index,
176+
isLast: index === children.length - 1,
177+
value: values[index],
178+
ref: refs[index],
179+
})
180+
)}
181+
</Context.Provider>
182+
);
183+
}
184+
185+
export function useInputVerificationCode() {
186+
const context = useContext(Context);
187+
188+
if (!context) {
189+
throw new Error(
190+
'useInputVerificationCode can only be used inside of a Provider'
191+
);
192+
}
193+
194+
return context;
195+
}

0 commit comments

Comments
 (0)