diff --git a/package.json b/package.json index 08e993b3d..2d45ae1df 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "react-hook-form": "^7.52.1", "react-i18next": "^15.0.0", "react-imask": "^7.6.1", + "react-number-format": "^5.4.2", "react-pdf": "^8.0.2", "react-router-dom": "^6.25.1", "typescript": "^5.5.4", diff --git a/src/components/elements/input/NumberInput.stories.tsx b/src/components/elements/input/NumberInput.stories.tsx index fa1ca5953..3c7d33fe6 100644 --- a/src/components/elements/input/NumberInput.stories.tsx +++ b/src/components/elements/input/NumberInput.stories.tsx @@ -1,4 +1,4 @@ -import { Box } from '@mui/material'; +import { useArgs } from '@storybook/preview-api'; import { Meta, StoryObj } from '@storybook/react'; import NumberInput from './NumberInput'; @@ -6,13 +6,11 @@ import NumberInput from './NumberInput'; export default { component: NumberInput, argTypes: { label: { control: 'text' } }, - decorators: [ - (Story) => ( - - - - ), - ], + render: (args: any) => { + const [{ value }, updateArgs] = useArgs(); + const onChange = (event: any) => updateArgs({ value: event.target.value }); + return ; + }, } as Meta; type Story = StoryObj; diff --git a/src/components/elements/input/NumberInput.tsx b/src/components/elements/input/NumberInput.tsx index fbb7e0f1e..4def94c2b 100644 --- a/src/components/elements/input/NumberInput.tsx +++ b/src/components/elements/input/NumberInput.tsx @@ -1,38 +1,46 @@ -import { Box } from '@mui/system'; +import { InputAdornment } from '@mui/material'; import { isFinite, isNil } from 'lodash-es'; -import { - KeyboardEventHandler, - useCallback, - useState, - WheelEventHandler, -} from 'react'; +import { ChangeEventHandler, useState } from 'react'; +import { + NumberFormatValues, + NumericFormat, + OnValueChange, +} from 'react-number-format'; import TextInput, { TextInputProps } from './TextInput'; -const NumberInput = ({ +// protect from integer overflows +const withValueLimit = ({ floatValue }: NumberFormatValues) => { + if (floatValue) { + return floatValue > 1 + ? floatValue < Number.MAX_SAFE_INTEGER + : floatValue > Number.MIN_SAFE_INTEGER; + } + return true; +}; + +interface Props extends TextInputProps { + onChange: ChangeEventHandler; + currency?: boolean; +} + +const NumberInput: React.FC = ({ inputProps, min = 0, max, InputProps, currency = false, - disableArrowKeys = false, value, error, + helperText, ariaLabelledBy, + onChange, + defaultValue, + type, ...props -}: TextInputProps & { currency?: boolean; disableArrowKeys?: boolean }) => { +}) => { const [errorMessage, setErrorMessage] = useState(null); - const currencyInputProps = currency - ? { - startAdornment: $, - sx: { - pl: 1, - '.MuiInputBase-input': { textAlign: 'left' }, - }, - } - : {}; - const handleBlur = () => { if (isNil(value) || value === '') { setErrorMessage(null); @@ -61,49 +69,52 @@ const NumberInput = ({ } }; - // Prevent form submission on Enter. Enter should toggle the state. - const onKeyDown: KeyboardEventHandler = useCallback((e) => { - if (e.key.match(/(ArrowDown|ArrowUp)/)) { - e.preventDefault(); - } - }, []); - - const preventValueChangeOnScroll: WheelEventHandler = - useCallback((e) => { - // Prevent the input value change - (e.target as HTMLInputElement).blur(); + const decimalScale = currency ? 2 : 0; + const prefix = currency ? '$' : undefined; - // Prevent the page/container scrolling - e.stopPropagation(); + const handleChange: OnValueChange = (v) => { + const syntheticEvent = { + target: { + value: v.value, + name: props.name, // If you have a name prop + }, + // Add other event properties you might need + preventDefault: () => {}, + stopPropagation: () => {}, + } as React.ChangeEvent; - // Refocus immediately, on the next tick (after the current - // function is done) - setTimeout(() => { - (e.target as HTMLInputElement).focus(); - }, 0); - }, []); + onChange(syntheticEvent); + }; return ( - {prefix} + ) : undefined, + ...InputProps, + }} + defaultValue={(defaultValue || '') as string} + type={type as any} {...props} - error={error || !!errorMessage} - // If there is a server error, show that instead of the local message - helperText={error ? undefined : errorMessage || props.helperText} /> ); }; diff --git a/src/modules/form/components/DynamicField.tsx b/src/modules/form/components/DynamicField.tsx index a8af043eb..d8292298f 100644 --- a/src/modules/form/components/DynamicField.tsx +++ b/src/modules/form/components/DynamicField.tsx @@ -278,7 +278,6 @@ const DynamicField: React.FC = ({ onChange={onChangeEvent} horizontal={horizontal} currency={item.type === ItemType.Currency} - disableArrowKeys={item.type === ItemType.Currency} {...commonInputProps} inputWidth={120} /> diff --git a/yarn.lock b/yarn.lock index 8a985c0fe..305cd524b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8483,6 +8483,11 @@ react-is@^18.3.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== +react-number-format@^5.4.2: + version "5.4.2" + resolved "https://registry.yarnpkg.com/react-number-format/-/react-number-format-5.4.2.tgz#aec282241f36cee31da13dc5e0f364c0fc6902ab" + integrity sha512-cg//jVdS49PYDgmcYoBnMMHl4XNTMuV723ZnHD2aXYtWWWqbVF3hjQ8iB+UZEuXapLbeA8P8H+1o6ZB1lcw3vg== + react-pdf@^8.0.2: version "8.0.2" resolved "https://registry.yarnpkg.com/react-pdf/-/react-pdf-8.0.2.tgz#ff4a0e9260c47f1a261b878a2cc0408080eecc06"