Skip to content

Feat/number input 2 #2333

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Dec 12, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions demo/src/screens/MenuStructure.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export const navigationData = {
{title: 'Color Picker', tags: 'color picker control', screen: 'unicorn.components.ColorPickerScreen'},
{title: 'Color Swatch', tags: 'color swatch and palette', screen: 'unicorn.components.ColorSwatchScreen'},
{title: 'TextField', tags: 'text input field form', screen: 'unicorn.components.TextFieldScreen'},
{title: 'NumberInput', tags: 'number input', screen: 'unicorn.components.NumberInputScreen'},
{title: 'Picker', tags: 'picker form', screen: 'unicorn.components.PickerScreen'},
{title: 'DateTimePicker', tags: 'date time picker form', screen: 'unicorn.components.DateTimePickerScreen'},
{title: 'RadioButton', tags: 'radio button group controls', screen: 'unicorn.components.RadioButtonScreen'},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React, {useState, useCallback, useRef, useMemo} from 'react';
import {StyleSheet, TouchableWithoutFeedback, Keyboard as RNKeyboard} from 'react-native';
import {
Text,
Spacings,
NumberInput,
NumberInputResult,
View,
Typography,
Constants,
Incubator
} from 'react-native-ui-lib';

const NumberInputErrorOnChangeScreen = () => {
const currentData = useRef<NumberInputResult>();
const minimum = useRef<number>(5000);
// const [initialNumber, setInitialNumber] = useState<number>(100);
const [text, setText] = useState<string>('');

const onChangeNumber = useCallback((result: NumberInputResult) => {
currentData.current = result;
}, []);

const processInput = useCallback(() => {
let newText = '';
if (currentData.current) {
switch (currentData.current.type) {
case 'valid':
newText = currentData.current.formattedNumber;
break;
case 'empty':
newText = 'Empty';
break;
case 'error':
newText = `Error: value '${currentData.current.userInput}' is invalid`;
break;
}
}

setText(newText);
}, []);

const isValid = useCallback(() => {
return currentData.current?.type === 'valid';
}, []);

const minimumPrice = useCallback(() => {
if (currentData.current?.type === 'valid') {
return currentData.current.number >= minimum.current;
}
}, []);

const validate = useMemo((): Incubator.TextFieldProps['validate'] => [isValid, minimumPrice],
[isValid, minimumPrice]);

const validationMessage = useMemo((): Incubator.TextFieldProps['validationMessage'] => [
'Please enter a valid number',
`Make sure your number is above ${minimum.current}`
],
[]);

return (
<TouchableWithoutFeedback onPress={RNKeyboard.dismiss}>
<View flex centerH>
<Text text40 margin-s10>
Number Input
</Text>

<View flex center>
<NumberInput
// initialNumber={initialNumber}
onChangeNumber={onChangeNumber}
placeholder={'Price'}
leadingText={'$'}
leadingTextStyle={styles.leadingText}
style={styles.mainText}
containerStyle={styles.containerStyle}
label={'Enter Price'}
labelStyle={styles.label}
validate={validate}
validationMessage={validationMessage}
validationMessageStyle={styles.validationMessage}
marginLeft={Spacings.s4}
marginRight={Spacings.s4}
onBlur={processInput}
validateOnChange
/>
<Text marginT-s5>{text}</Text>
</View>
</View>
</TouchableWithoutFeedback>
);
};

export default NumberInputErrorOnChangeScreen;

const styles = StyleSheet.create({
containerStyle: {
alignSelf: 'center',
marginBottom: 30,
marginLeft: Spacings.s5,
marginRight: Spacings.s5
},
mainText: {
height: 36,
marginVertical: Spacings.s1,
textAlign: 'center',
...Typography.text30M
},
leadingText: {
marginTop: Constants.isIOS ? Spacings.s2 : 0,
...Typography.text50M
},
label: {
textAlign: 'center',
marginBottom: Spacings.s1,
...Typography.bodySmallMedium
},
validationMessage: {
flexGrow: 1,
textAlign: 'center',
...Typography.bodySmallMedium
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React, {useState, useCallback, useRef} from 'react';
import {StyleSheet, TouchableWithoutFeedback, Keyboard as RNKeyboard} from 'react-native';
import {Text, Spacings, NumberInput, NumberInputResult, View, Typography, Constants, Button} from 'react-native-ui-lib';

const NumberInputProcessButtonScreen = () => {
const currentData = useRef<NumberInputResult>();
// const [initialNumber, setInitialNumber] = useState<number>(100);
const [text, setText] = useState<string>('');

const onChangeNumber = useCallback((result: NumberInputResult) => {
currentData.current = result;
}, []);

const processInput = useCallback(() => {
let newText = '';
if (currentData.current) {
switch (currentData.current.type) {
case 'valid':
newText = currentData.current.formattedNumber;
break;
case 'empty':
newText = 'Empty';
break;
case 'error':
newText = `Error: value '${currentData.current.userInput}' is invalid`;
break;
}
}

setText(newText);
}, []);

return (
<TouchableWithoutFeedback onPress={RNKeyboard.dismiss}>
<View flex centerH>
<Text text40 margin-s10>
Number Input
</Text>

<View flex center>
<NumberInput
// initialNumber={initialNumber}
onChangeNumber={onChangeNumber}
placeholder={'Price'}
leadingText={'$'}
leadingTextStyle={styles.leadingText}
style={styles.mainText}
containerStyle={styles.containerStyle}
label={'Enter Price'}
labelStyle={styles.label}
validate={'required'}
validationMessage={'Please enter a price'}
validationMessageStyle={styles.validationMessage}
marginLeft={Spacings.s4}
marginRight={Spacings.s4}
onBlur={processInput}
/>
<Button label={'Process'} onPress={processInput}/>
<Text marginT-s5>{text}</Text>
</View>
</View>
</TouchableWithoutFeedback>
);
};

export default NumberInputProcessButtonScreen;

const styles = StyleSheet.create({
containerStyle: {
alignSelf: 'center',
marginBottom: 30,
marginLeft: Spacings.s5,
marginRight: Spacings.s5
},
mainText: {
height: 36,
marginVertical: Spacings.s1,
textAlign: 'center',
...Typography.text30M
},
leadingText: {
marginTop: Constants.isIOS ? Spacings.s2 : 0,
...Typography.text50M
},
label: {
textAlign: 'center',
marginBottom: Spacings.s1,
...Typography.bodySmallMedium
},
validationMessage: {
flexGrow: 1,
textAlign: 'center',
...Typography.bodySmallMedium
}
});
42 changes: 42 additions & 0 deletions demo/src/screens/componentScreens/NumberInputScreen/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React, {Component} from 'react';
import {Colors, View, TabController} from 'react-native-ui-lib';
import _ from 'lodash';
import {gestureHandlerRootHOC} from 'react-native-gesture-handler';

import NumberInputProcessButtonScreen from './NumberInputProcessButtonScreen';
import NumberInputErrorOnChangeScreen from './NumberInputErrorOnChangeScreen';

const SCREENS = [
{title: 'Process button', screen: NumberInputProcessButtonScreen},
{title: 'Error on change', screen: NumberInputErrorOnChangeScreen}
];

class NumberInputScreen extends Component {
state = {};

renderPages() {
return (
<View flex>
{_.map(SCREENS, (item, index) => {
const Screen = item.screen;
return (
<TabController.TabPage key={`${item.title}_page`} index={index}>
<Screen/>
</TabController.TabPage>
);
})}
</View>
);
}

render() {
return (
<TabController items={SCREENS.map(item => ({label: item.title}))}>
<TabController.TabBar activeBackgroundColor={Colors.blue70}/>
{this.renderPages()}
</TabController>
);
}
}

export default gestureHandlerRootHOC(NumberInputScreen);
1 change: 1 addition & 0 deletions demo/src/screens/componentScreens/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export function registerScreens(registrar) {
registrar('unicorn.components.KeyboardAwareScrollViewScreen', () => require('./KeyboardAwareScrollViewScreen').default);
registrar('unicorn.components.MaskedInputScreen', () => require('./MaskedInputScreen').default);
registrar('unicorn.components.MarqueeScreen', () => require('./MarqueeScreen').default);
registrar('unicorn.components.NumberInputScreen', () => require('./NumberInputScreen').default);
registrar('unicorn.components.OverlaysScreen', () => require('./OverlaysScreen').default);
registrar('unicorn.components.PageControlScreen', () => require('./PageControlScreen').default);
registrar('unicorn.components.PanDismissibleScreen', () => require('./PanDismissibleScreen').default);
Expand Down
7 changes: 7 additions & 0 deletions jest-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,10 @@ jest.mock('react-native', () => {
reactNative.NativeModules.KeyboardTrackingViewTempManager = {};
return reactNative;
});

if (typeof String.prototype.replaceAll === 'undefined') {
// eslint-disable-next-line no-extend-native
String.prototype.replaceAll = function (match, replace) {
return this.split(match).join(replace);
};
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@
"./jest-setup.js"
],
"testMatch": [
"**/*.spec.(js|tsx)"
"**/*.spec.(js|ts|tsx)"
],
"moduleNameMapper": {
"^react-native-reanimated$": "<rootDir>/node_modules/react-native-reanimated/src/Animated.js"
Expand Down
3 changes: 3 additions & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ export default {
get Modal() {
return require('./modal').default;
},
get NumberInput() {
return require('./numberInput').default;
},
get ListItem() {
return require('./listItem').default;
},
Expand Down
3 changes: 3 additions & 0 deletions src/components/inputs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@ module.exports = {
},
get MaskedInput() {
return require('../maskedInput').default;
},
get NumberInput() {
return require('../numberInput').default;
}
};
72 changes: 72 additions & 0 deletions src/components/numberInput/Presenter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {isEmpty} from 'lodash';

export type NumberInputResult =
| {type: 'valid'; userInput: string; number: number; formattedNumber: string}
| {type: 'empty'}
| {type: 'error'; userInput: string};

export const EMPTY: NumberInputResult = {type: 'empty'};

export interface LocaleOptions {
locale: string;
decimalSeparator: string;
thousandSeparator: string;
}

export interface Options {
localeOptions: LocaleOptions;
fractionDigits: number;
}

function formatNumber(value: number, options: Options) {
return value.toLocaleString(options.localeOptions.locale, {maximumFractionDigits: options.fractionDigits});
}

function generateLocaleOptions(locale: string) {
const options: Options = {
localeOptions: {
locale,
decimalSeparator: '', // fake decimalSeparator, we're creating it now
thousandSeparator: '' // fake thousandSeparator, we're creating it now
},
fractionDigits: 1
};
const decimalSeparator = formatNumber(1.1, options).replace(/1/g, '');
const thousandSeparator = formatNumber(1111, options).replace(/1/g, '');

return {
locale,
decimalSeparator,
thousandSeparator
};
}

export function generateOptions(locale: string, fractionDigits: number): Options {
return {localeOptions: generateLocaleOptions(locale), fractionDigits};
}

export function parseInput(text: string, options: Options): NumberInputResult {
if (isEmpty(text)) {
return EMPTY;
}

let cleanInput: string = text.replaceAll(options.localeOptions.thousandSeparator, '');
cleanInput = cleanInput.replaceAll(options.localeOptions.decimalSeparator, '.');
let number = Number(cleanInput);
if (isNaN(number)) {
return {type: 'error', userInput: text};
}

number = Number(number.toFixed(options.fractionDigits));
const formattedNumber = formatNumber(number, options);

return {type: 'valid', userInput: text, number, formattedNumber};
}

export function getInitialResult(options: Options, initialValue?: number): NumberInputResult {
if (initialValue === undefined) {
return EMPTY;
}

return parseInput(formatNumber(initialValue, options), options);
}
Loading