diff --git a/src/component-library/TokenBalance/TokenBalance.style.tsx b/src/component-library/TokenBalance/TokenBalance.style.tsx index 9e45298a00..6b0f89255a 100644 --- a/src/component-library/TokenBalance/TokenBalance.style.tsx +++ b/src/component-library/TokenBalance/TokenBalance.style.tsx @@ -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}; `; @@ -12,8 +12,13 @@ const TokenBalanceLabel = styled.dt` color: ${theme.colors.textPrimary}; `; -const TokenBalanceValue = styled.dd` +type TokenBalanceValueProps = { + $clickable?: boolean; +}; + +const TokenBalanceValue = styled.span` color: ${theme.colors.textSecondary}; + cursor: ${(props) => props.$clickable && 'pointer'}; `; export { TokenBalanceLabel, TokenBalanceValue, TokenBalanceWrapper }; diff --git a/src/component-library/TokenBalance/TokenBalance.tsx b/src/component-library/TokenBalance/TokenBalance.tsx index 9b783ded36..168bbc2737 100644 --- a/src/component-library/TokenBalance/TokenBalance.tsx +++ b/src/component-library/TokenBalance/TokenBalance.tsx @@ -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 ( - + Balance: - - {value} {tokenSymbol} ({valueInUSD}) - +
+ + {value} {tokenSymbol} ({valueInUSD}) + +
); }; + export { TokenBalance }; export type { TokenBalanceProps }; diff --git a/src/component-library/TokenField/TokenField.stories.tsx b/src/component-library/TokenField/TokenField.stories.tsx index c3db745792..9bd14b3dc4 100644 --- a/src/component-library/TokenField/TokenField.stories.tsx +++ b/src/component-library/TokenField/TokenField.stories.tsx @@ -4,17 +4,16 @@ import { formatUSD } from '@/common/utils/utils'; import { TokenField, TokenFieldProps } from '.'; -const Template: Story = (args) => ; +const Template: Story = (args) => ; 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({}); diff --git a/src/component-library/TokenField/TokenField.style.tsx b/src/component-library/TokenField/TokenField.style.tsx index ebf716f22f..961f395ecd 100644 --- a/src/component-library/TokenField/TokenField.style.tsx +++ b/src/component-library/TokenField/TokenField.style.tsx @@ -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; @@ -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 }; diff --git a/src/component-library/TokenField/TokenField.tsx b/src/component-library/TokenField/TokenField.tsx index 534ab2a2a9..a7e8ac1c5f 100644 --- a/src/component-library/TokenField/TokenField.tsx +++ b/src/component-library/TokenField/TokenField.tsx @@ -1,9 +1,11 @@ -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, @@ -11,17 +13,47 @@ import { 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; + +type TokenFieldProps = Props & InheritAttrs; + +const TokenField = forwardRef( + ( + { + tokenSymbol, + valueInUSD, + balance, + balanceInUSD, + value: valueProp, + isDisabled, + className, + style, + hidden, + ...props + }, + ref + ): JSX.Element => { + const inputRef = useDOMRef(ref); + const [value, setValue] = useState(valueProp); + + const handleClickBalance = () => { + const newValue = Number(balance); + setValue(newValue); + triggerChangeEvent(inputRef, newValue); + }; + + useEffect(() => { + if (valueProp === undefined) return; + setValue(valueProp); + }, [valueProp]); -const TokenField = React.forwardRef( - ({ tokenSymbol, valueInUSD, balance, ...rest }, ref): JSX.Element => { const endAdornment = ( {tokenSymbol} @@ -30,17 +62,30 @@ const TokenField = React.forwardRef( ); return ( - - {balance ? ( - - ) : null} + ); } ); + TokenField.displayName = 'TokenField'; export { TokenField }; diff --git a/src/component-library/utils/input.ts b/src/component-library/utils/input.ts new file mode 100644 index 0000000000..e338ade608 --- /dev/null +++ b/src/component-library/utils/input.ts @@ -0,0 +1,16 @@ +import { RefObject } from 'react'; + +/** + * @param {RefObject} ref - input ref. + * @param {string | ReadonlyArray | number} value - value to be included in the event + * @return {void} - Manually emits onChange event + */ +const triggerChangeEvent = (ref: RefObject, value: string | ReadonlyArray | 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 };