fix: initial transitions (#3719)

* fix: rm action fade

* fix: disallow stale swaps

* fix: fade in buttons

* fix: fade in input text

* fix: standardize border handling

* fix: transition token button width

* fix: cleanup transitions

* fix: use transition for button

* chore: cleanup
This commit is contained in:
Zach Pomerantz 2022-04-13 11:45:29 -07:00 committed by GitHub
parent cbe421ee23
commit f50bcbdb2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 84 additions and 32 deletions

@ -5,17 +5,7 @@ import { ReactNode, useMemo } from 'react'
import Button from './Button'
import Row from './Row'
const fadeIn = keyframes`
from {
opacity: 0;
}
to {
opacity: 1;
}
`
const StyledButton = styled(Button)`
animation: ${fadeIn} 0.25s ease-in;
border-radius: ${({ theme }) => theme.borderRadius * 0.75}em;
flex-grow: 1;
transition: background-color 0.25s ease-out, border-radius 0.25s ease-out, flex-grow 0.25s ease-out;

@ -1,5 +1,5 @@
import { Icon } from 'lib/icons'
import styled, { Color } from 'lib/theme'
import styled, { Color, css } from 'lib/theme'
import { ComponentProps, forwardRef } from 'react'
export const BaseButton = styled.button`
@ -15,17 +15,26 @@ export const BaseButton = styled.button`
margin: 0;
padding: 0;
:enabled {
transition: filter 0.125s linear;
}
:disabled {
cursor: initial;
filter: saturate(0) opacity(0.4);
}
`
const transitionCss = css`
transition: background-color 0.125s linear, border-color 0.125s linear, filter 0.125s linear;
`
export default styled(BaseButton)<{ color?: Color }>`
export default styled(BaseButton)<{ color?: Color; transition?: boolean }>`
border: 1px solid transparent;
color: ${({ color = 'interactive', theme }) => color === 'interactive' && theme.onInteractive};
:enabled {
background-color: ${({ color = 'interactive', theme }) => theme[color]};
${({ transition = true }) => transition && transitionCss};
}
:enabled:hover {
@ -33,9 +42,8 @@ export default styled(BaseButton)<{ color?: Color }>`
}
:disabled {
border: 1px solid ${({ theme }) => theme.outline};
border-color: ${({ theme }) => theme.outline};
color: ${({ theme }) => theme.secondary};
cursor: initial;
}
`

@ -1,4 +1,6 @@
import { loadingOpacity } from 'lib/css/loading'
import styled, { css } from 'lib/theme'
import { transparentize } from 'polished'
import { ChangeEvent, forwardRef, HTMLProps, useCallback } from 'react'
const Input = styled.input`
@ -34,6 +36,16 @@ const Input = styled.input`
::placeholder {
color: ${({ theme }) => theme.secondary};
}
:enabled {
transition: color 0.125s linear;
}
:disabled {
// Overrides WebKit's override of input:disabled color.
-webkit-text-fill-color: ${({ theme }) => transparentize(1 - loadingOpacity, theme.primary)};
color: ${({ theme }) => transparentize(1 - loadingOpacity, theme.primary)};
}
`
export default Input

@ -80,7 +80,7 @@ export default function Input({ disabled, focused }: InputProps) {
// extract eagerly in case of reversal
usePrefetchCurrencyColor(inputCurrency)
const isRouteLoading = tradeState === TradeState.SYNCING || tradeState === TradeState.LOADING
const isRouteLoading = disabled || tradeState === TradeState.SYNCING || tradeState === TradeState.LOADING
const isDependentField = !useIsSwapFieldIndependent(Field.INPUT)
const isLoading = isRouteLoading && isDependentField

@ -46,7 +46,7 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
const [swapOutputAmount, updateSwapOutputAmount] = useSwapAmount(Field.OUTPUT)
const [swapOutputCurrency, updateSwapOutputCurrency] = useSwapCurrency(Field.OUTPUT)
const isRouteLoading = tradeState === TradeState.SYNCING || tradeState === TradeState.LOADING
const isRouteLoading = disabled || tradeState === TradeState.SYNCING || tradeState === TradeState.LOADING
const isDependentField = !useIsSwapFieldIndependent(Field.OUTPUT)
const isLoading = isRouteLoading && isDependentField

@ -33,6 +33,7 @@ const Overlay = styled.div`
const StyledReverseButton = styled(Button)<{ turns: number }>`
border-radius: ${({ theme }) => theme.borderRadius * 0.75}em;
color: ${({ theme }) => theme.primary};
height: 2.5em;
position: relative;
width: 2.5em;

@ -14,6 +14,7 @@ import { TransactionType } from 'lib/state/transactions'
import { useTheme } from 'lib/theme'
import { isAnimating } from 'lib/utils/animations'
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { TradeState } from 'state/routing/types'
import invariant from 'tiny-invariant'
import ActionButton, { ActionButtonProps } from '../../ActionButton'
@ -152,13 +153,17 @@ export default memo(function SwapButton({ disabled }: SwapButtonProps) {
if (disableSwap) {
return { disabled: true }
} else if (wrapType === WrapType.NONE) {
return approvalAction ? { action: approvalAction } : { onClick: () => setOpen(true) }
return approvalAction
? { action: approvalAction }
: trade.state === TradeState.VALID
? { onClick: () => setOpen(true) }
: { disabled: true }
} else {
return isPending
? { action: { message: <Trans>Confirm in your wallet</Trans>, icon: Spinner } }
: { onClick: onWrap }
}
}, [approvalAction, disableSwap, isPending, onWrap, wrapType])
}, [approvalAction, disableSwap, isPending, onWrap, trade.state, wrapType])
const Label = useCallback(() => {
switch (wrapType) {
case WrapType.UNWRAP:

@ -1,29 +1,33 @@
import { Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core'
import { ChevronDown } from 'lib/icons'
import styled, { ThemedText } from 'lib/theme'
import { useMemo } from 'react'
import styled, { css, ThemedText } from 'lib/theme'
import { useEffect, useMemo, useState } from 'react'
import Button from '../Button'
import Row from '../Row'
import TokenImg from '../TokenImg'
const StyledTokenButton = styled(Button)<{ empty?: boolean }>`
const transitionCss = css`
transition: background-color 0.125s linear, border-color 0.125s linear, filter 0.125s linear, width 0.125s ease-out;
`
const StyledTokenButton = styled(Button)`
border-radius: ${({ theme }) => theme.borderRadius}em;
padding: 0.25em;
padding-left: ${({ empty }) => (empty ? 0.75 : 0.25)}em;
:disabled {
// prevents border from expanding the button's box size
padding: calc(0.25em - 1px);
padding-left: calc(${({ empty }) => (empty ? 0.75 : 0.25)}em - 1px);
:enabled {
${({ transition }) => transition && transitionCss};
}
`
const TokenButtonRow = styled(Row)<{ collapsed: boolean }>`
const TokenButtonRow = styled(Row)<{ empty: boolean; collapsed: boolean }>`
float: right;
height: 1.2em;
// max-width must have an absolute value in order to transition.
max-width: ${({ collapsed }) => (collapsed ? '1.2em' : '12em')};
padding-left: ${({ empty }) => empty && 0.5}em;
width: fit-content;
overflow: hidden;
transition: max-width 0.25s linear;
@ -42,10 +46,42 @@ interface TokenButtonProps {
export default function TokenButton({ value, collapsed, disabled, onClick }: TokenButtonProps) {
const buttonBackgroundColor = useMemo(() => (value ? 'interactive' : 'accent'), [value])
const contentColor = useMemo(() => (value || disabled ? 'onInteractive' : 'onAccent'), [value, disabled])
// Transition the button only if transitioning from a disabled state.
// This makes initialization cleaner without adding distracting UX to normal swap flows.
const [shouldTransition, setShouldTransition] = useState(disabled)
useEffect(() => {
if (disabled) {
setShouldTransition(true)
}
}, [disabled])
// width must have an absolute value in order to transition, so it is taken from the row ref.
const [row, setRow] = useState<HTMLDivElement | null>(null)
const style = useMemo(() => {
if (!shouldTransition) return
return { width: row ? row.clientWidth + /* padding= */ 8 + /* border= */ 2 : undefined }
}, [row, shouldTransition])
return (
<StyledTokenButton onClick={onClick} empty={!value} color={buttonBackgroundColor} disabled={disabled}>
<StyledTokenButton
onClick={onClick}
color={buttonBackgroundColor}
disabled={disabled}
style={style}
transition={shouldTransition}
onTransitionEnd={() => setShouldTransition(false)}
>
<ThemedText.ButtonLarge color={contentColor}>
<TokenButtonRow gap={0.4} collapsed={Boolean(value) && collapsed}>
<TokenButtonRow
gap={0.4}
empty={!value}
collapsed={collapsed}
// ref is used to set an absolute width, so it must be reset for each value passed.
// To force this, value?.symbol is passed as a key.
ref={setRow}
key={value?.symbol}
>
{value ? (
<>
<TokenImg token={value} size={1.2} />

@ -31,7 +31,7 @@ const WidgetWrapper = styled.div<{ width?: number | string }>`
font-size: 16px;
font-smooth: always;
font-variant: none;
height: 356px;
height: 360px;
min-width: 300px;
padding: 0.25em;
position: relative;

@ -9,6 +9,6 @@ export const loadingCss = css`
// need to use isLoading as `loading` is a reserved prop
export const loadingTransitionCss = css<{ isLoading: boolean }>`
${({ isLoading }) => isLoading && loadingCss};
transition: opacity ${({ isLoading }) => (isLoading ? 0 : 0.2)}s ease-in-out;
opacity: ${({ isLoading }) => isLoading && loadingOpacity};
transition: color 0.125s linear, opacity ${({ isLoading }) => (isLoading ? 0 : 0.25)}s ease-in-out;
`