Skip to content

Commit 961f9a0

Browse files
authored
feat: Add CodeInput component (#128)
Add OneTime Code Input component.
1 parent 9d08bad commit 961f9a0

File tree

3 files changed

+215
-0
lines changed

3 files changed

+215
-0
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ React-native-dialog exposes a set of components that can be used to build the UI
4545
- **Dialog.Description**: A `Text` component styled as a native dialog description.
4646
- **Dialog.Button**: A component styled as a native dialog button.
4747
- **Dialog.Input**: A `TextInput` component styled as a native dialog input.
48+
- **Dialog.CodeInput**: A `TextInput` component styled as one time code input.
4849
- **Dialog.Switch**: A native `Switch` component with an optional label.
4950

5051
1. Import react-native-dialog:
@@ -188,6 +189,19 @@ const styles = StyleSheet.create({
188189
189190
`Dialog.Input` also accepts all the React-Native's `TextInput` component props.
190191
192+
### Dialog.CodeInput props
193+
194+
| Name | Type | Default | Description |
195+
| --------------------------- | ------ | --------- | ------------------------------------------------------------ |
196+
| wrapperStyle | any | undefined | The style applied to the input wrapper View |
197+
| digitContainerStyle | any | undefined | The style applied to the digit container View |
198+
| digitContainerFocusedStyle | any | undefined | The style applied to the digit container View when in focus |
199+
| digitStyle | any | undefined | The style applied to the digit text |
200+
| codeLength | number | 4 | The total number of digits |
201+
| onCodeChange | func | undefined | Called when the input changed |
202+
203+
`Dialog.CodeInput` also accepts all the React-Native's `TextInput` component props.
204+
191205
### Dialog.Title props
192206
193207
| Name | Type | Default | Description |

src/CodeInput.tsx

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import * as React from "react";
2+
import {
3+
Platform,
4+
StyleSheet,
5+
Text,
6+
TextInput,
7+
View,
8+
Pressable,
9+
PlatformColor,
10+
TextInputProps,
11+
ViewStyle,
12+
ViewPropTypes,
13+
StyleProp,
14+
TextStyle,
15+
} from "react-native";
16+
import useTheme from "./useTheme";
17+
import PropTypes from "prop-types";
18+
19+
export interface DialogCodeInputProps extends TextInputProps {
20+
wrapperStyle?: StyleProp<ViewStyle>;
21+
digitContainerStyle?: StyleProp<ViewStyle>;
22+
digitContainerFocusedStyle?: StyleProp<ViewStyle>;
23+
digitStyle?: StyleProp<TextStyle>;
24+
codeLength?: number;
25+
onCodeChange?: (code: string) => void;
26+
}
27+
28+
const DialogCodeInput: React.FC<DialogCodeInputProps> = (props) => {
29+
const {
30+
style,
31+
wrapperStyle,
32+
digitContainerStyle,
33+
digitContainerFocusedStyle,
34+
digitStyle,
35+
codeLength = 4,
36+
onCodeChange,
37+
...nodeProps
38+
} = props;
39+
const { styles } = useTheme(buildStyles);
40+
const codeRef = React.useRef<TextInput>(null);
41+
const [containerIsFocused, setContainerIsFocused] = React.useState(
42+
props.autoFocus || false
43+
);
44+
const [code, setCode] = React.useState("");
45+
const codeDigitsArray = new Array(codeLength).fill(0);
46+
const emptyInputChar = " ";
47+
const handleContainerPress = () => {
48+
setContainerIsFocused(true);
49+
codeRef?.current?.focus();
50+
};
51+
const onCodeChangePress = (t: string) => {
52+
setCode(t);
53+
typeof onCodeChange === "function" && onCodeChange(t);
54+
if (t.length === codeLength) {
55+
setContainerIsFocused(false);
56+
codeRef?.current?.blur();
57+
}
58+
};
59+
const handleOnBlur = () => setContainerIsFocused(false);
60+
const toDigitInput = (_value: number, idx: number) => {
61+
const digit = code[idx] || emptyInputChar;
62+
63+
const isCurrentDigit = idx === code.length;
64+
const isLastDigit = idx === codeLength - 1;
65+
const isCodeFull = code.length === codeLength;
66+
67+
const isFocused = isCurrentDigit || (isLastDigit && isCodeFull);
68+
69+
const containerStyle =
70+
containerIsFocused && isFocused
71+
? [
72+
styles.inputContainer,
73+
digitContainerStyle,
74+
styles.inputContainerFocused,
75+
digitContainerFocusedStyle,
76+
]
77+
: [styles.inputContainer, digitContainerStyle];
78+
79+
return (
80+
<View key={idx} style={containerStyle}>
81+
<Text style={[styles.inputText, digitStyle]}>{digit}</Text>
82+
</View>
83+
);
84+
};
85+
return (
86+
<View style={[styles.textInputWrapper, wrapperStyle]}>
87+
<Pressable
88+
onPress={handleContainerPress}
89+
style={[styles.codeContainer, style]}
90+
>
91+
{codeDigitsArray.map(toDigitInput)}
92+
</Pressable>
93+
<TextInput
94+
ref={codeRef}
95+
style={styles.hiddenInput}
96+
keyboardType="number-pad"
97+
returnKeyType="done"
98+
textContentType="oneTimeCode"
99+
onSubmitEditing={handleOnBlur}
100+
maxLength={codeLength}
101+
onChangeText={onCodeChangePress}
102+
{...nodeProps}
103+
/>
104+
</View>
105+
);
106+
};
107+
108+
DialogCodeInput.propTypes = {
109+
...ViewPropTypes,
110+
wrapperStyle: ViewPropTypes.style,
111+
digitContainerStyle: ViewPropTypes.style,
112+
digitContainerFocusedStyle: ViewPropTypes.style,
113+
digitStyle: ViewPropTypes.style,
114+
codeLength: PropTypes.number,
115+
onCodeChange: PropTypes.func,
116+
style: (Text as any).propTypes.style,
117+
};
118+
119+
DialogCodeInput.displayName = "DialogCodeInput";
120+
121+
const buildStyles = (isDark: boolean) =>
122+
StyleSheet.create({
123+
codeContainer: {
124+
width: "90%",
125+
flexDirection: "row",
126+
alignSelf: "center",
127+
justifyContent: "space-between",
128+
marginBottom: 20,
129+
},
130+
inputContainer: {
131+
flex: 1,
132+
borderColor: PlatformColor("separator"),
133+
borderBottomWidth: 3,
134+
paddingBottom: 5,
135+
marginHorizontal: 5,
136+
alignItems: "center",
137+
...Platform.select({
138+
ios: {
139+
borderColor: PlatformColor("separator"),
140+
},
141+
android: {
142+
//borderColor: PlatformColor(`@android:color/${isDark ? "secondary_text_dark" : "secondary_text_light"}`),
143+
borderColor: isDark ? "#efefef" : "#8d8d8d",
144+
},
145+
default: {},
146+
}),
147+
},
148+
inputContainerFocused: Platform.select({
149+
ios: {
150+
borderColor: PlatformColor("label"),
151+
},
152+
android: {
153+
/* borderColor: PlatformColor(
154+
`@android:color/${
155+
isDark ? "primary_text_dark" : "primary_text_light"
156+
}`
157+
),*/
158+
borderColor: isDark ? "#58c7b9" : "#169689",
159+
},
160+
default: {},
161+
}),
162+
inputText: Platform.select({
163+
ios: {
164+
fontSize: 20,
165+
color: PlatformColor("label"),
166+
},
167+
android: {
168+
color: PlatformColor(
169+
`@android:color/${
170+
isDark ? "primary_text_dark" : "primary_text_light"
171+
}`
172+
),
173+
fontSize: 20,
174+
},
175+
default: {},
176+
}),
177+
label: Platform.select({
178+
ios: {
179+
color: PlatformColor("label"),
180+
},
181+
android: {
182+
color: PlatformColor(
183+
`@android:color/${
184+
isDark ? "primary_text_dark" : "primary_text_light"
185+
}`
186+
),
187+
fontSize: 14,
188+
},
189+
default: {},
190+
}),
191+
hiddenInput: {
192+
position: "absolute",
193+
height: 0,
194+
width: 0,
195+
opacity: 0,
196+
},
197+
});
198+
199+
export default DialogCodeInput;

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import DialogButton from "./Button";
22
import DialogContainer from "./Container";
33
import DialogDescription from "./Description";
44
import DialogInput from "./Input";
5+
import DialogCodeInput from "./CodeInput";
56
import DialogSwitch from "./Switch";
67
import DialogTitle from "./Title";
78

@@ -10,6 +11,7 @@ export default {
1011
Container: DialogContainer,
1112
Description: DialogDescription,
1213
Input: DialogInput,
14+
CodeInput: DialogCodeInput,
1315
Switch: DialogSwitch,
1416
Title: DialogTitle,
1517
};

0 commit comments

Comments
 (0)