refactor: consolidate price delta components (#7268)

* refactor: move delta component out of price chart file

* refactor: remove PortfolioArrow

* refactor: search row arrow cell

* fix: svg prop + snapshots failing unit tests

* refactor: css nit
This commit is contained in:
cartcrom 2023-09-08 14:58:39 -04:00 committed by GitHub
parent f69226c1d1
commit bb28235bee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 164 additions and 223 deletions

@ -5,13 +5,11 @@ import { useWeb3React } from '@web3-react/core'
import { sendAnalyticsEvent, TraceEvent } from 'analytics'
import { ButtonEmphasis, ButtonSize, LoadingButtonSpinner, ThemeButton } from 'components/Button'
import Column from 'components/Column'
import { ArrowChangeDown } from 'components/Icons/ArrowChangeDown'
import { ArrowChangeUp } from 'components/Icons/ArrowChangeUp'
import { Power } from 'components/Icons/Power'
import { Settings } from 'components/Icons/Settings'
import { AutoRow } from 'components/Row'
import { LoadingBubble } from 'components/Tokens/loading'
import { formatDelta } from 'components/Tokens/TokenDetails/PriceChart'
import { DeltaArrow, formatDelta } from 'components/Tokens/TokenDetails/Delta'
import Tooltip from 'components/Tooltip'
import { getConnection } from 'connection'
import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes'
@ -20,11 +18,11 @@ import { useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hoo
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
import { ProfilePageStateType } from 'nft/types'
import { useCallback, useState } from 'react'
import { CreditCard, IconProps, Info } from 'react-feather'
import { CreditCard, Info } from 'react-feather'
import { useNavigate } from 'react-router-dom'
import { useAppDispatch } from 'state/hooks'
import { updateSelectedWallet } from 'state/user/reducer'
import styled, { useTheme } from 'styled-components'
import styled from 'styled-components'
import { CopyHelper, ExternalLink, ThemedText } from 'theme'
import { shortenAddress } from 'utils'
import { formatNumber, NumberType } from 'utils/formatNumbers'
@ -151,15 +149,6 @@ const PortfolioDrawerContainer = styled(Column)`
flex: 1;
`
export function PortfolioArrow({ change, ...rest }: { change: number } & IconProps) {
const theme = useTheme()
return change < 0 ? (
<ArrowChangeDown color={theme.critical} width={16} {...rest} />
) : (
<ArrowChangeUp color={theme.success} width={16} {...rest} />
)
}
export default function AuthenticatedHeader({ account, openSettings }: { account: string; openSettings: () => void }) {
const { connector } = useWeb3React()
const { ENSName } = useENSName(account)
@ -287,7 +276,7 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
<AutoRow marginBottom="20px">
{absoluteChange !== 0 && percentChange && (
<>
<PortfolioArrow change={absoluteChange as number} />
<DeltaArrow delta={absoluteChange} />
<ThemedText.BodySecondary>
{`${formatNumber({
input: Math.abs(absoluteChange as number),

@ -2,7 +2,7 @@ import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/an
import { TraceEvent } from 'analytics'
import { useCachedPortfolioBalancesQuery } from 'components/AccountDrawer/PrefetchBalancesWrapper'
import Row from 'components/Row'
import { formatDelta } from 'components/Tokens/TokenDetails/PriceChart'
import { DeltaArrow, formatDelta } from 'components/Tokens/TokenDetails/Delta'
import { TokenBalance } from 'graphql/data/__generated__/types-and-hooks'
import { getTokenDetailsURL, gqlToCurrency, logSentryErrorForUnsupportedChain } from 'graphql/data/util'
import { useAtomValue } from 'jotai/utils'
@ -15,7 +15,6 @@ import { formatNumber, NumberType } from 'utils/formatNumbers'
import { splitHiddenTokens } from 'utils/splitHiddenTokens'
import { useToggleAccountDrawer } from '../..'
import { PortfolioArrow } from '../../AuthenticatedHeader'
import { hideSmallBalancesAtom } from '../../SmallBalanceToggle'
import { ExpandoRow } from '../ExpandoRow'
import { PortfolioLogo } from '../PortfolioLogo'
@ -115,7 +114,7 @@ function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: Tok
})}
</ThemedText.SubHeader>
<Row justify="flex-end">
<PortfolioArrow change={percentChange} size={20} strokeWidth={1.75} />
<DeltaArrow delta={percentChange} />
<ThemedText.BodySecondary>{formatDelta(percentChange)}</ThemedText.BodySecondary>
</Row>
</>

@ -20,18 +20,15 @@ import styled from 'styled-components'
import { ThemedText } from 'theme'
import { formatUSDPrice } from 'utils/formatNumbers'
import { DeltaText, getDeltaArrow } from '../Tokens/TokenDetails/PriceChart'
import { DeltaArrow, DeltaText } from '../Tokens/TokenDetails/Delta'
import { useAddRecentlySearchedAsset } from './RecentlySearchedAssets'
import * as styles from './SearchBar.css'
const PriceChangeContainer = styled.div`
display: flex;
align-items: center;
`
const ArrowCell = styled.span`
padding-top: 4px;
padding-right: 2px;
gap: 2px;
`
interface CollectionRowProps {
@ -156,8 +153,6 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
}
}, [toggleOpen, isHovered, token, navigate, handleClick, tokenDetailsPath])
const arrow = getDeltaArrow(token.market?.pricePercentChange?.value, 16)
return (
<Link
data-testid={`searchbar-token-row-${token.chain}-${token.address ?? 'NATIVE'}`}
@ -192,7 +187,7 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
<Box className={styles.primaryText}>{formatUSDPrice(token.market.price.value)}</Box>
</Row>
<PriceChangeContainer>
<ArrowCell>{arrow}</ArrowCell>
<DeltaArrow delta={token.market?.pricePercentChange?.value} />
<ThemedText.BodySmall>
<DeltaText delta={token.market?.pricePercentChange?.value}>
{Math.abs(token.market?.pricePercentChange?.value ?? 0).toFixed(2)}%

@ -0,0 +1,47 @@
import { ArrowChangeDown } from 'components/Icons/ArrowChangeDown'
import { ArrowChangeUp } from 'components/Icons/ArrowChangeUp'
import styled from 'styled-components'
const StyledUpArrow = styled(ArrowChangeUp)<{ $noColor?: boolean }>`
color: ${({ theme, $noColor }) => ($noColor ? theme.neutral2 : theme.success)};
`
const StyledDownArrow = styled(ArrowChangeDown)<{ $noColor?: boolean }>`
color: ${({ theme, $noColor }) => ($noColor ? theme.neutral2 : theme.critical)};
`
export function calculateDelta(start: number, current: number) {
return (current / start - 1) * 100
}
function isValidDelta(delta: number | null | undefined): delta is number {
// Null-check not including zero
return delta !== null && delta !== undefined && delta !== Infinity && !isNaN(delta)
}
interface DeltaArrowProps {
delta?: number | null
noColor?: boolean
size?: number
}
export function formatDelta(delta: number | null | undefined) {
if (!isValidDelta(delta)) return '-'
const formattedDelta = Math.abs(delta).toFixed(2) + '%'
return formattedDelta
}
export function DeltaArrow({ delta, noColor = false, size = 16 }: DeltaArrowProps) {
if (!isValidDelta(delta)) return null
return Math.sign(delta) < 0 ? (
<StyledDownArrow width={size} height={size} key="arrow-down" aria-label="down" $noColor={noColor} />
) : (
<StyledUpArrow width={size} height={size} key="arrow-up" aria-label="up" $noColor={noColor} />
)
}
export const DeltaText = styled.span<{ delta?: number }>`
color: ${({ theme, delta }) =>
delta !== undefined ? (Math.sign(delta) < 0 ? theme.critical : theme.success) : theme.neutral1};
`

@ -6,14 +6,12 @@ import { GlyphCircle } from '@visx/glyph'
import { Line } from '@visx/shape'
import AnimatedInLineChart from 'components/Charts/AnimatedInLineChart'
import FadedInLineChart from 'components/Charts/FadeInLineChart'
import { ArrowChangeDown } from 'components/Icons/ArrowChangeDown'
import { ArrowChangeUp } from 'components/Icons/ArrowChangeUp'
import { MouseoverTooltip } from 'components/Tooltip'
import { bisect, curveCardinal, NumberValue, scaleLinear, timeDay, timeHour, timeMinute, timeMonth } from 'd3'
import { PricePoint, TimePeriod } from 'graphql/data/util'
import { useActiveLocale } from 'hooks/useActiveLocale'
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
import { ArrowDownRight, ArrowUpRight, Info, TrendingUp } from 'react-feather'
import { Info, TrendingUp } from 'react-feather'
import styled, { useTheme } from 'styled-components'
import { ThemedText } from 'theme'
import { textFadeIn } from 'theme/styles'
@ -27,6 +25,8 @@ import {
} from 'utils/formatChartTimes'
import { formatUSDPrice } from 'utils/formatNumbers'
import { calculateDelta, DeltaArrow, formatDelta } from './Delta'
const DATA_EMPTY = { value: 0, timestamp: 0 }
export function getPriceBounds(pricePoints: PricePoint[]): [number, number] {
@ -36,56 +36,6 @@ export function getPriceBounds(pricePoints: PricePoint[]): [number, number] {
return [min, max]
}
const StyledUpArrow = styled(ArrowChangeUp)`
color: ${({ theme }) => theme.success};
`
const StyledDownArrow = styled(ArrowChangeDown)`
color: ${({ theme }) => theme.critical};
`
const DefaultUpArrow = styled(ArrowUpRight)`
color: ${({ theme }) => theme.neutral3};
`
const DefaultDownArrow = styled(ArrowDownRight)`
color: ${({ theme }) => theme.neutral3};
`
function calculateDelta(start: number, current: number) {
return (current / start - 1) * 100
}
export function getDeltaArrow(delta: number | null | undefined, iconSize = 16, styled = true) {
// Null-check not including zero
if (delta === null || delta === undefined) {
return null
} else if (Math.sign(delta) < 0) {
return styled ? (
<StyledDownArrow width={iconSize} height={iconSize} key="arrow-down" aria-label="down" />
) : (
<DefaultDownArrow size={iconSize} key="arrow-down" aria-label="down" />
)
}
return styled ? (
<StyledUpArrow width={iconSize} height={iconSize} key="arrow-up" aria-label="up" />
) : (
<DefaultUpArrow size={iconSize} key="arrow-up" aria-label="up" />
)
}
export function formatDelta(delta: number | null | undefined) {
// Null-check not including zero
if (delta === null || delta === undefined || delta === Infinity || isNaN(delta)) {
return '-'
}
const formattedDelta = Math.abs(delta).toFixed(2) + '%'
return formattedDelta
}
export const DeltaText = styled.span<{ delta?: number }>`
color: ${({ theme, delta }) =>
delta !== undefined ? (Math.sign(delta) < 0 ? theme.critical : theme.success) : theme.neutral1};
`
const ChartHeader = styled.div`
position: absolute;
${textFadeIn};
@ -113,10 +63,6 @@ const DeltaContainer = styled.div`
margin-top: 4px;
color: ${({ theme }) => theme.neutral2};
`
export const ArrowCell = styled.div`
padding-right: 3px;
display: flex;
`
const OutdatedPriceContainer = styled.div`
display: flex;
@ -151,6 +97,22 @@ function fixChart(prices: PricePoint[] | undefined | null) {
const margin = { top: 100, bottom: 48, crosshair: 72 }
const timeOptionsHeight = 44
interface ChartDeltaProps {
startingPrice: PricePoint
endingPrice: PricePoint
noColor?: boolean
}
function ChartDelta({ startingPrice, endingPrice, noColor }: ChartDeltaProps) {
const delta = calculateDelta(startingPrice.value, endingPrice.value)
return (
<DeltaContainer>
{formatDelta(delta)}
<DeltaArrow delta={delta} noColor={noColor} />
</DeltaContainer>
)
}
interface PriceChartProps {
width: number
height: number
@ -202,10 +164,6 @@ export function PriceChart({ width, height, prices: originalPrices, timePeriod }
return DATA_EMPTY
}, [prices])
const totalDelta = calculateDelta(firstPrice.value, lastPrice.value)
const formattedTotalDelta = formatDelta(totalDelta)
const defaultArrow = getDeltaArrow(totalDelta, 20, false)
// first price point on the x-axis of the current time period's chart
const startingPrice = originalPrices?.[0] ?? DATA_EMPTY
// last price point on the x-axis of the current time period's chart
@ -334,9 +292,6 @@ export function PriceChart({ width, height, prices: originalPrices, timePeriod }
}
const updatedTicks = maxTicks > 0 ? (ticks.length > maxTicks ? calculateTicks(ticks) : ticks) : []
const delta = calculateDelta(startingPrice.value, displayPrice.value)
const formattedDelta = formatDelta(delta)
const arrow = getDeltaArrow(delta)
const crosshairEdgeMax = width * 0.85
const crosshairAtEdge = !!crosshair && crosshair > crosshairEdgeMax
@ -355,10 +310,7 @@ export function PriceChart({ width, height, prices: originalPrices, timePeriod }
{displayPrice.value ? (
<>
<TokenPrice>{formatUSDPrice(displayPrice.value)}</TokenPrice>
<DeltaContainer>
{formattedDelta}
<ArrowCell>{arrow}</ArrowCell>
</DeltaContainer>
<ChartDelta startingPrice={startingPrice} endingPrice={displayPrice} />
</>
) : lastPrice.value ? (
<OutdatedContainer>
@ -368,10 +320,7 @@ export function PriceChart({ width, height, prices: originalPrices, timePeriod }
<Info size={16} />
</MouseoverTooltip>
</OutdatedPriceContainer>
<DeltaContainer>
{formattedTotalDelta}
<ArrowCell>{defaultArrow}</ArrowCell>
</DeltaContainer>
<ChartDelta startingPrice={firstPrice} endingPrice={lastPrice} noColor />
</OutdatedContainer>
) : (
<>

@ -2,7 +2,7 @@
exports[`PriceChart renders correctly with all prices filled 1`] = `
<DocumentFragment>
.c4 {
.c3 {
color: #40B66B;
}
@ -34,14 +34,6 @@ exports[`PriceChart renders correctly with all prices filled 1`] = `
color: #7D7D7D;
}
.c3 {
padding-right: 3px;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
<div
class="c0"
data-cy="chart-header"
@ -55,12 +47,9 @@ exports[`PriceChart renders correctly with all prices filled 1`] = `
class="c2"
>
0.00%
<div
class="c3"
>
<svg
aria-label="up"
class="c4"
class="c3"
fill="none"
height="16"
viewBox="0 0 24 24"
@ -74,7 +63,6 @@ exports[`PriceChart renders correctly with all prices filled 1`] = `
</svg>
</div>
</div>
</div>
<svg
data-cy="price-chart"
height="392"
@ -426,8 +414,8 @@ exports[`PriceChart renders correctly with some prices filled 1`] = `
height: inherit;
}
.c7 {
color: #CECECE;
.c6 {
color: #7D7D7D;
}
.c0 {
@ -462,14 +450,6 @@ exports[`PriceChart renders correctly with some prices filled 1`] = `
color: #7D7D7D;
}
.c6 {
padding-right: 3px;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.c2 {
display: -webkit-box;
display: -webkit-flex;
@ -535,36 +515,23 @@ exports[`PriceChart renders correctly with some prices filled 1`] = `
class="c5"
>
0.00%
<div
class="c6"
>
<svg
aria-label="up"
class="c7"
class="c6"
fill="none"
height="20"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
height="16"
viewBox="0 0 24 24"
width="20"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<line
x1="7"
x2="17"
y1="17"
y2="7"
/>
<polyline
points="7 7 17 7 17 17"
<path
d="M13.3021 7.7547L17.6821 14.2475C18.4182 15.3388 17.7942 17 16.6482 17L7.3518 17C6.2058 17 5.5818 15.3376 6.3179 14.2475L10.6979 7.7547C11.377 6.7484 12.623 6.7484 13.3021 7.7547Z"
fill="currentColor"
/>
</svg>
</div>
</div>
</div>
</div>
<svg
data-cy="price-chart"
height="392"

@ -33,7 +33,7 @@ import {
TokenSortMethod,
useSetSortMethod,
} from '../state'
import { ArrowCell, DeltaText, formatDelta, getDeltaArrow } from '../TokenDetails/PriceChart'
import { DeltaArrow, DeltaText, formatDelta } from '../TokenDetails/Delta'
const Cell = styled.div`
display: flex;
@ -103,8 +103,9 @@ const StyledTokenRow = styled.div<{
}
`
const ClickableContent = styled.div`
const ClickableContent = styled.div<{ gap?: number }>`
display: flex;
${({ gap }) => gap && `gap: ${gap}px`};
text-decoration: none;
color: ${({ theme }) => theme.neutral1};
align-items: center;
@ -184,6 +185,7 @@ const PercentChangeInfoCell = styled(Cell)`
@media only screen and (max-width: ${SMALL_MEDIA_BREAKPOINT}) {
display: flex;
gap: 3px;
justify-content: flex-end;
color: ${({ theme }) => theme.neutral2};
font-size: 12px;
@ -445,8 +447,6 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
const chainId = supportedChainIdFromGQLChain(filterNetwork)
const timePeriod = useAtomValue(filterTimeAtom)
const delta = token.market?.pricePercentChange?.value
const arrow = getDeltaArrow(delta)
const smallArrow = getDeltaArrow(delta, 14)
const formattedDelta = formatDelta(delta)
const exploreTokenSelectedEventProperties = {
@ -489,15 +489,15 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
<PriceInfoCell>
{price}
<PercentChangeInfoCell>
<ArrowCell>{smallArrow}</ArrowCell>
<DeltaArrow delta={delta} size={14} />
<DeltaText delta={delta}>{formattedDelta}</DeltaText>
</PercentChangeInfoCell>
</PriceInfoCell>
</ClickableContent>
}
percentChange={
<ClickableContent>
<ArrowCell>{arrow}</ArrowCell>
<ClickableContent gap={3}>
<DeltaArrow delta={delta} />
<DeltaText delta={delta}>{formattedDelta}</DeltaText>
</ClickableContent>
}

@ -2,22 +2,14 @@
exports[`LoadedRow.tsx renders a row 1`] = `
<DocumentFragment>
.c18 {
color: #40B66B;
}
.c19 {
color: #40B66B;
}
.c20 {
color: #40B66B;
}
.c18 {
padding-right: 3px;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.c9 {
opacity: 0;
-webkit-transition: opacity 250ms ease-in;
@ -113,6 +105,22 @@ exports[`LoadedRow.tsx renders a row 1`] = `
cursor: pointer;
}
.c21 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 3px;
-webkit-text-decoration: none;
text-decoration: none;
color: #222222;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
cursor: pointer;
}
.c6 {
gap: 8px;
max-width: 100%;
@ -156,7 +164,7 @@ exports[`LoadedRow.tsx renders a row 1`] = `
padding-right: 8px;
}
.c21 {
.c20 {
padding-right: 8px;
}
@ -264,7 +272,7 @@ exports[`LoadedRow.tsx renders a row 1`] = `
}
@media only screen and (max-width:540px) {
.c21 {
.c20 {
display: none;
}
}
@ -275,6 +283,7 @@ exports[`LoadedRow.tsx renders a row 1`] = `
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 3px;
-webkit-box-pack: end;
-webkit-justify-content: flex-end;
-ms-flex-pack: end;
@ -407,13 +416,10 @@ exports[`LoadedRow.tsx renders a row 1`] = `
$1.00
<div
class="c2 c17"
>
<div
class="c18"
>
<svg
aria-label="up"
class="c19"
class="c18"
fill="none"
height="14"
viewBox="0 0 24 24"
@ -425,9 +431,8 @@ exports[`LoadedRow.tsx renders a row 1`] = `
fill="currentColor"
/>
</svg>
</div>
<span
class="c20"
class="c19"
>
0.00%
</span>
@ -436,18 +441,15 @@ exports[`LoadedRow.tsx renders a row 1`] = `
</div>
</div>
<div
class="c2 c14 c21"
class="c2 c14 c20"
data-testid="percent-change-cell"
>
<div
class="c5"
>
<div
class="c18"
class="c21"
>
<svg
aria-label="up"
class="c19"
class="c18"
fill="none"
height="16"
viewBox="0 0 24 24"
@ -459,9 +461,8 @@ exports[`LoadedRow.tsx renders a row 1`] = `
fill="currentColor"
/>
</svg>
</div>
<span
class="c20"
class="c19"
>
0.00%
</span>

@ -1,4 +1,4 @@
import { getDeltaArrow } from 'components/Tokens/TokenDetails/PriceChart'
import { DeltaArrow } from 'components/Tokens/TokenDetails/Delta'
import { useScreenSize } from 'hooks/useScreenSize'
import { Box, BoxProps } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
@ -352,7 +352,6 @@ const StatsRow = ({ stats, isMobile, ...props }: { stats: GenieCollection; isMob
const floorPriceStr = floorFormatter(stats.stats?.floor_price ?? 0)
// graphQL formatted %age values out of 100, whereas v3 endpoint did a decimal between 0 & 1
const floorChangeStr = Math.round(Math.abs(stats?.stats?.one_day_floor_change ?? 0))
const arrow = stats?.stats?.one_day_floor_change ? getDeltaArrow(stats.stats.one_day_floor_change) : undefined
const isBagExpanded = useBag((state) => state.bagExpanded)
const isScreenSize = useScreenSize()
@ -372,7 +371,7 @@ const StatsRow = ({ stats, isMobile, ...props }: { stats: GenieCollection; isMob
{stats.stats?.one_day_floor_change !== undefined ? (
<StatsItem label="Floor 24H" shouldHide={false}>
<PercentChange isNegative={stats.stats.one_day_floor_change < 0}>
{arrow}
<DeltaArrow delta={stats?.stats?.one_day_floor_change} />
{floorChangeStr}%
</PercentChange>
</StatsItem>

@ -1,6 +1,5 @@
import { formatEther } from '@ethersproject/units'
import { ArrowChangeDown } from 'components/Icons/ArrowChangeDown'
import { ArrowChangeUp } from 'components/Icons/ArrowChangeUp'
import { DeltaArrow } from 'components/Tokens/TokenDetails/Delta'
import { VerifiedIcon } from 'nft/components/icons'
import { useIsMobile } from 'nft/hooks'
import { Denomination } from 'nft/types'
@ -162,11 +161,7 @@ export const ChangeCell = ({ change, children }: { children?: ReactNode; change?
const TextComponent = isMobile ? ThemedText.BodySmall : ThemedText.BodyPrimary
return (
<ChangeCellContainer change={change ?? 0}>
{!change || change > 0 ? (
<ArrowChangeUp width="16px" height="16px" />
) : (
<ArrowChangeDown width="16px" height="16px" />
)}
<DeltaArrow delta={change} />
<TextComponent color="currentColor">{children || `${change ? Math.abs(Math.round(change)) : 0}%`}</TextComponent>
</ChangeCellContainer>
)