Skip to content

Commit

Permalink
feat(TokenField): allow apply balance (#494)
Browse files Browse the repository at this point in the history
  • Loading branch information
danielsimao authored Sep 14, 2022
1 parent 59ca825 commit 163b874
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 33 deletions.
9 changes: 7 additions & 2 deletions src/component-library/TokenBalance/TokenBalance.style.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import styled from 'styled-components';
import { theme } from '../theme';

const TokenBalanceWrapper = styled.dl`
display: flex;
display: inline-flex;
font-weight: ${theme.fontWeight.book};
gap: ${theme.spacing.spacing1};
`;
Expand All @@ -12,8 +12,13 @@ const TokenBalanceLabel = styled.dt`
color: ${theme.colors.textPrimary};
`;

const TokenBalanceValue = styled.dd`
type TokenBalanceValueProps = {
$clickable?: boolean;
};

const TokenBalanceValue = styled.span<TokenBalanceValueProps>`
color: ${theme.colors.textSecondary};
cursor: ${(props) => props.$clickable && 'pointer'};
`;

export { TokenBalanceLabel, TokenBalanceValue, TokenBalanceWrapper };
44 changes: 35 additions & 9 deletions src/component-library/TokenBalance/TokenBalance.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,46 @@
import { usePress } from '@react-aria/interactions';

import { TokenBalanceLabel, TokenBalanceValue, TokenBalanceWrapper } from './TokenBalance.style';

interface TokenBalanceProps {
type TokenBalanceProps = {
tokenSymbol: string;
value: string;
valueInUSD: string;
}
value: string | number;
valueInUSD: string | number;
onClickBalance?: () => void;
isDisabled?: boolean;
className?: string;
};

const TokenBalance = ({
tokenSymbol,
value,
valueInUSD,
onClickBalance,
className,
isDisabled
}: TokenBalanceProps): JSX.Element => {
const isClickable = !!onClickBalance && !isDisabled;
const { pressProps } = usePress({ onPress: onClickBalance, isDisabled: !isClickable });
const balanceValueProps = isClickable
? {
role: 'button',
tabIndex: 0,
'aria-label': 'apply balance',
...pressProps
}
: {};

const TokenBalance = ({ tokenSymbol, value, valueInUSD }: TokenBalanceProps): JSX.Element => {
return (
<TokenBalanceWrapper>
<TokenBalanceWrapper className={className}>
<TokenBalanceLabel>Balance:</TokenBalanceLabel>
<TokenBalanceValue>
{value} {tokenSymbol} ({valueInUSD})
</TokenBalanceValue>
<dd>
<TokenBalanceValue $clickable={isClickable} {...balanceValueProps}>
{value} {tokenSymbol} ({valueInUSD})
</TokenBalanceValue>
</dd>
</TokenBalanceWrapper>
);
};

export { TokenBalance };
export type { TokenBalanceProps };
11 changes: 5 additions & 6 deletions src/component-library/TokenField/TokenField.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,16 @@ import { formatUSD } from '@/common/utils/utils';

import { TokenField, TokenFieldProps } from '.';

const Template: Story<TokenFieldProps> = (args) => <TokenField {...args} />;
const Template: Story<TokenFieldProps> = (args) => <TokenField {...args} aria-label='token field' />;

const WithBalance = Template.bind({});
WithBalance.args = {
tokenSymbol: 'KSM',
valueInUSD: formatUSD(100.0),
defaultValue: 100.0, // `value`
balance: {
value: '1000.00',
valueInUSD: formatUSD(1000.0)
}
value: 100.0, // `value`
balance: 1000.0,
balanceInUSD: formatUSD(1000.0),
disabled: false
};

const WithoutBalance = Template.bind({});
Expand Down
8 changes: 7 additions & 1 deletion src/component-library/TokenField/TokenField.style.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import styled from 'styled-components';

import { NumberInput } from '../NumberInput';
import { theme } from '../theme';
import { TokenBalance } from '../TokenBalance';

const TokenFieldInnerWrapper = styled.div`
position: relative;
Expand Down Expand Up @@ -31,4 +32,9 @@ const TokenAdornment = styled.span`
align-items: center;
`;

export { TokenAdornment, TokenFieldInnerWrapper, TokenFieldInput, TokenFieldSymbol, TokenFieldUSD };
const StyledTokenBalance = styled(TokenBalance)`
font-size: ${theme.text.s};
line-height: ${theme.lineHeight.s};
`;

export { StyledTokenBalance, TokenAdornment, TokenFieldInnerWrapper, TokenFieldInput, TokenFieldSymbol, TokenFieldUSD };
75 changes: 60 additions & 15 deletions src/component-library/TokenField/TokenField.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,59 @@
import * as React from 'react';
import { forwardRef, useEffect, useState } from 'react';

import { NumberInputProps } from '../NumberInput';
import { Stack } from '../Stack';
import { TokenBalance } from '../TokenBalance';
import { useDOMRef } from '../utils/dom';
import { triggerChangeEvent } from '../utils/input';
import {
StyledTokenBalance,
TokenAdornment,
TokenFieldInnerWrapper,
TokenFieldInput,
TokenFieldSymbol,
TokenFieldUSD
} from './TokenField.style';

interface TokenFieldProps extends NumberInputProps {
balance?: {
value: string;
valueInUSD: string;
};
type Props = {
tokenSymbol: string;
valueInUSD: string;
}
balance?: string | number;
balanceInUSD?: string | number;
};

type InheritAttrs = Omit<NumberInputProps, keyof Props>;

type TokenFieldProps = Props & InheritAttrs;

const TokenField = forwardRef<HTMLInputElement, TokenFieldProps>(
(
{
tokenSymbol,
valueInUSD,
balance,
balanceInUSD,
value: valueProp,
isDisabled,
className,
style,
hidden,
...props
},
ref
): JSX.Element => {
const inputRef = useDOMRef(ref);
const [value, setValue] = useState<number | undefined>(valueProp);

const handleClickBalance = () => {
const newValue = Number(balance);
setValue(newValue);
triggerChangeEvent(inputRef, newValue);
};

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

const TokenField = React.forwardRef<HTMLInputElement, TokenFieldProps>(
({ tokenSymbol, valueInUSD, balance, ...rest }, ref): JSX.Element => {
const endAdornment = (
<TokenAdornment>
<TokenFieldSymbol>{tokenSymbol}</TokenFieldSymbol>
Expand All @@ -30,17 +62,30 @@ const TokenField = React.forwardRef<HTMLInputElement, TokenFieldProps>(
);

return (
<Stack spacing='half'>
{balance ? (
<TokenBalance tokenSymbol={tokenSymbol} value={balance.value} valueInUSD={balance.valueInUSD} />
) : null}
<Stack spacing='half' className={className} style={style} hidden={hidden}>
{balance && balanceInUSD && (
<StyledTokenBalance
tokenSymbol={tokenSymbol}
value={balance}
valueInUSD={balanceInUSD}
onClickBalance={handleClickBalance}
isDisabled={isDisabled}
/>
)}
<TokenFieldInnerWrapper>
<TokenFieldInput ref={ref} endAdornment={endAdornment} {...rest} />
<TokenFieldInput
ref={inputRef}
endAdornment={endAdornment}
value={value}
isDisabled={isDisabled}
{...props}
/>
</TokenFieldInnerWrapper>
</Stack>
);
}
);

TokenField.displayName = 'TokenField';

export { TokenField };
Expand Down
16 changes: 16 additions & 0 deletions src/component-library/utils/input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { RefObject } from 'react';

/**
* @param {RefObject<HTMLInputElement>} ref - input ref.
* @param {string | ReadonlyArray<string> | number} value - value to be included in the event
* @return {void} - Manually emits onChange event
*/
const triggerChangeEvent = (ref: RefObject<HTMLInputElement>, value: string | ReadonlyArray<string> | number): void => {
const setValue = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
setValue?.call(ref.current, value);

const e = new Event('input', { bubbles: true });
ref.current?.dispatchEvent(e);
};

export { triggerChangeEvent };

2 comments on commit 163b874

@vercel
Copy link

@vercel vercel bot commented on 163b874 Sep 14, 2022

Choose a reason for hiding this comment

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

@vercel
Copy link

@vercel vercel bot commented on 163b874 Sep 14, 2022

Choose a reason for hiding this comment

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

Please sign in to comment.