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: allow amount inputs to be empty #288

Merged
merged 5 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from 3 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
16 changes: 3 additions & 13 deletions components/bank/forms/ibcSendForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { useChain } from '@cosmos-kit/react';
import { useToast } from '@/contexts';

import { Any } from '@liftedinit/manifestjs/dist/codegen/google/protobuf/any';
import { AmountInput } from '@/components';

//TODO: switch to main-net names
export default function IbcSendForm({
Expand Down Expand Up @@ -521,21 +522,10 @@ export default function IbcSendForm({
</span>
</label>
<div className="relative">
<input
type="text"
inputMode="decimal"
pattern="[0-9]*[.]?[0-9]*"
<AmountInput
name="amount"
placeholder="0.00"
value={values.amount}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
if (value === '' || /^\d*\.?\d*$/.test(value)) {
setFieldValue('amount', e.target.value);
}
}}
style={{ borderRadius: '12px' }}
className="input input-md border border-[#00000033] dark:border-[#FFFFFF33] bg-[#E0E0FF0A] dark:bg-[#E0E0FF0A] w-full pr-24 dark:text-[#FFFFFF] text-[#161616]"
onValueChange={v => setFieldValue('amount', v)}
/>
<div className="absolute inset-y-1 right-1 flex items-center">
<div className="dropdown dropdown-end h-full">
Expand Down
20 changes: 3 additions & 17 deletions components/bank/forms/sendForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { MdContacts } from 'react-icons/md';
import env from '@/config/env';
import { Any } from 'cosmjs-types/google/protobuf/any';
import { MsgSend } from '@liftedinit/manifestjs/dist/codegen/cosmos/bank/v1beta1/tx';
import { AmountInput } from '@/components';

export default function SendForm({
address,
Expand Down Expand Up @@ -197,25 +198,10 @@ export default function SendForm({
</span>
</label>
<div className="relative">
<input
className="input input-md border border-[#00000033] dark:border-[#FFFFFF33] bg-[#E0E0FF0A] dark:bg-[#E0E0FF0A] w-full pr-24 dark:text-[#FFFFFF] text-[#161616]"
<AmountInput
name="amount"
inputMode="decimal"
pattern="[0-9]*[.]?[0-9]*"
placeholder="0.00"
style={{ borderRadius: '12px' }}
value={values.amount}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
if (/^\d*\.?\d*$/.test(value) && parseFloat(value) >= 0) {
setFieldValue('amount', value);
}
}}
onKeyPress={(e: React.KeyboardEvent<HTMLInputElement>) => {
if (!/[\d.]/.test(e.key)) {
e.preventDefault();
}
}}
onValueChange={value => setFieldValue('amount', value)}
/>

<div className="absolute inset-y-1 right-1 flex items-center">
Expand Down
53 changes: 53 additions & 0 deletions components/react/inputs/AmountInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';

export interface AmountInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
value: string;
onValueChange: (newAmount: string) => void;
}

/**
* A component for entering an amount of tokens. Allows only positive decimal
* numbers, or empty values (for an empty field).
* @param value The current value of the input field.
* @param onValueChange A callback that is called when the value of the input field
* changes, with the new amount.
* @param props Additional props to pass to the input field.
* @constructor
*/
export const AmountInput: React.FC<AmountInputProps> = ({ value, onValueChange, ...props }) => {
function onChange(event: React.ChangeEvent<HTMLInputElement>) {
const v = event.target.value;

if (v === '') {
onValueChange('');
return;
}
if (v === '.') {
// Allow for `.` to be entered on its way to a real number.
onValueChange('.');
return;

Check warning on line 28 in components/react/inputs/AmountInput.tsx

View check run for this annotation

Codecov / codecov/patch

components/react/inputs/AmountInput.tsx#L27-L28

Added lines #L27 - L28 were not covered by tests
}
const newValue = /^\d*\.?\d*$/.test(v) ? parseFloat(v) : NaN;

if (Number.isFinite(newValue)) {
onValueChange(v);
} else if (value !== '') {
onValueChange(value);
} else {

Check warning on line 36 in components/react/inputs/AmountInput.tsx

View check run for this annotation

Codecov / codecov/patch

components/react/inputs/AmountInput.tsx#L36

Added line #L36 was not covered by tests
onValueChange('');
}
}

return (
<input
className="input input-md border border-[#00000033] dark:border-[#FFFFFF33] bg-[#E0E0FF0A] dark:bg-[#E0E0FF0A] w-full pr-24 dark:text-[#FFFFFF] text-[#161616] rounded-xl"
type="text"
inputMode="decimal"
placeholder="0.00"
min={0}
value={value}
onChange={onChange}
{...props}
/>
);
};
78 changes: 78 additions & 0 deletions components/react/inputs/__tests__/AmountInput.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { jest, test, expect, afterEach, describe } from 'bun:test';
import React from 'react';
import { render, screen, cleanup, fireEvent } from '@testing-library/react';
import { AmountInput, NumberInput } from '@/components/react/inputs';
import { Formik, Form } from 'formik';

const TestForm = ({ children }: { children: React.ReactNode }) => (
<Formik initialValues={{ test: '' }} onSubmit={() => {}}>
<Form>{children}</Form>
</Formik>
);

describe('AmountInput', () => {
afterEach(() => {
cleanup();
});

test('renders correctly', () => {
const value = '42';
const onValueChange = jest.fn();

render(
<TestForm>
<AmountInput name="test" value={value} onValueChange={onValueChange} />
</TestForm>
);

const input = screen.getByPlaceholderText('0.00');
expect(input).toBeInTheDocument();
expect(input.tagName.toLowerCase()).toBe('input');
expect(input).toHaveAttribute('type', 'text');
});

test('calls onValueChange with the new value', () => {
const value = '42';
const onValueChange = jest.fn();

render(
<TestForm>
<AmountInput name="test" value={value} onValueChange={onValueChange} />
</TestForm>
);

const input = screen.getByPlaceholderText('0.00');
fireEvent.change(input, { target: { value: '42.42' } });
expect(onValueChange).toHaveBeenCalledWith('42.42');
});

test('calls onValueChange with an empty string when the input is empty', () => {
const value = '42';
const onValueChange = jest.fn();

render(
<TestForm>
<AmountInput name="test" value={value} onValueChange={onValueChange} />
</TestForm>
);

const input = screen.getByPlaceholderText('0.00');
fireEvent.change(input, { target: { value: '' } });
expect(onValueChange).toHaveBeenCalledWith('');
});

test('calls onValueChange with the same value when the input is invalid', () => {
const value = '42';
const onValueChange = jest.fn();

render(
<TestForm>
<AmountInput name="test" value={value} onValueChange={onValueChange} />
</TestForm>
);

const input = screen.getByPlaceholderText('0.00');
fireEvent.change(input, { target: { value: '42.42.42' } });
expect(onValueChange).toHaveBeenCalledWith('42');
});
});
1 change: 1 addition & 0 deletions components/react/inputs/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './AmountInput';
export * from './TextInput';
export * from './NumberInput';
export * from './TextArea';