From 43ed4963d6541428b6c409b48d9740a047b8eb87 Mon Sep 17 00:00:00 2001 From: nitin <142569587+ehconitin@users.noreply.github.com> Date: Fri, 5 Dec 2025 22:58:26 +0530 Subject: [PATCH] [Dashboards] Improve tooltip animations (#16357) before - https://github.com/user-attachments/assets/069a8737-fcc9-4186-bdbf-e9ed0dfa41a8 after - https://github.com/user-attachments/assets/dbc8604c-dda0-46a9-a67b-f59cc91d7f53 --- .../components/GraphWidgetFloatingTooltip.tsx | 74 +++++++++++-------- .../graph/constants/GraphTooltipOffsetPx.ts | 1 - .../components/GraphBarChartTooltip.tsx | 46 ++++-------- .../components/GraphWidgetBarChart.tsx | 2 +- .../constants/BarChartTooltipOffsetPx.ts | 1 + .../components/GraphLineChartTooltip.tsx | 46 +++++------- .../components/GraphWidgetLineChart.tsx | 2 +- .../constants/LineChartTooltipOffsetPx.ts | 1 + .../components/GraphPieChartTooltip.tsx | 51 +++++++------ .../components/GraphWidgetPieChart.tsx | 2 +- .../constants/PieChartTooltipOffsetPx.ts | 1 + .../hooks/useGraphWidgetTooltipFloating.ts | 30 ++++---- ...oltipReferenceFromBarChartElementAnchor.ts | 18 ----- ...ooltipReferenceFromLineChartPointAnchor.ts | 27 ------- 14 files changed, 127 insertions(+), 175 deletions(-) delete mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/constants/GraphTooltipOffsetPx.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartTooltipOffsetPx.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartTooltipOffsetPx.ts create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/constants/PieChartTooltipOffsetPx.ts delete mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getTooltipReferenceFromBarChartElementAnchor.ts delete mode 100644 packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getTooltipReferenceFromLineChartPointAnchor.ts diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetFloatingTooltip.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetFloatingTooltip.tsx index d363b1c101..a6a553dc36 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetFloatingTooltip.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetFloatingTooltip.tsx @@ -5,12 +5,13 @@ import { import { useGraphWidgetTooltipFloating } from '@/page-layout/widgets/graph/hooks/useGraphWidgetTooltipFloating'; import { useTheme } from '@emotion/react'; import { FloatingPortal, type VirtualElement } from '@floating-ui/react'; -import { AnimatePresence, motion } from 'framer-motion'; +import { animated, useSpring } from '@react-spring/web'; import { isDefined } from 'twenty-shared/utils'; type GraphWidgetFloatingTooltipProps = { - reference: Element | VirtualElement; - boundary: Element; + reference: Element | VirtualElement | null; + boundary: Element | null; + tooltipOffsetFromAnchorInPx: number; items: GraphWidgetTooltipItem[]; indexLabel?: string; highlightedKey?: string; @@ -23,6 +24,7 @@ type GraphWidgetFloatingTooltipProps = { export const GraphWidgetFloatingTooltip = ({ reference, boundary, + tooltipOffsetFromAnchorInPx, items, indexLabel, highlightedKey, @@ -33,51 +35,65 @@ export const GraphWidgetFloatingTooltip = ({ }: GraphWidgetFloatingTooltipProps) => { const theme = useTheme(); - const { refs, floatingStyles } = useGraphWidgetTooltipFloating( + const { refs, x, y, isPositioned } = useGraphWidgetTooltipFloating( reference, boundary, + tooltipOffsetFromAnchorInPx, ); + const xPos = x ?? 0; + const yPos = y ?? 0; + + const shouldShow = isDefined(reference) && items.length > 0; + const isVisible = shouldShow && isPositioned; + + const springStyles = useSpring({ + from: { + transform: `translate(${xPos}px, ${yPos}px)`, + opacity: 0, + }, + to: { + transform: `translate(${xPos}px, ${yPos}px)`, + opacity: isVisible ? 1 : 0, + }, + config: { + tension: 300, + friction: 30, + }, + immediate: !isPositioned || !shouldShow, + reset: !shouldShow, + }); + if (!isDefined(boundary) || !(boundary instanceof HTMLElement)) { return null; } return ( -
- - - - - -
+ {items.length > 0 && ( + + )} +
); }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/constants/GraphTooltipOffsetPx.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/constants/GraphTooltipOffsetPx.ts deleted file mode 100644 index 8ab8b96dc2..0000000000 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/constants/GraphTooltipOffsetPx.ts +++ /dev/null @@ -1 +0,0 @@ -export const GRAPH_TOOLTIP_OFFSET_PX = 2; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphBarChartTooltip.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphBarChartTooltip.tsx index fe88fe58ad..2cb006f88d 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphBarChartTooltip.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphBarChartTooltip.tsx @@ -1,15 +1,16 @@ import { GraphWidgetFloatingTooltip } from '@/page-layout/widgets/graph/components/GraphWidgetFloatingTooltip'; +import { BAR_CHART_TOOLTIP_OFFSET_PX } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartTooltipOffsetPx'; import { graphWidgetBarTooltipComponentState } from '@/page-layout/widgets/graph/graphWidgetBarChart/states/graphWidgetBarTooltipComponentState'; import { type BarChartEnrichedKey } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartEnrichedKey'; import { getBarChartTooltipData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartTooltipData'; -import { getTooltipReferenceFromBarChartElementAnchor } from '@/page-layout/widgets/graph/utils/getTooltipReferenceFromBarChartElementAnchor'; import { type GraphValueFormatOptions } from '@/page-layout/widgets/graph/utils/graphFormatters'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { type BarDatum, type ComputedDatum } from '@nivo/bar'; +import { type RefObject } from 'react'; import { isDefined } from 'twenty-shared/utils'; type GraphBarChartTooltipProps = { - containerId: string; + containerRef: RefObject; enrichedKeys: BarChartEnrichedKey[]; formatOptions: GraphValueFormatOptions; enableGroupTooltip?: boolean; @@ -20,7 +21,7 @@ type GraphBarChartTooltipProps = { }; export const GraphBarChartTooltip = ({ - containerId, + containerRef, enrichedKeys, formatOptions, enableGroupTooltip = true, @@ -33,6 +34,11 @@ export const GraphBarChartTooltip = ({ graphWidgetBarTooltipComponentState, ); + const containerElement = containerRef.current; + if (!isDefined(containerElement)) { + return null; + } + const handleTooltipClick: (() => void) | undefined = isDefined(onBarClick) ? () => { if (isDefined(tooltipState)) { @@ -51,38 +57,16 @@ export const GraphBarChartTooltip = ({ layout, }); - let reference = null; - let boundary = null; - - if (isDefined(tooltipState)) { - try { - const positioning = getTooltipReferenceFromBarChartElementAnchor( - tooltipState.anchorElement, - containerId, - ); - reference = positioning.reference; - boundary = positioning.boundary; - } catch { - reference = null; - boundary = null; - } - } - - if ( - !isDefined(tooltipData) || - !isDefined(reference) || - !isDefined(boundary) - ) { - return null; - } + const reference = isDefined(tooltipState) ? tooltipState.anchorElement : null; return ( ; enrichedSeries: LineChartEnrichedSeries[]; formatOptions: GraphValueFormatOptions; onSliceClick?: (point: Point) => void; @@ -18,7 +20,7 @@ type GraphLineChartTooltipProps = { }; export const GraphLineChartTooltip = ({ - containerId, + containerRef, enrichedSeries, formatOptions, onSliceClick, @@ -29,6 +31,11 @@ export const GraphLineChartTooltip = ({ graphWidgetLineTooltipComponentState, ); + const containerElement = containerRef.current; + if (!isDefined(containerElement)) { + return null; + } + const handleTooltipClick: (() => void) | undefined = isDefined(onSliceClick) ? () => { if (!isDefined(tooltipState)) return; @@ -52,38 +59,21 @@ export const GraphLineChartTooltip = ({ formatOptions, }); - let reference = null; - let boundary = null; - - if (isDefined(tooltipState)) { - try { - const positioning = getTooltipReferenceFromLineChartPointAnchor( - containerId, + const reference = !isDefined(tooltipState) + ? null + : createVirtualElementFromContainerOffset( + containerElement, tooltipState.offsetLeft, tooltipState.offsetTop, ); - reference = positioning.reference; - boundary = positioning.boundary; - } catch { - reference = null; - boundary = null; - } - } - - if ( - !isDefined(tooltipData) || - !isDefined(reference) || - !isDefined(boundary) - ) { - return null; - } return ( ; enrichedData: PieChartEnrichedData[]; formatOptions: GraphValueFormatOptions; displayType?: string; @@ -17,7 +19,7 @@ type GraphPieChartTooltipProps = { }; export const GraphPieChartTooltip = ({ - containerId, + containerRef, enrichedData, formatOptions, displayType, @@ -27,41 +29,42 @@ export const GraphPieChartTooltip = ({ graphWidgetPieTooltipComponentState, ); - if (!isDefined(tooltipState)) { - return null; - } - - const containerElement = document.getElementById(containerId); + const containerElement = containerRef.current; if (!isDefined(containerElement)) { return null; } - const tooltipData = getPieChartTooltipData({ - datum: tooltipState.datum, - enrichedData, - formatOptions, - displayType, - }); + const tooltipData = !isDefined(tooltipState) + ? null + : getPieChartTooltipData({ + datum: tooltipState.datum, + enrichedData, + formatOptions, + displayType, + }); const handleTooltipClick: (() => void) | undefined = isDefined(onSliceClick) - ? () => onSliceClick(tooltipState.datum.data) + ? () => { + if (isDefined(tooltipState)) { + onSliceClick(tooltipState.datum.data); + } + } : undefined; - if (!isDefined(tooltipData)) { - return null; - } - - const reference = createVirtualElementFromContainerOffset( - containerElement, - tooltipState.offsetLeft, - tooltipState.offsetTop, - ); + const reference = !isDefined(tooltipState) + ? null + : createVirtualElementFromContainerOffset( + containerElement, + tooltipState.offsetLeft, + tooltipState.offsetTop, + ); return ( diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart.tsx index c1e7e0b006..a9eb57ba9f 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChart.tsx @@ -178,7 +178,7 @@ export const GraphWidgetPieChart = ({ { const virtualElement = useMemo(() => { - if (!isDefined(element)) return null; - if (element instanceof Element) { - return createVirtualElementFromSVGElement(element); + if (!isDefined(referenceElement)) return null; + if (referenceElement instanceof Element) { + return createVirtualElementFromSVGElement(referenceElement); } - return element; - }, [element]); + return referenceElement; + }, [referenceElement]); - return useFloating({ + const rootBoundary = document.querySelector('#root') ?? undefined; + + const { refs, x, y, isPositioned } = useFloating({ elements: { reference: virtualElement, }, placement: 'left', strategy: 'fixed', middleware: [ - offset(GRAPH_TOOLTIP_OFFSET_PX), + offset(tooltipOffsetFromAnchorInPx), flip({ fallbackPlacements: ['right', 'top', 'bottom'], - boundary: - boundaryElement ?? document.querySelector('#root') ?? undefined, + boundary: boundaryElement ?? rootBoundary, }), shift({ - boundary: - boundaryElement ?? document.querySelector('#root') ?? undefined, + boundary: boundaryElement ?? rootBoundary, padding: GRAPH_TOOLTIP_BOUNDARY_PADDING_PX, }), ], whileElementsMounted: autoUpdate, }); + + return { refs, x, y, isPositioned }; }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getTooltipReferenceFromBarChartElementAnchor.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getTooltipReferenceFromBarChartElementAnchor.ts deleted file mode 100644 index f47ffe189f..0000000000 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getTooltipReferenceFromBarChartElementAnchor.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { type VirtualElement } from '@floating-ui/react'; -import { isDefined } from 'twenty-shared/utils'; - -export const getTooltipReferenceFromBarChartElementAnchor = ( - anchorElement: Element, - containerId: string, -): { - reference: Element | VirtualElement; - boundary: Element; -} => { - const containerElement = document.getElementById(containerId); - - if (!isDefined(containerElement)) { - throw new Error(`Bar chart container not found: ${containerId}`); - } - - return { reference: anchorElement, boundary: containerElement }; -}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getTooltipReferenceFromLineChartPointAnchor.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getTooltipReferenceFromLineChartPointAnchor.ts deleted file mode 100644 index e4a257d02f..0000000000 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/getTooltipReferenceFromLineChartPointAnchor.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { createVirtualElementFromContainerOffset } from '@/page-layout/widgets/graph/utils/createVirtualElementFromContainerOffset'; -import { type VirtualElement } from '@floating-ui/react'; -import { isDefined } from 'twenty-shared/utils'; - -export const getTooltipReferenceFromLineChartPointAnchor = ( - containerId: string, - offsetLeft: number, - offsetTop: number, -): { - reference: VirtualElement; - boundary: Element; -} => { - const containerElement = document.getElementById(containerId); - - if (!isDefined(containerElement)) { - throw new Error(`Chart container not found: ${containerId}`); - } - - return { - reference: createVirtualElementFromContainerOffset( - containerElement, - offsetLeft, - offsetTop, - ), - boundary: containerElement, - }; -};