Skip to content
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

7058-input-validation #988

Merged
merged 6 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 6 additions & 8 deletions src/components/elements/input/NumberInput.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import { Box } from '@mui/material';
import { useArgs } from '@storybook/preview-api';
import { Meta, StoryObj } from '@storybook/react';

import NumberInput from './NumberInput';

export default {
component: NumberInput,
argTypes: { label: { control: 'text' } },
decorators: [
(Story) => (
<Box sx={{ width: 400 }}>
<Story />
</Box>
),
],
render: (args: any) => {
const [{ value }, updateArgs] = useArgs();
const onChange = (event: any) => updateArgs({ value: event.target.value });
return <NumberInput {...args} onChange={onChange} value={value} />;
},
} as Meta<typeof NumberInput>;

type Story = StoryObj<typeof NumberInput>;
Expand Down
111 changes: 61 additions & 50 deletions src/components/elements/input/NumberInput.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement | HTMLTextAreaElement>;
currency?: boolean;
}

const NumberInput: React.FC<Props> = ({
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<string | null>(null);

const currencyInputProps = currency
? {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replaced with MUI's InputAdornment

startAdornment: <Box sx={{ color: 'text.secondary', pr: 1 }}>$</Box>,
sx: {
pl: 1,
'.MuiInputBase-input': { textAlign: 'left' },
},
}
: {};

const handleBlur = () => {
if (isNil(value) || value === '') {
setErrorMessage(null);
Expand Down Expand Up @@ -61,49 +69,52 @@ const NumberInput = ({
}
};

// Prevent form submission on Enter. Enter should toggle the state.
const onKeyDown: KeyboardEventHandler<HTMLInputElement> = useCallback((e) => {
if (e.key.match(/(ArrowDown|ArrowUp)/)) {
e.preventDefault();
}
}, []);

const preventValueChangeOnScroll: WheelEventHandler<HTMLDivElement> =
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<HTMLInputElement>;

// Refocus immediately, on the next tick (after the current
// function is done)
setTimeout(() => {
(e.target as HTMLInputElement).focus();
}, 0);
}, []);
onChange(syntheticEvent);
};

return (
<TextInput
type='text'
<NumericFormat
error={!!(error || errorMessage)}
helperText={error ? undefined : errorMessage || helperText}
customInput={TextInput}
onValueChange={handleChange}
onBlur={handleBlur}
value={(value || '') as string}
isAllowed={withValueLimit}
thousandSeparator
decimalScale={decimalScale}
inputProps={{
pattern: '[0-9]*', // hint mobile keyboards
inputMode: 'numeric',
pattern: '[0-9]*',
min,
max,
onKeyDown: disableArrowKeys ? onKeyDown : undefined,
'aria-labelledby': ariaLabelledBy,
...inputProps,
}}
onWheel={preventValueChangeOnScroll}
InputProps={{ ...currencyInputProps, ...InputProps }}
onBlur={handleBlur}
value={value}
placeholder={currency ? '0' : undefined}
InputProps={{
startAdornment: prefix ? (
<InputAdornment position='start'>{prefix}</InputAdornment>
) : 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}
/>
);
};
Expand Down
1 change: 0 additions & 1 deletion src/modules/form/components/DynamicField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,6 @@ const DynamicField: React.FC<DynamicFieldProps> = ({
onChange={onChangeEvent}
horizontal={horizontal}
currency={item.type === ItemType.Currency}
disableArrowKeys={item.type === ItemType.Currency}
{...commonInputProps}
inputWidth={120}
/>
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading