Skip to content

Commit 590a50b

Browse files
committed
chore: initial commit
1 parent b51fb1c commit 590a50b

23 files changed

+15064
-10
lines changed

example/src/App.tsx

+31-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,37 @@
1-
import { multiply } from 'react-native-input-code-otp';
2-
import { Text, View, StyleSheet } from 'react-native';
3-
4-
const result = multiply(3, 7);
1+
import { useRef } from 'react';
2+
import { StyleSheet, View, useColorScheme } from 'react-native';
3+
import {
4+
TextInputOTP,
5+
TextInputOTPSlot,
6+
TextInputOTPGroup,
7+
TextInputOTPSeparator,
8+
type TextInputOTPRef,
9+
} from 'react-native-input-code-otp';
510

611
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+
720
return (
8-
<View style={styles.container}>
9-
<Text>Result: {result}</Text>
21+
<View style={[styles.container, { backgroundColor }]}>
22+
<TextInputOTP ref={inputRef} maxLength={6} onFilled={handleSubmit}>
23+
<TextInputOTPGroup>
24+
<TextInputOTPSlot index={0} />
25+
<TextInputOTPSlot index={1} />
26+
<TextInputOTPSlot index={2} />
27+
</TextInputOTPGroup>
28+
<TextInputOTPSeparator />
29+
<TextInputOTPGroup>
30+
<TextInputOTPSlot index={3} />
31+
<TextInputOTPSlot index={4} />
32+
<TextInputOTPSlot index={5} />
33+
</TextInputOTPGroup>
34+
</TextInputOTP>
1035
</View>
1136
);
1237
}

src/__tests__/index.test.tsx

-1
This file was deleted.

src/components/animated-caret.tsx

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { StyleSheet, Animated, useAnimatedValue } from 'react-native';
2+
import { useThemeColor } from '../theme/use-theme-color';
3+
import { theme } from '../theme/theme';
4+
import { useEffect } from 'react';
5+
6+
export function AnimatedCaret() {
7+
const backgroundColor = useThemeColor({
8+
light: theme.colorBlack,
9+
dark: theme.colorWhite,
10+
});
11+
const opacityValue = useAnimatedValue(0);
12+
13+
useEffect(() => {
14+
Animated.loop(
15+
Animated.sequence([
16+
Animated.timing(opacityValue, {
17+
toValue: 0,
18+
duration: 500,
19+
useNativeDriver: true,
20+
}),
21+
Animated.timing(opacityValue, {
22+
toValue: 1,
23+
duration: 500,
24+
useNativeDriver: true,
25+
}),
26+
])
27+
).start();
28+
}, [opacityValue]);
29+
30+
return (
31+
<Animated.View
32+
style={[styles.caret, { backgroundColor, opacity: opacityValue }]}
33+
/>
34+
);
35+
}
36+
37+
const styles = StyleSheet.create({
38+
caret: {
39+
width: 2,
40+
height: 16,
41+
borderRadius: 16,
42+
},
43+
});

src/components/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './text-input-otp';
2+
export * from './text-input-otp-group';
3+
export * from './text-input-otp-slot';
4+
export * from './text-input-otp-separator';
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import {
2+
Children,
3+
cloneElement,
4+
isValidElement,
5+
type ReactElement,
6+
} from 'react';
7+
import { View, StyleSheet } from 'react-native';
8+
import type {
9+
TextInputOTPGroupProps,
10+
TextInputOTPSlotInternalProps,
11+
} from '../types';
12+
13+
export function TextInputOTPGroup({
14+
groupStyles,
15+
children,
16+
}: TextInputOTPGroupProps) {
17+
const slots = Children.toArray(children).filter(
18+
(child): child is ReactElement<TextInputOTPSlotInternalProps> =>
19+
isValidElement(child)
20+
);
21+
22+
return (
23+
<View style={StyleSheet.flatten([styles.inputGroup, groupStyles])}>
24+
{slots.map((child, index) =>
25+
cloneElement(child, {
26+
isFirst: index === 0,
27+
isLast: index === slots.length - 1,
28+
})
29+
)}
30+
</View>
31+
);
32+
}
33+
34+
const styles = StyleSheet.create({
35+
inputGroup: {
36+
flexDirection: 'row',
37+
alignItems: 'center',
38+
},
39+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { View, StyleSheet } from 'react-native';
2+
import { useThemeColor } from '../theme/use-theme-color';
3+
import { theme } from '../theme/theme';
4+
import type { TextInputOTPSeparatorProps } from '../types';
5+
6+
export function TextInputOTPSeparator({
7+
separatorStyles,
8+
}: TextInputOTPSeparatorProps) {
9+
const backgroundColor = useThemeColor({
10+
light: theme.colorBlack,
11+
dark: theme.colorDarkGrey,
12+
});
13+
14+
return (
15+
<View
16+
style={StyleSheet.flatten([
17+
styles.separator,
18+
{ backgroundColor },
19+
separatorStyles,
20+
])}
21+
/>
22+
);
23+
}
24+
25+
const styles = StyleSheet.create({
26+
separator: {
27+
width: 10,
28+
height: 4,
29+
backgroundColor: '#030712',
30+
borderRadius: 15,
31+
},
32+
});
+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { memo } from 'react';
2+
import { Pressable, Text, StyleSheet } from 'react-native';
3+
import { useTextInputOTP } from '../hooks/use-text-input-otp';
4+
import type {
5+
TextInputOTPSlotInternalProps,
6+
TextInputOTPSlotProps,
7+
} from '../types';
8+
import { useSlotBorderStyles } from '../hooks/use-slot-border-styles';
9+
import { useThemeColor } from '../theme/use-theme-color';
10+
import { theme } from '../theme/theme';
11+
import { AnimatedCaret } from './animated-caret';
12+
import { SLOT_HEIGHT, SLOT_WIDTH } from '../constants';
13+
14+
function TextInputOTPSlotComponent({
15+
index,
16+
isFirst,
17+
isLast,
18+
focusedSlotStyles,
19+
focusedSlotTextStyles,
20+
slotStyles,
21+
slotTextStyles,
22+
...rest
23+
}: TextInputOTPSlotProps & TextInputOTPSlotInternalProps) {
24+
const { code, currentIndex, handlePress } = useTextInputOTP();
25+
const isFocused = currentIndex === index;
26+
const borderStyles = useSlotBorderStyles({ isFocused, isFirst, isLast });
27+
const slotTextColor = useThemeColor({
28+
light: theme.colorBlack,
29+
dark: theme.colorWhite,
30+
});
31+
32+
return (
33+
<Pressable
34+
style={StyleSheet.flatten([
35+
styles.slot,
36+
borderStyles,
37+
isFocused ? focusedSlotStyles : slotStyles,
38+
])}
39+
onPress={() => handlePress(index)}
40+
{...rest}
41+
>
42+
{code[index] && (
43+
<Text
44+
style={StyleSheet.flatten([
45+
styles.slotText,
46+
{ color: slotTextColor },
47+
isFocused ? focusedSlotTextStyles : slotTextStyles,
48+
])}
49+
>
50+
{code[index]}
51+
</Text>
52+
)}
53+
54+
{isFocused && !code[index] && <AnimatedCaret />}
55+
</Pressable>
56+
);
57+
}
58+
59+
export const TextInputOTPSlot = memo(TextInputOTPSlotComponent);
60+
61+
const styles = StyleSheet.create({
62+
slot: {
63+
width: SLOT_WIDTH,
64+
height: SLOT_HEIGHT,
65+
justifyContent: 'center',
66+
alignItems: 'center',
67+
},
68+
slotText: {
69+
fontSize: 14,
70+
fontWeight: 'bold',
71+
},
72+
});

src/components/text-input-otp.tsx

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { View, StyleSheet } from 'react-native';
2+
import { TextInputOTPProvider } from '../hooks/use-text-input-otp';
3+
import { TextInput } from './text-input';
4+
import { forwardRef } from 'react';
5+
import type { TextInputOTPProps, TextInputOTPRef } from '../types';
6+
7+
export const TextInputOTP = forwardRef<TextInputOTPRef, TextInputOTPProps>(
8+
({ children, containerStyles, ...rest }, ref) => {
9+
return (
10+
<TextInputOTPProvider {...rest}>
11+
<View style={StyleSheet.flatten([styles.container, containerStyles])}>
12+
<TextInput ref={ref} {...rest} />
13+
{children}
14+
</View>
15+
</TextInputOTPProvider>
16+
);
17+
}
18+
);
19+
20+
const styles = StyleSheet.create({
21+
container: {
22+
flexDirection: 'row',
23+
alignItems: 'center',
24+
justifyContent: 'center',
25+
gap: 10,
26+
},
27+
});

src/components/text-input.tsx

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { forwardRef, useImperativeHandle } from 'react';
2+
import { TextInput as RNTextInput, StyleSheet } from 'react-native';
3+
import { useTextInputOTP } from '../hooks/use-text-input-otp';
4+
import type { TextInputOTPProps, TextInputOTPRef } from '../types';
5+
6+
export const TextInput = forwardRef<
7+
TextInputOTPRef,
8+
Omit<TextInputOTPProps, 'children'>
9+
>((props, ref) => {
10+
const {
11+
inputRef,
12+
handleKeyPress,
13+
handleChangeText,
14+
setValue,
15+
focus,
16+
blur,
17+
clear,
18+
} = useTextInputOTP();
19+
20+
useImperativeHandle(ref, () => ({
21+
setValue,
22+
focus,
23+
blur,
24+
clear,
25+
}));
26+
27+
return (
28+
<RNTextInput
29+
value=""
30+
ref={inputRef}
31+
onKeyPress={handleKeyPress}
32+
onChangeText={handleChangeText}
33+
style={styles.input}
34+
keyboardType="number-pad"
35+
{...props}
36+
/>
37+
);
38+
});
39+
40+
const styles = StyleSheet.create({
41+
input: {
42+
...StyleSheet.absoluteFillObject,
43+
opacity: 0,
44+
},
45+
});

src/constants.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const BACKSPACE_KEY = 'Backspace';
2+
export const SLOT_WIDTH = 50;
3+
export const SLOT_HEIGHT = 50;

src/hooks/use-slot-border-styles.ts

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { theme } from '../theme/theme';
2+
import { useThemeColor } from '../theme/use-theme-color';
3+
import type { UseSlotBorderStylesProps } from '../types/text-input-otp-slot';
4+
5+
export function useSlotBorderStyles({
6+
isFocused,
7+
isFirst,
8+
isLast,
9+
}: UseSlotBorderStylesProps) {
10+
const blurredBorderColor = useThemeColor({
11+
light: theme.colorLightGrey,
12+
dark: theme.colorDarkGrey,
13+
});
14+
const focusedBorderColor = useThemeColor({
15+
light: theme.colorBlack,
16+
dark: theme.colorWhite,
17+
});
18+
19+
return {
20+
height: isFocused ? 54 : 50,
21+
borderColor: isFocused ? focusedBorderColor : blurredBorderColor,
22+
borderTopWidth: 2,
23+
borderBottomWidth: 2,
24+
borderLeftWidth: isFocused || isFirst ? 2 : 1,
25+
borderRightWidth: isFocused || isLast ? 2 : 1,
26+
borderTopLeftRadius: isFirst ? 8 : 0,
27+
borderTopRightRadius: isLast ? 8 : 0,
28+
borderBottomLeftRadius: isFirst ? 8 : 0,
29+
borderBottomRightRadius: isLast ? 8 : 0,
30+
};
31+
}

0 commit comments

Comments
 (0)