diff --git a/src/components/Swap/Input.tsx b/src/components/Swap/Input.tsx index b2d086f8d..434cd2182 100644 --- a/src/components/Swap/Input.tsx +++ b/src/components/Swap/Input.tsx @@ -2,26 +2,20 @@ import { Trans } from '@lingui/macro' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { TextButton } from 'components/Button' import { loadingTransitionCss } from 'css/loading' -import { - useIsSwapFieldIndependent, - useSwapAmount, - useSwapCurrency, - useSwapCurrencyAmount, - useSwapInfo, -} from 'hooks/swap' +import { useIsSwapFieldIndependent, useSwapAmount, useSwapCurrency, useSwapInfo } from 'hooks/swap' +import { useIsWrap } from 'hooks/swap/useWrapCallback' import { usePrefetchCurrencyColor } from 'hooks/useCurrencyColor' -import { useCallback, useMemo, useState } from 'react' +import { PriceImpact } from 'hooks/usePriceImpact' +import { MouseEvent, useCallback, useMemo, useRef, useState } from 'react' import { TradeState } from 'state/routing/types' import { Field } from 'state/swap' import styled from 'styled-components/macro' import { ThemedText } from 'theme' -import invariant from 'tiny-invariant' import { formatCurrencyAmount } from 'utils/formatCurrencyAmount' import { maxAmountSpend } from 'utils/maxAmountSpend' import Column from '../Column' import Row from '../Row' -import TokenImg from '../TokenImg' import TokenInput, { TokenInputHandle } from './TokenInput' export const USDC = styled(Row)` @@ -32,33 +26,48 @@ export const Balance = styled(ThemedText.Body2)` transition: color 0.25s ease-in-out; ` -export const InputColumn = styled(Column)<{ approved?: boolean; hasColor?: boolean | null }>` +const InputColumn = styled(Column)<{ approved?: boolean; disableHover?: boolean }>` background-color: ${({ theme }) => theme.module}; border-radius: ${({ theme }) => theme.borderRadius - 0.25}em; margin-bottom: 4px; padding: 20px 0 24px 0; position: relative; - // Set transitions to reduce color flashes when switching color/token. - // When color loads, transition the background so that it transitions from the empty or last state, but not _to_ the empty state. - transition: ${({ hasColor }) => (hasColor ? 'background-color 0.25s ease-out' : undefined)}; - > { - // When color is loading, delay the color/stroke so that it seems to transition from the last state. - transition: ${({ hasColor }) => (hasColor === null ? 'color 0.25s ease-in, stroke 0.25s ease-in' : undefined)}; + &:before { + background-size: 100%; + border: 1px solid ${({ theme }) => theme.module}; + border-radius: inherit; + + box-sizing: border-box; + content: ''; + height: 100%; + + left: 0; + pointer-events: none; + position: absolute; + top: 0; + transition: 125ms ease border-color; + width: 100%; } - ${TokenImg} { - filter: ${({ approved }) => (approved ? undefined : 'saturate(0) opacity(0.4)')}; - transition: filter 0.25s; - } + ${({ theme, disableHover }) => + !disableHover && + ` &:hover:before { + border-color: ${theme.interactive}; + } + + &:focus-within:before { + border-color: ${theme.outline}; + }`} ` -interface UseFormattedFieldAmountArguments { +export function useFormattedFieldAmount({ + currencyAmount, + fieldAmount, +}: { currencyAmount?: CurrencyAmount fieldAmount?: string -} - -export function useFormattedFieldAmount({ currencyAmount, fieldAmount }: UseFormattedFieldAmountArguments) { +}) { return useMemo(() => { if (fieldAmount !== undefined) { return fieldAmount @@ -70,78 +79,85 @@ export function useFormattedFieldAmount({ currencyAmount, fieldAmount }: UseForm }, [currencyAmount, fieldAmount]) } -export default function Input() { +interface InputWrapperProps { + field: Field + impact?: PriceImpact + maxAmount?: string + isSufficientBalance?: boolean +} + +export function InputWrapper({ + field, + impact, + maxAmount, + isSufficientBalance, + className, +}: InputWrapperProps & { className?: string }) { const { - [Field.INPUT]: { balance, amount: tradeCurrencyAmount, usdc }, + [field]: { balance, amount: currencyAmount, usdc }, error, trade: { state: tradeState }, } = useSwapInfo() - const [inputAmount, updateInputAmount] = useSwapAmount(Field.INPUT) - const [inputCurrency, updateInputCurrency] = useSwapCurrency(Field.INPUT) - const inputCurrencyAmount = useSwapCurrencyAmount(Field.INPUT) + const [amount, updateAmount] = useSwapAmount(field) + const [currency, updateCurrency] = useSwapCurrency(field) + + const wrapper = useRef(null) const [input, setInput] = useState(null) + const onClick = useCallback( + (event: MouseEvent) => { + if (event.target === wrapper.current) { + input?.focus() + } + }, + [input] + ) // extract eagerly in case of reversal - usePrefetchCurrencyColor(inputCurrency) + usePrefetchCurrencyColor(currency) const isDisabled = error !== undefined const isRouteLoading = isDisabled || tradeState === TradeState.LOADING const isDependentField = !useIsSwapFieldIndependent(Field.INPUT) const isLoading = isRouteLoading && isDependentField - const amount = useFormattedFieldAmount({ - currencyAmount: tradeCurrencyAmount, - fieldAmount: inputAmount, - }) - - //TODO(ianlapham): mimic logic from app swap page - const mockApproved = true - - const insufficientBalance = useMemo( - () => - balance && - (inputCurrencyAmount ? inputCurrencyAmount.greaterThan(balance) : tradeCurrencyAmount?.greaterThan(balance)), - [balance, inputCurrencyAmount, tradeCurrencyAmount] - ) + const isWrap = useIsWrap() + const formattedAmount = useMemo(() => { + if (amount !== undefined) return amount + if (!currencyAmount) return '' + return isWrap ? currencyAmount.toExact() : formatCurrencyAmount({ amount: currencyAmount }) + }, [amount, currencyAmount, isWrap]) - const max = useMemo(() => { - // account for gas needed if using max on native token - const max = maxAmountSpend(balance) - if (!max || !balance) return - if (max.equalTo(0) || balance.lessThan(max)) return - if (inputCurrencyAmount && max.equalTo(inputCurrencyAmount)) return - return max.toExact() - }, [balance, inputCurrencyAmount]) const onClickMax = useCallback(() => { - invariant(max) - updateInputAmount(max) + if (!maxAmount) return + updateAmount(maxAmount) input?.focus() - }, [input, max, updateInputAmount]) + }, [input, maxAmount, updateAmount]) return ( - + {usdc && `${formatCurrencyAmount({ amount: usdc, isUsdPrice: true })}`} + {impact && ({impact.toString()})} {balance && ( - + Balance: {formatCurrencyAmount({ amount: balance })} - {max && ( + {maxAmount && ( Max @@ -156,3 +172,25 @@ export default function Input() { ) } + +export default function Input() { + const { + [Field.INPUT]: { balance, amount: currencyAmount }, + } = useSwapInfo() + + const isSufficientBalance = useMemo(() => { + if (!balance || !currencyAmount) return undefined + return !currencyAmount.greaterThan(balance) + }, [balance, currencyAmount]) + + const maxAmount = useMemo(() => { + // account for gas needed if using max on native token + const max = maxAmountSpend(balance) + if (!max || !balance) return + if (max.equalTo(0) || balance.lessThan(max)) return + if (currencyAmount && max.equalTo(currencyAmount)) return + return max.toExact() + }, [balance, currencyAmount]) + + return +} diff --git a/src/components/Swap/Output.tsx b/src/components/Swap/Output.tsx index 85210b6f8..016522b0c 100644 --- a/src/components/Swap/Output.tsx +++ b/src/components/Swap/Output.tsx @@ -1,82 +1,44 @@ -import { useIsSwapFieldIndependent, useSwapAmount, useSwapCurrency, useSwapInfo } from 'hooks/swap' +import { useSwapCurrency, useSwapInfo } from 'hooks/swap' import useCurrencyColor from 'hooks/useCurrencyColor' import { atom } from 'jotai' import { useAtomValue } from 'jotai/utils' -import { TradeState } from 'state/routing/types' import { Field } from 'state/swap' import styled from 'styled-components/macro' -import { DynamicThemeProvider, ThemedText } from 'theme' -import { formatCurrencyAmount } from 'utils/formatCurrencyAmount' +import { DynamicThemeProvider } from 'theme' -import Row from '../Row' -import { Balance, InputColumn, USDC, useFormattedFieldAmount } from './Input' -import TokenInput from './TokenInput' +import { InputWrapper } from './Input' export const colorAtom = atom(undefined) -const StyledInputColumn = styled(InputColumn)` +const OutputWrapper = styled(InputWrapper)<{ hasColor?: boolean | null }>` border-bottom: 1px solid ${({ theme }) => theme.container}; border-bottom-left-radius: 0; border-bottom-right-radius: 0; margin-bottom: 0; padding: 24px 0 20px 0; + + // Set transitions to reduce color flashes when switching color/token. + // When color loads, transition the background so that it transitions from the empty or last state, but not _to_ the empty state. + transition: ${({ hasColor }) => (hasColor ? 'background-color 0.25s ease-out' : undefined)}; + > { + // When color is loading, delay the color/stroke so that it seems to transition from the last state. + transition: ${({ hasColor }) => (hasColor === null ? 'color 0.25s ease-in, stroke 0.25s ease-in' : undefined)}; + } ` export default function Output() { - const { - [Field.OUTPUT]: { balance, amount: outputCurrencyAmount, usdc: outputUSDC }, - error, - trade: { state: tradeState }, - impact, - } = useSwapInfo() - - const [swapOutputAmount, updateSwapOutputAmount] = useSwapAmount(Field.OUTPUT) - const [swapOutputCurrency, updateSwapOutputCurrency] = useSwapCurrency(Field.OUTPUT) - - const isDisabled = error !== undefined - const isRouteLoading = isDisabled || tradeState === TradeState.LOADING - const isDependentField = !useIsSwapFieldIndependent(Field.OUTPUT) - const isLoading = isRouteLoading && isDependentField + const { impact } = useSwapInfo() + const [currency] = useSwapCurrency(Field.OUTPUT) const overrideColor = useAtomValue(colorAtom) - const dynamicColor = useCurrencyColor(swapOutputCurrency) + const dynamicColor = useCurrencyColor(currency) const color = overrideColor || dynamicColor - // different state true/null/false allow smoother color transition - const hasColor = swapOutputCurrency ? Boolean(color) || null : false - - const amount = useFormattedFieldAmount({ - currencyAmount: outputCurrencyAmount, - fieldAmount: swapOutputAmount, - }) + const hasColor = currency ? Boolean(color) || null : false return ( - - - - - - {outputUSDC && `${formatCurrencyAmount({ amount: outputUSDC, isUsdPrice: true })} `} - {impact && ({impact.toString()})} - - {balance && ( - - Balance: {formatCurrencyAmount({ amount: balance })} - - )} - - - - + ) } diff --git a/src/components/Swap/TokenInput.tsx b/src/components/Swap/TokenInput.tsx index e05182018..94a893850 100644 --- a/src/components/Swap/TokenInput.tsx +++ b/src/components/Swap/TokenInput.tsx @@ -21,14 +21,6 @@ const ValueInput = styled(DecimalInput)` height: 1.5em; margin: -0.25em 0; - :hover:not(:focus-within) { - color: ${({ theme }) => theme.onHover(theme.primary)}; - } - - :hover:not(:focus-within)::placeholder { - color: ${({ theme }) => theme.onHover(theme.secondary)}; - } - ${loadingTransitionCss} ` diff --git a/src/hooks/swap/index.ts b/src/hooks/swap/index.ts index 08db1e23c..1f687b9a1 100644 --- a/src/hooks/swap/index.ts +++ b/src/hooks/swap/index.ts @@ -1,11 +1,10 @@ -import { Currency, CurrencyAmount } from '@uniswap/sdk-core' +import { Currency } from '@uniswap/sdk-core' import { useAtom } from 'jotai' import { useAtomValue, useUpdateAtom } from 'jotai/utils' import { useCallback, useMemo } from 'react' import { pickAtom } from 'state/atoms' import { Field, swapAtom, swapEventHandlersAtom } from 'state/swap' import { invertTradeType, toTradeType } from 'utils/tradeType' -import tryParseCurrencyAmount from 'utils/tryParseCurrencyAmount' export { ChainError, default as useSwapInfo } from './useSwapInfo' function otherField(field: Field) { @@ -89,15 +88,3 @@ export function useSwapAmount(field: Field): [string | undefined, (amount: strin ) return [amount, updateAmount] } - -export function useSwapCurrencyAmount(field: Field): CurrencyAmount | undefined { - const isFieldIndependent = useIsSwapFieldIndependent(field) - const isAmountPopulated = useIsAmountPopulated() - const [swapAmount] = useSwapAmount(field) - const [swapCurrency] = useSwapCurrency(field) - const currencyAmount = useMemo(() => tryParseCurrencyAmount(swapAmount, swapCurrency), [swapAmount, swapCurrency]) - if (isFieldIndependent && isAmountPopulated) { - return currencyAmount - } - return -}