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,
- };
-};