Commit bb27b7a2 authored by Ian Lapham's avatar Ian Lapham Committed by GitHub

feat: widget loading animations polish (#3232)

* create use best trade hook for widgets

* update comment in hook file

* add loading states to input / output fields

* update to not use imports from app

* remove custom loading component

* update var name and syncing detection logic

* fix USD div type

* simplify loading css, small changes
parent c595ba95
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { useUSDCValue } from 'hooks/useUSDCPrice' import { useUSDCValue } from 'hooks/useUSDCPrice'
import { useAtomValue } from 'jotai/utils'
import { loadingOpacityCss } from 'lib/css/loading'
import { useSwapAmount, useSwapCurrency, useSwapInfo } from 'lib/hooks/swap' import { useSwapAmount, useSwapCurrency, useSwapInfo } from 'lib/hooks/swap'
import { usePrefetchCurrencyColor } from 'lib/hooks/useCurrencyColor' import { usePrefetchCurrencyColor } from 'lib/hooks/useCurrencyColor'
import { Field } from 'lib/state/swap' import { Field, independentFieldAtom } from 'lib/state/swap'
import styled, { ThemedText } from 'lib/theme' import styled, { ThemedText } from 'lib/theme'
import { useMemo } from 'react' import { useMemo } from 'react'
import { TradeState } from 'state/routing/types'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount' import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import Column from '../Column' import Column from '../Column'
...@@ -12,6 +15,10 @@ import Row from '../Row' ...@@ -12,6 +15,10 @@ import Row from '../Row'
import TokenImg from '../TokenImg' import TokenImg from '../TokenImg'
import TokenInput from './TokenInput' import TokenInput from './TokenInput'
const LoadingSpan = styled.span<{ $loading: boolean }>`
${loadingOpacityCss};
`
const InputColumn = styled(Column)<{ approved?: boolean }>` const InputColumn = styled(Column)<{ approved?: boolean }>`
margin: 0.75em; margin: 0.75em;
position: relative; position: relative;
...@@ -28,6 +35,7 @@ interface InputProps { ...@@ -28,6 +35,7 @@ interface InputProps {
export default function Input({ disabled }: InputProps) { export default function Input({ disabled }: InputProps) {
const { const {
trade: { state: tradeState },
currencyBalances: { [Field.INPUT]: balance }, currencyBalances: { [Field.INPUT]: balance },
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount }, currencyAmounts: { [Field.INPUT]: inputCurrencyAmount },
} = useSwapInfo() } = useSwapInfo()
...@@ -39,6 +47,13 @@ export default function Input({ disabled }: InputProps) { ...@@ -39,6 +47,13 @@ export default function Input({ disabled }: InputProps) {
// extract eagerly in case of reversal // extract eagerly in case of reversal
usePrefetchCurrencyColor(swapInputCurrency) usePrefetchCurrencyColor(swapInputCurrency)
const isTradeLoading = useMemo(
() => TradeState.LOADING === tradeState || TradeState.SYNCING === tradeState,
[tradeState]
)
const isDependentField = useAtomValue(independentFieldAtom) !== Field.INPUT
const isLoading = isDependentField && isTradeLoading
//TODO(ianlapham): mimic logic from app swap page //TODO(ianlapham): mimic logic from app swap page
const mockApproved = true const mockApproved = true
...@@ -63,10 +78,11 @@ export default function Input({ disabled }: InputProps) { ...@@ -63,10 +78,11 @@ export default function Input({ disabled }: InputProps) {
onMax={onMax} onMax={onMax}
onChangeInput={updateSwapInputAmount} onChangeInput={updateSwapInputAmount}
onChangeCurrency={updateSwapInputCurrency} onChangeCurrency={updateSwapInputCurrency}
loading={isLoading}
> >
<ThemedText.Body2 color="secondary"> <ThemedText.Body2 color="secondary">
<Row> <Row>
<span>{inputUSDC ? `$${inputUSDC.toFixed(2)}` : '-'}</span> <LoadingSpan $loading={isLoading}>{inputUSDC ? `$${inputUSDC.toFixed(2)}` : '-'}</LoadingSpan>
{balance && ( {balance && (
<ThemedText.Body2 color={inputCurrencyAmount?.greaterThan(balance) ? 'error' : undefined}> <ThemedText.Body2 color={inputCurrencyAmount?.greaterThan(balance) ? 'error' : undefined}>
Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4)}</span> Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4)}</span>
......
...@@ -3,11 +3,13 @@ import { useUSDCValue } from 'hooks/useUSDCPrice' ...@@ -3,11 +3,13 @@ import { useUSDCValue } from 'hooks/useUSDCPrice'
import { atom } from 'jotai' import { atom } from 'jotai'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import BrandedFooter from 'lib/components/BrandedFooter' import BrandedFooter from 'lib/components/BrandedFooter'
import { loadingOpacityCss } from 'lib/css/loading'
import { useSwapAmount, useSwapCurrency, useSwapInfo } from 'lib/hooks/swap' import { useSwapAmount, useSwapCurrency, useSwapInfo } from 'lib/hooks/swap'
import useCurrencyColor from 'lib/hooks/useCurrencyColor' import useCurrencyColor from 'lib/hooks/useCurrencyColor'
import { Field } from 'lib/state/swap' import { Field, independentFieldAtom } from 'lib/state/swap'
import styled, { DynamicThemeProvider, ThemedText } from 'lib/theme' import styled, { DynamicThemeProvider, ThemedText } from 'lib/theme'
import { ReactNode, useMemo } from 'react' import { ReactNode, useMemo } from 'react'
import { TradeState } from 'state/routing/types'
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact' import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount' import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
...@@ -17,6 +19,10 @@ import TokenInput from './TokenInput' ...@@ -17,6 +19,10 @@ import TokenInput from './TokenInput'
export const colorAtom = atom<string | undefined>(undefined) export const colorAtom = atom<string | undefined>(undefined)
const LoadingSpan = styled.span<{ $loading: boolean }>`
${loadingOpacityCss};
`
const OutputColumn = styled(Column)<{ hasColor: boolean | null }>` const OutputColumn = styled(Column)<{ hasColor: boolean | null }>`
background-color: ${({ theme }) => theme.module}; background-color: ${({ theme }) => theme.module};
border-radius: ${({ theme }) => theme.borderRadius - 0.25}em; border-radius: ${({ theme }) => theme.borderRadius - 0.25}em;
...@@ -40,6 +46,7 @@ interface OutputProps { ...@@ -40,6 +46,7 @@ interface OutputProps {
export default function Output({ disabled, children }: OutputProps) { export default function Output({ disabled, children }: OutputProps) {
const { const {
trade: { state: tradeState },
currencyBalances: { [Field.OUTPUT]: balance }, currencyBalances: { [Field.OUTPUT]: balance },
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount }, currencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount },
} = useSwapInfo() } = useSwapInfo()
...@@ -47,6 +54,15 @@ export default function Output({ disabled, children }: OutputProps) { ...@@ -47,6 +54,15 @@ export default function Output({ disabled, children }: OutputProps) {
const [swapOutputAmount, updateSwapOutputAmount] = useSwapAmount(Field.OUTPUT) const [swapOutputAmount, updateSwapOutputAmount] = useSwapAmount(Field.OUTPUT)
const [swapOutputCurrency, updateSwapOutputCurrency] = useSwapCurrency(Field.OUTPUT) const [swapOutputCurrency, updateSwapOutputCurrency] = useSwapCurrency(Field.OUTPUT)
//loading status of the trade
const isTradeLoading = useMemo(
() => TradeState.LOADING === tradeState || TradeState.SYNCING === tradeState,
[tradeState]
)
const isDependentField = useAtomValue(independentFieldAtom) !== Field.OUTPUT
const isLoading = isDependentField && isTradeLoading
const overrideColor = useAtomValue(colorAtom) const overrideColor = useAtomValue(colorAtom)
const dynamicColor = useCurrencyColor(swapOutputCurrency) const dynamicColor = useCurrencyColor(swapOutputCurrency)
const color = overrideColor || dynamicColor const color = overrideColor || dynamicColor
...@@ -83,10 +99,11 @@ export default function Output({ disabled, children }: OutputProps) { ...@@ -83,10 +99,11 @@ export default function Output({ disabled, children }: OutputProps) {
disabled={disabled} disabled={disabled}
onChangeInput={updateSwapOutputAmount} onChangeInput={updateSwapOutputAmount}
onChangeCurrency={updateSwapOutputCurrency} onChangeCurrency={updateSwapOutputCurrency}
loading={isLoading}
> >
<ThemedText.Body2 color="secondary"> <ThemedText.Body2 color="secondary">
<Row> <Row>
<span>{usdc}</span> <LoadingSpan $loading={isLoading}>{usdc}</LoadingSpan>
{balance && ( {balance && (
<span> <span>
Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4)}</span> Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4)}</span>
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core' import { Currency } from '@uniswap/sdk-core'
import { loadingOpacityCss } from 'lib/css/loading'
import styled, { keyframes, ThemedText } from 'lib/theme' import styled, { keyframes, ThemedText } from 'lib/theme'
import { FocusEvent, ReactNode, useCallback, useRef, useState } from 'react' import { FocusEvent, ReactNode, useCallback, useRef, useState } from 'react'
...@@ -13,7 +14,7 @@ const TokenInputRow = styled(Row)` ...@@ -13,7 +14,7 @@ const TokenInputRow = styled(Row)`
grid-template-columns: 1fr; grid-template-columns: 1fr;
` `
const ValueInput = styled(DecimalInput)` const ValueInput = styled(DecimalInput)<{ $loading: boolean }>`
color: ${({ theme }) => theme.primary}; color: ${({ theme }) => theme.primary};
:hover:not(:focus-within) { :hover:not(:focus-within) {
...@@ -23,6 +24,8 @@ const ValueInput = styled(DecimalInput)` ...@@ -23,6 +24,8 @@ const ValueInput = styled(DecimalInput)`
:hover:not(:focus-within)::placeholder { :hover:not(:focus-within)::placeholder {
color: ${({ theme }) => theme.onHover(theme.secondary)}; color: ${({ theme }) => theme.onHover(theme.secondary)};
} }
${loadingOpacityCss}
` `
const delayedFadeIn = keyframes` const delayedFadeIn = keyframes`
...@@ -50,6 +53,7 @@ interface TokenInputProps { ...@@ -50,6 +53,7 @@ interface TokenInputProps {
onMax?: () => void onMax?: () => void
onChangeInput: (input: string) => void onChangeInput: (input: string) => void
onChangeCurrency: (currency: Currency) => void onChangeCurrency: (currency: Currency) => void
loading?: boolean
children: ReactNode children: ReactNode
} }
...@@ -60,6 +64,7 @@ export default function TokenInput({ ...@@ -60,6 +64,7 @@ export default function TokenInput({
onMax, onMax,
onChangeInput, onChangeInput,
onChangeCurrency, onChangeCurrency,
loading,
children, children,
}: TokenInputProps) { }: TokenInputProps) {
const max = useRef<HTMLButtonElement>(null) const max = useRef<HTMLButtonElement>(null)
...@@ -70,6 +75,7 @@ export default function TokenInput({ ...@@ -70,6 +75,7 @@ export default function TokenInput({
setShowMax(false) setShowMax(false)
} }
}, []) }, [])
return ( return (
<Column gap={0.25}> <Column gap={0.25}>
<TokenInputRow gap={0.5} onBlur={onBlur}> <TokenInputRow gap={0.5} onBlur={onBlur}>
...@@ -79,6 +85,7 @@ export default function TokenInput({ ...@@ -79,6 +85,7 @@ export default function TokenInput({
onFocus={onFocus} onFocus={onFocus}
onChange={onChangeInput} onChange={onChangeInput}
disabled={disabled || !currency} disabled={disabled || !currency}
$loading={Boolean(loading)}
></ValueInput> ></ValueInput>
</ThemedText.H2> </ThemedText.H2>
{showMax && ( {showMax && (
......
import { css } from 'lib/theme'
// need to use $loading as `loading` is a reserved prop
export const loadingOpacityCss = css<{ $loading: boolean }>`
filter: ${({ $loading }) => ($loading ? 'grayscale(1)' : 'none')};
opacity: ${({ $loading }) => ($loading ? '0.4' : '1')};
transition: opacity 0.2s ease-in-out;
`
...@@ -78,13 +78,15 @@ export function useBestTrade( ...@@ -78,13 +78,15 @@ export function useBestTrade(
const debouncing = const debouncing =
(amountSpecified && debouncedAmount && amountSpecified !== debouncedAmount) || (amountSpecified && debouncedAmount && amountSpecified !== debouncedAmount) ||
(debouncedOtherCurrency && otherCurrency && debouncedOtherCurrency !== otherCurrency) (amountSpecified && debouncedOtherCurrency && otherCurrency && debouncedOtherCurrency !== otherCurrency)
const syncing = isTradeDebouncing({ const syncing =
amounts: [amountFromLatestTrade, debouncedAmount], amountSpecified &&
indepdenentCurrencies: [currencyFromTrade, debouncedOtherCurrency], isTradeDebouncing({
dependentCurrencies: [otherCurrencyFromTrade, otherCurrency], amounts: [amountFromLatestTrade, amountSpecified],
}) indepdenentCurrencies: [currencyFromTrade, amountSpecified?.currency],
dependentCurrencies: [otherCurrencyFromTrade, debouncedOtherCurrency],
})
const useFallback = !syncing && clientSORTrade.state === TradeState.NO_ROUTE_FOUND const useFallback = !syncing && clientSORTrade.state === TradeState.NO_ROUTE_FOUND
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment