UNPKG

react-native-confirmation-code-field

Version:

A react-native component to input confirmation code for both Android and IOS

311 lines (258 loc) 7.42 kB
// @flow import React, { PureComponent, createRef } from 'react'; import { View, TextInput as TextInputNative, Platform } from 'react-native'; import { concatStyles } from '../../styles'; import Cursor from '../Cursor'; import Cell from '../Cell'; import TextInputCustom from '../TextInputCustom'; import { getCellStyle, getContainerStyle, styles } from './styles'; import type { Props, State } from './types'; import type { LayoutEvent, PressEvent, } from 'react-native/Libraries/Types/CoreEventTypes'; class ConfirmationCodeInput extends PureComponent<Props, State> { static defaultProps = { normalizeCode: (code: string): string => code, cellProps: null, activeColor: '#fff', autoFocus: false, cellBorderWidth: 1, codeLength: 5, containerProps: {}, defaultCode: null, inputProps: {}, inactiveColor: '#ffffff40', inputPosition: 'center', size: 40, space: 8, variant: 'border-box', keyboardType: 'number-pad', maskSymbol: '', }; _input = createRef(); state = { isFocused: false, codeValue: this.props.defaultCode ? this.truncateString(this.props.defaultCode) : '', }; cellsLayouts: { [key: string]: {| x: number, y: number, xEnd: number, yEnd: number, |}, } = {}; clear() { this.handlerOnTextChange(''); } handlerOnLayoutCell = (index: number, event: LayoutEvent) => { const { width, x, y, height } = event.nativeEvent.layout; this.cellsLayouts[`${index}`] = { x, xEnd: x + width, y, yEnd: y + height }; }; renderCode = (codeSymbol: string, index: number) => { const { cellProps, maskSymbol } = this.props; const isActive = this.getCurrentIndex() === index; let customProps = null; if (cellProps) { customProps = typeof cellProps === 'function' ? cellProps({ index, isFocused: isActive, hasValue: Boolean(codeSymbol), }) : cellProps; } const customStyle = customProps && customProps.style; return ( // $FlowFixMe - Strange bag with `onLayout` property <Cell key={index} {...customProps} editable={false} index={index} onLayout={this.handlerOnLayoutCell} style={concatStyles( getCellStyle(this.props, { isActive }), customStyle, )} > {isActive ? this.renderCursor() : (codeSymbol && maskSymbol) || codeSymbol} </Cell> ); }; renderCursor() { if (this.state.isFocused) { return <Cursor />; } return null; } renderCodeCells() { // $FlowFixMe return this.getCodeSymbols().map(this.renderCode); } inheritTextInputMethod(methodName: string, handler: Function) { return (e: mixed) => { handler(e); const { inputProps } = this.props; if (inputProps && inputProps[methodName]) { inputProps[methodName](e); } }; } handlerOnTextChange = this.inheritTextInputMethod( 'onTextChange', (text: string) => { const codeValue = this.truncateString(text); const { codeLength, onFulfill } = this.props; this.setState( { codeValue, }, () => { if (this.getCodeLength() === codeLength) { this.blur(); onFulfill(codeValue); } }, ); }, ); getCodeSymbols(): Array<string> { const { codeLength } = this.props; const { codeValue } = this.state; return codeValue .split('') .concat(new Array(codeLength).fill('')) .slice(0, codeLength); } blur() { const { current } = this._input; if (current) { current.blur(); } } focus() { const { current } = this._input; if (current) { current.focus(); } } getCurrentIndex() { return this.state.codeValue.length; } getCodeLength() { return this.truncateString(this.state.codeValue).length; } truncateString(str: string): string { const { codeLength, normalizeCode } = this.props; return normalizeCode(str.substr(0, codeLength)); } findIndex(locationX: number, locationY: number): number { // $FlowFixMe for (const [index, { x, y, xEnd, yEnd }] of Object.entries( this.cellsLayouts, )) { if ( x < locationX && locationX < xEnd && (y < locationY && locationY < yEnd) ) { return parseInt(index, 10); } } return -1; } clearCodeByCoords(locationX: number, locationY: number) { const index = this.findIndex(locationX, locationY); if (index !== -1) { this.handlerOnTextChange(this.state.codeValue.slice(0, index)); } } handlerOnPress = ({ nativeEvent: { locationX, locationY } }: PressEvent) => { this.clearCodeByCoords(locationX, locationY); }; // For support react-native-web handlerOnClick = (e: any) => { const offset = e.target.getClientRects()[0]; const locationX = e.clientX - offset.left; const locationY = e.clientY - offset.top; this.clearCodeByCoords(locationX, locationY); }; handlerOnFocus = this.inheritTextInputMethod('onFocus', () => this.setState({ isFocused: true }), ); handlerOnBlur = this.inheritTextInputMethod('onBlur', () => this.setState({ isFocused: false }), ); renderInput() { const { autoFocus, inputProps, keyboardType, codeLength } = this.props; const handlers = Platform.OS === 'web' ? { onClick: this.handlerOnClick } : { onPress: this.handlerOnPress }; return ( // $FlowFixMe - onClick strange prop <TextInputCustom ref={this._input} maxLength={codeLength} {...inputProps} {...handlers} autoFocus={autoFocus} keyboardType={keyboardType} onBlur={this.handlerOnBlur} onFocus={this.handlerOnFocus} style={concatStyles(styles.maskInput, inputProps.style)} onChangeText={this.handlerOnTextChange} value={this.state.codeValue} /> ); } render() { const { containerProps, testID } = this.props; return ( <View {...containerProps} testID={testID} style={getContainerStyle(this.props)} > {this.renderCodeCells()} {this.renderInput()} </View> ); } } if (process.env.NODE_ENV !== 'production') { const PropTypes = require('prop-types'); const { validateCompareCode } = require('./validation'); ConfirmationCodeInput.propTypes = { onFulfill: PropTypes.func.isRequired, normalizeCode: PropTypes.func, activeColor: PropTypes.string, autoFocus: PropTypes.bool, cellBorderWidth: PropTypes.number, codeLength: PropTypes.number, containerProps: PropTypes.object, defaultCode: validateCompareCode, cellProps: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), inputProps: PropTypes.object, inactiveColor: PropTypes.string, inputPosition: PropTypes.oneOf(['center', 'left', 'right', 'full-width']), size: PropTypes.number, space: PropTypes.number, variant: PropTypes.oneOf([ 'border-box', 'border-circle', 'border-b', 'clear', ]), keyboardType: TextInputNative.propTypes.keyboardType, maskSymbol: PropTypes.string, }; } export default ConfirmationCodeInput;