├── .expo-shared └── assets.json ├── screen-recording.gif ├── .prettierrc ├── CHANGELOG.md ├── babel.config.js ├── .gitignore ├── index.js ├── tsconfig.json ├── app.json ├── App.tsx ├── package.json ├── LICENSE ├── lib ├── NumberPadContext.ts ├── styles.ts ├── AvoidingView.tsx ├── NumberPad.tsx ├── Input.tsx └── Display.tsx └── README.md /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /screen-recording.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glancemoney/react-native-numpad/HEAD/screen-recording.gif -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | - 0.3.0 - Migrate to TypeScript + publish TypeScript definitions 4 | - 0.2.0 - Initial published version 5 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .expo 3 | npm-debug.* 4 | *.jks 5 | *.p8 6 | *.p12 7 | *.key 8 | *.mobileprovision 9 | *.orig.* 10 | web-build/ 11 | web-report/ 12 | yarn-error.log 13 | .DS_Store 14 | dist 15 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import AvoidingView from './lib/AvoidingView'; 2 | import Display from './lib/Display'; 3 | import Input from './lib/Input'; 4 | import NumberPad from './lib/NumberPad'; 5 | import NumberPadContext from './lib/NumberPadContext'; 6 | 7 | export default NumberPad; 8 | export { AvoidingView, Display, Input, NumberPadContext }; 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-native", 4 | "target": "es5", 5 | "module": "commonjs", 6 | "lib": ["es2017", "es7", "es6", "dom"], 7 | "allowJs": true, 8 | "skipLibCheck": true, 9 | "allowSyntheticDefaultImports": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "moduleResolution": "node", 13 | "declaration": true, 14 | "outDir": "dist" 15 | }, 16 | "exclude": ["node_modules", "dist"] 17 | } 18 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "react-native-number-pad", 4 | "slug": "react-native-number-pad", 5 | "platforms": ["ios", "android", "web"], 6 | "version": "1.0.0", 7 | "orientation": "portrait", 8 | "splash": { 9 | "resizeMode": "contain", 10 | "backgroundColor": "#ffffff" 11 | }, 12 | "updates": { 13 | "fallbackToCacheTimeout": 0 14 | }, 15 | "assetBundlePatterns": ["**/*"], 16 | "ios": { 17 | "supportsTablet": true 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SafeAreaView } from 'react-native'; 3 | import NumberPad, { Input, Display } from './index'; 4 | import { Ionicons } from '@expo/vector-icons'; 5 | 6 | export default class App extends React.Component { 7 | render() { 8 | return ( 9 | 10 | 11 | {[0, 1, 2].map((i) => ( 12 | 13 | ))} 14 | 15 | } 17 | hideIcon={} 18 | /> 19 | 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-numpad", 3 | "version": "0.3.0", 4 | "main": "dist/index.js", 5 | "types": "dist/index.d.ts", 6 | "scripts": { 7 | "start": "expo start", 8 | "build": "tsc", 9 | "android": "expo start --android", 10 | "ios": "expo start --ios", 11 | "web": "expo start --web", 12 | "eject": "expo eject", 13 | "prepublish": "tsc" 14 | }, 15 | "devDependencies": { 16 | "@babel/core": "^7.8.6", 17 | "@types/react": "^17.0.3", 18 | "@types/react-dom": "^17.0.2", 19 | "@types/react-native": "^0.63.52", 20 | "babel-preset-expo": "~8.1.0", 21 | "expo": "~40.0.0", 22 | "react": ">=16.9.0", 23 | "react-dom": ">=16.9.0", 24 | "react-native": "https://github.com/expo/react-native/archive/sdk-40.0.1.tar.gz", 25 | "typescript": "~4.0.0" 26 | }, 27 | "peerDependencies": { 28 | "react": ">=16.9.0", 29 | "react-dom": ">=16.9.0", 30 | "react-native": ">=0.58.0", 31 | "react-native-web": ">=0.11.7" 32 | }, 33 | "dependencies": {} 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Glance Money, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /lib/NumberPadContext.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import type Input from './Input'; 3 | import type Display from './Display'; 4 | import type AvoidingView from './AvoidingView'; 5 | 6 | type NumberPadContextType = { 7 | display: null | string; 8 | input: null | Input; 9 | height: number; 10 | focus: (display: Display) => void; 11 | blur: () => void; 12 | onInputEvent: (ev: string) => void; 13 | registerDisplay: (display: Display) => void; 14 | unregisterDisplay: (display: Display) => void; 15 | registerAvoidingView: (view: AvoidingView) => void; 16 | unregisterAvoidingView: (view: AvoidingView) => void; 17 | registerInput: (input: Input) => void; 18 | setHeight: (height: number) => void; 19 | }; 20 | 21 | const nullFn = () => {}; 22 | 23 | const defaultContext: NumberPadContextType = { 24 | display: null, 25 | input: null, 26 | height: 0, 27 | focus: nullFn, 28 | blur: nullFn, 29 | onInputEvent: nullFn, 30 | registerDisplay: nullFn, 31 | unregisterDisplay: nullFn, 32 | registerAvoidingView: nullFn, 33 | unregisterAvoidingView: nullFn, 34 | registerInput: nullFn, 35 | setHeight: nullFn, 36 | }; 37 | 38 | export default React.createContext(defaultContext); 39 | -------------------------------------------------------------------------------- /lib/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create({ 4 | input: { 5 | width: '100%', 6 | paddingVertical: 10, 7 | backgroundColor: 'white', 8 | }, 9 | display: { 10 | padding: 20, 11 | justifyContent: 'flex-end', 12 | borderBottomWidth: 1, 13 | borderBottomColor: '#eee', 14 | flexDirection: 'row', 15 | backgroundColor: 'white', 16 | }, 17 | activeDisplay: { 18 | backgroundColor: '#f8f8f8', 19 | }, 20 | activeDisplayText: {}, 21 | invalidDisplayText: {}, 22 | displayText: { 23 | fontSize: 30, 24 | color: '#666', 25 | }, 26 | placeholderDisplayText: { 27 | color: '#ddd', 28 | }, 29 | cursor: { 30 | borderBottomWidth: 2, 31 | borderBottomColor: 'transparent', 32 | }, 33 | pad: { 34 | flexWrap: 'wrap', 35 | flexDirection: 'row', 36 | }, 37 | button: { 38 | alignItems: 'center', 39 | justifyContent: 'center', 40 | padding: 10, 41 | width: '33%', 42 | }, 43 | buttonText: { 44 | color: '#888', 45 | fontSize: 26, 46 | textAlign: 'center', 47 | }, 48 | hide: { 49 | paddingVertical: 5, 50 | alignItems: 'center', 51 | }, 52 | blinkOn: { 53 | borderBottomColor: '#ddd', 54 | }, 55 | blinkOff: { 56 | borderBottomColor: 'transparent', 57 | }, 58 | }); 59 | -------------------------------------------------------------------------------- /lib/AvoidingView.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Animated, StyleProp, ViewStyle } from 'react-native'; 3 | 4 | import NumberPadContext from './NumberPadContext'; 5 | 6 | type AvoidingViewProps = { 7 | style: StyleProp; 8 | }; 9 | 10 | export default class AvoidingView extends React.Component { 11 | animation: Animated.Value; 12 | 13 | static contextType = NumberPadContext; 14 | 15 | constructor(props: AvoidingViewProps) { 16 | super(props); 17 | 18 | this.animation = new Animated.Value(0); 19 | } 20 | 21 | show = () => { 22 | Animated.timing(this.animation, { 23 | duration: 200, 24 | toValue: this.context.height, 25 | useNativeDriver: false, 26 | }).start(); 27 | }; 28 | 29 | hide = () => { 30 | Animated.timing(this.animation, { 31 | duration: 200, 32 | toValue: 0, 33 | useNativeDriver: false, 34 | }).start(); 35 | }; 36 | 37 | componentDidMount() { 38 | this.context.registerAvoidingView(this); 39 | } 40 | 41 | componentWillUnmount() { 42 | Animated.timing(this.animation, { 43 | duration: 200, 44 | toValue: 0, 45 | useNativeDriver: false, 46 | }).start(); 47 | } 48 | 49 | render() { 50 | return ( 51 | 62 | {this.props.children} 63 | 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/NumberPad.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import NumberPadContext from './NumberPadContext'; 4 | import type Display from './Display'; 5 | import type AvoidingView from './AvoidingView'; 6 | import type Input from './Input'; 7 | 8 | type NumberPadProps = {}; 9 | 10 | type NumberPadState = { 11 | display: null | string; 12 | input: null | Input; 13 | height: number; 14 | }; 15 | 16 | export default class NumberPad extends React.Component< 17 | NumberPadProps, 18 | NumberPadState 19 | > { 20 | displays: Record; 21 | avoidingViews: Record; 22 | 23 | constructor(props: NumberPadProps) { 24 | super(props); 25 | 26 | this.displays = {}; 27 | this.avoidingViews = {}; 28 | 29 | this.state = { 30 | display: null, // currently focused display 31 | input: null, // input component 32 | height: 0, // height of input component 33 | }; 34 | } 35 | 36 | focus = (display: Display) => { 37 | // blur all displays except for this one and do not propagate 38 | Object.values(this.displays) 39 | .filter((d) => d !== display) 40 | .map((d) => d.blur(false)); 41 | 42 | // set current display 43 | this.setState({ 44 | display: (display as any)._reactInternalFiber.key, 45 | }); 46 | 47 | // show input 48 | if (this.state.input) { 49 | this.state.input.show(); 50 | } 51 | 52 | // show avoiding views 53 | Object.values(this.avoidingViews).map((view) => view.show()); 54 | }; 55 | 56 | blur = () => { 57 | const display = this.display(); 58 | 59 | // call current display's blur method 60 | if (display) { 61 | display.blur(false); 62 | } 63 | 64 | // set current display to null 65 | this.setState({ 66 | display: null, 67 | }); 68 | 69 | // hide input 70 | if (this.state.input) { 71 | this.state.input.hide(); 72 | } 73 | 74 | // hide avoiding views 75 | Object.values(this.avoidingViews).map((view) => view.hide()); 76 | }; 77 | 78 | registerDisplay = (display: Display) => { 79 | this.displays[(display as any)._reactInternalFiber.key] = display; 80 | }; 81 | 82 | unregisterDisplay = (display: Display) => { 83 | delete this.displays[(display as any)._reactInternalFiber.key]; 84 | }; 85 | 86 | registerAvoidingView = (view: AvoidingView) => { 87 | this.avoidingViews[(view as any)._reactInternalFiber.key] = view; 88 | }; 89 | 90 | unregisterAvoidingView = (view: AvoidingView) => { 91 | delete this.avoidingViews[(view as any)._reactInternalFiber.key]; 92 | }; 93 | 94 | registerInput = (input: Input) => { 95 | this.setState({ 96 | input, 97 | }); 98 | }; 99 | 100 | setHeight = (height: number) => { 101 | this.setState({ 102 | height, 103 | }); 104 | }; 105 | 106 | onInputEvent = (event: string) => { 107 | const display = this.display(); 108 | display && display.onInputEvent(event); 109 | }; 110 | 111 | display = () => { 112 | return this.state.display && this.displays[this.state.display]; 113 | }; 114 | 115 | render() { 116 | return ( 117 | 133 | {this.props.children} 134 | 135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /lib/Input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | TouchableOpacity, 4 | View, 5 | Text, 6 | Animated, 7 | ViewStyle, 8 | StyleProp, 9 | } from 'react-native'; 10 | 11 | import NumberPadContext from './NumberPadContext'; 12 | import styles from './styles'; 13 | 14 | const inputs = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '0']; 15 | 16 | type InputProps = { 17 | height: number; 18 | position: 'relative' | 'absolute'; 19 | style?: StyleProp; 20 | backspaceIcon?: JSX.Element; 21 | hideIcon?: JSX.Element; 22 | onWillHide?: () => void; 23 | onDidHide?: () => void; 24 | onWillShow?: () => void; 25 | onDidShow?: () => void; 26 | }; 27 | 28 | export default class Input extends React.Component { 29 | animation: Animated.Value; 30 | 31 | static contextType = NumberPadContext; 32 | 33 | static defaultProps = { 34 | height: 270, 35 | position: 'absolute', 36 | }; 37 | 38 | static iconStyle = { 39 | color: styles.buttonText.color || '#888', 40 | size: styles.buttonText.fontSize || 36, 41 | }; 42 | 43 | constructor(props: InputProps) { 44 | super(props); 45 | 46 | this.animation = new Animated.Value(0); 47 | } 48 | 49 | show = () => { 50 | if (this.props.onWillShow) this.props.onWillShow(); 51 | Animated.timing(this.animation, { 52 | duration: 200, 53 | toValue: this.props.height, 54 | useNativeDriver: true, 55 | }).start(this.props.onDidShow); 56 | }; 57 | 58 | hide = () => { 59 | if (this.props.onWillHide) this.props.onWillHide(); 60 | Animated.timing(this.animation, { 61 | duration: 200, 62 | toValue: 0, 63 | useNativeDriver: true, 64 | }).start(this.props.onDidHide); 65 | }; 66 | 67 | componentDidMount() { 68 | this.context.registerInput(this); 69 | this.context.setHeight(this.props.height); 70 | } 71 | 72 | componentWillUnmount() { 73 | Animated.timing(this.animation, { 74 | duration: 200, 75 | toValue: 0, 76 | useNativeDriver: true, 77 | }).start(); 78 | } 79 | 80 | getStyle = () => { 81 | const interpolation = this.animation.interpolate({ 82 | inputRange: [0, this.props.height], 83 | outputRange: [this.props.height, 0], 84 | }); 85 | return this.props.position === 'absolute' 86 | ? { 87 | position: 'absolute', 88 | bottom: 0, 89 | height: this.props.height, 90 | transform: [ 91 | { 92 | translateY: interpolation, 93 | }, 94 | ], 95 | } 96 | : { 97 | height: interpolation, 98 | }; 99 | }; 100 | 101 | render() { 102 | return ( 103 | 104 | 105 | 106 | {inputs.map((value, index) => { 107 | return ( 108 | this.context.onInputEvent(value)} 112 | > 113 | {value} 114 | 115 | ); 116 | })} 117 | this.context.onInputEvent('backspace')} 121 | > 122 | {this.props.backspaceIcon ? ( 123 | this.props.backspaceIcon 124 | ) : ( 125 | 126 | )} 127 | 128 | 129 | 130 | {this.props.hideIcon ? ( 131 | this.props.hideIcon 132 | ) : ( 133 | 134 | )} 135 | 136 | 137 | 138 | ); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /lib/Display.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | TouchableOpacity, 4 | View, 5 | Text, 6 | ViewStyle, 7 | StyleProp, 8 | TextStyle, 9 | } from 'react-native'; 10 | 11 | import NumberPadContext from './NumberPadContext'; 12 | import styles from './styles'; 13 | 14 | const parse = (str: string) => { 15 | return parseFloat(str.replace(/,/g, '')); 16 | }; 17 | 18 | const format = ( 19 | str: string, 20 | initial?: boolean, 21 | integerPlaces: number = 9, 22 | decimalPoints: number = 2, 23 | minimumDecimalPoints: number = 2 24 | ) => { 25 | let decimal: boolean; 26 | let [whole = '', part = ''] = str.split('.'); 27 | if (initial) { 28 | decimal = 29 | str !== '0' && (minimumDecimalPoints > 0 || part.length > 0) 30 | ? true 31 | : false; 32 | } else { 33 | decimal = str.includes('.'); 34 | } 35 | whole = whole.replace(/,/g, '').substring(0, integerPlaces); 36 | whole = whole ? parseInt(whole).toLocaleString('en-US') : '0'; 37 | part = part.substring(0, decimalPoints); 38 | part = initial && decimal ? part.padEnd(minimumDecimalPoints, '0') : part; 39 | return `${whole}${decimal ? '.' : ''}${part}`; 40 | }; 41 | 42 | type DisplayProps = { 43 | value: number; 44 | style: StyleProp; 45 | textStyle: StyleProp; 46 | activeStyle: StyleProp; 47 | activeTextStyle: StyleProp; 48 | invalidTextStyle: StyleProp; 49 | placeholderTextStyle: StyleProp; 50 | cursorStyle: StyleProp; 51 | blinkOnStyle: StyleProp; 52 | blinkOffStyle: StyleProp; 53 | onChange: (val: number) => void; 54 | isValid: (val: string) => boolean; 55 | cursor: boolean; 56 | autofocus: boolean; 57 | /** Custom number formatter (Advanced) 58 | * @param str The string to format from 59 | * @param initial If the value is not in the middle of editing; this is true 60 | */ 61 | format?: (str: string, initial?: boolean) => string; 62 | /** The number of decimal places to use when using the default formatter*/ 63 | decimalPlaces: number; 64 | /** The number of integer places to use when using the default formatter*/ 65 | integerPlaces: number; 66 | /** The minimum decimal places to show when using the default formatter*/ 67 | minimumDecimalPlaces: number; 68 | 69 | onFocus: () => void; 70 | onBlur: () => void; 71 | }; 72 | 73 | type DisplayState = { 74 | valid: boolean; 75 | active: boolean; 76 | blink: boolean; 77 | value: string; 78 | lastValue: string; 79 | empty: boolean; 80 | }; 81 | 82 | export default class Display extends React.Component< 83 | DisplayProps, 84 | DisplayState 85 | > { 86 | blink: null | ReturnType; 87 | static contextType = NumberPadContext; 88 | context!: React.ContextType; 89 | 90 | static defaultProps = { 91 | value: 0.0, 92 | style: styles.display, 93 | textStyle: styles.displayText, 94 | activeStyle: styles.activeDisplay, 95 | activeTextStyle: styles.activeDisplayText, 96 | invalidTextStyle: styles.invalidDisplayText, 97 | placeholderTextStyle: styles.placeholderDisplayText, 98 | cursorStyle: styles.cursor, 99 | blinkOnStyle: styles.blinkOn, 100 | blinkOffStyle: styles.blinkOff, 101 | onChange: () => {}, 102 | isValid: () => true, 103 | cursor: false, 104 | autofocus: false, 105 | decimalPlaces: 2, 106 | integerPlaces: 9, 107 | minimumDecimalPlaces: 2, 108 | onFocus: () => {}, 109 | onBlur: () => {}, 110 | }; 111 | 112 | constructor(props: DisplayProps) { 113 | super(props); 114 | 115 | this.blink = null; 116 | 117 | const formatter = props.format ? props.format : format; 118 | 119 | const value = formatter( 120 | String(this.props.value), 121 | true, 122 | props.integerPlaces, 123 | props.decimalPlaces, 124 | props.minimumDecimalPlaces 125 | ); 126 | 127 | this.state = { 128 | valid: true, 129 | active: false, 130 | blink: true, 131 | value, 132 | lastValue: value, 133 | empty: value === '0', 134 | }; 135 | } 136 | 137 | format(str: string, initial?: boolean): string { 138 | const { 139 | format: fmt = format, 140 | integerPlaces, 141 | decimalPlaces, 142 | minimumDecimalPlaces, 143 | } = this.props; 144 | return fmt( 145 | str, 146 | initial, 147 | integerPlaces, 148 | decimalPlaces, 149 | minimumDecimalPlaces 150 | ); 151 | } 152 | 153 | componentDidMount() { 154 | this.context.registerDisplay(this); 155 | if (this.props.autofocus) { 156 | setTimeout(this.focus, 0); // setTimeout fixes an issue with it sometimes not focusing 157 | } 158 | } 159 | 160 | componentWillUnmount() { 161 | this.context.unregisterDisplay(this); 162 | if (this.blink) clearInterval(this.blink); 163 | } 164 | 165 | focus = (propagate: any = true) => { 166 | if (propagate) this.context.focus(this); 167 | if (!this.state.active) { 168 | // Explicitly check if was active because 169 | // otherwise if tapped again while focussed, value will be reset 170 | this.setState({ 171 | active: true, 172 | lastValue: this.format(this.state.value, true), 173 | value: '0', 174 | }); 175 | } 176 | this.props.onFocus(); 177 | if (this.props.cursor) { 178 | if (this.blink) clearInterval(this.blink); 179 | this.blink = setInterval(() => { 180 | this.setState({ 181 | blink: !this.state.blink, 182 | }); 183 | }, 600); 184 | } 185 | }; 186 | 187 | blur = (propagate = true) => { 188 | if ( 189 | propagate && 190 | this.context.display === (this as any)._reactInternalFiber.key 191 | ) { 192 | this.context.blur(); 193 | } 194 | 195 | const value = this.format(this.state.value, true); 196 | this.props.onBlur(); 197 | this.setState({ 198 | active: false, 199 | value: this.value(value), 200 | }); 201 | }; 202 | 203 | empty = (value?: string) => { 204 | value = value ? value : this.state.value; 205 | return value === '0'; 206 | }; 207 | 208 | value = (value?: string) => { 209 | value = value ? value : this.state.value; 210 | return this.empty(value) ? this.state.lastValue : value; 211 | }; 212 | 213 | onInputEvent = (event: string) => { 214 | const value = this.format( 215 | event === 'backspace' 216 | ? this.state.value.substring(0, this.state.value.length - 1) 217 | : `${this.state.value}${event}` 218 | ); 219 | const valid = this.props.isValid(value); 220 | this.setState({ 221 | value, 222 | valid, 223 | }); 224 | this.props.onChange(parse(this.value(value))); 225 | }; 226 | 227 | render() { 228 | const { valid, value, active } = this.state; 229 | const empty = this.empty(); 230 | const blink = this.state.blink 231 | ? this.props.blinkOnStyle 232 | : this.props.blinkOffStyle; 233 | const style: StyleProp[] = [ 234 | { flexDirection: 'row' }, 235 | this.props.style, 236 | active ? this.props.activeStyle : null, 237 | ]; 238 | const textStyle = [ 239 | this.props.textStyle, 240 | active ? this.props.activeTextStyle : null, 241 | ]; 242 | const cursorStyle = [this.props.cursorStyle]; 243 | return ( 244 | 245 | 246 | 253 | {empty ? this.state.lastValue : value} 254 | 255 | 256 | 257 | ); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Native Numpad 2 | 3 | A simple React Native number pad for quickly updating multiple number inputs. 4 | 5 | [![npm version](https://badge.fury.io/js/react-native-numpad.svg)](https://badge.fury.io/js/react-native-numpad) 6 | [![runs with expo](https://img.shields.io/badge/Runs%20with%20Expo-000.svg?style=flat-square&logo=EXPO&labelColor=f3f3f3&logoColor=000)](https://expo.io/) 7 | 8 | - ✅ **No Dependencies** 9 | - ✅ iOS 10 | - ✅ Android 11 | - ✅ React Native Web 12 | - ✅ JS-Only (No Native Code / No Linking Necessary) 13 | 14 | ![Screen Recording](screen-recording.gif) 15 | 16 | ## Demo 👉 Expo Snack 17 | 18 | ## Install 19 | 20 | ``` 21 | yarn add react-native-numpad 22 | ``` 23 | 24 | ## Use Cases 25 | 26 | - Splitting expenses 27 | - Forms with multiple number inputs 28 | - Spreadsheets 29 | - Calculators 30 | 31 | ## Usage 32 | 33 | ```js 34 | import React from 'react'; 35 | import NumberPad, { Input, Display } from './index'; 36 | 37 | export default () => ( 38 | 39 | 40 | 41 | 42 | 43 | ); 44 | ``` 45 | 46 | ## Custom Icons 47 | 48 | ```js 49 | import React from 'react'; 50 | import NumberPad, { Input, Display } from './index'; 51 | import { Ionicons } from '@expo/vector-icons'; 52 | 53 | export default () => ( 54 | 55 | 56 | 57 | } 59 | hideIcon={} 60 | /> 61 | 62 | ); 63 | ``` 64 | 65 | ## API 66 | 67 | Under the hood, `react-native-numpad` uses the [React Context API](https://reactjs.org/docs/context.html) to link the number inputs (the ``s) to the number pad (the ``). 68 | 69 | ### `` Component 70 | 71 | The `` component is a [HOC (Higher Order Component)](https://reactjs.org/docs/higher-order-components.html) that does not accept any props besides `children`. It creates a `reactNativeNumpad` context that listens for press events on the number inputs, opens the number input when it detects a press, and then updates the input values when the user presses on the number buttons in the number pad. 72 | 73 | ### `` Component 74 | 75 | The `` is the number pad's equivalent of React Native's [``](https://reactnative.dev/docs/textinput) component. It is a [controlled component](https://reactjs.org/docs/forms.html#controlled-components) that, when pressed, opens the number pad. 76 | 77 | | Prop | Description | Default | 78 | | -------------------------- | ------------------------------------------------------------------------------------------------ | ------- | 79 | | **`value`** | Current value of the input (number only) | _None_ | 80 | | **`style`** | Any valid style object for [``](https://reactnative.dev/docs/touchableopacity) | _None_ | 81 | | **`textStyle`** | Any valid style object for a [``](https://reactnative.dev/docs/text) component | _None_ | 82 | | **`activeStyle`** | Any valid style object for a [``](https://reactnative.dev/docs/text) component | _None_ | 83 | | **`invalidTextStyle`** | Any valid style object for a [``](https://reactnative.dev/docs/text) component | _None_ | 84 | | **`placeholderTextStyle`** | Any valid style object for a [``](https://reactnative.dev/docs/text) component | _None_ | 85 | | **`cursorStyle`** | Any valid style object for a [``](https://reactnative.dev/docs/view) component | _None_ | 86 | | **`blinkOnStyle`** | Any valid style object for a [``](https://reactnative.dev/docs/view) component | _None_ | 87 | | **`blinkOffStyle`** | Any valid style object for a [``](https://reactnative.dev/docs/view) component | _None_ | 88 | | **`onChange`** | An event handler function that receives the new value (number) as an argument | _None_ | 89 | | **`cursor`** | Whether or not to show the cursor when the input is focused (boolean) | true | 90 | | **`autofocus`** | Whether or not to autofocus the input when the component is loaded (boolean) | false | 91 | 92 | ### `` Component 93 | 94 | The `` a custom number pad keyboard that, unlike the native keyboard, does not minimize when the user presses on a new number input if it is already open. It is stylable and easy to customize. 95 | 96 | | Prop | Description | Default | 97 | | ------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------ | 98 | | **`height`** | Height of the number pad | 270 | 99 | | **`position`** | How the number pad will be positioned | 'absolute' \| 'relative' | 100 | | **`style`** | Any valid style object for a [``](https://reactnative.dev/docs/view) component (`Animated.View`, actually) | _None_ | 101 | | **`backspaceIcon`** | An Icon element (eg from `react-native-vector-icons` or `@expo/vector-icons`) | _None_ | 102 | | **`hideIcon`** | An Icon element (eg from `react-native-vector-icons` or `@expo/vector-icons`) | _None_ | 103 | | **`onWillHide`** | Called just before the number pad will hide | _None_ | 104 | | **`onDidHide`** | Called just after the number pad hides | _None_ | 105 | | **`onWillShow`** | Called just before the number pad will show | _None_ | 106 | | **`onDidShow`** | Called just after the number pad shows | _None_ | 107 | 108 | ### `` Component 109 | 110 | Sometimes React Native's built-in [](https://reactnative.dev/docs/keyboardavoidingview) does not work smoothly with the number pad: it can either have performance issues where animations are choppy or it can be difficult to configure its height properly altogether. We've included a number pad context-aware version that adjusts it's height based on the keyboard animation to achieve a smooth frame rate. 111 | 112 | | Prop | Description | Default | 113 | | ----------- | ---------------------------------------------------------------------------------------------------------------- | ------- | 114 | | **`style`** | Any valid style object for a [``](https://reactnative.dev/docs/view) component (`Animated.View`, actually) | _None_ | 115 | 116 | ## Version History (Change Log) 117 | 118 | View [here](./CHANGELOG.md). 119 | 120 | ## Contribute 121 | 122 | We welcome contributions! If you are interested in contributing, consider helping us with one of the following tasks: 123 | 124 | - Rewrite components in TypeScript using arrow-function components and [React hooks](https://reactjs.org/docs/hooks-intro.html) 125 | - Add TypeScript bindings 126 | - Add Tests 127 | 128 | ## Glance Money 129 | 130 | [![Glance Money Logo](https://uploads-ssl.webflow.com/5ec80cb02fe1c031a342c6cc/5ecdb5f349bce13a545a2dae_Artboard%20Copy%2062.png)](https://glance.money) 131 | 132 | We wrote this for, actively use, and maintain this library for [Glance Money](https://glance.money). Now it is free and open for the world to use ❤️ 133 | 134 | ## License 135 | 136 | [MIT licensed.](./LICENSE) 137 | --------------------------------------------------------------------------------