diff --git a/config/setupTests.ts b/config/setupTests.ts index a53b8b36..c1843fc4 100644 --- a/config/setupTests.ts +++ b/config/setupTests.ts @@ -1,2 +1,8 @@ /* eslint-env jest */ import '@testing-library/jest-dom'; +import React from 'react'; + +// Ensure React is available for hooks in test environment +if (typeof globalThis !== 'undefined') { + (globalThis as unknown as { React: typeof React }).React = React; +} diff --git a/jest.config.ts b/jest.config.ts index c4215be5..2b0801cb 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -13,7 +13,14 @@ const config: Config = { '^@/(.*)$': '/src/$1', }, transform: { - '^.+\\.(ts|tsx)$': 'ts-jest', + '^.+\\.(ts|tsx)$': [ + 'ts-jest', + { + tsconfig: { + jsx: 'react-jsx', + }, + }, + ], }, }; diff --git a/package.json b/package.json index 9cf566eb..7f1c8d2b 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,8 @@ "luxon": "^3.5.0", "match-sorter": "^6.3.1", "motion": "^11.15.0", + "numora": "^3.0.2", + "numora-react": "^3.0.2", "qrcode.react": "^3.1.0", "react": "^19.0.0", "react-daisyui": "^5.0.5", diff --git a/src/components/Form/Amount/index.tsx b/src/components/Form/Amount/index.tsx index e1047227..2c168370 100644 --- a/src/components/Form/Amount/index.tsx +++ b/src/components/Form/Amount/index.tsx @@ -1,12 +1,13 @@ -import { UseFormRegisterReturn } from 'react-hook-form'; +import { Control, FieldPath, FieldValues } from 'react-hook-form'; import { roundNumber } from '../../../shared/parseNumbers/metric'; import { NumericInput } from '../From/NumericInput'; -export interface AmountProps { +export interface AmountProps { className?: string; max?: number; min?: number; - register: UseFormRegisterReturn; + control: Control; + name: FieldPath; setValue: (n: number) => void; assetSuffix?: string; error?: string; @@ -25,9 +26,10 @@ function calculateMaxAmount(max: number, fullMax: boolean) { return Math.max(0, maxAmount); } -const Amount = ({ +const Amount = ({ className, - register, + control, + name, max, setValue, error, @@ -36,7 +38,7 @@ const Amount = ({ hideMaxButton = false, readOnly = false, defaultValue, -}: AmountProps): JSX.Element | null => ( +}: AmountProps): JSX.Element | null => ( <>
) => React.ReactNode; + defaultValues?: { testInput: string }; +}) => { + const methods = useForm({ defaultValues }); + return {children(methods)}; }; describe('NumericInput Component', () => { it('should render the component', () => { - const { getByPlaceholderText } = render(); + const { getByPlaceholderText } = render( + + {(methods) => } + , + ); const inputElement = getByPlaceholderText('0.0'); expect(inputElement).toBeInTheDocument(); }); it('should allow numeric input', async () => { - const { getByPlaceholderText } = render(); + const { getByPlaceholderText } = render( + + {(methods) => } + , + ); const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; await userEvent.type(inputElement, '1234567890'); @@ -28,7 +39,11 @@ describe('NumericInput Component', () => { }); it('should prevent non-numeric input', async () => { - const { getByPlaceholderText } = render(); + const { getByPlaceholderText } = render( + + {(methods) => } + , + ); const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; await userEvent.type(inputElement, 'qwertyuiopasdfghjklzxcvbnm'); @@ -36,7 +51,11 @@ describe('NumericInput Component', () => { }); it('should prevent multiple decimal points', async () => { - const { getByPlaceholderText } = render(); + const { getByPlaceholderText } = render( + + {(methods) => } + , + ); const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; await userEvent.type(inputElement, '1.1.1,2.3,4'); @@ -44,7 +63,11 @@ describe('NumericInput Component', () => { }); it('should replace comma with period', async () => { - const { getByPlaceholderText } = render(); + const { getByPlaceholderText } = render( + + {(methods) => } + , + ); const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; await userEvent.type(inputElement, '1,1'); @@ -52,7 +75,11 @@ describe('NumericInput Component', () => { }); it('should work with readOnly prop', async () => { - const { getByPlaceholderText } = render(); + const { getByPlaceholderText } = render( + + {(methods) => } + , + ); const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; expect(inputElement).toHaveAttribute('readOnly'); @@ -62,22 +89,34 @@ describe('NumericInput Component', () => { }); it('should apply additional styles', () => { - const { getByPlaceholderText } = render(); + const { getByPlaceholderText } = render( + + {(methods) => } + , + ); const inputElement = getByPlaceholderText('0.0'); expect(inputElement).toHaveClass('extra-style'); }); it('should handle leading zeros correctly', async () => { - const { getByPlaceholderText } = render(); + const { getByPlaceholderText } = render( + + {(methods) => } + , + ); const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; await userEvent.type(inputElement, '007'); - expect(inputElement.value).toBe('007'); + expect(inputElement.value).toBe('7'); }); it('should allow backspace and delete', async () => { - const { getByPlaceholderText } = render(); + const { getByPlaceholderText } = render( + + {(methods) => } + , + ); const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; await userEvent.type(inputElement, '123'); @@ -89,7 +128,11 @@ describe('NumericInput Component', () => { }); it('should not allow negative numbers', async () => { - const { getByPlaceholderText } = render(); + const { getByPlaceholderText } = render( + + {(methods) => } + , + ); const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; await userEvent.type(inputElement, '-123'); @@ -97,7 +140,11 @@ describe('NumericInput Component', () => { }); it('should not allow more decimals than maxDecimals', async () => { - const { getByPlaceholderText } = render(); + const { getByPlaceholderText } = render( + + {(methods) => } + , + ); const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; await userEvent.type(inputElement, '123.45479187249871298774985'); @@ -105,7 +152,11 @@ describe('NumericInput Component', () => { }); it('should not allow more decimals than default maxDecimals', async () => { - const { getByPlaceholderText } = render(); + const { getByPlaceholderText } = render( + + {(methods) => } + , + ); const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; await userEvent.type(inputElement, '123.4567890123456789abcgdehyu0123456.2746472.93.2.7.3.5.3'); @@ -113,7 +164,11 @@ describe('NumericInput Component', () => { }); it('should allow replace any digit user wants', async () => { - const { getByPlaceholderText } = render(); + const { getByPlaceholderText } = render( + + {(methods) => } + , + ); const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; await userEvent.type(inputElement, '123.421'); @@ -132,14 +187,22 @@ describe('NumericInput Component', () => { }); it('should initialize with default value', () => { - const { getByPlaceholderText } = render(); + const { getByPlaceholderText } = render( + + {(methods) => } + , + ); const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; expect(inputElement.value).toBe('123.45'); }); it('should remain unchanged on invalid input', async () => { - const { getByPlaceholderText } = render(); + const { getByPlaceholderText } = render( + + {(methods) => } + , + ); const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; await userEvent.type(inputElement, '!!!'); @@ -147,7 +210,11 @@ describe('NumericInput Component', () => { }); it('should handle paste invalid characters', async () => { - const { getByPlaceholderText } = render(); + const { getByPlaceholderText } = render( + + {(methods) => } + , + ); const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; inputElement.focus(); @@ -156,7 +223,11 @@ describe('NumericInput Component', () => { }); it('Should not cut the number if user is trying to type more than one "."', async () => { - const { getByPlaceholderText } = render(); + const { getByPlaceholderText } = render( + + {(methods) => } + , + ); const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; await userEvent.type(inputElement, '0.23'); @@ -165,7 +236,11 @@ describe('NumericInput Component', () => { }); it('Should not cut the number and do not move . position', async () => { - const { getByPlaceholderText } = render(); + const { getByPlaceholderText } = render( + + {(methods) => } + , + ); const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; await userEvent.type(inputElement, '12.34'); @@ -174,7 +249,11 @@ describe('NumericInput Component', () => { }); it('Should not paste the number if more than one .', async () => { - const { getByPlaceholderText } = render(); + const { getByPlaceholderText } = render( + + {(methods) => } + , + ); const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; await userEvent.paste('12.34.56'); @@ -182,7 +261,11 @@ describe('NumericInput Component', () => { }); it('should accept only one "."', async () => { - const { getByPlaceholderText } = render(); + const { getByPlaceholderText } = render( + + {(methods) => } + , + ); const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; await userEvent.type(inputElement, '...........'); @@ -190,7 +273,11 @@ describe('NumericInput Component', () => { }); it('should paste properly', async () => { - const { getByPlaceholderText } = render(); + const { getByPlaceholderText } = render( + + {(methods) => } + , + ); const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; await userEvent.type(inputElement, '123'); @@ -202,40 +289,38 @@ describe('NumericInput Component', () => { describe('NumericInput onPaste should sanitize the user input', () => { const testCases = [ - { input: '1.......4.....2', maxLength: 8, expected: '1.42' }, - { input: '12....34.....56', maxLength: 8, expected: '12.3456' }, - { input: '....56789...', maxLength: 5, expected: '.56789' }, - { input: '1.23..4..56.', maxLength: 6, expected: '1.23456' }, - { input: '1.....2', maxLength: 8, expected: '1.2' }, - { input: '123..4...56.7', maxLength: 7, expected: '123.4567' }, - { input: 'a.b.c.123.4.def56', maxLength: 8, expected: '.123456' }, - { input: '12abc34....def567', maxLength: 2, expected: '1234.56' }, - { input: '.....a.b.c......', maxLength: 8, expected: '.' }, - { input: '12.....3..4..5abc6', maxLength: 7, expected: '12.3456' }, - { input: '1a2b3c4d5e.1234567', maxLength: 4, expected: '12345.1234' }, - { input: '12abc@#34..def$%^567', maxLength: 2, expected: '1234.56' }, - { input: '....!@#$$%^&*((', maxLength: 8, expected: '.' }, - { input: '123....abc.def456ghi789', maxLength: 4, expected: '123.4567' }, - { input: '00.00123...4', maxLength: 4, expected: '00.0012' }, - { input: '.1...2.67.865', maxLength: 3, expected: '.126' }, - { input: '123abc...', maxLength: 6, expected: '123.' }, + { input: '1.......4.....2', maxDecimals: 8, expected: '1.42' }, + { input: '12....34.....56', maxDecimals: 8, expected: '12.3456' }, + { input: '....56789...', maxDecimals: 5, expected: '.56789' }, + { input: '1.23..4..56.', maxDecimals: 6, expected: '1.23456' }, + { input: '1.....2', maxDecimals: 8, expected: '1.2' }, + { input: '123..4...56.7', maxDecimals: 7, expected: '123.4567' }, + { input: 'a.b.c.123.4.def56', maxDecimals: 8, expected: '.123456' }, + { input: '12abc34....def567', maxDecimals: 2, expected: '1234.56' }, + { input: '.....a.b.c......', maxDecimals: 8, expected: '.' }, + { input: '12.....3..4..5abc6', maxDecimals: 7, expected: '12.3456' }, + { input: '1a2b3c4d5e.1234567', maxDecimals: 4, expected: '12345.1234' }, + { input: '12abc@#34..def$%^567', maxDecimals: 2, expected: '1234.56' }, + { input: '....!@#$$%^&*((', maxDecimals: 8, expected: '.' }, + { input: '123....abc.def456ghi789', maxDecimals: 4, expected: '123.4567' }, + { input: '00.00123...4', maxDecimals: 4, expected: '0.0012' }, + { input: '.1...2.67.865', maxDecimals: 3, expected: '.126' }, + { input: '123abc...', maxDecimals: 6, expected: '123.' }, ]; test.each(testCases)( - 'should sanitize the pasted input with maxLength (decimal)', - ({ input, maxLength, expected }) => { - const mockEvent = { - target: { - setSelectionRange: jest.fn(), - value: '', - }, - preventDefault: jest.fn(), - clipboardData: { - getData: jest.fn().mockReturnValue(input), - }, - } as unknown as ClipboardEvent; - - expect(handleOnPasteNumericInput(mockEvent, maxLength)).toBe(expected); + 'should sanitize the pasted input with maxDecimals: $maxDecimals', + async ({ input, maxDecimals, expected }) => { + const { getByPlaceholderText } = render( + + {(methods) => } + , + ); + const inputElement = getByPlaceholderText('0.0') as HTMLInputElement; + + inputElement.focus(); + await userEvent.paste(input); + expect(inputElement.value).toBe(expected); }, ); }); diff --git a/src/components/Form/From/NumericInput/helpers.ts b/src/components/Form/From/NumericInput/helpers.ts deleted file mode 100644 index d29f7f9b..00000000 --- a/src/components/Form/From/NumericInput/helpers.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { ChangeEvent, ClipboardEvent, KeyboardEvent } from 'react'; -import { trimToMaxDecimals } from '../../../../shared/parseNumbers/maxDecimals'; - -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)); -} - -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: ChangeEvent, 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); -} - -/** - * Checks if the input already has a decimal point and prevents the user from entering another one. - * Why onKeyDown? Because it is triggered before the character is processed and added to the input value. - */ - -function alreadyHasDecimal(e: KeyboardEvent) { - const decimalChars = ['.', ',']; - return decimalChars.some((char) => e.key === char && e.target && (e.target as HTMLInputElement).value.includes('.')); -} - -export function handleOnKeyDownNumericInput(e: KeyboardEvent): void { - if (alreadyHasDecimal(e)) { - e.preventDefault(); - } -} - -/** - * 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); - - const newCursorPosition = - (selectionStart || 0) + clipboardData.length - (combinedValue.length - sanitizedValue.length); - inputElement.setSelectionRange(newCursorPosition, newCursorPosition); - - return trimToMaxDecimals(sanitizedValue, maxDecimals); -} diff --git a/src/components/Form/From/NumericInput/index.tsx b/src/components/Form/From/NumericInput/index.tsx index 579630d9..44ed9c73 100644 --- a/src/components/Form/From/NumericInput/index.tsx +++ b/src/components/Form/From/NumericInput/index.tsx @@ -1,64 +1,62 @@ -import { Input } from 'react-daisyui'; -import { UseFormRegisterReturn } from 'react-hook-form'; +import { Control, Controller, FieldPath, FieldValues, RegisterOptions } from 'react-hook-form'; +import { NumoraInput, ThousandStyle } from 'numora-react'; import { USER_INPUT_MAX_DECIMALS } from '../../../../shared/parseNumbers/maxDecimals'; -import { handleOnChangeNumericInput, handleOnKeyDownNumericInput, handleOnPasteNumericInput } from './helpers'; -import { ChangeEvent, ClipboardEvent } from 'react'; -interface NumericInputProps { - register: UseFormRegisterReturn; +interface NumericInputProps { + control: Control; + name: FieldPath; readOnly?: boolean; additionalStyle?: string; maxDecimals?: number; defaultValue?: string; autoFocus?: boolean; + rules?: RegisterOptions; + onChange?: (value: string) => void; } -export const NumericInput = ({ - register, +export const NumericInput = ({ + control, + name, readOnly = false, additionalStyle, maxDecimals = USER_INPUT_MAX_DECIMALS.PENDULUM, - defaultValue, autoFocus, -}: NumericInputProps) => { - function handleOnChange(e: ChangeEvent): void { - handleOnChangeNumericInput(e, maxDecimals); - register.onChange(e); - } - - function handleOnPaste(e: ClipboardEvent): void { - handleOnPasteNumericInput(e, maxDecimals); - register.onChange(e); - } - - return ( + rules, + onChange: customOnChange, +}: NumericInputProps) => (
- ( + { + field.onChange(e); + if (customOnChange) { + const value = typeof e === 'string' ? e : (e.target as HTMLInputElement).value; + customOnChange(value); + } + }} + autoComplete="off" + autoCorrect="off" + autoCapitalize="none" + className={ + 'font-outfit input-ghost w-full pl-0 text-4xl text-accent-content focus:text-accent-content focus:outline-none ' + + (additionalStyle || '') + } + placeholder="0.0" + readOnly={readOnly} + spellCheck="false" + maxDecimals={maxDecimals} + autoFocus={autoFocus} + thousandStyle={ThousandStyle.None} + /> + )} />
- ); -}; +); diff --git a/src/components/Form/From/index.tsx b/src/components/Form/From/index.tsx index 53a66d2d..97b5fd05 100644 --- a/src/components/Form/From/index.tsx +++ b/src/components/Form/From/index.tsx @@ -1,4 +1,4 @@ -import { UseFormRegisterReturn } from 'react-hook-form'; +import { Control, FieldPath, FieldValues, RegisterOptions } from 'react-hook-form'; import { Dispatch } from 'react'; import { Asset } from '@stellar/stellar-sdk'; @@ -8,17 +8,20 @@ import { SwapFrom } from './variants/SwapFrom'; import { StandardFrom } from './variants/StandardFrom'; import { BadgeProps } from './Badges'; -export interface FromProps { +export interface FromProps { className?: string; formControl: { max?: number; min?: number; - register: UseFormRegisterReturn; + control: Control; + name: FieldPath; error?: string; readOnly?: boolean; disabled?: boolean; setValue?: (n: string) => void; maxDecimals?: number; + rules?: RegisterOptions; + onChange?: (value: string) => void; }; asset: { assets?: BlockchainAsset[]; @@ -41,9 +44,13 @@ export enum FromVariants { SWAP = 'SWAP', } -export type FromPropsWithVariant = FromProps & { variant?: FromVariants }; +export type FromPropsWithVariant = FromProps & { + variant?: FromVariants; +}; -const From = (props: FromPropsWithVariant): JSX.Element | null => { +const From = ( + props: FromPropsWithVariant, +): JSX.Element | null => { const { variant } = props; if (variant === FromVariants.SWAP) { diff --git a/src/components/Form/From/variants/StandardFrom.test.tsx b/src/components/Form/From/variants/StandardFrom.test.tsx index 35aad7ff..40a667b6 100644 --- a/src/components/Form/From/variants/StandardFrom.test.tsx +++ b/src/components/Form/From/variants/StandardFrom.test.tsx @@ -31,12 +31,13 @@ const assets: BlockchainAsset[] = [mockAsset]; const edgeCaseMaxBalances = [9e-7, 1e-7, 1e-10, 1e21, 1e22, 0.000000025164, 0.0000000025164]; const TestingComponent = ({ max }: { max: number }) => { - const { setValue, register } = useForm(); + const { setValue, control } = useForm(); const defaultProps: FromProps = { formControl: { max: max, - register: register('amount'), + control, + name: 'amount', setValue: (n: string) => setValue('amount', n), error: '', maxDecimals: 12, diff --git a/src/components/Form/From/variants/StandardFrom.tsx b/src/components/Form/From/variants/StandardFrom.tsx index 46bb8acc..2dca97a2 100644 --- a/src/components/Form/From/variants/StandardFrom.tsx +++ b/src/components/Form/From/variants/StandardFrom.tsx @@ -1,3 +1,4 @@ +import { FieldValues } from 'react-hook-form'; import { FromProps } from '..'; import { NumericInput } from '../NumericInput'; import { AssetSelector } from '../../../Selector'; @@ -5,12 +6,12 @@ import { AssetSelectorOnChange } from '../../../Selector/AssetSelector/helpers'; import { FromDescription } from '../Description'; import { AvailableActions } from '../AvailableActions'; -export const StandardFrom = ({ +export const StandardFrom = ({ className, - formControl: { max, register, readOnly, error, setValue, maxDecimals, disabled }, + formControl: { max, control, name, readOnly, error, setValue, maxDecimals, disabled, rules, onChange }, asset: { assetSuffix, assets, selectedAsset, setSelectedAsset }, description: { customText, network }, -}: FromProps) => ( +}: FromProps) => ( <>
{assets && setSelectedAsset && ( ({ className, - formControl: { register, readOnly = false, error, disabled = false, maxDecimals }, + formControl: { control, name, readOnly = false, error, disabled = false, maxDecimals, rules, onChange }, asset: { assetSuffix, assets, selectedAsset, setSelectedAsset }, description: { customText, network }, badges: { minBadge, maxBadge }, -}: FromProps) => ( +}: FromProps) => (
{error ? ( diff --git a/src/components/nabla/common/AmountSelector.tsx b/src/components/nabla/common/AmountSelector.tsx index b6e387bf..b37a0bf3 100644 --- a/src/components/nabla/common/AmountSelector.tsx +++ b/src/components/nabla/common/AmountSelector.tsx @@ -71,7 +71,8 @@ export function AmountSelector @@ -82,10 +83,18 @@ export function AmountSelector +
+ setValue(formFieldName, n as K)} + max={maxBalance?.approximateNumber} + hideAvailableBalance={true} + /> +
{showAvailableActions ? (
= ({ calcTo, submissionPending, }) => { - const { handleSubmit, register, setValue, watch, formState, clearErrors, trigger } = useForm({ + const { handleSubmit, control, setValue, watch, formState, clearErrors, trigger } = useForm({ defaultValues: { isMax: false, isMin: false }, mode: 'onChange', }); const { balances } = useBalances(); - const registerFromAmount = register('fromAmount', { - max: { value: Number(calcMax().amount), message: 'Amount exceeds the maximum allowable buyout' }, - min: { value: Number(calcMin().amount), message: 'Amount is too low to meet the minimum buyout requirement' }, - required: 'This field is required', - onChange: (n: ChangeEvent) => { - const value = (n.target as HTMLInputElement).value; - const valueWithoutSpaces = value.replace(/\s+/g, '').replace(/,/g, '.'); - setValue('isMax', false); - setValue('isMin', false); - setValue('fromAmount', valueWithoutSpaces); - setValue('toAmount', calcTo(valueWithoutSpaces)); - }, - validate: { - balance: (value) => { - if (balances && selectedFromToken) { - const selectedTokenBalance = balances.find( - (balance) => balance.token === (selectedFromToken as OrmlTraitsAssetRegistryAssetMetadata).metadata.symbol, - )?.amount; - if (Number(value) > Number(selectedTokenBalance || 0)) { - return 'Insufficient balance'; - } - } - }, - }, - }); - const useCreateBadge = (calcValue: () => { amount: string; native: number }, isMax: boolean) => { const value = calcValue(); return useMemo(() => { @@ -96,13 +70,38 @@ export const GasForm: FC = ({ const maxBadge = useCreateBadge(calcMax, true); const fromPropsError = formState.errors.fromAmount?.message; - const FromProps: FromPropsWithVariant = useMemo( + const FromProps: FromPropsWithVariant = useMemo( () => ({ formControl: { - register: registerFromAmount, + control, + name: 'fromAmount', disabled: submissionPending, readOnly: submissionPending, error: fromPropsError, + rules: { + max: { value: Number(calcMax().amount), message: 'Amount exceeds the maximum allowable buyout' }, + min: { value: Number(calcMin().amount), message: 'Amount is too low to meet the minimum buyout requirement' }, + required: 'This field is required', + validate: { + balance: (value) => { + if (balances && selectedFromToken) { + const selectedTokenBalance = balances.find( + (balance) => balance.token === (selectedFromToken as OrmlTraitsAssetRegistryAssetMetadata).metadata.symbol, + )?.amount; + if (Number(value) > Number(selectedTokenBalance || 0)) { + return 'Insufficient balance'; + } + } + }, + }, + }, + onChange: (value: string) => { + const valueWithoutSpaces = value.replace(/\s+/g, '').replace(/,/g, '.'); + setValue('isMax', false); + setValue('isMin', false); + setValue('fromAmount', valueWithoutSpaces); + setValue('toAmount', calcTo(valueWithoutSpaces)); + }, }, badges: { minBadge, @@ -119,21 +118,27 @@ export const GasForm: FC = ({ variant: FromVariants.SWAP, }), [ + control, currencies, fromPropsError, maxBadge, minBadge, - registerFromAmount, selectedFromToken, setSelectedFromToken, submissionPending, + balances, + calcMax, + calcMin, + calcTo, + setValue, ], ); const toPropsError = formState.errors.toAmount?.message; - const ToProps: FromPropsWithVariant = { + const ToProps: FromPropsWithVariant = { formControl: { - register: register('toAmount'), + control, + name: 'toAmount', readOnly: true, disabled: submissionPending, error: toPropsError, diff --git a/src/pages/spacewalk/bridge/Issue/index.tsx b/src/pages/spacewalk/bridge/Issue/index.tsx index 0e9c6d13..d5968fdc 100644 --- a/src/pages/spacewalk/bridge/Issue/index.tsx +++ b/src/pages/spacewalk/bridge/Issue/index.tsx @@ -72,7 +72,7 @@ function Issue(props: IssueProps): JSX.Element { const maxIssuable = nativeToDecimal(selectedVault?.issuableTokens || 0).toNumber(); - const { handleSubmit, watch, register, formState, setValue, trigger } = useForm({ + const { handleSubmit, watch, control, register, formState, setValue, trigger } = useForm({ resolver: yupResolver(getIssueValidationSchema(maxIssuable, transferable, tokenSymbol)), mode: 'onChange', }); @@ -176,7 +176,8 @@ function Issue(props: IssueProps): JSX.Element { {...{ formControl: { maxDecimals: USER_INPUT_MAX_DECIMALS.STELLAR, - register: register('amount'), + control, + name: 'amount', setValue: (n: string) => setValue('amount', n), error: getFirstErrorMessage(formState, ['amount', 'securityDeposit']) || diff --git a/src/pages/spacewalk/bridge/Redeem/index.tsx b/src/pages/spacewalk/bridge/Redeem/index.tsx index 90a98ed1..46dc5286 100644 --- a/src/pages/spacewalk/bridge/Redeem/index.tsx +++ b/src/pages/spacewalk/bridge/Redeem/index.tsx @@ -63,7 +63,7 @@ function Redeem(props: RedeemProps): JSX.Element { const maxRedeemable = nativeToDecimal(selectedVault?.redeemableTokens || 0).toNumber(); - const { handleSubmit, watch, register, formState, setValue, trigger } = useForm({ + const { handleSubmit, watch, control, register, formState, setValue, trigger } = useForm({ defaultValues: {}, resolver: yupResolver(getRedeemValidationSchema(maxRedeemable, selectedAssetsBalance)), mode: 'onChange', @@ -149,7 +149,8 @@ function Redeem(props: RedeemProps): JSX.Element { {...{ formControl: { max: selectedAssetsBalance, - register: register('amount'), + control, + name: 'amount', setValue: (n: string) => { setValue('amount', n); trigger('amount'); diff --git a/src/pages/staking/dialogs/ClaimRewardsDialog.tsx b/src/pages/staking/dialogs/ClaimRewardsDialog.tsx index 14267f0d..aa624bca 100644 --- a/src/pages/staking/dialogs/ClaimRewardsDialog.tsx +++ b/src/pages/staking/dialogs/ClaimRewardsDialog.tsx @@ -47,7 +47,7 @@ function ClaimRewardsDialog(props: Props) { }, }); - const { formState, register, setValue, getValues } = form; + const { formState, control, setValue, getValues } = form; useMemo(() => { if (!visible) @@ -99,7 +99,8 @@ function ClaimRewardsDialog(props: Props) {
setValue('amount', n)} error={formState.errors.amount?.message?.toString()} @@ -116,7 +117,7 @@ function ClaimRewardsDialog(props: Props) {
); } - }, [step, formState, register, setValue, userRewardsBalance]); + }, [step, formState, control, setValue, userRewardsBalance]); const getButtonText = (step: ClaimStep) => { switch (step) { diff --git a/src/pages/staking/dialogs/DelegateToCollatorDialog.tsx b/src/pages/staking/dialogs/DelegateToCollatorDialog.tsx index 08024792..bf983484 100644 --- a/src/pages/staking/dialogs/DelegateToCollatorDialog.tsx +++ b/src/pages/staking/dialogs/DelegateToCollatorDialog.tsx @@ -65,7 +65,7 @@ function DelegateToCollatorDialog(props: DelegateToCollatorDialogProps) { const titleAction = useMemo(() => (mode === 'unstaking' ? 'Unstake' : 'Stake'), [mode]); const max = nativeToDecimal(availableBalance).toNumber(); - const { handleSubmit, watch, register, formState, setValue } = useForm({ + const { handleSubmit, watch, control, formState, setValue } = useForm({ resolver: yupResolver(getStakingValidationSchema(max)), }); @@ -84,7 +84,8 @@ function DelegateToCollatorDialog(props: DelegateToCollatorDialogProps) { {CollatorInfo}
setValue('amount', n)} error={formState.errors.amount?.message?.toString()} max={max} @@ -92,7 +93,7 @@ function DelegateToCollatorDialog(props: DelegateToCollatorDialogProps) { /> ), - [CollatorInfo, formState.errors.amount?.message, max, mode, register, setValue], + [CollatorInfo, control, formState.errors.amount?.message, max, mode, setValue], ); const actions = useMemo( diff --git a/src/pages/staking/dialogs/unlock/UnlockConfirmStep.tsx b/src/pages/staking/dialogs/unlock/UnlockConfirmStep.tsx index dc36ce94..5069ab37 100644 --- a/src/pages/staking/dialogs/unlock/UnlockConfirmStep.tsx +++ b/src/pages/staking/dialogs/unlock/UnlockConfirmStep.tsx @@ -1,16 +1,20 @@ -import { FC } from 'react'; import Amount from '../../../../components/Form/Amount'; import gasolinePump from '../../../../assets/gasoline-pump.svg'; -import { UseFormRegisterReturn } from 'react-hook-form'; +import { Control, FieldPath, FieldValues } from 'react-hook-form'; -interface UnlockConfirmStepProps { - register: UseFormRegisterReturn<'amount'>; +interface UnlockConfirmStepProps { + control: Control; balance: number; gasFee: string; error?: string; } -export const UnlockConfirmStep: FC = ({ register, balance, gasFee, error }) => { +export const UnlockConfirmStep = ({ + control, + balance, + gasFee, + error, +}: UnlockConfirmStepProps) => { const noSetValue = () => null; return (
@@ -20,7 +24,8 @@ export const UnlockConfirmStep: FC = ({ register, balanc
} max={balance} setValue={noSetValue} error={error} diff --git a/src/pages/staking/dialogs/unlock/UnlockDialog.tsx b/src/pages/staking/dialogs/unlock/UnlockDialog.tsx index ffe85526..286ba438 100644 --- a/src/pages/staking/dialogs/unlock/UnlockDialog.tsx +++ b/src/pages/staking/dialogs/unlock/UnlockDialog.tsx @@ -52,7 +52,7 @@ export const UnlockDialog: FC = ({ }, }); - const { formState, register } = form; + const { formState, control } = form; const showGasFee = useMemo((): string => { const fee = nativeToDecimal(gasFee.toString()).toNumber(); @@ -66,7 +66,7 @@ export const UnlockDialog: FC = ({ return ( = ({ /> ); } - }, [step, formState, register, userAvailableBalanceForUnlock, showGasFee, balance]); + }, [step, formState, control, userAvailableBalanceForUnlock, showGasFee, balance]); const onConfirm = () => { setLoading(true); diff --git a/yarn.lock b/yarn.lock index 47e16be5..8950f21e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11299,6 +11299,20 @@ __metadata: languageName: node linkType: hard +"numora-react@npm:^3.0.2": + version: 3.0.2 + resolution: "numora-react@npm:3.0.2" + checksum: 10c0/b736319f3d0bfd5eea6632e0ca10e0b892fbe61128891df6cf764bb43d9b5e5cdf00cbaca5b11d0b49566cbb6f7b4a29c45826d8f5b81a679f1b8c9150fcbe22 + languageName: node + linkType: hard + +"numora@npm:^3.0.2": + version: 3.0.2 + resolution: "numora@npm:3.0.2" + checksum: 10c0/4f4789683a0392aa5109cbc0644d812e1e9a9ac5381bc0cac2388dcdfec113e5871b977af38bc29c6b2eddc897d50661cb44003221974c92e79456bb1cadc8a6 + languageName: node + linkType: hard + "nwsapi@npm:^2.2.2": version: 2.2.13 resolution: "nwsapi@npm:2.2.13" @@ -11801,6 +11815,8 @@ __metadata: luxon: "npm:^3.5.0" match-sorter: "npm:^6.3.1" motion: "npm:^11.15.0" + numora: "npm:^3.0.2" + numora-react: "npm:^3.0.2" postcss: "npm:^8.4.47" postcss-import: "npm:^16.1.0" prettier: "npm:^3.3.3"