nitin
2025-12-05 22:58:26 +05:30
committed by GitHub
parent b706664c99
commit 43ed4963d6
14 changed files with 127 additions and 175 deletions
@@ -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 (
<FloatingPortal root={boundary}>
<div
<animated.div
ref={refs.setFloating}
style={{
...floatingStyles,
...springStyles,
position: 'fixed',
top: 0,
left: 0,
zIndex: theme.lastLayerZIndex,
pointerEvents: disablePointerEvents ? 'none' : 'auto',
pointerEvents: disablePointerEvents || !isVisible ? 'none' : 'auto',
}}
role="tooltip"
aria-live="polite"
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<AnimatePresence>
<motion.div
initial={{
opacity: 0,
scale: 0.95,
}}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{
duration: theme.animation.duration.fast,
ease: 'easeInOut',
}}
>
<GraphWidgetTooltip
items={items}
indexLabel={indexLabel}
highlightedKey={highlightedKey}
onGraphWidgetTooltipClick={onGraphWidgetTooltipClick}
/>
</motion.div>
</AnimatePresence>
</div>
{items.length > 0 && (
<GraphWidgetTooltip
items={items}
indexLabel={indexLabel}
highlightedKey={highlightedKey}
onGraphWidgetTooltipClick={onGraphWidgetTooltipClick}
/>
)}
</animated.div>
</FloatingPortal>
);
};
@@ -1 +0,0 @@
export const GRAPH_TOOLTIP_OFFSET_PX = 2;
@@ -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<HTMLDivElement>;
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 (
<GraphWidgetFloatingTooltip
reference={reference}
boundary={boundary}
items={tooltipData.tooltipItems}
indexLabel={tooltipData.indexLabel}
highlightedKey={tooltipData.hoveredKey}
boundary={containerElement}
tooltipOffsetFromAnchorInPx={BAR_CHART_TOOLTIP_OFFSET_PX}
items={tooltipData?.tooltipItems ?? []}
indexLabel={tooltipData?.indexLabel}
highlightedKey={tooltipData?.hoveredKey}
onGraphWidgetTooltipClick={handleTooltipClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
@@ -284,7 +284,7 @@ export const GraphWidgetBarChart = ({
</GraphWidgetChartContainer>
<GraphBarChartTooltip
containerId={id}
containerRef={containerRef}
enrichedKeys={enrichedKeys}
formatOptions={formatOptions}
enableGroupTooltip={groupMode === 'stacked'}
@@ -0,0 +1 @@
export const BAR_CHART_TOOLTIP_OFFSET_PX = 2;
@@ -1,15 +1,17 @@
import { GraphWidgetFloatingTooltip } from '@/page-layout/widgets/graph/components/GraphWidgetFloatingTooltip';
import { LINE_CHART_TOOLTIP_OFFSET_PX } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartTooltipOffsetPx';
import { graphWidgetLineTooltipComponentState } from '@/page-layout/widgets/graph/graphWidgetLineChart/states/graphWidgetLineTooltipComponentState';
import { type LineChartEnrichedSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartEnrichedSeries';
import { getLineChartTooltipData } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/getLineChartTooltipData';
import { getTooltipReferenceFromLineChartPointAnchor } from '@/page-layout/widgets/graph/utils/getTooltipReferenceFromLineChartPointAnchor';
import { createVirtualElementFromContainerOffset } from '@/page-layout/widgets/graph/utils/createVirtualElementFromContainerOffset';
import { type GraphValueFormatOptions } from '@/page-layout/widgets/graph/utils/graphFormatters';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { type LineSeries, type Point } from '@nivo/line';
import { type RefObject } from 'react';
import { isDefined } from 'twenty-shared/utils';
type GraphLineChartTooltipProps = {
containerId: string;
containerRef: RefObject<HTMLDivElement>;
enrichedSeries: LineChartEnrichedSeries[];
formatOptions: GraphValueFormatOptions;
onSliceClick?: (point: Point<LineSeries>) => 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 (
<GraphWidgetFloatingTooltip
reference={reference}
boundary={boundary}
items={tooltipData.items}
indexLabel={tooltipData.indexLabel}
boundary={containerElement}
tooltipOffsetFromAnchorInPx={LINE_CHART_TOOLTIP_OFFSET_PX}
items={tooltipData?.items ?? []}
indexLabel={tooltipData?.indexLabel}
highlightedKey={tooltipState?.highlightedSeriesId}
onGraphWidgetTooltipClick={handleTooltipClick}
onMouseEnter={onMouseEnter}
@@ -269,7 +269,7 @@ export const GraphWidgetLineChart = ({
/>
</GraphWidgetChartContainer>
<GraphLineChartTooltip
containerId={id}
containerRef={containerRef}
enrichedSeries={enrichedSeries}
formatOptions={formatOptions}
onSliceClick={onSliceClick}
@@ -0,0 +1 @@
export const LINE_CHART_TOOLTIP_OFFSET_PX = 4;
@@ -1,4 +1,5 @@
import { GraphWidgetFloatingTooltip } from '@/page-layout/widgets/graph/components/GraphWidgetFloatingTooltip';
import { PIE_CHART_TOOLTIP_OFFSET_PX } from '@/page-layout/widgets/graph/graphWidgetPieChart/constants/PieChartTooltipOffsetPx';
import { graphWidgetPieTooltipComponentState } from '@/page-layout/widgets/graph/graphWidgetPieChart/states/graphWidgetPieTooltipComponentState';
import { type PieChartDataItem } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartDataItem';
import { type PieChartEnrichedData } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartEnrichedData';
@@ -6,10 +7,11 @@ import { getPieChartTooltipData } from '@/page-layout/widgets/graph/graphWidgetP
import { createVirtualElementFromContainerOffset } from '@/page-layout/widgets/graph/utils/createVirtualElementFromContainerOffset';
import { type GraphValueFormatOptions } from '@/page-layout/widgets/graph/utils/graphFormatters';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { type RefObject } from 'react';
import { isDefined } from 'twenty-shared/utils';
type GraphPieChartTooltipProps = {
containerId: string;
containerRef: RefObject<HTMLDivElement>;
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 (
<GraphWidgetFloatingTooltip
reference={reference}
boundary={containerElement}
items={[tooltipData.tooltipItem]}
tooltipOffsetFromAnchorInPx={PIE_CHART_TOOLTIP_OFFSET_PX}
items={tooltipData?.tooltipItem ? [tooltipData.tooltipItem] : []}
disablePointerEvents
onGraphWidgetTooltipClick={handleTooltipClick}
/>
@@ -178,7 +178,7 @@ export const GraphWidgetPieChart = ({
</StyledPieChartWrapper>
</GraphWidgetChartContainer>
<GraphPieChartTooltip
containerId={id}
containerRef={containerRef}
enrichedData={enrichedData}
formatOptions={formatOptions}
displayType={displayType}
@@ -0,0 +1 @@
export const PIE_CHART_TOOLTIP_OFFSET_PX = 14;
@@ -1,5 +1,4 @@
import { GRAPH_TOOLTIP_BOUNDARY_PADDING_PX } from '@/page-layout/widgets/graph/constants/GraphTooltipBoundaryPaddingPx';
import { GRAPH_TOOLTIP_OFFSET_PX } from '@/page-layout/widgets/graph/constants/GraphTooltipOffsetPx';
import { createVirtualElementFromSVGElement } from '@/page-layout/widgets/graph/utils/createVirtualElementFromSVGElement';
import {
autoUpdate,
@@ -13,36 +12,39 @@ import { useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useGraphWidgetTooltipFloating = (
element: Element | VirtualElement | null,
boundaryElement?: Element | null,
referenceElement: Element | VirtualElement | null,
boundaryElement: Element | null,
tooltipOffsetFromAnchorInPx: number,
) => {
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 };
};
@@ -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 };
};
@@ -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,
};
};