From ff13aa7e706a25d5ebc2d56c80d90827e0f6394c Mon Sep 17 00:00:00 2001 From: John Oshalusi Date: Fri, 6 Oct 2023 13:37:15 +0100 Subject: [PATCH 1/3] feat: lw-8684-add tooltip to delegation piechart --- .../delegation-card/DelegationCard.tsx | 15 ++-- .../delegation-card/DelegationTooltip.css.ts | 11 +++ .../delegation-card/DelegationTooltip.tsx | 39 +++++++++ .../src/features/delegation-card/types.ts | 9 +++ .../preferences/StepPreferencesContent.tsx | 4 +- .../src/features/overview/Overview.tsx | 4 +- .../src/features/overview/OverviewPopup.tsx | 4 +- packages/staking/src/features/theme/colors.ts | 3 + packages/ui/src/design-system/index.ts | 2 +- .../ui/src/design-system/pie-chart/index.ts | 6 +- .../pie-chart/pie-chart.component.tsx | 80 +++++++++++++++++-- .../pie-chart/pie-chart.stories.tsx | 14 +++- 12 files changed, 169 insertions(+), 22 deletions(-) create mode 100644 packages/staking/src/features/delegation-card/DelegationTooltip.css.ts create mode 100644 packages/staking/src/features/delegation-card/DelegationTooltip.tsx create mode 100644 packages/staking/src/features/delegation-card/types.ts diff --git a/packages/staking/src/features/delegation-card/DelegationCard.tsx b/packages/staking/src/features/delegation-card/DelegationCard.tsx index 1b0865d2db..a5e4b9ce89 100644 --- a/packages/staking/src/features/delegation-card/DelegationCard.tsx +++ b/packages/staking/src/features/delegation-card/DelegationCard.tsx @@ -3,8 +3,9 @@ import cn from 'classnames'; import { Fragment, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { TranslationKey } from '../i18n'; -// import { PERCENTAGE_SCALE_MAX } from '../store'; import * as styles from './DelegationCard.css'; +import { DelegationTooltip } from './DelegationTooltip'; +import { DistributionItem } from './types'; // TODO const PERCENTAGE_SCALE_MAX = 100; @@ -15,17 +16,11 @@ export type DelegationStatus = | 'under-allocated' | 'no-selection'; -type Distribution = Array<{ - name: string; - percentage: number; - color: PieChartColor; -}>; - type DelegationCardProps = { arrangement?: 'vertical' | 'horizontal'; balance: string; cardanoCoinSymbol: string; - distribution: Distribution; + distribution: DistributionItem[]; status: DelegationStatus; showDistribution?: boolean; }; @@ -72,7 +67,7 @@ export const DelegationCard = ({ const { data, colorSet = PIE_CHART_DEFAULT_COLOR_SET } = useMemo((): { colorSet?: PieChartColor[]; - data: Distribution; + data: DistributionItem[]; } => { const GREY_COLOR: PieChartColor = '#C0C0C0'; const RED_COLOR: PieChartColor = '#FF5470'; @@ -118,7 +113,7 @@ export const DelegationCard = ({ data-testid="delegation-info-card" >
- + {showDistribution && {totalPercentage}%}
>): ReactElement | null => { + const { t } = useTranslation(); + if (active && payload) { + const { apy, saturation } = payload; + + return ( + + + + {t('browsePools.stakePoolTableBrowser.tableHeader.ros.title')} + {apy ? `${apy}%` : '-'} + + + {t('browsePools.stakePoolTableBrowser.tableHeader.saturation.title')} + {saturation ? `${saturation}%` : '-'} + + + } + /> + + ); + } + + // eslint-disable-next-line unicorn/no-null + return null; +}; diff --git a/packages/staking/src/features/delegation-card/types.ts b/packages/staking/src/features/delegation-card/types.ts new file mode 100644 index 0000000000..5a987de47a --- /dev/null +++ b/packages/staking/src/features/delegation-card/types.ts @@ -0,0 +1,9 @@ +import { PieChartColor } from '@lace/ui'; + +export type DistributionItem = { + name: string; + percentage: number; + color: PieChartColor; + apy?: string; + saturation?: string; +}; diff --git a/packages/staking/src/features/drawer/preferences/StepPreferencesContent.tsx b/packages/staking/src/features/drawer/preferences/StepPreferencesContent.tsx index 5eed8c21f9..390b839cee 100644 --- a/packages/staking/src/features/drawer/preferences/StepPreferencesContent.tsx +++ b/packages/staking/src/features/drawer/preferences/StepPreferencesContent.tsx @@ -48,18 +48,20 @@ export const StepPreferencesContent = () => { const displayData = draftPortfolio.map((draftPool, i) => { const { - displayData: { name }, + displayData: { name, apy, saturation }, id, sliderIntegerPercentage, } = draftPool; return { + apy: apy ? String(apy) : undefined, cardanoCoinSymbol, color: PIE_CHART_DEFAULT_COLOR_SET[i] as PieChartColor, id, name: name || '-', onChainPercentage: draftPool.basedOnCurrentPortfolio ? draftPool.onChainPercentage : undefined, percentage: sliderIntegerPercentage, + saturation: saturation ? String(saturation) : undefined, savedIntegerPercentage: draftPool.basedOnCurrentPortfolio ? draftPool.savedIntegerPercentage : undefined, // TODO sliderIntegerPercentage, diff --git a/packages/staking/src/features/overview/Overview.tsx b/packages/staking/src/features/overview/Overview.tsx index 9abe204d2b..4163bb5a0a 100644 --- a/packages/staking/src/features/overview/Overview.tsx +++ b/packages/staking/src/features/overview/Overview.tsx @@ -95,10 +95,12 @@ export const Overview = () => { ({ + distribution={displayData.map(({ color, name = '-', onChainPercentage, apy, saturation }) => ({ + apy: apy ? String(apy) : undefined, color, name, percentage: onChainPercentage, + saturation: saturation ? String(saturation) : undefined, }))} status={currentPortfolio.length === 1 ? 'simple-delegation' : 'multi-delegation'} /> diff --git a/packages/staking/src/features/overview/OverviewPopup.tsx b/packages/staking/src/features/overview/OverviewPopup.tsx index 4bf20e17e3..24cd9c472e 100644 --- a/packages/staking/src/features/overview/OverviewPopup.tsx +++ b/packages/staking/src/features/overview/OverviewPopup.tsx @@ -98,10 +98,12 @@ export const OverviewPopup = () => { balance={compactNumber(balancesBalance.available.coinBalance)} cardanoCoinSymbol={walletStoreWalletUICardanoCoin.symbol} arrangement="vertical" - distribution={displayData.map(({ color, name = '-', onChainPercentage }) => ({ + distribution={displayData.map(({ color, name = '-', onChainPercentage, apy, saturation }) => ({ + apy: apy ? String(apy) : undefined, color, name, percentage: onChainPercentage, + saturation: saturation ? String(saturation) : undefined, }))} status={currentPortfolio.length === 1 ? 'simple-delegation' : 'multi-delegation'} /> diff --git a/packages/staking/src/features/theme/colors.ts b/packages/staking/src/features/theme/colors.ts index f07e7605d3..08336b6004 100644 --- a/packages/staking/src/features/theme/colors.ts +++ b/packages/staking/src/features/theme/colors.ts @@ -23,6 +23,7 @@ export const colorsContract = { $sliderFillSecondary: '', $sliderKnobFill: '', $sliderRailFill: '', + $tooltipBgColor: '', }; export const lightThemeColors: typeof colorsContract = { @@ -48,6 +49,7 @@ export const lightThemeColors: typeof colorsContract = { $sliderFillSecondary: lightColorScheme.$primary_dark_grey, $sliderKnobFill: lightColorScheme.$primary_white, $sliderRailFill: lightColorScheme.$primary_light_grey_plus, + $tooltipBgColor: lightColorScheme.$primary_white, }; export const darkThemeColors: typeof colorsContract = { @@ -78,4 +80,5 @@ export const darkThemeColors: typeof colorsContract = { $sliderFillSecondary: darkColorScheme.$primary_light_grey, $sliderKnobFill: lightColorScheme.$primary_black, $sliderRailFill: darkColorScheme.$primary_dark_grey_plus, + $tooltipBgColor: darkColorScheme.$primary_mid_grey, }; diff --git a/packages/ui/src/design-system/index.ts b/packages/ui/src/design-system/index.ts index 8ce8e96a48..69b4af6b60 100644 --- a/packages/ui/src/design-system/index.ts +++ b/packages/ui/src/design-system/index.ts @@ -21,7 +21,7 @@ export * as FlowCard from './flow-card'; export * as IconButton from './icon-buttons'; export * as TransactionSummary from './transaction-summary'; export { ToastBar } from './toast-bar'; -export { Tooltip } from './tooltip'; +export * from './tooltip'; export { Message } from './message'; export { PasswordBox } from './password-box'; export { Metadata } from './metadata'; diff --git a/packages/ui/src/design-system/pie-chart/index.ts b/packages/ui/src/design-system/pie-chart/index.ts index 1ce2905df6..11f173fba6 100644 --- a/packages/ui/src/design-system/pie-chart/index.ts +++ b/packages/ui/src/design-system/pie-chart/index.ts @@ -1,4 +1,8 @@ -export type { PieChartColor, PieChartProps } from './pie-chart.component'; +export type { + PieChartColor, + PieChartProps, + TooltipContentRendererProps, +} from './pie-chart.component'; export { PieChart } from './pie-chart.component'; export { PIE_CHART_DEFAULT_COLOR_SET, diff --git a/packages/ui/src/design-system/pie-chart/pie-chart.component.tsx b/packages/ui/src/design-system/pie-chart/pie-chart.component.tsx index 76e98218f1..155e08f821 100644 --- a/packages/ui/src/design-system/pie-chart/pie-chart.component.tsx +++ b/packages/ui/src/design-system/pie-chart/pie-chart.component.tsx @@ -1,6 +1,8 @@ /* eslint-disable functional/prefer-immutable-types */ -import React from 'react'; +import type { ReactElement, ReactNode } from 'react'; +import React, { isValidElement, useMemo, useState } from 'react'; +import isFunction from 'lodash/isFunction'; import { Cell, Pie, @@ -15,20 +17,58 @@ import { } from './pie-chart.data'; import type { ColorValueHex } from '../../types'; -import type { CellProps, TooltipProps } from 'recharts'; +import type { CellProps } from 'recharts'; import type { PickByValue } from 'utility-types'; type PieChartDataProps = Partial<{ overrides: CellProps; }>; export type PieChartColor = ColorValueHex | PieChartGradientColor; + +export interface TooltipContentRendererProps { + active?: boolean; + name?: string; + payload?: T; +} +export type TooltipContentRenderer = ( + props: TooltipContentRendererProps, +) => ReactNode; +type TooltipContent = ReactElement | TooltipContentRenderer; + +interface RechartTooltipContentRendererProps { + name?: string; + active?: boolean; + payload?: { name?: string; payload?: T }[]; +} + +type RechartTooltipContentRenderer = ( + props: RechartTooltipContentRendererProps, +) => ReactNode; + +// Recharts passes to the renderer for some reason the payload as +// a list which is a bit cumbersome because in practice we care just about the +// first element and the adapter below removes this inconvenience +const transformTooltipContentRenderer = + ( + tooltipContentRenderer: TooltipContentRenderer, + ): RechartTooltipContentRenderer => + ({ + active, + payload, + }: { + active?: boolean; + payload?: { name?: string; payload?: T }[]; + }) => + tooltipContentRenderer({ active, ...payload?.[0] }); + interface PieChartBaseProps { animate?: boolean; colors?: PieChartColor[]; data: (PieChartDataProps & T)[]; direction?: 'clockwise' | 'counterclockwise'; - tooltip?: TooltipProps['content']; + tooltip?: TooltipContent; } + interface PieChartCustomKeyProps extends PieChartBaseProps { nameKey: keyof PickByValue; @@ -63,6 +103,8 @@ const formatPieColor = (color: PieChartColor): string => * @param tooltip component accepted by Recharts Tooltip `content` prop * @param valueKey object key of a `data` item that will be used as value (displayed in the tooltip) */ + +// eslint-disable-next-line react/no-multi-comp export const PieChart = ({ animate = true, colors = PIE_CHART_DEFAULT_COLOR_SET, @@ -73,17 +115,45 @@ export const PieChart = ({ valueKey = 'value', }: PieChartProps): JSX.Element => { const data = inputData.slice(0, colors.length); + const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 }); + + const tooltipContent = useMemo(() => { + if (!tooltip || isValidElement(tooltip)) { + return tooltip; + } + + if (isFunction(tooltip)) { + return transformTooltipContentRenderer(tooltip); + } + }, [tooltip]); + + const handleMouseMove = (event: React.MouseEvent): void => { + if (event.target instanceof SVGSVGElement) { + const { x, y } = event.target.getBoundingClientRect(); + setTooltipPosition({ x: event.clientX - x, y: event.clientY - y }); + } + }; return ( - + { + handleMouseMove(event as React.MouseEvent); + }} + > - {Boolean(tooltip) && } + {tooltipContent && ( + + )} = { export default meta; +const CustomTooltip = (): ReactElement => ( + + + +); + export const Overview = (): JSX.Element => ( @@ -231,12 +239,13 @@ export const Overview = (): JSX.Element => ( type ConfigurableStoryProps = Pick< PieChartProps<{ name: string; value: number }>, - 'colors' | 'data' | 'direction' | 'tooltip' ->; + 'colors' | 'data' | 'direction' +> & { tooltip: boolean }; export const Controls = ({ colors, data, + tooltip, ...props }: Readonly): JSX.Element => ( @@ -245,6 +254,7 @@ export const Controls = ({ animate={isNotInChromatic} colors={colors} data={data} + tooltip={tooltip ? CustomTooltip : undefined} {...props} /> From c468e5cc6596281935f584ea3a6e81420c6b711b Mon Sep 17 00:00:00 2001 From: John Oshalusi Date: Tue, 24 Oct 2023 21:44:31 +0100 Subject: [PATCH 2/3] fix: tooltip improvements --- .../ui/src/design-system/pie-chart/pie-chart.component.tsx | 3 ++- packages/ui/src/design-tokens/elevation.data.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/design-system/pie-chart/pie-chart.component.tsx b/packages/ui/src/design-system/pie-chart/pie-chart.component.tsx index 155e08f821..e41a9af1c3 100644 --- a/packages/ui/src/design-system/pie-chart/pie-chart.component.tsx +++ b/packages/ui/src/design-system/pie-chart/pie-chart.component.tsx @@ -128,7 +128,8 @@ export const PieChart = ({ }, [tooltip]); const handleMouseMove = (event: React.MouseEvent): void => { - if (event.target instanceof SVGSVGElement) { + const clientWidth = window.innerWidth; + if (event.target instanceof SVGSVGElement && clientWidth > 360) { const { x, y } = event.target.getBoundingClientRect(); setTooltipPosition({ x: event.clientX - x, y: event.clientY - y }); } diff --git a/packages/ui/src/design-tokens/elevation.data.ts b/packages/ui/src/design-tokens/elevation.data.ts index 3e716303ba..ba5f2371d6 100644 --- a/packages/ui/src/design-tokens/elevation.data.ts +++ b/packages/ui/src/design-tokens/elevation.data.ts @@ -1,5 +1,6 @@ export const elevation = { - $tooltip: '', + $tooltip: + '0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05)', $dialog: '', $primaryButton: '', $assets: '', From e5f86e560647aac6118917bad7c57c2ae7e2c7bd Mon Sep 17 00:00:00 2001 From: John Oshalusi Date: Wed, 25 Oct 2023 13:37:48 +0100 Subject: [PATCH 3/3] fix: add custom tooltip dot style --- .../src/features/delegation-card/DelegationTooltip.tsx | 5 +++-- .../ui/src/design-system/pie-chart/pie-chart.component.tsx | 1 - .../tooltip/rich-tooltip-content-inner.component.tsx | 7 ++++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/staking/src/features/delegation-card/DelegationTooltip.tsx b/packages/staking/src/features/delegation-card/DelegationTooltip.tsx index 57be33feb3..25e8232c78 100644 --- a/packages/staking/src/features/delegation-card/DelegationTooltip.tsx +++ b/packages/staking/src/features/delegation-card/DelegationTooltip.tsx @@ -8,15 +8,16 @@ export const DelegationTooltip = ({ active, name, payload, -}: Readonly>): ReactElement | null => { +}: Readonly>): ReactElement | null => { const { t } = useTranslation(); if (active && payload) { - const { apy, saturation } = payload; + const { apy, saturation, fill } = payload; return ( diff --git a/packages/ui/src/design-system/pie-chart/pie-chart.component.tsx b/packages/ui/src/design-system/pie-chart/pie-chart.component.tsx index e41a9af1c3..d0520448f2 100644 --- a/packages/ui/src/design-system/pie-chart/pie-chart.component.tsx +++ b/packages/ui/src/design-system/pie-chart/pie-chart.component.tsx @@ -104,7 +104,6 @@ const formatPieColor = (color: PieChartColor): string => * @param valueKey object key of a `data` item that will be used as value (displayed in the tooltip) */ -// eslint-disable-next-line react/no-multi-comp export const PieChart = ({ animate = true, colors = PIE_CHART_DEFAULT_COLOR_SET, diff --git a/packages/ui/src/design-system/tooltip/rich-tooltip-content-inner.component.tsx b/packages/ui/src/design-system/tooltip/rich-tooltip-content-inner.component.tsx index d94b8845d6..6d72a8d4a6 100644 --- a/packages/ui/src/design-system/tooltip/rich-tooltip-content-inner.component.tsx +++ b/packages/ui/src/design-system/tooltip/rich-tooltip-content-inner.component.tsx @@ -10,16 +10,21 @@ import * as cx from './rich-tooltip-content-inner.css'; export interface RichContentInnerProps { title: string; description: ReactNode; + dotColor?: string; } export const RichContentInner = ({ title, description, + dotColor, }: Readonly): JSX.Element => { + const customDotStyle = + dotColor == undefined ? {} : { backgroundColor: dotColor }; + return ( - +