Skip to content

Commit

Permalink
fix: tooltips in animated dialogs (Uniswap#432)
Browse files Browse the repository at this point in the history
* fix: update popper on dialog animation finish

* fix: reverted unecessary changes

* fix: modal animation corners

* fix: hardcoded test color

* refactor: use animation theme variables

* fix: memoize boundaries

* fix: initial value for boundary context

* fix: animation variable usage
  • Loading branch information
cartcrom authored Jan 20, 2023
1 parent 6cda162 commit 5b9e21e
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 62 deletions.
98 changes: 89 additions & 9 deletions src/components/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,23 @@ import { globalFontStyles } from 'css/font'
import { useOnEscapeHandler } from 'hooks/useOnEscapeHandler'
import { largeIconCss } from 'icons'
import { ArrowLeft } from 'icons'
import { createContext, ReactElement, ReactNode, useContext, useEffect, useRef, useState } from 'react'
import ms from 'ms.macro'
import {
createContext,
PropsWithChildren,
ReactElement,
ReactNode,
useContext,
useEffect,
useRef,
useState,
} from 'react'
import { createPortal } from 'react-dom'
import styled from 'styled-components/macro'
import { Color, Layer, Provider as ThemeProvider, ThemedText } from 'theme'
import styled, { keyframes } from 'styled-components/macro'
import { AnimationSpeed, Color, Layer, Provider as ThemeProvider, ThemedText, TransitionDuration } from 'theme'
import { useUnmountingAnimation } from 'utils/animations'

import { PopoverBoundaryProvider } from './Popover'
import Row from './Row'

// Include inert from wicg-inert.
Expand Down Expand Up @@ -115,14 +126,81 @@ export const Modal = styled.div<{ color: Color }>`
flex-direction: column;
height: 100%;
left: 0;
overflow: hidden;
padding: 0.5em;
position: absolute;
right: 0;
top: 0;
z-index: ${Layer.DIALOG};
`

const slideInLeft = keyframes`
from {
transform: translateX(calc(100% - 0.25em));
}
`
const slideOutLeft = keyframes`
to {
transform: translateX(calc(0.25em - 100%));
}
`
const slideOutRight = keyframes`
to {
transform: translateX(calc(100% - 0.25em));
}
`

const HiddenWrapper = styled.div`
border-radius: ${({ theme }) => theme.borderRadius}em;
height: 100%;
left: 0;
overflow: hidden;
padding: 0.5em;
position: absolute;
top: 0;
width: 100%;
@supports (overflow: clip) {
overflow: clip;
}
`

const AnimationWrapper = styled.div`
${Modal} {
animation: ${slideInLeft} ${AnimationSpeed.Medium} ease-in;
&.${Animation.PAGING} {
animation: ${slideOutLeft} ${AnimationSpeed.Medium} ease-in;
}
&.${Animation.CLOSING} {
animation: ${slideOutRight} ${AnimationSpeed.Medium} ease-out;
}
}
`

// Accounts for any animation lag
const PopoverAnimationUpdateDelay = ms`100`

/* Allows slide in animation to occur without popovers appearing at pre-animated location. */
function AnimatedPopoverProvider({ children }: PropsWithChildren) {
const popoverRef = useRef<HTMLDivElement>(null)
const [updatePopover, setUpdatePopover] = useState(false)
useEffect(() => {
setTimeout(() => {
setUpdatePopover(true)
}, TransitionDuration.Medium + PopoverAnimationUpdateDelay)
}, [])

return (
<PopoverBoundaryProvider value={popoverRef.current} updateTrigger={updatePopover}>
<div ref={popoverRef}>
<HiddenWrapper>
<AnimationWrapper>{children}</AnimationWrapper>
</HiddenWrapper>
</div>
</PopoverBoundaryProvider>
)
}

interface DialogProps {
color: Color
children: ReactNode
Expand Down Expand Up @@ -150,11 +228,13 @@ export default function Dialog({ color, children, onClose }: DialogProps) {
context.element &&
createPortal(
<ThemeProvider>
<OnCloseContext.Provider value={onClose}>
<Modal color={color} ref={modal}>
{children}
</Modal>
</OnCloseContext.Provider>
<AnimatedPopoverProvider>
<OnCloseContext.Provider value={onClose}>
<Modal color={color} ref={modal}>
{children}
</Modal>
</OnCloseContext.Provider>
</AnimatedPopoverProvider>
</ThemeProvider>,
context.element
)
Expand Down
23 changes: 18 additions & 5 deletions src/components/Popover.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import { Options, Placement } from '@popperjs/core'
import maxSize from 'popper-max-size-modifier'
import React, { createContext, useContext, useMemo, useRef, useState } from 'react'
import React, { createContext, PropsWithChildren, useContext, useEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { usePopper } from 'react-popper'
import styled from 'styled-components/macro'
import { AnimationSpeed, Layer } from 'theme'

const BoundaryContext = createContext<HTMLDivElement | null>(null)
type PopoverBoundary = { boundary?: HTMLDivElement | null; updateTrigger?: any }
const BoundaryContext = createContext<PopoverBoundary>({})

/* Defines a boundary component past which a Popover should not overflow. */
export const PopoverBoundaryProvider = BoundaryContext.Provider
export function PopoverBoundaryProvider({
value,
updateTrigger,
children,
}: PropsWithChildren<{ value: HTMLDivElement | null; updateTrigger?: any }>) {
const boundaryContextValue = useMemo(() => ({ boundary: value, updateTrigger }), [updateTrigger, value])
return <BoundaryContext.Provider value={boundaryContextValue}>{children}</BoundaryContext.Provider>
}

const PopoverContainer = styled.div<{ show: boolean }>`
background-color: ${({ theme }) => theme.dialog};
Expand Down Expand Up @@ -98,7 +106,7 @@ export default function Popover({
contained,
showArrow = true,
}: PopoverProps) {
const boundary = useContext(BoundaryContext)
const { boundary, updateTrigger } = useContext(BoundaryContext)
const reference = useRef<HTMLDivElement>(null)

// Use callback refs to be notified when instantiated
Expand Down Expand Up @@ -139,7 +147,12 @@ export default function Popover({
}
}, [offset, arrow, contained, placement, boundary])

const { styles, attributes } = usePopper(reference.current, popover, options)
const { styles, attributes, update } = usePopper(reference.current, popover, options)

// Manually triggers an update, if prop is provided
useEffect(() => {
update?.()
}, [update, updateTrigger])

return (
<>
Expand Down
5 changes: 2 additions & 3 deletions src/components/Swap/Status/StatusDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ import ErrorDialog, { StatusHeader } from 'components/Error/ErrorDialog'
import EtherscanLink from 'components/EtherscanLink'
import Row from 'components/Row'
import SwapSummary from 'components/Swap/Summary'
import { DialogAnimationLengthMs } from 'components/Widget'
import { MS_IN_SECOND } from 'constants/misc'
import { LargeArrow, LargeCheck, LargeSpinner } from 'icons'
import { useEffect, useMemo, useState } from 'react'
import { Transaction, TransactionType } from 'state/transactions'
import styled from 'styled-components/macro'
import { AnimationSpeed, ThemedText } from 'theme'
import { AnimationSpeed, ThemedText, TransitionDuration } from 'theme'
import { ExplorerDataType } from 'utils/getExplorerLink'

import ActionButton from '../../ActionButton'
Expand Down Expand Up @@ -43,7 +42,7 @@ function TransactionStatus({ tx, onClose }: TransactionStatusProps) {
// which should start after the entrance animation is complete.
const handle = setTimeout(() => {
setShowConfirmation(false)
}, MS_IN_SECOND + DialogAnimationLengthMs)
}, MS_IN_SECOND + TransitionDuration.Medium)
return () => {
clearTimeout(handle)
}
Expand Down
21 changes: 16 additions & 5 deletions src/components/Swap/Summary/Details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
import Column from 'components/Column'
import Row from 'components/Row'
import Rule from 'components/Rule'
import Tooltip from 'components/Tooltip'
import { PriceImpact } from 'hooks/usePriceImpact'
import { Slippage } from 'hooks/useSlippage'
import { useWidgetWidth } from 'hooks/useWidgetWidth'
Expand Down Expand Up @@ -57,6 +58,10 @@ function Detail({ label, value, color }: DetailProps) {
)
}

const ToolTipBody = styled(ThemedText.Caption)`
max-width: 220px;
`

interface AmountProps {
tooltipText?: string
label: string
Expand All @@ -83,11 +88,17 @@ function Amount({ tooltipText, label, amount, usdcAmount }: AmountProps) {

return (
<Row flex align="flex-start">
<ThemedText.Body2 userSelect>
<Label>{label}</Label>
{/* TODO(cartcrom): WEB-2764 figure out why tooltips don't work on Dialog components */}
{/* {tooltipText && <Tooltip placement={'right'}>{tooltipText}</Tooltip>} */}
</ThemedText.Body2>
<Row>
<ThemedText.Body2 userSelect>
<Label>{label}</Label>
</ThemedText.Body2>
{tooltipText && (
<Tooltip placement="right" offset={8}>
<ToolTipBody>{tooltipText}</ToolTipBody>
</Tooltip>
)}
</Row>

<Column flex align="flex-end" grow>
<ThemedText.H1 color="primary" fontSize={amountFontSize} lineHeight={amountLineHeight}>
{formattedAmount} {amount.currency.symbol}
Expand Down
38 changes: 2 additions & 36 deletions src/components/Widget.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { TokenInfo } from '@uniswap/token-lists'
import { Animation, Modal, Provider as DialogProvider } from 'components/Dialog'
import { Provider as DialogProvider } from 'components/Dialog'
import ErrorBoundary, { OnError } from 'components/Error/ErrorBoundary'
import { SupportedLocale } from 'constants/locales'
import { TransactionEventHandlers, TransactionsUpdater } from 'hooks/transactions'
Expand All @@ -14,55 +14,21 @@ import { PropsWithChildren, StrictMode, useState } from 'react'
import { Provider as ReduxProvider } from 'react-redux'
import { store } from 'state'
import { MulticallUpdater } from 'state/multicall'
import styled, { keyframes } from 'styled-components/macro'
import styled from 'styled-components/macro'
import { Provider as ThemeProvider, Theme } from 'theme'

import WidgetWrapper from './WidgetWrapper'

const slideInLeft = keyframes`
from {
transform: translateX(calc(100% - 0.25em));
}
`
const slideOutLeft = keyframes`
to {
transform: translateX(calc(0.25em - 100%));
}
`
const slideOutRight = keyframes`
to {
transform: translateX(calc(100% - 0.25em));
}
`

export const DialogAnimationLengthMs = 250

export const DialogWrapper = styled.div`
border: ${({ theme }) => `1px solid ${theme.outline}`};
border-radius: ${({ theme }) => theme.borderRadius}em;
height: 100%;
left: 0;
overflow: hidden;
padding: 0.5em;
position: absolute;
top: 0;
width: 100%;
@supports (overflow: clip) {
overflow: clip;
}
${Modal} {
animation: ${slideInLeft} ${DialogAnimationLengthMs}ms ease-in;
&.${Animation.PAGING} {
animation: ${slideOutLeft} ${DialogAnimationLengthMs}ms ease-in;
}
&.${Animation.CLOSING} {
animation: ${slideOutRight} ${DialogAnimationLengthMs}ms ease-out;
}
}
`

export interface WidgetProps extends Flags, TransactionEventHandlers, Web3Props, WidgetEventHandlers {
Expand Down
14 changes: 10 additions & 4 deletions src/theme/animations.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
export enum AnimationSpeed {
Fast = '0.125s',
Medium = '0.25s',
Slow = '0.5s',
export enum TransitionDuration {
Fast = 125,
Medium = 250,
Slow = 250,
}

export const AnimationSpeed = {
Fast: `${TransitionDuration.Fast}ms`,
Medium: `${TransitionDuration.Medium}ms`,
Slow: `${TransitionDuration.Slow}ms`,
}

0 comments on commit 5b9e21e

Please sign in to comment.