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

Fix input field bugs #166

87 changes: 87 additions & 0 deletions src/components/NumericInput/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
const removeNonNumericCharacters = (value: string): string => value.replace(/[^0-9.]/g, '');

const removeExtraDots = (value: string): string => value.replace(/(\..*?)\./g, '$1');

function sanitizeNumericInput(value: string): string {
return removeExtraDots(removeNonNumericCharacters(value));
}

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

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 = sanitizeNumericInput(target.value);

target.value = trimToMaxDecimals(target.value, maxDecimals);

target.value = handleLeadingZeros(target.value);

target.value = replaceInvalidOrEmptyString(target.value);
}

function replaceInvalidOrEmptyString(value: string): string {
if (value === '' || value === '.') {
return '0';
}
return value;
}

function handleLeadingZeros(value: string): string {
if (Number(value) >= 1) {
return value.replace(/^0+/, '');
}

// Add leading zeros for numbers < 1 that don't start with '0'
if (Number(value) < 1 && value[0] !== '0') {
return '0' + value;
}

// No more than one leading zero
return value.replace(/^0+/, '0');
}

/**
* Handles the paste 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 clipboard event triggered by the input.
* @param maxDecimals - The maximum number of decimal places allowed.
* @returns The sanitized value after the paste event.
*/

export function handleOnPasteNumericInput(e: ClipboardEvent, maxDecimals: number): string {
const inputElement = e.target as HTMLInputElement;
const { value, selectionStart, selectionEnd } = inputElement;

const clipboardData = sanitizeNumericInput(e.clipboardData?.getData('text/plain') || '');

const combinedValue = value.slice(0, selectionStart || 0) + clipboardData + value.slice(selectionEnd || 0);

const [integerPart, ...decimalParts] = combinedValue.split('.');
const sanitizedValue = integerPart + (decimalParts.length > 0 ? '.' + decimalParts.join('') : '');

e.preventDefault();
inputElement.value = trimToMaxDecimals(sanitizedValue, maxDecimals);
inputElement.value = handleLeadingZeros(inputElement.value);

const newCursorPosition =
(selectionStart || 0) + clipboardData.length - (combinedValue.length - sanitizedValue.length);
inputElement.setSelectionRange(newCursorPosition, newCursorPosition);

return trimToMaxDecimals(sanitizedValue, maxDecimals);
}
56 changes: 13 additions & 43 deletions src/components/NumericInput/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import { Input } from 'react-daisyui';
import { UseFormRegisterReturn } from 'react-hook-form';
import { useEventsContext } from '../../contexts/events';

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;
}
import { handleOnChangeNumericInput, handleOnPasteNumericInput } from './helpers';

interface NumericInputProps {
register: UseFormRegisterReturn;
Expand All @@ -15,51 +9,32 @@ interface NumericInputProps {
maxDecimals?: number;
defaultValue?: string;
autoFocus?: boolean;
disabled?: boolean;
disableStyles?: boolean;
}

function isValidNumericInput(value: string): boolean {
return /^[0-9.,]*$/.test(value);
}

function alreadyHasDecimal(e: KeyboardEvent) {
const decimalChars = ['.', ','];

// 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('.'));
}

function handleOnInput(e: KeyboardEvent): void {
const target = e.target as HTMLInputElement;
target.value = target.value.replace(/,/g, '.');
}

function handleOnKeyPress(e: KeyboardEvent, maxDecimals: number): void {
if (!isValidNumericInput(e.key) || alreadyHasDecimal(e)) {
e.preventDefault();
}
const target = e.target as HTMLInputElement;
if (exceedsMaxDecimals(target.value, maxDecimals - 1)) {
target.value = target.value.slice(0, -1);
}
}

export const NumericInput = ({
register,
readOnly = false,
additionalStyle,
maxDecimals = 2,
defaultValue,
autoFocus,
disabled,
disableStyles = false,
}: NumericInputProps) => {
const { trackEvent } = useEventsContext();
function handleOnChange(e: KeyboardEvent): void {
handleOnChangeNumericInput(e, maxDecimals);
register.onChange(e);
}

function handleOnPaste(e: ClipboardEvent): void {
handleOnPasteNumericInput(e, maxDecimals);
register.onChange(e);
}

return (
<div className={disableStyles ? 'flex-grow' : 'flex-grow text-black font-outfit'}>
<Input
{...register}
autocomplete="off"
autocorrect="off"
autocapitalize="none"
Expand All @@ -69,11 +44,8 @@ export const NumericInput = ({
: 'input-ghost w-full text-lg pl-2 focus:outline-none text-accent-content ' + additionalStyle
}
minlength="1"
onKeyPress={(e: KeyboardEvent) => handleOnKeyPress(e, maxDecimals)}
onInput={(e: KeyboardEvent) => {
trackEvent({ event: 'amount_type' });
handleOnInput(e);
}}
onChange={handleOnChange}
onPaste={handleOnPaste}
pattern="^[0-9]*[.,]?[0-9]*$"
placeholder="0.0"
readOnly={readOnly}
Expand All @@ -83,8 +55,6 @@ export const NumericInput = ({
inputmode="decimal"
value={defaultValue}
autoFocus={autoFocus}
disabled={disabled}
{...register}
/>
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions src/components/TextInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function getPattern(textInputType?: string) {
return patterns.default;
}

interface NumericInputProps {
interface TextInputProps {
register: UseFormRegisterReturn;
readOnly?: boolean;
additionalStyle?: string;
Expand All @@ -34,7 +34,7 @@ export const TextInput = ({
error,
placeholder,
type,
}: NumericInputProps) => (
}: TextInputProps) => (
<div className="flex-grow text-black font-outfit">
<Input
className={
Expand Down
18 changes: 9 additions & 9 deletions src/pages/swap/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
import { Fragment } from 'preact';
Copy link
Member

Choose a reason for hiding this comment

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

This is not used.

import { ArrowDownIcon } from '@heroicons/react/20/solid';
import { useAccount } from 'wagmi';
import Big from 'big.js';
Expand Down Expand Up @@ -35,7 +36,6 @@ const Arrow = () => (
);

export const SwapPage = () => {
const [isQuoteSubmitted, setIsQuoteSubmitted] = useState(false);
const formRef = useRef<HTMLDivElement | null>(null);
const [api, setApi] = useState<ApiPromise | null>(null);

Expand Down Expand Up @@ -129,12 +129,12 @@ export const SwapPage = () => {
// Calculate the final amount after the offramp fees
const totalReceive = calculateTotalReceive(toAmount.toString(), toToken);
form.setValue('toAmount', totalReceive);

setIsQuoteSubmitted(false);
} else if (!tokenOutData.isLoading || tokenOutData.error) {
form.setValue('toAmount', '0');
} else {
form.setValue('toAmount', '');
// Do nothing
}
}, [form, tokenOutData.data, toToken]);
}, [form, tokenOutData.data, tokenOutData.error, tokenOutData.isLoading, toToken]);

const ReceiveNumericInput = useMemo(
() => (
Expand All @@ -143,18 +143,18 @@ export const SwapPage = () => {
tokenSymbol={toToken.fiat.symbol}
onClick={() => setModalType('to')}
registerInput={form.register('toAmount')}
disabled={isQuoteSubmitted || tokenOutData.isLoading}
disabled={tokenOutData.isLoading}
readOnly={true}
/>
),
[toToken.fiat.symbol, toToken.fiat.assetIcon, form, isQuoteSubmitted, tokenOutData.isLoading, setModalType],
[toToken.fiat.symbol, toToken.fiat.assetIcon, form, tokenOutData.isLoading, setModalType],
);

const WithdrawNumericInput = useMemo(
() => (
<>
<AssetNumericInput
registerInput={form.register('fromAmount', { onChange: () => setIsQuoteSubmitted(true) })}
registerInput={form.register('fromAmount')}
tokenSymbol={fromToken.assetSymbol}
assetIcon={fromToken.polygonAssetIcon}
onClick={() => setModalType('from')}
Expand Down Expand Up @@ -316,7 +316,7 @@ export const SwapPage = () => {
) : (
<SwapSubmitButton
text={isInitiating ? 'Confirming' : offrampingStarted ? 'Processing Details' : 'Confirm'}
disabled={false}
disabled={Boolean(getCurrentErrorMessage()) || !inputAmountIsStable}
pending={isInitiating || offrampingStarted || offrampingState !== undefined}
/>
)}
Expand Down
Loading