Skip to content

Commit 35d2e80

Browse files
author
johelder
committed
refactor: change input logic to handle with focus and add support to react-hook-form
1 parent 94d78a7 commit 35d2e80

12 files changed

+100
-166
lines changed

example/src/App.tsx

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,15 @@
1-
import { useRef } from 'react';
2-
import { StyleSheet, View, useColorScheme } from 'react-native';
1+
import { StyleSheet, View } from 'react-native';
32
import {
43
TextInputOTP,
54
TextInputOTPSlot,
65
TextInputOTPGroup,
76
TextInputOTPSeparator,
8-
type TextInputOTPRef,
97
} from 'react-native-input-code-otp';
108

119
export default function App() {
12-
const colorSchema = useColorScheme();
13-
const inputRef = useRef<TextInputOTPRef>(null);
14-
const backgroundColor = colorSchema === 'light' ? 'white' : 'black';
15-
16-
function handleSubmit(code: string) {
17-
console.log({ code });
18-
}
19-
2010
return (
21-
<View style={[styles.container, { backgroundColor }]}>
22-
<TextInputOTP ref={inputRef} maxLength={6} onFilled={handleSubmit}>
11+
<View style={styles.container}>
12+
<TextInputOTP maxLength={6} onFilled={(code) => console.log(code)}>
2313
<TextInputOTPGroup>
2414
<TextInputOTPSlot index={0} />
2515
<TextInputOTPSlot index={1} />

src/components/text-input-otp-slot.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ function TextInputOTPSlotComponent({
2525

2626
return (
2727
<Pressable
28+
onPress={handlePress}
2829
style={StyleSheet.flatten([
2930
styles.slot,
3031
borderStyles,
3132
isFocused ? focusedSlotStyles : slotStyles,
3233
])}
33-
onPress={() => handlePress(index)}
3434
{...rest}
3535
>
3636
{code[index] && (

src/components/text-input.tsx

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,8 @@ export const TextInput = forwardRef<
77
TextInputOTPRef,
88
Omit<TextInputOTPProps, 'children'>
99
>(({ autoFocus = true, ...rest }, ref) => {
10-
const {
11-
inputRef,
12-
handleKeyPress,
13-
handleChangeText,
14-
setValue,
15-
focus,
16-
blur,
17-
clear,
18-
} = useTextInputOTP();
10+
const { inputRef, handleChangeText, setValue, focus, blur, clear } =
11+
useTextInputOTP();
1912

2013
useImperativeHandle(ref, () => ({
2114
setValue,
@@ -26,14 +19,12 @@ export const TextInput = forwardRef<
2619

2720
return (
2821
<RNTextInput
29-
value=""
3022
ref={inputRef}
31-
onKeyPress={handleKeyPress}
32-
onChangeText={handleChangeText}
33-
style={styles.input}
3423
keyboardType="number-pad"
3524
autoFocus={autoFocus}
25+
style={styles.input}
3626
{...rest}
27+
onChangeText={handleChangeText}
3728
/>
3829
);
3930
});

src/hooks/use-text-input-otp.tsx

Lines changed: 28 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,13 @@ import {
77
useState,
88
type PropsWithChildren,
99
} from 'react';
10-
import type {
11-
NativeSyntheticEvent,
12-
TextInput,
13-
TextInputKeyPressEventData,
14-
} from 'react-native';
15-
import { BACKSPACE_KEY } from '../constants';
10+
import type { TextInput } from 'react-native';
1611
import type { TextInputOTPContextProps, TextInputOTPProps } from '../types';
1712

1813
const TextInputOTPContext = createContext<TextInputOTPContextProps>({
19-
code: [],
14+
code: '',
2015
currentIndex: 0,
2116
inputRef: { current: null },
22-
handleKeyPress: () => {},
2317
handleChangeText: () => {},
2418
handlePress: () => {},
2519
setValue: () => {},
@@ -31,76 +25,50 @@ const TextInputOTPContext = createContext<TextInputOTPContextProps>({
3125
export function TextInputOTPProvider({
3226
autoFocus = true,
3327
maxLength,
28+
value = '',
3429
onFilled,
30+
onChangeText,
3531
children,
3632
}: PropsWithChildren<
37-
Pick<TextInputOTPProps, 'autoFocus' | 'maxLength' | 'onFilled'>
33+
Pick<
34+
TextInputOTPProps,
35+
'autoFocus' | 'maxLength' | 'onFilled' | 'onChangeText' | 'value'
36+
>
3837
>) {
39-
const [code, setCode] = useState(Array(maxLength).fill(''));
40-
const [currentIndex, setCurrentIndex] = useState(autoFocus ? 0 : -1);
38+
const [code, setCode] = useState(value);
39+
const [currentIndex, setCurrentIndex] = useState(() => (autoFocus ? 0 : -1));
4140
const inputRef = useRef<TextInput>(null);
4241

43-
const updateCodeAtIndex = useCallback(
44-
(index: number, value: string) => {
45-
const newCode = [...code];
46-
newCode[index] = value;
47-
setCode(newCode);
48-
49-
return newCode;
50-
},
51-
[code]
52-
);
53-
54-
const handleKeyPress = useCallback(
55-
(event: NativeSyntheticEvent<TextInputKeyPressEventData>) => {
56-
const { key } = event.nativeEvent;
57-
58-
if (key !== BACKSPACE_KEY) {
59-
return;
60-
}
61-
62-
if (!code[currentIndex] && currentIndex > 0) {
63-
updateCodeAtIndex(currentIndex - 1, '');
64-
setCurrentIndex((prev) => prev - 1);
65-
return;
66-
}
67-
68-
updateCodeAtIndex(currentIndex, '');
69-
},
70-
[code, currentIndex, updateCodeAtIndex]
71-
);
72-
7342
const handleChangeText = useCallback(
7443
(text: string) => {
75-
if (text.length > 1) {
44+
if (text.length > maxLength) {
7645
return;
7746
}
7847

79-
const updatedCode = updateCodeAtIndex(currentIndex, text);
48+
setCode(text);
49+
onChangeText?.(text);
8050

81-
if (currentIndex < maxLength - 1) {
82-
setCurrentIndex((prev) => prev + 1);
83-
return;
51+
if (text.length === maxLength) {
52+
onFilled?.(text);
8453
}
8554

86-
const finalCode = [...updatedCode].join('');
87-
88-
if (finalCode.length === maxLength) {
89-
onFilled?.(finalCode);
55+
if (text.length < maxLength) {
56+
setCurrentIndex(text.length);
9057
}
9158
},
92-
[currentIndex, maxLength, onFilled, updateCodeAtIndex]
59+
[maxLength, onChangeText, onFilled]
9360
);
9461

95-
function handlePress(index: number) {
96-
setCurrentIndex(index);
62+
const handlePress = useCallback(() => {
63+
setCurrentIndex(code.length);
9764
inputRef.current?.focus();
98-
}
65+
}, [code.length]);
9966

10067
const setValue = useCallback(
10168
(text: string) => {
102-
const value = text.length > maxLength ? text.slice(0, maxLength) : text;
103-
setCode(Array.from(value));
69+
const filledCode =
70+
text.length > maxLength ? text.slice(0, maxLength) : text;
71+
setCode(filledCode);
10472
setCurrentIndex(maxLength - 1);
10573
},
10674
[maxLength]
@@ -115,24 +83,23 @@ export function TextInputOTPProvider({
11583
}
11684

11785
const clear = useCallback(() => {
118-
setCode(Array(maxLength).fill(''));
86+
setCode('');
11987
setCurrentIndex(0);
120-
}, [maxLength]);
88+
}, []);
12189

12290
const contextValue = useMemo(
12391
() => ({
124-
code,
92+
code: value || code,
12593
currentIndex,
12694
inputRef,
127-
handleKeyPress,
12895
handleChangeText,
12996
handlePress,
13097
setValue,
13198
focus,
13299
blur,
133100
clear,
134101
}),
135-
[clear, code, currentIndex, handleChangeText, handleKeyPress, setValue]
102+
[clear, code, currentIndex, handleChangeText, handlePress, setValue, value]
136103
);
137104

138105
return (

src/types.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type { ReactNode, RefObject } from 'react';
2+
import type {
3+
PressableProps,
4+
StyleProp,
5+
TextInput,
6+
TextInputProps,
7+
TextStyle,
8+
ViewProps,
9+
ViewStyle,
10+
} from 'react-native';
11+
12+
export type TextInputOTPContextProps = {
13+
code: string;
14+
currentIndex: number;
15+
inputRef: RefObject<TextInput | null>;
16+
handleChangeText: (text: string) => void;
17+
handlePress: () => void;
18+
setValue: (text: string) => void;
19+
focus: () => void;
20+
blur: () => void;
21+
clear: () => void;
22+
};
23+
24+
export type TextInputOTPGroupProps = {
25+
groupStyles?: StyleProp<ViewStyle>;
26+
} & Omit<ViewProps, 'style'>;
27+
28+
export type TextInputOTPRef = {
29+
setValue: (text: string) => void;
30+
focus: () => void;
31+
blur: () => void;
32+
clear: () => void;
33+
};
34+
35+
export type TextInputOTPSeparatorProps = {
36+
separatorStyles?: StyleProp<ViewStyle>;
37+
} & Omit<ViewProps, 'style'>;
38+
39+
export type TextInputOTPSlotProps = {
40+
index: number;
41+
focusedSlotStyles?: StyleProp<ViewStyle>;
42+
slotStyles?: StyleProp<ViewStyle>;
43+
focusedSlotTextStyles?: StyleProp<TextStyle>;
44+
slotTextStyles?: StyleProp<TextStyle>;
45+
} & PressableProps;
46+
47+
export type TextInputOTPSlotInternalProps = {
48+
isFirst?: boolean;
49+
isLast?: boolean;
50+
};
51+
52+
export type UseSlotBorderStylesProps = {
53+
isFocused: boolean;
54+
isFirst?: boolean;
55+
isLast?: boolean;
56+
};
57+
58+
export type TextInputOTPProps = {
59+
children: ReactNode;
60+
autoFocus?: boolean;
61+
maxLength: number;
62+
onFilled?: (text: string) => void;
63+
containerStyles?: StyleProp<ViewStyle>;
64+
} & Omit<TextInputProps, 'style'>;

src/types/index.ts

Lines changed: 0 additions & 6 deletions
This file was deleted.

src/types/text-input-otp-context.ts

Lines changed: 0 additions & 21 deletions
This file was deleted.

src/types/text-input-otp-group.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/types/text-input-otp-ref.ts

Lines changed: 0 additions & 6 deletions
This file was deleted.

src/types/text-input-otp-separator.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/types/text-input-otp-slot.ts

Lines changed: 0 additions & 25 deletions
This file was deleted.

src/types/text-input-otp.ts

Lines changed: 0 additions & 10 deletions
This file was deleted.

0 commit comments

Comments
 (0)