├── .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 | [](https://badge.fury.io/js/react-native-numpad)
6 | [](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 | 
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 | [](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 |
--------------------------------------------------------------------------------