Skip to content

Commit

Permalink
✨ number selector handles decimals
Browse files Browse the repository at this point in the history
  • Loading branch information
Mibou committed Oct 26, 2023
1 parent 13a8ab9 commit 3fc95dc
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 70 deletions.
6 changes: 6 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"tabWidth": 4,
"useTabs": false,
"singleQuote": true,
"jsxSingleQuote": true
}
29 changes: 15 additions & 14 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
{
"singleQuote": true,
"semi": true,
"printWidth": 100,
"tabWidth": 4,
"trailingComma": "all",
"endOfLine": "lf",
"overrides": [
{
"files": "*.json",
"options": {
"tabWidth": 2
}
}
]
"singleQuote": true,
"semi": true,
"printWidth": 100,
"tabWidth": 4,
"trailingComma": "all",
"endOfLine": "lf",
"jsxSingleQuote": true,
"overrides": [
{
"files": "*.json",
"options": {
"tabWidth": 2
}
}
]
}
29 changes: 24 additions & 5 deletions Storybook/components/NumberField/NumberField.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react-native';
import React from 'react';
import React, { useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { NumberField } from 'smartway-react-native-ui';

Expand All @@ -9,9 +9,9 @@ export default {
title: 'components/NumberField',
component: NumberField,
args: {
value: '0',
minValue: 0,
maxValue: 10,
value: '-999.9',
minValue: -999.9,
maxValue: 999.9,
},
argTypes: {
state: {
Expand All @@ -26,6 +26,7 @@ export default {
],
},
size: { control: { type: 'radio' }, options: ['m', 's'] },
decimal: { control: { type: 'radio' }, options: [true, false] },
},

decorators: [
Expand All @@ -44,7 +45,25 @@ export default {

type Story = StoryObj<ComponentProps>;

const NumberFieldTester = (args) => {
const [quantity, setQuantity] = useState<number>(args.value);
const onValueChange = (newQuantity: number) => {
setQuantity(newQuantity);
};
return (
<NumberField
minValue={args.minValue}
maxValue={args.maxValue}
size={args.size}
decimal={args.decimal}
value={quantity}
onValueChange={onValueChange}
/>
);
};
export const Default: Story = {
args: {},
render: (args) => {
return NumberFieldTester(args);
},
};
Default.parameters = { noSafeArea: false };
32 changes: 25 additions & 7 deletions Storybook/components/NumberSelector/NumberSelector.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react-native';
import React from 'react';
import React, { useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { NumberSelector } from 'smartway-react-native-ui';

Expand All @@ -10,9 +10,12 @@ export default {
component: NumberSelector,
args: {
value: 0,
minValue: -999,
maxValue: 999,
},
argTypes: {
onValueChange: { action: 'onValueChange' },
decimal: { control: { type: 'radio' }, options: [true, false] },
},

decorators: [
Expand All @@ -31,13 +34,28 @@ export default {

type Story = StoryObj<ComponentProps>;

const NumberSelectorTester = (args) => {
const [quantity, setQuantity] = useState<number>(args.value);
const onValueChange = (newQuantity: number) => {
setQuantity(newQuantity);
};
return (
<NumberSelector
showSoftInputOnFocus={args.showSoftInputOnFocus}
decimal={args.decimal}
minValue={args.minValue}
maxValue={args.maxValue}
minusIcon={args.minusIcon}
plusIcon={args.plusIcon}
value={quantity}
onValueChange={onValueChange}
/>
);
};

export const Default: Story = {
args: {
showSoftInputOnFocus: true,
minValue: 0,
maxValue: 999,
minusIcon: 'arrow-back',
plusIcon: 'arrow-forward',
render: (args) => {
return NumberSelectorTester(args);
},
};
Default.parameters = { noSafeArea: false };
94 changes: 62 additions & 32 deletions src/components/numberField/NumberField.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
/* eslint-disable no-useless-escape */
import React, { useEffect, useState } from 'react';
import { StyleSheet, TextInput, TextInputBase } from 'react-native';
import { useTheme } from '../../styles/themes';

type FieldBaseProps = React.ComponentProps<typeof TextInputBase>;
export interface NumberFieldProps extends FieldBaseProps {
state?: 'readonly' | 'filled' | 'prefilled' | 'filled-focused' | 'prefilled-focused' | 'error';
state?:
| 'readonly'
| 'filled'
| 'prefilled'
| 'filled-focused'
| 'prefilled-focused'
| 'error';
size?: 'm' | 's';
minValue?: number;
maxValue?: number;
decimal?: boolean;
}

export const NumberField = React.forwardRef<TextInput, NumberFieldProps>(
Expand All @@ -17,21 +25,30 @@ export const NumberField = React.forwardRef<TextInput, NumberFieldProps>(
size = 'm',
minValue = 0,
maxValue = 999,
decimal = false,
...props
}: NumberFieldProps,
ref,
ref
) => {
const theme = useTheme();

const decimalRegex =
minValue !== undefined && minValue < 0
? /^-?\d+[\.]?\d?$/
: /^\d+[\.]?\d?$/;
const integerRegex =
minValue !== undefined && minValue < 0 ? /^-?\d+$/ : /^\d+$/;
const numberRegex = decimal ? decimalRegex : integerRegex;
const [currentState, setCurrentState] = useState<string>(state);
const [filled, setFilled] = useState<boolean>(false);
const [error, setError] = useState<boolean>(false);
const [focused, setFocused] = useState<boolean>(false);
const [forcedState, setForcedState] = useState<boolean>(true);
const [firstContentChange, setFirstContentChange] = useState<boolean>(true);
const [firstContentChange, setFirstContentChange] =
useState<boolean>(true);
const [firstValue, setFirstValue] = useState<string>();
const [value, setValue] = useState<string>(props.value ?? '');
const [lastValue, setLastValue] = useState<string>();
const parser = decimal ? parseFloat : parseInt;

useEffect(() => {
if (forcedState) {
Expand All @@ -48,7 +65,7 @@ export const NumberField = React.forwardRef<TextInput, NumberFieldProps>(
return;
}
setFilled(props.value !== firstValue);
if (cleanContent(props.value) !== '') setLastValue(value);
if (numberRegex.test(props.value ?? '')) setLastValue(value);
checkContent(props.value);
}, [props.value]);

Check warning on line 70 in src/components/numberField/NumberField.tsx

View workflow job for this annotation

GitHub Actions / unittests / unittests

React Hook useEffect has missing dependencies: 'checkContent', 'firstContentChange', 'firstValue', 'numberRegex', and 'value'. Either include them or remove the dependency array. You can also replace multiple useState variables with useReducer if 'setFilled' needs the current value of 'firstValue'

Check warning on line 70 in src/components/numberField/NumberField.tsx

View workflow job for this annotation

GitHub Actions / unittests / unittests

React Hook useEffect has missing dependencies: 'checkContent', 'firstContentChange', 'firstValue', 'numberRegex', and 'value'. Either include them or remove the dependency array. You can also replace multiple useState variables with useReducer if 'setFilled' needs the current value of 'firstValue'

Expand All @@ -59,7 +76,7 @@ export const NumberField = React.forwardRef<TextInput, NumberFieldProps>(
return;
}
setFilled(value !== firstValue);
if (cleanContent(value) !== '') {
if (numberRegex.test(props.value ?? '')) {
setLastValue(value);
}
checkContent(value);
Expand All @@ -73,7 +90,8 @@ export const NumberField = React.forwardRef<TextInput, NumberFieldProps>(
case 'prefilled':
textColor = theme.sw.colors.neutral[500];
borderColor = undefined;
backgroundColor = theme.sw.colors.neutral[500] + theme.sw.transparency[8];
backgroundColor =
theme.sw.colors.neutral[500] + theme.sw.transparency[8];
break;
case 'filled-focused':
textColor = theme.sw.colors.neutral[800];
Expand All @@ -83,17 +101,21 @@ export const NumberField = React.forwardRef<TextInput, NumberFieldProps>(
case 'prefilled-focused':
textColor = theme.sw.colors.primary.main;
borderColor = theme.sw.colors.primary.main;
backgroundColor = theme.sw.colors.primary.main + theme.sw.transparency[16];
backgroundColor =
theme.sw.colors.primary.main +
theme.sw.transparency[16];
break;
case 'filled':
textColor = theme.sw.colors.neutral[800];
borderColor = undefined;
backgroundColor = theme.sw.colors.neutral[500] + theme.sw.transparency[8];
backgroundColor =
theme.sw.colors.neutral[500] + theme.sw.transparency[8];
break;
case 'error':
textColor = theme.sw.colors.error.main;
borderColor = undefined;
backgroundColor = theme.sw.colors.error.main + theme.sw.transparency[8];
backgroundColor =
theme.sw.colors.error.main + theme.sw.transparency[8];
break;
case undefined:
break;
Expand All @@ -106,7 +128,8 @@ export const NumberField = React.forwardRef<TextInput, NumberFieldProps>(
borderWidth: borderColor !== undefined ? 1 : 0,
borderColor: borderColor,

width: size === 's' ? 43 : 72,
width:
size === 's' ? (decimal ? 63 : 43) : decimal ? 110 : 72,

color: textColor,
fontStyle: 'normal',
Expand All @@ -132,45 +155,50 @@ export const NumberField = React.forwardRef<TextInput, NumberFieldProps>(
else setCurrentState('prefilled');
}
};

const checkContent = (text: string | undefined) => {
if (text !== undefined && text !== '') {
const cleanNumber = text.replace(/[^0-9]/g, '');
const parsedValue = parseInt(cleanNumber);
if (parsedValue !== undefined) {
if (
text !== undefined &&
text !== '' &&
((minValue !== undefined && minValue < 0 && text !== '-') ||
minValue === undefined ||
(minValue !== undefined && minValue >= 0))
) {
const parsedValue = parser(text);
if (!Number.isNaN(parsedValue)) {
setError(
(minValue !== undefined && parsedValue < minValue) ||
(maxValue !== undefined && parsedValue >= maxValue),
(maxValue !== undefined && parsedValue >= maxValue)
);
}
}
};
const cleanContent = (text: string | undefined) => {
if (text !== undefined && text !== '') {
const cleanNumber = text.replace(/[^-0-9]/g, '');
const parsedValue = parseInt(cleanNumber);
return parsedValue.toString();
}
return '';
};
const onChangeText = (e: any) => {
if (props?.onChangeText !== undefined) {
props.onChangeText(e);
checkContent(props.value);
} else {
if (e == '') setValue('');
else if (cleanContent(e) != '') setValue(cleanContent(e));
if (e == '' || (allowedMinus() && e == '-')) setValue(e);
else if (numberRegex.test(e)) setValue(e);
checkContent(value);
}
};
const onFocus = (e: any) => {
setFocused(true);
if (props?.onFocus !== undefined) props.onFocus(e);
};
const allowedMinus = (): boolean => {
return minValue !== undefined && minValue < 0;
};
const onBlur = (e: any) => {
setFocused(false);
if ((value === '' || props.value === '') && firstValue !== '') onChangeText(firstValue);
else if ((value === '' || props.value === '') && lastValue !== '') {
onChangeText(lastValue);
if (
value === '' ||
props.value === '' ||
(allowedMinus() && (value === '-' || props.value === '-'))
) {
if (firstValue !== '') onChangeText(firstValue);
else if (lastValue !== '') onChangeText(lastValue);
}
if (props?.onBlur !== undefined) props.onBlur(e);
};
Expand All @@ -185,14 +213,16 @@ export const NumberField = React.forwardRef<TextInput, NumberFieldProps>(
onChangeText={(e) => onChangeText(e)}
onBlur={(e) => onBlur(e)}
onFocus={(e) => onFocus(e)}
selectionColor={theme.sw.colors.primary.main + theme.sw.transparency[16]}
selectionColor={
theme.sw.colors.primary.main + theme.sw.transparency[16]
}
cursorColor={theme.sw.colors.primary.main}
keyboardType="number-pad"
keyboardType='number-pad'
editable={state !== 'readonly'}
textAlign={'center'}
/>
);
},
}
);

NumberField.displayName = 'NumberField';
Loading

0 comments on commit 3fc95dc

Please sign in to comment.