chore: currency percentages (#7358)
* formatPercent * hook deps * price chart * price chart formatting * bug bash findings * Update src/utils/formatNumbers.ts Co-authored-by: Zach Pomerantz <zzmp@uniswap.org> * fixing merge errors * unit tests * special cases --------- Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
This commit is contained in:
parent
82aaf0784a
commit
2694379c97
@ -9,7 +9,7 @@ import { Power } from 'components/Icons/Power'
|
||||
import { Settings } from 'components/Icons/Settings'
|
||||
import { AutoRow } from 'components/Row'
|
||||
import { LoadingBubble } from 'components/Tokens/loading'
|
||||
import { DeltaArrow, formatDelta } from 'components/Tokens/TokenDetails/Delta'
|
||||
import { DeltaArrow } from 'components/Tokens/TokenDetails/Delta'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { getConnection } from 'connection'
|
||||
import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes'
|
||||
@ -161,7 +161,7 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
|
||||
const clearCollectionFilters = useWalletCollections((state) => state.clearCollectionFilters)
|
||||
const isClaimAvailable = useIsNftClaimAvailable((state) => state.isClaimAvailable)
|
||||
const shouldShowBuyFiatButton = useIsNotOriginCountry('GB')
|
||||
const { formatNumber } = useFormatter()
|
||||
const { formatNumber, formatPercent } = useFormatter()
|
||||
|
||||
const shouldDisableNFTRoutes = useDisableNFTRoutes()
|
||||
|
||||
@ -284,7 +284,7 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
|
||||
{`${formatNumber({
|
||||
input: Math.abs(absoluteChange as number),
|
||||
type: NumberType.PortfolioBalance,
|
||||
})} (${formatDelta(percentChange)})`}
|
||||
})} (${formatPercent(percentChange)})`}
|
||||
</ThemedText.BodySecondary>
|
||||
</>
|
||||
)}
|
||||
|
@ -2,7 +2,7 @@ import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/an
|
||||
import { TraceEvent } from 'analytics'
|
||||
import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
|
||||
import Row from 'components/Row'
|
||||
import { DeltaArrow, formatDelta } from 'components/Tokens/TokenDetails/Delta'
|
||||
import { DeltaArrow } 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'
|
||||
@ -71,6 +71,7 @@ const TokenNameText = styled(ThemedText.SubHeader)`
|
||||
type PortfolioToken = NonNullable<TokenBalance['token']>
|
||||
|
||||
function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: TokenBalance & { token: PortfolioToken }) {
|
||||
const { formatPercent } = useFormatter()
|
||||
const percentChange = tokenProjectMarket?.pricePercentChange?.value ?? 0
|
||||
|
||||
const navigate = useNavigate()
|
||||
@ -120,7 +121,7 @@ function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: Tok
|
||||
</ThemedText.SubHeader>
|
||||
<Row justify="flex-end">
|
||||
<DeltaArrow delta={percentChange} />
|
||||
<ThemedText.BodySecondary>{formatDelta(percentChange)}</ThemedText.BodySecondary>
|
||||
<ThemedText.BodySecondary>{formatPercent(percentChange)}</ThemedText.BodySecondary>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
|
@ -20,7 +20,7 @@ import { ThemedText } from 'theme/components'
|
||||
import { textFadeIn } from 'theme/styles'
|
||||
import { useFormatter } from 'utils/formatNumbers'
|
||||
|
||||
import { calculateDelta, DeltaArrow, formatDelta } from '../../Tokens/TokenDetails/Delta'
|
||||
import { calculateDelta, DeltaArrow } from '../../Tokens/TokenDetails/Delta'
|
||||
|
||||
const CHART_MARGIN = { top: 100, bottom: 48, crosshair: 72 }
|
||||
|
||||
@ -52,9 +52,11 @@ interface ChartDeltaProps {
|
||||
|
||||
function ChartDelta({ startingPrice, endingPrice, noColor }: ChartDeltaProps) {
|
||||
const delta = calculateDelta(startingPrice.value, endingPrice.value)
|
||||
const { formatPercent } = useFormatter()
|
||||
|
||||
return (
|
||||
<DeltaContainer>
|
||||
{formatDelta(delta)}
|
||||
{formatPercent(delta)}
|
||||
<DeltaArrow delta={delta} noColor={noColor} />
|
||||
</DeltaContainer>
|
||||
)
|
||||
|
@ -128,7 +128,7 @@ interface TokenRowProps {
|
||||
export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index, eventProperties }: TokenRowProps) => {
|
||||
const addRecentlySearchedAsset = useAddRecentlySearchedAsset()
|
||||
const navigate = useNavigate()
|
||||
const { formatFiatPrice } = useFormatter()
|
||||
const { formatFiatPrice, formatPercent } = useFormatter()
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
const address = !token.address && token.standard === TokenStandard.Native ? 'NATIVE' : token.address
|
||||
@ -191,7 +191,7 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
|
||||
<DeltaArrow delta={token.market?.pricePercentChange?.value} />
|
||||
<ThemedText.BodySmall>
|
||||
<DeltaText delta={token.market?.pricePercentChange?.value}>
|
||||
{Math.abs(token.market?.pricePercentChange?.value ?? 0).toFixed(2)}%
|
||||
{formatPercent(Math.abs(token.market?.pricePercentChange?.value ?? 0))}
|
||||
</DeltaText>
|
||||
</ThemedText.BodySmall>
|
||||
</PriceChangeContainer>
|
||||
|
@ -24,13 +24,6 @@ interface DeltaArrowProps {
|
||||
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
|
||||
|
||||
|
@ -34,7 +34,7 @@ import {
|
||||
TokenSortMethod,
|
||||
useSetSortMethod,
|
||||
} from '../state'
|
||||
import { DeltaArrow, DeltaText, formatDelta } from '../TokenDetails/Delta'
|
||||
import { DeltaArrow, DeltaText } from '../TokenDetails/Delta'
|
||||
|
||||
const Cell = styled.div`
|
||||
display: flex;
|
||||
@ -441,7 +441,7 @@ interface LoadedRowProps {
|
||||
|
||||
/* Loaded State: row component with token information */
|
||||
export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
const { formatFiatPrice, formatNumber } = useFormatter()
|
||||
const { formatFiatPrice, formatNumber, formatPercent } = useFormatter()
|
||||
|
||||
const { tokenListIndex, tokenListLength, token, sortRank } = props
|
||||
const filterString = useAtomValue(filterStringAtom)
|
||||
@ -450,7 +450,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
|
||||
const chainId = supportedChainIdFromGQLChain(filterNetwork)
|
||||
const timePeriod = useAtomValue(filterTimeAtom)
|
||||
const delta = token.market?.pricePercentChange?.value
|
||||
const formattedDelta = formatDelta(delta)
|
||||
const formattedDelta = formatPercent(delta)
|
||||
|
||||
const exploreTokenSelectedEventProperties = {
|
||||
chain_id: chainId,
|
||||
|
@ -2,7 +2,7 @@ import { Trans } from '@lingui/macro'
|
||||
import Column from 'components/Column'
|
||||
import CurrencyLogo from 'components/Logo/CurrencyLogo'
|
||||
import Row from 'components/Row'
|
||||
import { DeltaArrow, formatDelta } from 'components/Tokens/TokenDetails/Delta'
|
||||
import { DeltaArrow } from 'components/Tokens/TokenDetails/Delta'
|
||||
import { PoolData } from 'graphql/thegraph/PoolData'
|
||||
import { useCurrency } from 'hooks/Tokens'
|
||||
import { useColor } from 'hooks/useColor'
|
||||
@ -199,7 +199,7 @@ const StatItemText = styled(Text)`
|
||||
`
|
||||
|
||||
function StatItem({ title, value, delta }: { title: ReactNode; value: number; delta?: number }) {
|
||||
const { formatNumber } = useFormatter()
|
||||
const { formatNumber, formatPercent } = useFormatter()
|
||||
|
||||
return (
|
||||
<StatItemColumn>
|
||||
@ -214,7 +214,7 @@ function StatItem({ title, value, delta }: { title: ReactNode; value: number; de
|
||||
{!!delta && (
|
||||
<Row width="max-content" padding="4px 0px">
|
||||
<DeltaArrow delta={delta} />
|
||||
<ThemedText.BodySecondary>{formatDelta(delta)}</ThemedText.BodySecondary>
|
||||
<ThemedText.BodySecondary>{formatPercent(delta)}</ThemedText.BodySecondary>
|
||||
</Row>
|
||||
)}
|
||||
</StatsTextContainer>
|
||||
|
@ -460,3 +460,37 @@ describe('formatReviewSwapCurrencyAmount', () => {
|
||||
expect(formatReviewSwapCurrencyAmount(currencyAmount)).toBe('2000000')
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatPercent', () => {
|
||||
beforeEach(() => {
|
||||
mocked(useLocalCurrencyConversionRate).mockReturnValue({ data: 1.0, isLoading: false })
|
||||
mocked(useCurrencyConversionFlagEnabled).mockReturnValue(true)
|
||||
})
|
||||
|
||||
it.each([[null], [undefined], [Infinity], [NaN]])('should correctly format %p', (value) => {
|
||||
const { formatPercent } = renderHook(() => useFormatter()).result.current
|
||||
|
||||
expect(formatPercent(value)).toBe('-')
|
||||
})
|
||||
|
||||
it('correctly formats a percent with 2 decimal places', () => {
|
||||
const { formatPercent } = renderHook(() => useFormatter()).result.current
|
||||
|
||||
expect(formatPercent(0)).toBe('0.00%')
|
||||
expect(formatPercent(0.1)).toBe('0.10%')
|
||||
expect(formatPercent(1)).toBe('1.00%')
|
||||
expect(formatPercent(10)).toBe('10.00%')
|
||||
expect(formatPercent(100)).toBe('100.00%')
|
||||
})
|
||||
|
||||
it('correctly formats a percent with 2 decimal places in french locale', () => {
|
||||
mocked(useActiveLocale).mockReturnValue('fr-FR')
|
||||
const { formatPercent } = renderHook(() => useFormatter()).result.current
|
||||
|
||||
expect(formatPercent(0)).toBe('0,00%')
|
||||
expect(formatPercent(0.1)).toBe('0,10%')
|
||||
expect(formatPercent(1)).toBe('1,00%')
|
||||
expect(formatPercent(10)).toBe('10,00%')
|
||||
expect(formatPercent(100)).toBe('100,00%')
|
||||
})
|
||||
})
|
||||
|
@ -478,6 +478,18 @@ function formatSlippage(slippage: Percent | undefined, locale: SupportedLocale =
|
||||
})}%`
|
||||
}
|
||||
|
||||
function formatPercent(percent: Nullish<number>, locale: SupportedLocale = DEFAULT_LOCALE) {
|
||||
if (percent === null || percent === undefined || percent === Infinity || isNaN(percent)) {
|
||||
return '-'
|
||||
}
|
||||
|
||||
return `${Number(Math.abs(percent).toFixed(2)).toLocaleString(locale, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
useGrouping: false,
|
||||
})}%`
|
||||
}
|
||||
|
||||
interface FormatPriceOptions {
|
||||
price: Nullish<Price<Currency, Currency>>
|
||||
type: FormatterType
|
||||
@ -723,12 +735,18 @@ export function useFormatter() {
|
||||
[currencyToFormatWith, formatterLocale, localCurrencyConversionRateToFormatWith]
|
||||
)
|
||||
|
||||
const formatPercentWithLocales = useCallback(
|
||||
(percent: Nullish<number>) => formatPercent(percent, formatterLocale),
|
||||
[formatterLocale]
|
||||
)
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
formatCurrencyAmount: formatCurrencyAmountWithLocales,
|
||||
formatFiatPrice: formatFiatPriceWithLocales,
|
||||
formatNumber: formatNumberWithLocales,
|
||||
formatNumberOrString: formatNumberOrStringWithLocales,
|
||||
formatPercent: formatPercentWithLocales,
|
||||
formatPrice: formatPriceWithLocales,
|
||||
formatPriceImpact: formatPriceImpactWithLocales,
|
||||
formatReviewSwapCurrencyAmount: formatReviewSwapCurrencyAmountWithLocales,
|
||||
@ -740,6 +758,7 @@ export function useFormatter() {
|
||||
formatFiatPriceWithLocales,
|
||||
formatNumberOrStringWithLocales,
|
||||
formatNumberWithLocales,
|
||||
formatPercentWithLocales,
|
||||
formatPriceImpactWithLocales,
|
||||
formatPriceWithLocales,
|
||||
formatReviewSwapCurrencyAmountWithLocales,
|
||||
|
Loading…
Reference in New Issue
Block a user