Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
danielsimao committed May 3, 2024
1 parent 619b6ca commit f6bdbd3
Show file tree
Hide file tree
Showing 11 changed files with 225 additions and 153 deletions.
53 changes: 33 additions & 20 deletions packages/components/src/TokenInput/BaseTokenInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { AriaTextFieldOptions, useTextField } from '@react-aria/textfield';
import { mergeProps } from '@react-aria/utils';
import { ChangeEventHandler, FocusEvent, forwardRef, ReactNode, useCallback, useEffect, useState } from 'react';

import { trimDecimals } from '../utils';
import { HelperTextProps } from '../HelperText';
import { LabelProps } from '../Label';
import { Field, FieldProps, useFieldProps } from '../Field';
Expand Down Expand Up @@ -42,7 +43,7 @@ type Props = {
value?: string;
defaultValue?: string;
// TODO: use Currency from bob-ui
currency: { decimals: number };
currency?: { decimals: number };
onValueChange?: (value: string | number) => void;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
onFocus?: (e: FocusEvent<Element>) => void;
Expand Down Expand Up @@ -89,24 +90,23 @@ const BaseTokenInput = forwardRef<HTMLInputElement, BaseTokenInputProps>(

const format = useCurrencyFormatter();

const handleChange: ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => {
const value = e.target.value;
// Observes currency field and correct decimals places if needed
useEffect(() => {
if (value && currency) {
const trimmedValue = trimDecimals(value, currency.decimals);

const isEmpty = value === '';
const hasValidDecimalFormat = RegExp(`^\\d*(?:\\\\[.])?\\d*$`).test(escapeRegExp(value));
const hasValidDecimalsAmount = hasCorrectDecimals(value, currency.decimals);
if (value !== trimmedValue) {
setValue(trimmedValue);
onValueChange?.(trimmedValue);
}
}
}, [currency]);

const isValid = hasValidDecimalFormat && hasValidDecimalsAmount;
useEffect(() => {
if (valueProp === undefined) return;

if (isEmpty || isValid) {
onChange?.(e);
onValueChange?.(value);
setValue(value);
}
},
[onChange, onValueChange]
);
setValue(valueProp.toString());
}, [valueProp]);

const { inputProps, descriptionProps, errorMessageProps, labelProps } = useTextField(
{
Expand All @@ -121,11 +121,24 @@ const BaseTokenInput = forwardRef<HTMLInputElement, BaseTokenInputProps>(
inputRef
);

useEffect(() => {
if (valueProp === undefined) return;
const handleChange: ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => {
const value = e.target.value;

setValue(valueProp.toString());
}, [valueProp]);
const isEmpty = value === '';
const hasValidDecimalFormat = RegExp(`^\\d*(?:\\\\[.])?\\d*$`).test(escapeRegExp(value));
const hasValidDecimalsAmount = currency ? hasCorrectDecimals(value, currency.decimals) : true;

const isValid = hasValidDecimalFormat && hasValidDecimalsAmount;

if (isEmpty || isValid) {
onChange?.(e);
onValueChange?.(value);
setValue(value);
}
},
[onChange, onValueChange, currency]
);

const hasLabel = !!label || !!balance;

Expand Down
6 changes: 3 additions & 3 deletions packages/components/src/TokenInput/FixedTokenInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { BaseTokenInput, BaseTokenInputProps } from './BaseTokenInput';
import { TokenInputBalance } from './TokenInputBalance';

type Props = {
currency: any;
balance?: string;
humanBalance?: string | number;
balanceLabel?: ReactNode;
onClickBalance?: (balance: string | number) => void;
ticker: string;
logoUrl: string;
};

Expand All @@ -24,11 +24,11 @@ const FixedTokenInput = forwardRef<HTMLInputElement, FixedTokenInputProps>(
humanBalance,
balanceLabel,
onClickBalance,
ticker: tickerProp,
logoUrl,
isDisabled,
id,
size = 'md',
currency,
...props
},
ref
Expand All @@ -49,7 +49,7 @@ const FixedTokenInput = forwardRef<HTMLInputElement, FixedTokenInputProps>(
{...props}
ref={ref}
balance={balance}
endAdornment={<TokenAdornment logoUrl={logoUrl} size={size} ticker={tickerProp} />}
endAdornment={<TokenAdornment logoUrl={logoUrl} size={size} ticker={currency.symbol} />}
id={id}
isDisabled={isDisabled}
size={size}
Expand Down
30 changes: 22 additions & 8 deletions packages/components/src/TokenInput/SelectableTokenInput.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { chain, useId } from '@react-aria/utils';
import { Key, ReactNode, forwardRef, useEffect, useState } from 'react';
import { Key, ReactNode, forwardRef, useCallback, useEffect, useState } from 'react';

import { HelperText } from '../HelperText';

import { BaseTokenInput, BaseTokenInputProps } from './BaseTokenInput';
import { TokenInputBalance } from './TokenInputBalance';
import { TokenSelect, TokenSelectProps } from './TokenSelect';
import { TokenData, TokenSelect, TokenSelectProps } from './TokenSelect';

type Props = {
balance?: string;
Expand Down Expand Up @@ -39,17 +39,30 @@ const SelectableTokenInput = forwardRef<HTMLInputElement, SelectableTokenInputPr
): JSX.Element => {
const selectHelperTextId = useId();

const [ticker, setTicker] = useState<string | undefined>(selectProps?.defaultValue?.toString());
const defaultCurrency = (selectProps.items as TokenData[]).find(
(item) => item.currency.symbol === selectProps.defaultValue
);

const [currency, setCurrency] = useState<any | undefined>(defaultCurrency);

useEffect(() => {
const value = selectProps?.value;

if (value === undefined) return;

setTicker(value.toString());
const tokenData = (selectProps.items as TokenData[]).find((item) => item.currency.symbol === value);

setCurrency(tokenData?.currency);
}, [selectProps?.value]);

const handleTokenChange = (ticker: Key) => setTicker(ticker as string);
const handleTokenChange = useCallback(
(ticker: Key) => {
const tokenData = (selectProps.items as TokenData[]).find((item) => item.currency.symbol === ticker);

setCurrency(tokenData?.currency);
},
[selectProps]
);

// Prioritise Number Input description and error message
const hasNumberFieldMessages = !!(errorMessage || description);
Expand All @@ -68,17 +81,17 @@ const SelectableTokenInput = forwardRef<HTMLInputElement, SelectableTokenInputPr
errorMessage={undefined}
isInvalid={isInvalid}
size={size}
value={ticker}
value={currency?.symbol}
onSelectionChange={chain(onSelectionChange, handleTokenChange)}
/>
);

const balance = balanceProp !== undefined && (
<TokenInputBalance
balance={ticker ? balanceProp : '0'}
balance={currency ? balanceProp : '0'}
balanceHuman={humanBalance}
inputId={id}
isDisabled={isDisabled || !ticker}
isDisabled={isDisabled || !currency}
label={balanceLabel}
onClickBalance={onClickBalance}
/>
Expand All @@ -88,6 +101,7 @@ const SelectableTokenInput = forwardRef<HTMLInputElement, SelectableTokenInputPr
<BaseTokenInput
ref={ref}
balance={balance}
currency={currency}
description={description}
endAdornment={endAdornment}
errorMessage={errorMessage}
Expand Down
91 changes: 50 additions & 41 deletions packages/components/src/TokenInput/TokenInput.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { useDOMRef } from '@interlay/hooks';
import { mergeProps, useId } from '@react-aria/utils';
import { ChangeEvent, FocusEvent, forwardRef, useEffect, useState } from 'react';
import { ChangeEvent, FocusEvent, forwardRef, useCallback, useEffect, useState } from 'react';

import { FixedTokenInput, FixedTokenInputProps } from './FixedTokenInput';
import { SelectableTokenInput, SelectableTokenInputProps } from './SelectableTokenInput';

type Props = {
onValueChange?: (value?: string | number) => void;
// TODO: define type when repo moved to bob-ui
currency: any;
};

type FixedAttrs = Omit<FixedTokenInputProps, keyof Props>;
Expand All @@ -19,34 +18,43 @@ type InheritAttrs = ({ type?: 'fixed' } & FixedAttrs) | ({ type?: 'selectable' }

type TokenInputProps = Props & InheritAttrs;

const TokenInput = forwardRef<HTMLInputElement, TokenInputProps>(
({ type = 'fixed', defaultValue, value: valueProp, onValueChange, balance, onBlur, ...props }, ref): JSX.Element => {
const inputRef = useDOMRef<HTMLInputElement>(ref);
const TokenInput = forwardRef<HTMLInputElement, TokenInputProps>((props, ref): JSX.Element => {
const { defaultValue, value: valueProp, onValueChange, balance, onBlur, ...otherProps } = props;

const [value, setValue] = useState(defaultValue);
const inputRef = useDOMRef<HTMLInputElement>(ref);

const inputId = useId();
const [value, setValue] = useState(defaultValue);

useEffect(() => {
if (valueProp === undefined) return;
const inputId = useId();

setValue(valueProp);
}, [valueProp]);
useEffect(() => {
if (valueProp === undefined) return;

const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setValue(valueProp);
}, [valueProp]);

const handleChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;

onValueChange?.(value);
setValue(value);
};
},
[onValueChange]
);

const handleClickBalance = (balance: string) => {
const handleClickBalance = useCallback(
(balance: string) => {
inputRef.current?.focus();

setValue(balance);
onValueChange?.(balance);
};
},
[onValueChange, inputRef.current]
);

const handleBlur = (e: FocusEvent<Element>) => {
const handleBlur = useCallback(
(e: FocusEvent<Element>) => {
const relatedTargetEl = e.relatedTarget;

if (!relatedTargetEl || !relatedTargetEl.getAttribute) {
Expand All @@ -58,32 +66,33 @@ const TokenInput = forwardRef<HTMLInputElement, TokenInputProps>(
if (!isTargetingMaxBtn) {
onBlur?.(e);
}
};

const numberInputProps: Partial<TokenInputProps> =
balance !== undefined
? {
id: inputId,
balance,
onBlur: handleBlur,
onClickBalance: handleClickBalance
}
: { onBlur };

const commonProps = {
...numberInputProps,
ref: inputRef,
value,
onChange: handleChange
};

if (type === 'selectable') {
return <SelectableTokenInput {...mergeProps(props, commonProps)} />;
}

return <FixedTokenInput {...mergeProps(props, commonProps)} />;
},
[onBlur]
);

const numberInputProps: Partial<TokenInputProps> =
balance !== undefined
? {
id: inputId,
balance,
onBlur: handleBlur,
onClickBalance: handleClickBalance
}
: { onBlur };

const commonProps = {
...numberInputProps,
ref: inputRef,
value,
onChange: handleChange
};

if (props.type === 'selectable') {
return <SelectableTokenInput {...mergeProps(otherProps, commonProps)} />;
}
);

return <FixedTokenInput {...mergeProps(otherProps, commonProps)} />;
});

TokenInput.displayName = 'TokenInput';

Expand Down
8 changes: 4 additions & 4 deletions packages/components/src/TokenInput/TokenListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ import { TokenData } from './TokenSelect';

type TokenListItemProps = { isDisabled?: boolean } & TokenData;

const TokenListItem = ({ balance, balanceUSD, ticker, logoUrl, isDisabled }: TokenListItemProps): JSX.Element => {
const isSelected = useSelectModalContext().selectedItem?.key === ticker && !isDisabled;
const TokenListItem = ({ balance, balanceUSD, currency, logoUrl, isDisabled }: TokenListItemProps): JSX.Element => {
const isSelected = useSelectModalContext().selectedItem?.value.currency.symbol === currency.symbol && !isDisabled;
const format = useCurrencyFormatter();

return (
<>
<StyledListTokenWrapper alignItems='center' flex='1' gap='s'>
<StyledTokenImg $size='lg' alt={ticker} src={logoUrl} />
<StyledListItemLabel $isSelected={isSelected}>{ticker}</StyledListItemLabel>
<StyledTokenImg $size='lg' alt={currency.symbol} src={logoUrl} />
<StyledListItemLabel $isSelected={isSelected}>{currency.symbol}</StyledListItemLabel>
</StyledListTokenWrapper>
<Flex alignItems='flex-end' direction='column' flex='0' gap='xs'>
<StyledListItemLabel $isSelected={isSelected}>{balance}</StyledListItemLabel>
Expand Down
9 changes: 5 additions & 4 deletions packages/components/src/TokenInput/TokenSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,21 @@ import { TokenListItem } from './TokenListItem';

const Value = ({ data }: { data: TokenData }) => (
<Flex alignItems='center' gap='xs' justifyContent='space-evenly'>
<StyledTokenImg $size='md' alt={data.ticker} src={data.logoUrl} />
<StyledTicker>{data.ticker}</StyledTicker>
<StyledTokenImg $size='md' alt={data.currency.symbol} src={data.logoUrl} />
<StyledTicker>{data.currency.symbol}</StyledTicker>
</Flex>
);

type TokenData = {
ticker: string;
currency: any;
logoUrl: string;
balance: string | number;
balanceUSD: number;
};

type TokenSelectProps = Omit<ModalSelectProps<TokenData>, 'children' | 'type'>;

// TODO: value control from currency object
const TokenSelect = ({ modalProps, size, ...props }: TokenSelectProps): JSX.Element => {
return (
<Select<TokenData>
Expand All @@ -40,7 +41,7 @@ const TokenSelect = ({ modalProps, size, ...props }: TokenSelectProps): JSX.Elem
type='modal'
>
{(data: TokenData) => (
<Item key={data.ticker} textValue={data.ticker}>
<Item key={data.currency.symbol} textValue={data.currency.symbol}>
<TokenListItem {...data} />
</Item>
)}
Expand Down
Loading

0 comments on commit f6bdbd3

Please sign in to comment.