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 617f55e
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 57 deletions.
4 changes: 4 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"tabWidth": 2,
"useTabs": false
}
26 changes: 21 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, { useCallback, useState } from 'react';

Check warning on line 2 in Storybook/components/NumberField/NumberField.stories.tsx

View workflow job for this annotation

GitHub Actions / unittests / unittests

'useCallback' is defined but never used
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 @@ -45,6 +46,21 @@ export default {
type Story = StoryObj<ComponentProps>;

export const Default: Story = {
args: {},
render: (args) => {
const [quantity, setQuantity] = useState<number>(args.value);

Check failure on line 50 in Storybook/components/NumberField/NumberField.stories.tsx

View workflow job for this annotation

GitHub Actions / unittests / unittests

React Hook "useState" is called in function "render" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use"
const onValueChange = (newQuantity: number) => {
setQuantity(newQuantity);
};
return (
<NumberField
minValue={args.minValue}
maxValue={args.maxValue}
size={args.size}
decimal={args.decimal}
value={quantity}
onValueChange={onValueChange}
/>
);
},
};
Default.parameters = { noSafeArea: false };
28 changes: 21 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, { useCallback, useState } from 'react';

Check warning on line 2 in Storybook/components/NumberSelector/NumberSelector.stories.tsx

View workflow job for this annotation

GitHub Actions / unittests / unittests

'useCallback' is defined but never used
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 @@ -32,12 +35,23 @@ export default {
type Story = StoryObj<ComponentProps>;

export const Default: Story = {
args: {
showSoftInputOnFocus: true,
minValue: 0,
maxValue: 999,
minusIcon: 'arrow-back',
plusIcon: 'arrow-forward',
render: (args) => {
const [quantity, setQuantity] = useState<number>(args.value);

Check failure on line 39 in Storybook/components/NumberSelector/NumberSelector.stories.tsx

View workflow job for this annotation

GitHub Actions / unittests / unittests

React Hook "useState" is called in function "render" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use"
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}
/>
);
},
};
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 @@
import React, { useEffect, useState } from 'react';
import { StyleSheet, TextInput, TextInputBase } from 'react-native';
import { useTheme } from '../../styles/themes';
import { parse } from 'react-native-svg';

Check failure on line 4 in src/components/numberField/NumberField.tsx

View workflow job for this annotation

GitHub Actions / build

'parse' is declared but its value is never read.

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?$/

Check failure on line 36 in src/components/numberField/NumberField.tsx

View workflow job for this annotation

GitHub Actions / unittests / unittests

Unnecessary escape character: \.
: /^\d+[\.]?\d?$/;

Check failure on line 37 in src/components/numberField/NumberField.tsx

View workflow job for this annotation

GitHub Actions / unittests / unittests

Unnecessary escape character: \.
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]);

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 617f55e

Please sign in to comment.