import React, { Component } from 'react'; import { View, TextInput, StyleSheet, ViewStyle, KeyboardType, NativeSyntheticEvent, TextInputKeyPressEventData, TextInputProps } from 'react-native'; interface IState { focusedInput: number; otpText: string[]; } interface IProps extends TextInputProps { defaultValue: string; inputCount: number; containerStyle: ViewStyle; textInputStyle: ViewStyle; inputCellLength: number; tintColor: string | string[]; offTintColor: string | string[]; handleTextChange(text: string): void; handleCellTextChange?(text: string, cellIndex: number): void; keyboardType: KeyboardType; testIDPrefix: string; autoFocus: boolean; onFocus(): void; onBlur(): void; disabled: boolean; } const styles = StyleSheet.create({ container: { flexDirection: 'row', justifyContent: 'space-between', }, textInput: { borderBottomWidth: 4, color: '#000000', fontSize: 22, fontWeight: '500', height: 50, margin: 5, textAlign: 'center', width: 50, }, }); const DEFAULT_TINT_COLOR: string = '#3CB371'; const DEFAULT_OFF_TINT_COLOR: string = '#DCDCDC'; const DEFAULT_TEST_ID_PREFIX: string = 'otp_input_'; const DEFAULT_KEYBOARD_TYPE: KeyboardType = 'numeric'; class OTPTextView extends Component { static defaultProps: Partial = { defaultValue: '', inputCount: 4, tintColor: DEFAULT_TINT_COLOR, offTintColor: DEFAULT_OFF_TINT_COLOR, inputCellLength: 1, containerStyle: {}, textInputStyle: {}, handleTextChange: () => {}, keyboardType: DEFAULT_KEYBOARD_TYPE, testIDPrefix: DEFAULT_TEST_ID_PREFIX, autoFocus: false, onFocus: () => {}, onBlur: () => {}, disabled: false, }; inputs: TextInput[]; constructor(props: IProps) { super(props); this.state = { focusedInput: 0, otpText: this.getOTPTextChucks( props.inputCount || 4, props.inputCellLength, props.defaultValue, ), }; this.inputs = []; this.checkTintColorCount(); } getOTPTextChucks = ( inputCount: number, inputCellLength: number, text: string, ): string[] => { let matches = text.match(new RegExp('.{1,' + inputCellLength + '}', 'g')) || []; return matches.slice(0, inputCount); }; checkTintColorCount = () => { const { tintColor, offTintColor, inputCount } = this.props; if (typeof tintColor !== 'string' && tintColor.length !== inputCount) { throw new Error( "If tint color is an array it's length should be equal to input count", ); } if ( typeof offTintColor !== 'string' && offTintColor.length !== inputCount ) { throw new Error( "If off tint color is an array it's length should be equal to input count", ); } }; basicValidation = (text: string) => { const validText = /^[0-9a-zA-Z]+$/; return text.match(validText); }; onTextChange = (text: string, i: number) => { const { inputCellLength, inputCount, handleTextChange, handleCellTextChange, } = this.props; if (text && !this.basicValidation(text)) { return; } this.setState( (prevState: IState) => { let { otpText } = prevState; otpText[i] = text; return { otpText, }; }, () => { handleTextChange(this.state.otpText.join('')); handleCellTextChange && handleCellTextChange(text, i); if (text.length === inputCellLength && i !== inputCount - 1) { this.inputs[i + 1].focus(); } }, ); }; onInputFocus = (i: number) => { const { otpText } = this.state; const prevIndex = i - 1; if (prevIndex > -1 && !otpText[prevIndex] && !otpText.join('')) { this.inputs[prevIndex].focus(); return; } this.setState({ focusedInput: i }); }; onKeyPress = ( e: NativeSyntheticEvent, i: number, ) => { const val = this.state.otpText[i] || ''; const { handleTextChange, inputCellLength, inputCount } = this.props; const { otpText } = this.state; if (e.nativeEvent.key !== 'Backspace' && val && i !== inputCount - 1) { this.inputs[i + 1].focus(); return; } if (e.nativeEvent.key === 'Backspace' && i !== 0) { if (!val.length && otpText[i - 1].length === inputCellLength) { this.setState( prevState => { let { otpText: prevStateOtpText } = prevState; prevStateOtpText[i - 1] = prevStateOtpText[i - 1] .split('') .splice(0, prevStateOtpText[i - 1].length - 1) .join(''); return { otpText: prevStateOtpText, }; }, () => { handleTextChange(this.state.otpText.join('')); this.inputs[i - 1].focus(); }, ); } } }; clear = () => { this.setState( { otpText: [], }, () => { this.inputs[0].focus(); this.props.handleTextChange(''); }, ); }; setValue = (value: string, isPaste: boolean = false) => { const { inputCount, inputCellLength } = this.props; const updatedFocusInput = isPaste ? inputCount - 1 : value.length - 1; this.setState( { otpText: this.getOTPTextChucks(inputCount, inputCellLength, value), }, () => { if (this.inputs[updatedFocusInput]) { this.inputs[updatedFocusInput].focus(); } this.props.handleTextChange(value); }, ); }; render() { const { inputCount, offTintColor, tintColor, defaultValue, inputCellLength, containerStyle, textInputStyle, keyboardType, testIDPrefix, autoFocus, onFocus, onBlur, disabled, ...textInputProps } = this.props; const { focusedInput, otpText } = this.state; const TextInputs = []; for (let i = 0; i < inputCount; i += 1) { const _tintColor = typeof tintColor === 'string' ? tintColor : tintColor[i]; const _offTintColor = typeof offTintColor === 'string' ? offTintColor : offTintColor[i]; const inputStyle = [ styles.textInput, textInputStyle, { borderColor: _offTintColor, }, ]; if (focusedInput === i) { inputStyle.push({ borderColor: _tintColor, }); } TextInputs.push( { if (e) { this.inputs[i] = e; } }} key={i} autoCorrect={false} keyboardType={keyboardType} autoFocus={autoFocus && i === 0} value={otpText[i] || ''} style={inputStyle} maxLength={this.props.inputCellLength} onFocus={() => { this.props.onFocus(); this.onInputFocus(i); }} onBlur={() => this.props.onBlur()} onChangeText={text => this.onTextChange(text, i)} multiline={false} onKeyPress={e => this.onKeyPress(e, i)} selectionColor={_tintColor} cursorColor={_tintColor} editable={!disabled} {...textInputProps} testID={`${testIDPrefix}${i}`} />, ); } return {TextInputs}; } } export default OTPTextView;