diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..222861c3 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 2, + "useTabs": false +} diff --git a/Storybook/components/NumberField/NumberField.stories.tsx b/Storybook/components/NumberField/NumberField.stories.tsx index dd190cf1..e147c986 100644 --- a/Storybook/components/NumberField/NumberField.stories.tsx +++ b/Storybook/components/NumberField/NumberField.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react-native'; -import React from 'react'; +import React, { useCallback, useState } from 'react'; import { StyleSheet, View } from 'react-native'; import { NumberField } from 'smartway-react-native-ui'; @@ -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: { @@ -26,6 +26,7 @@ export default { ], }, size: { control: { type: 'radio' }, options: ['m', 's'] }, + decimal: { control: { type: 'radio' }, options: [true, false] }, }, decorators: [ @@ -45,6 +46,21 @@ export default { type Story = StoryObj; export const Default: Story = { - args: {}, + render: (args) => { + const [quantity, setQuantity] = useState(args.value); + const onValueChange = (newQuantity: number) => { + setQuantity(newQuantity); + }; + return ( + + ); + }, }; Default.parameters = { noSafeArea: false }; diff --git a/Storybook/components/NumberSelector/NumberSelector.stories.tsx b/Storybook/components/NumberSelector/NumberSelector.stories.tsx index 702b0b2d..e3063419 100644 --- a/Storybook/components/NumberSelector/NumberSelector.stories.tsx +++ b/Storybook/components/NumberSelector/NumberSelector.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react-native'; -import React from 'react'; +import React, { useCallback, useState } from 'react'; import { StyleSheet, View } from 'react-native'; import { NumberSelector } from 'smartway-react-native-ui'; @@ -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: [ @@ -32,12 +35,23 @@ export default { type Story = StoryObj; export const Default: Story = { - args: { - showSoftInputOnFocus: true, - minValue: 0, - maxValue: 999, - minusIcon: 'arrow-back', - plusIcon: 'arrow-forward', + render: (args) => { + const [quantity, setQuantity] = useState(args.value); + const onValueChange = (newQuantity: number) => { + setQuantity(newQuantity); + }; + return ( + + ); }, }; Default.parameters = { noSafeArea: false }; diff --git a/src/components/numberField/NumberField.tsx b/src/components/numberField/NumberField.tsx index a5403547..d9075a79 100644 --- a/src/components/numberField/NumberField.tsx +++ b/src/components/numberField/NumberField.tsx @@ -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'; type FieldBaseProps = React.ComponentProps; 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( @@ -17,21 +25,30 @@ export const NumberField = React.forwardRef( 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(state); const [filled, setFilled] = useState(false); const [error, setError] = useState(false); const [focused, setFocused] = useState(false); const [forcedState, setForcedState] = useState(true); - const [firstContentChange, setFirstContentChange] = useState(true); + const [firstContentChange, setFirstContentChange] = + useState(true); const [firstValue, setFirstValue] = useState(); const [value, setValue] = useState(props.value ?? ''); const [lastValue, setLastValue] = useState(); + const parser = decimal ? parseFloat : parseInt; useEffect(() => { if (forcedState) { @@ -48,7 +65,7 @@ export const NumberField = React.forwardRef( return; } setFilled(props.value !== firstValue); - if (cleanContent(props.value) !== '') setLastValue(value); + if (numberRegex.test(props.value ?? '')) setLastValue(value); checkContent(props.value); }, [props.value]); @@ -59,7 +76,7 @@ export const NumberField = React.forwardRef( return; } setFilled(value !== firstValue); - if (cleanContent(value) !== '') { + if (numberRegex.test(props.value ?? '')) { setLastValue(value); } checkContent(value); @@ -73,7 +90,8 @@ export const NumberField = React.forwardRef( 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]; @@ -83,17 +101,21 @@ export const NumberField = React.forwardRef( 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; @@ -106,7 +128,8 @@ export const NumberField = React.forwardRef( 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', @@ -132,33 +155,31 @@ export const NumberField = React.forwardRef( 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); } }; @@ -166,11 +187,18 @@ export const NumberField = React.forwardRef( 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); }; @@ -185,14 +213,16 @@ export const NumberField = React.forwardRef( 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'; diff --git a/src/components/numberSelector/NumberSelector.tsx b/src/components/numberSelector/NumberSelector.tsx index 26d8ec6b..d2ebc097 100644 --- a/src/components/numberSelector/NumberSelector.tsx +++ b/src/components/numberSelector/NumberSelector.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from 'react'; +import React, { useCallback, useRef, useState } from 'react'; import { Keyboard, StyleSheet, View, ViewStyle } from 'react-native'; import { IconButton } from '../buttons/IconButton'; import { NumberField } from '../numberField/NumberField'; @@ -15,6 +15,7 @@ export interface Props { showSoftInputOnFocus?: boolean; variant?: 'filled' | 'outlined'; size?: 'm' | 's'; + decimal?: boolean; } export const NumberSelector = ({ @@ -27,32 +28,56 @@ export const NumberSelector = ({ plusIcon = 'add', showSoftInputOnFocus = false, variant = 'outlined', + decimal = false, size = 'm', }: Props) => { const refInput = useRef(); - + const parser = decimal ? parseFloat : parseInt; const [tempValue, setTempValue] = useState(value.toString()); const [softInputOnFocus, setSoftInputOnFocus] = useState(false); + const allowedMinus = (): boolean => { + return minValue !== undefined && minValue < 0; + }; + const decimalRegex = allowedMinus() ? /^-?\d+[\.]?\d?$/ : /^\d+[\.]?\d?$/; + const integerRegex = allowedMinus() ? /^-?\d+$/ : /^\d+$/; + const numberRegex = decimal ? decimalRegex : integerRegex; const onAdd = () => { Keyboard.dismiss(); - if (!addDisabled) onChangeText((value + 1).toString()); + if (!addDisabled) { + onChangeText( + ( + Math.round( + (value + 1 < maxValue ? value + 1 : maxValue) * 10 + ) / 10 + ).toString() + ); + } }; const onMinus = () => { Keyboard.dismiss(); - if (!minusDisabled) onChangeText((value - 1).toString()); + if (!minusDisabled) + onChangeText( + ( + Math.round( + (value - 1 > minValue ? value - 1 : minValue) * 10 + ) / 10 + ).toString() + ); }; const onChangeText = (text: string) => { - const cleanNumber = text.replace(/[^-0-9]/g, ''); - if (tempValue !== '') refInput?.current?.focus(); - if (cleanNumber !== '') { - const parsedValue = parseInt(cleanNumber); - if (parsedValue !== undefined && parsedValue >= minValue && parsedValue <= maxValue) { + if (tempValue !== '' && tempValue != '-') refInput?.current?.focus(); + if (text == '' || (allowedMinus() && text == '-')) { + setTempValue(text); + } else if (numberRegex.test(text)) { + const parsedValue = parser(text); + if ( + parsedValue !== undefined && + (minValue === undefined || parsedValue >= minValue) && + (maxValue === undefined || parsedValue <= maxValue) + ) { onValueChange(parsedValue); setTempValue(parsedValue.toString()); } - } else { - onValueChange(0); - setTempValue(''); } }; @@ -87,12 +112,13 @@ export const NumberSelector = ({ showSoftInputOnFocus={softInputOnFocus} onPressIn={() => setSoftInputOnFocus(true)} onPressOut={() => setSoftInputOnFocus(false)} - keyboardType="number-pad" + keyboardType='number-pad' value={getDisplayedValue()} minValue={minValue} maxValue={maxValue} onChangeText={onChangeText} selectTextOnFocus={showSoftInputOnFocus} + decimal={decimal} size={size} />