Skip to content

Commit

Permalink
refactor NumericInput events to one onChange event, handle onPaste, a…
Browse files Browse the repository at this point in the history
…dd important test cases for NumericInput
  • Loading branch information
Sharqiewicz committed Jun 26, 2024
1 parent 3b7d9b8 commit b2174b0
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 50 deletions.
4 changes: 2 additions & 2 deletions src/components/Form/From/AvailableActions/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { trimMaxDecimals, USER_INPUT_MAX_DECIMALS } from '../../../../shared/parseNumbers/maxDecimals';
import { trimToMaxDecimals, USER_INPUT_MAX_DECIMALS } from '../../../../shared/parseNumbers/maxDecimals';

interface AvailableActionsProps {
max?: number;
Expand All @@ -13,7 +13,7 @@ export const AvailableActions = ({
}: AvailableActionsProps) => {
const handleSetValue = (percentage: number) => {
if (max !== undefined && setValue !== undefined) {
const trimmedValue = trimMaxDecimals(String(max * percentage), maxDecimals);
const trimmedValue = trimToMaxDecimals(String(max * percentage), maxDecimals);
setValue(Number(trimmedValue));
}
};
Expand Down
29 changes: 28 additions & 1 deletion src/components/Form/From/NumericInput/NumericInput.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,14 @@ describe('NumericInput Component', () => {
expect(inputElement.value).toBe('1.1');
});

it('should work with readOnly prop', () => {
it('should work with readOnly prop', async () => {
const { getByPlaceholderText } = render(<NumericInput register={mockRegister} readOnly={true} />);
const inputElement = getByPlaceholderText('0.0') as HTMLInputElement;

expect(inputElement).toHaveAttribute('readOnly');

await userEvent.type(inputElement, '123');
expect(inputElement.value).toBe('');
});

it('should apply additional styles', () => {
Expand Down Expand Up @@ -126,4 +129,28 @@ describe('NumericInput Component', () => {
await userEvent.keyboard('{arrowleft}{arrowleft}{arrowleft}9');
expect(inputElement.value).toBe('1439721');
});

it('should initialize with default value', () => {
const { getByPlaceholderText } = render(<NumericInput register={mockRegister} defaultValue="123.45" />);
const inputElement = getByPlaceholderText('0.0') as HTMLInputElement;

expect(inputElement.value).toBe('123.45');
});

it('should remain unchanged on invalid input', async () => {
const { getByPlaceholderText } = render(<NumericInput register={mockRegister} defaultValue="123.45" />);
const inputElement = getByPlaceholderText('0.0') as HTMLInputElement;

await userEvent.type(inputElement, '!!!');
expect(inputElement.value).toBe('123.45');
});

it('should handle paste invalid characters', async () => {
const { getByPlaceholderText } = render(<NumericInput register={mockRegister} />);
const inputElement = getByPlaceholderText('0.0') as HTMLInputElement;

inputElement.focus();
await userEvent.paste('123.4567890123456789abcgdehyu0123456.2746472.93.2.7.3.5.3');
expect(inputElement.value).toBe('123.456789012345');
});
});
22 changes: 22 additions & 0 deletions src/components/Form/From/NumericInput/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { trimToMaxDecimals } from '../../../../shared/parseNumbers/maxDecimals';

const removeNonNumericCharacters = (value: string): string => value.replace(/[^0-9.]/g, '');

const replaceCommasWithDots = (value: string): string => value.replace(/,/g, '.');

/**
* Handles the input change event to ensure the value does not exceed the maximum number of decimal places,
* replaces commas with dots, and removes invalid non-numeric characters.
*
* @param e - The keyboard event triggered by the input.
* @param maxDecimals - The maximum number of decimal places allowed.
*/
export function handleOnChangeNumericInput(e: KeyboardEvent, maxDecimals: number): void {
const target = e.target as HTMLInputElement;

target.value = replaceCommasWithDots(target.value);

target.value = removeNonNumericCharacters(target.value);

target.value = trimToMaxDecimals(target.value, maxDecimals);
}
28 changes: 7 additions & 21 deletions src/components/Form/From/NumericInput/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { Input } from 'react-daisyui';
import { UseFormRegisterReturn } from 'react-hook-form';

import {
alreadyHasDecimal,
handleOnInputExceedsMaxDecimals,
USER_INPUT_MAX_DECIMALS,
} from '../../../../shared/parseNumbers/maxDecimals';
import { USER_INPUT_MAX_DECIMALS } from '../../../../shared/parseNumbers/maxDecimals';
import { handleOnChangeNumericInput } from './helpers';

interface NumericInputProps {
register: UseFormRegisterReturn;
Expand All @@ -16,14 +13,6 @@ interface NumericInputProps {
autoFocus?: boolean;
}

const isValidNumericInput = (value: string): boolean => /^[0-9.,]*$/.test(value);

function handleOnKeyPressNumericInput(e: KeyboardEvent): void {
if (!isValidNumericInput(e.key) || alreadyHasDecimal(e)) {
e.preventDefault();
}
}

export const NumericInput = ({
register,
readOnly = false,
Expand All @@ -32,27 +21,25 @@ export const NumericInput = ({
defaultValue,
autoFocus,
}: NumericInputProps) => {
function handleOnInput(e: KeyboardEvent): void {
const target = e.target as HTMLInputElement;
target.value = target.value.replace(/,/g, '.');

handleOnInputExceedsMaxDecimals(e, maxDecimals);
function handleOnChange(e: KeyboardEvent): void {
handleOnChangeNumericInput(e, maxDecimals);
register.onChange(e);
}

return (
<div className="flex justify-between w-full">
<div className="flex-grow text-4xl text-black font-outfit">
<Input
{...register}
autocomplete="off"
autocorrect="off"
autocapitalize="none"
className={
'input-ghost w-full text-4xl font-outfit pl-0 focus:outline-none focus:text-accent-content text-accent-content ' +
additionalStyle
}
onKeyPress={handleOnKeyPressNumericInput}
minlength="1"
onInput={handleOnInput}
onChange={handleOnChange}
pattern="^[0-9]*[.,]?[0-9]*$"
placeholder="0.0"
readOnly={readOnly}
Expand All @@ -62,7 +49,6 @@ export const NumericInput = ({
inputmode="decimal"
value={defaultValue}
autoFocus={autoFocus}
{...register}
/>
</div>
</div>
Expand Down
34 changes: 8 additions & 26 deletions src/shared/parseNumbers/maxDecimals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,15 @@ export enum USER_INPUT_MAX_DECIMALS {
STELLAR = 7,
}

export function alreadyHasDecimal(e: KeyboardEvent) {
const decimalChars = ['.', ','];
/**
* Trims the decimal part of a numeric string to a specified maximum number of decimal places.
*
* @param value - The numeric string to be trimmed.
* @param maxDecimals - The maximum number of decimal places allowed.
* @returns The trimmed numeric string with at most maxDecimals decimal places.
*/

// In the onInput event, "," is replaced by ".", so we check if the e.target.value already contains a "."
return decimalChars.some((char) => e.key === char && e.target && (e.target as HTMLInputElement).value.includes('.'));
}

export function trimMaxDecimals(value: string, maxDecimals: number): string {
export function trimToMaxDecimals(value: string, maxDecimals: number): string {
const [integer, decimal] = value.split('.');
return decimal ? `${integer}.${decimal.slice(0, maxDecimals)}` : value;
}

export function exceedsMaxDecimals(value: unknown, maxDecimals: number) {
if (value === undefined || value === null) return true;
const decimalPlaces = value.toString().split('.')[1];
return decimalPlaces ? decimalPlaces.length > maxDecimals : false;
}

function truncateIfExceedsMaxDecimals(value: string, maxDecimals: number): string {
if (exceedsMaxDecimals(value, maxDecimals)) {
return value.slice(0, -1);
}
return value;
}

export function handleOnInputExceedsMaxDecimals(e: KeyboardEvent, maxDecimals: number): void {
const target = e.target as HTMLInputElement;

target.value = truncateIfExceedsMaxDecimals(target.value, maxDecimals);
}

0 comments on commit b2174b0

Please sign in to comment.