Skip to content

Commit

Permalink
Factor out Asset and Account form input components.
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonfancher committed Nov 10, 2021
1 parent 9bc3ad2 commit 40e1a22
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 75 deletions.
128 changes: 127 additions & 1 deletion packages/webapp/src/_app/ui/form.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import React, { HTMLProps } from "react";

import { tokenConfig } from "config";
import { Button } from "_app";

export const Label: React.FC<{
htmlFor: string;
}> = (props) => (
Expand All @@ -8,12 +11,17 @@ export const Label: React.FC<{
</label>
);

export const Input: React.FC<HTMLProps<HTMLInputElement>> = (props) => (
export const Input: React.FC<
HTMLProps<HTMLInputElement> & {
inputRef?: React.Ref<HTMLInputElement> | null;
}
> = ({ inputRef, ...props }) => (
<input
name={props.id}
className={`w-full bg-white border border-gray-300 focus:border-yellow-500 focus:ring-2 focus:ring-yellow-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out ${
props.disabled ? "bg-gray-50" : ""
}`}
ref={inputRef}
{...props}
/>
);
Expand Down Expand Up @@ -99,6 +107,122 @@ export const LabeledSet: React.FC<{
);
};

export const ChainAccountInput = (
props: HTMLProps<HTMLInputElement> & { id: string }
) => {
const validateAccountField = (e: React.FormEvent<HTMLInputElement>) => {
const target = e.target as HTMLInputElement;
if (target.validity.valueMissing) {
target.setCustomValidity("Enter an account name");
} else {
target.setCustomValidity("Invalid account name");
}
};

const clearErrorMessages = (e: React.FormEvent<HTMLInputElement>) => {
(e.target as HTMLInputElement).setCustomValidity("");
};

const onInput = (e: React.FormEvent<HTMLInputElement>) => {
clearErrorMessages(e);
props.onInput?.(e);
};

const onInvalid = (e: React.FormEvent<HTMLInputElement>) => {
validateAccountField(e);
props.onInvalid?.(e);
};

return (
<Form.LabeledSet
label={`${tokenConfig.symbol} account name (12 characters)`}
htmlFor={props.id}
>
<Form.Input
type="text"
maxLength={12}
pattern="^[a-z,1-5.]{1,12}$"
{...props}
onInvalid={onInvalid}
onInput={onInput}
/>
</Form.LabeledSet>
);
};

export const AssetInput = (
props: HTMLProps<HTMLInputElement> & {
label: string; // required
id: string; // required
inputRef?: React.RefObject<HTMLInputElement>;
}
) => {
const { label, inputRef, ...inputProps } = props;

const amountRef = inputRef ?? React.useRef<HTMLInputElement>(null);

const amountInputPreventChangeOnScroll = (
e: React.WheelEvent<HTMLInputElement>
) => (e.target as HTMLInputElement).blur();

const validateAmountField = (e: React.FormEvent<HTMLInputElement>) => {
const target = e.target as HTMLInputElement;
if (target.validity.rangeOverflow) {
target.setCustomValidity("Insufficient funds available");
} else {
target.setCustomValidity("Enter a valid amount");
}
};

const clearErrorMessages = (e: React.FormEvent<HTMLInputElement>) => {
(e.target as HTMLInputElement).setCustomValidity("");
};

const setMaxAmount = () => {
amountRef.current?.setCustomValidity("");

// ensures this works with uncontrolled instances of this input too
Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value"
)?.set?.call?.(amountRef.current, inputProps.max);
amountRef.current?.dispatchEvent(
new Event("change", { bubbles: true })
);
// (adapted from: https://coryrylan.com/blog/trigger-input-updates-with-react-controlled-inputs)
};

return (
<Form.LabeledSet label={label} htmlFor={inputProps.id}>
<div className="flex space-x-2">
<div className="relative flex-1">
<Form.Input
type="number"
inputMode="decimal"
min={1 / Math.pow(10, tokenConfig.precision)}
step="any"
onWheel={amountInputPreventChangeOnScroll}
onInvalid={validateAmountField}
onInput={clearErrorMessages}
inputRef={amountRef}
{...inputProps}
/>
<div className="absolute top-3 right-2">
<p className="text-sm text-gray-400">
{tokenConfig.symbol}
</p>
</div>
</div>
{inputProps.max ? (
<Button type="neutral" onClick={setMaxAmount}>
Max
</Button>
) : null}
</div>
</Form.LabeledSet>
);
};

export const Form = {
Label,
Input,
Expand All @@ -107,6 +231,8 @@ export const Form = {
LabeledSet,
FileInput,
Checkbox,
ChainAccountInput,
AssetInput,
};

export default Form;
Original file line number Diff line number Diff line change
Expand Up @@ -27,43 +27,14 @@ export const WithdrawModalStepForm = ({
setFields(e);
};

const validateAccountField = (e: React.FormEvent<HTMLInputElement>) => {
const target = e.target as HTMLInputElement;
if (target.validity.valueMissing) {
target.setCustomValidity("Enter an account name");
} else {
target.setCustomValidity("Invalid account name");
}
};

const validateAmountField = (e: React.FormEvent<HTMLInputElement>) => {
const target = e.target as HTMLInputElement;
if (target.validity.rangeOverflow) {
target.setCustomValidity("Insufficient funds available");
} else {
target.setCustomValidity("Enter a valid withdrawal amount");
}
};

const clearErrorMessages = (e: React.FormEvent<HTMLInputElement>) => {
(e.target as HTMLInputElement).setCustomValidity("");
};

if (!availableFunds) return null;

const maxWithdrawal = Number(
assetToString(availableFunds!, availableFunds.precision).split(" ")[0]
);

const setMaxAmount = () =>
setFields({ target: { id: "amount", value: maxWithdrawal } });

const isThirdPartyWithdrawal = ualAccount.accountName !== formState[0].to;

const amountInputPreventChangeOnScroll = (
e: React.WheelEvent<HTMLInputElement>
) => (e.target as HTMLInputElement).blur();

return (
<div className="space-y-4">
<Heading>Withdraw funds</Heading>
Expand All @@ -81,51 +52,20 @@ export const WithdrawModalStepForm = ({
</span>
</Text>
<form onSubmit={onPreview} className="space-y-3">
<Form.LabeledSet
label={`${tokenConfig.symbol} account name (12 characters)`}
htmlFor="to"
>
<Form.Input
id="to"
type="text"
required
value={fields.to}
onChange={onChangeFields}
maxLength={12}
pattern="^[a-z,1-5.]{1,12}$"
onInvalid={validateAccountField}
onInput={clearErrorMessages}
/>
</Form.LabeledSet>
<Form.LabeledSet label="Amount to withdraw" htmlFor="amount">
<div className="flex space-x-2">
<div className="relative flex-1">
<Form.Input
id="amount"
type="number"
inputMode="decimal"
min={1 / Math.pow(10, tokenConfig.precision)}
max={maxWithdrawal}
step="any"
required
value={fields.amount}
onChange={onChangeFields}
maxLength={12}
onWheel={amountInputPreventChangeOnScroll}
onInvalid={validateAmountField}
onInput={clearErrorMessages}
/>
<div className="absolute top-3 right-2">
<p className="text-sm text-gray-400">
{availableFunds?.symbol}
</p>
</div>
</div>
<Button type="neutral" onClick={setMaxAmount}>
Max
</Button>
</div>
</Form.LabeledSet>
<Form.ChainAccountInput
id="to"
onChange={onChangeFields}
value={fields.to}
required
/>
<Form.AssetInput
id="amount"
label="Amount to withdraw"
max={maxWithdrawal}
required
value={fields.amount}
onChange={onChangeFields}
/>
{isThirdPartyWithdrawal && (
<Form.LabeledSet label="Memo" htmlFor="memo">
<Form.Input
Expand Down

0 comments on commit 40e1a22

Please sign in to comment.