Skip to content

Commit

Permalink
feat: widget loading animations polish (#3232)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ianlapham authored Feb 7, 2022
1 parent c595ba9 commit bb27b7a
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 11 deletions.
20 changes: 18 additions & 2 deletions src/lib/components/Swap/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import { Trans } from '@lingui/macro'
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 { 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 { useMemo } from 'react'
import { TradeState } from 'state/routing/types'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'

import Column from '../Column'
import Row from '../Row'
import TokenImg from '../TokenImg'
import TokenInput from './TokenInput'

const LoadingSpan = styled.span<{ $loading: boolean }>`
${loadingOpacityCss};
`

const InputColumn = styled(Column)<{ approved?: boolean }>`
margin: 0.75em;
position: relative;
Expand All @@ -28,6 +35,7 @@ interface InputProps {

export default function Input({ disabled }: InputProps) {
const {
trade: { state: tradeState },
currencyBalances: { [Field.INPUT]: balance },
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount },
} = useSwapInfo()
Expand All @@ -39,6 +47,13 @@ export default function Input({ disabled }: InputProps) {
// extract eagerly in case of reversal
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
const mockApproved = true

Expand All @@ -63,10 +78,11 @@ export default function Input({ disabled }: InputProps) {
onMax={onMax}
onChangeInput={updateSwapInputAmount}
onChangeCurrency={updateSwapInputCurrency}
loading={isLoading}
>
<ThemedText.Body2 color="secondary">
<Row>
<span>{inputUSDC ? `$${inputUSDC.toFixed(2)}` : '-'}</span>
<LoadingSpan $loading={isLoading}>{inputUSDC ? `$${inputUSDC.toFixed(2)}` : '-'}</LoadingSpan>
{balance && (
<ThemedText.Body2 color={inputCurrencyAmount?.greaterThan(balance) ? 'error' : undefined}>
Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4)}</span>
Expand Down
21 changes: 19 additions & 2 deletions src/lib/components/Swap/Output.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { useUSDCValue } from 'hooks/useUSDCPrice'
import { atom } from 'jotai'
import { useAtomValue } from 'jotai/utils'
import BrandedFooter from 'lib/components/BrandedFooter'
import { loadingOpacityCss } from 'lib/css/loading'
import { useSwapAmount, useSwapCurrency, useSwapInfo } from 'lib/hooks/swap'
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 { ReactNode, useMemo } from 'react'
import { TradeState } from 'state/routing/types'
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'

Expand All @@ -17,6 +19,10 @@ import TokenInput from './TokenInput'

export const colorAtom = atom<string | undefined>(undefined)

const LoadingSpan = styled.span<{ $loading: boolean }>`
${loadingOpacityCss};
`

const OutputColumn = styled(Column)<{ hasColor: boolean | null }>`
background-color: ${({ theme }) => theme.module};
border-radius: ${({ theme }) => theme.borderRadius - 0.25}em;
Expand All @@ -40,13 +46,23 @@ interface OutputProps {

export default function Output({ disabled, children }: OutputProps) {
const {
trade: { state: tradeState },
currencyBalances: { [Field.OUTPUT]: balance },
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount },
} = useSwapInfo()

const [swapOutputAmount, updateSwapOutputAmount] = useSwapAmount(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 dynamicColor = useCurrencyColor(swapOutputCurrency)
const color = overrideColor || dynamicColor
Expand Down Expand Up @@ -83,10 +99,11 @@ export default function Output({ disabled, children }: OutputProps) {
disabled={disabled}
onChangeInput={updateSwapOutputAmount}
onChangeCurrency={updateSwapOutputCurrency}
loading={isLoading}
>
<ThemedText.Body2 color="secondary">
<Row>
<span>{usdc}</span>
<LoadingSpan $loading={isLoading}>{usdc}</LoadingSpan>
{balance && (
<span>
Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4)}</span>
Expand Down
9 changes: 8 additions & 1 deletion src/lib/components/Swap/TokenInput.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core'
import { loadingOpacityCss } from 'lib/css/loading'
import styled, { keyframes, ThemedText } from 'lib/theme'
import { FocusEvent, ReactNode, useCallback, useRef, useState } from 'react'

Expand All @@ -13,7 +14,7 @@ const TokenInputRow = styled(Row)`
grid-template-columns: 1fr;
`

const ValueInput = styled(DecimalInput)`
const ValueInput = styled(DecimalInput)<{ $loading: boolean }>`
color: ${({ theme }) => theme.primary};
:hover:not(:focus-within) {
Expand All @@ -23,6 +24,8 @@ const ValueInput = styled(DecimalInput)`
:hover:not(:focus-within)::placeholder {
color: ${({ theme }) => theme.onHover(theme.secondary)};
}
${loadingOpacityCss}
`

const delayedFadeIn = keyframes`
Expand Down Expand Up @@ -50,6 +53,7 @@ interface TokenInputProps {
onMax?: () => void
onChangeInput: (input: string) => void
onChangeCurrency: (currency: Currency) => void
loading?: boolean
children: ReactNode
}

Expand All @@ -60,6 +64,7 @@ export default function TokenInput({
onMax,
onChangeInput,
onChangeCurrency,
loading,
children,
}: TokenInputProps) {
const max = useRef<HTMLButtonElement>(null)
Expand All @@ -70,6 +75,7 @@ export default function TokenInput({
setShowMax(false)
}
}, [])

return (
<Column gap={0.25}>
<TokenInputRow gap={0.5} onBlur={onBlur}>
Expand All @@ -79,6 +85,7 @@ export default function TokenInput({
onFocus={onFocus}
onChange={onChangeInput}
disabled={disabled || !currency}
$loading={Boolean(loading)}
></ValueInput>
</ThemedText.H2>
{showMax && (
Expand Down
8 changes: 8 additions & 0 deletions src/lib/css/loading.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
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;
`
14 changes: 8 additions & 6 deletions src/lib/hooks/swap/useBestTrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,15 @@ export function useBestTrade(

const debouncing =
(amountSpecified && debouncedAmount && amountSpecified !== debouncedAmount) ||
(debouncedOtherCurrency && otherCurrency && debouncedOtherCurrency !== otherCurrency)
(amountSpecified && debouncedOtherCurrency && otherCurrency && debouncedOtherCurrency !== otherCurrency)

const syncing = isTradeDebouncing({
amounts: [amountFromLatestTrade, debouncedAmount],
indepdenentCurrencies: [currencyFromTrade, debouncedOtherCurrency],
dependentCurrencies: [otherCurrencyFromTrade, otherCurrency],
})
const syncing =
amountSpecified &&
isTradeDebouncing({
amounts: [amountFromLatestTrade, amountSpecified],
indepdenentCurrencies: [currencyFromTrade, amountSpecified?.currency],
dependentCurrencies: [otherCurrencyFromTrade, debouncedOtherCurrency],
})

const useFallback = !syncing && clientSORTrade.state === TradeState.NO_ROUTE_FOUND

Expand Down

0 comments on commit bb27b7a

Please sign in to comment.