diff --git a/pages/03-core/core-line-chart.page.tsx b/pages/03-core/core-line-chart.page.tsx
index a7d8bb6d..7d0f70fb 100644
--- a/pages/03-core/core-line-chart.page.tsx
+++ b/pages/03-core/core-line-chart.page.tsx
@@ -122,18 +122,18 @@ export default function () {
chartHeight={400}
tooltip={{ placement: "outside" }}
getTooltipContent={() => ({
- point({ item }) {
+ point({ item, hideTooltip }) {
const value = item ? (item.point.y ?? null) : null;
return {
value: (
- {numberFormatter(value)}
+ {numberFormatter(value)}
),
};
},
- footer() {
- return ;
+ footer({ hideTooltip }) {
+ return ;
},
})}
getLegendTooltipContent={({ legendItem }) => ({
diff --git a/src/core/__tests__/chart-core-tooltip.test.tsx b/src/core/__tests__/chart-core-tooltip.test.tsx
index 12bb13b3..2fe28bc2 100644
--- a/src/core/__tests__/chart-core-tooltip.test.tsx
+++ b/src/core/__tests__/chart-core-tooltip.test.tsx
@@ -6,8 +6,8 @@ import { waitFor } from "@testing-library/react";
import highcharts from "highcharts";
import { vi } from "vitest";
-import { CoreChartProps } from "../../../lib/components/core/interfaces";
import testClasses from "../../../lib/components/core/test-classes/styles.selectors";
+import { CoreChartProps } from "../../../lib/components/internal-do-not-use/core-chart";
import { createChartWrapper, renderChart } from "./common";
import { HighchartsTestHelper } from "./highcharts-utils";
@@ -460,4 +460,132 @@ describe("CoreChart: tooltip", () => {
expect(wrapper.findTooltip()!.findBody()!.getElement().textContent).toBe("[P3] [60] [custom key] [custom value]");
});
+
+ describe("dismissTooltip", () => {
+ test.each<{
+ name: string;
+ series: highcharts.SeriesOptionsType[];
+ getTooltipContent: () => CoreChartProps.GetTooltipContent;
+ }>(
+ [
+ [lineSeries, "line"],
+ [pieSeries, "pie"],
+ ].flatMap(([series, type]) => {
+ return [
+ {
+ name: `header renderer - ${type} chart`,
+ series,
+ getTooltipContent: () => ({
+ body: () => "Body",
+ footer: () => "Footer",
+ header: ({ hideTooltip }) => {
+ return (
+
+ );
+ },
+ }),
+ },
+ {
+ name: `body renderer - ${type} chart`,
+ series,
+ getTooltipContent: () => ({
+ header: () => "Header",
+ footer: () => "Footer",
+ body: ({ hideTooltip }) => {
+ return (
+
+ );
+ },
+ }),
+ },
+ {
+ name: `footer renderer - ${type} chart`,
+ series,
+ getTooltipContent: () => ({
+ header: () => "Header",
+ body: () => "Body",
+ footer: ({ hideTooltip }) => {
+ return (
+
+ );
+ },
+ }),
+ },
+ ];
+ }),
+ )("provides dismissTooltip callback to $name", async ({ series, getTooltipContent }) => {
+ const { wrapper } = renderChart({
+ highcharts,
+ options: { series },
+ getTooltipContent: getTooltipContent,
+ });
+
+ act(() => hc.highlightChartPoint(0, 0));
+
+ await waitFor(() => {
+ expect(wrapper.findTooltip()).not.toBe(null);
+ });
+
+ act(() => {
+ hoverTooltip();
+ });
+
+ await waitFor(() => {
+ expect(wrapper.findTooltip()).not.toBe(null);
+ });
+
+ act(() => {
+ wrapper.findTooltip()!.find(`[data-testid="hideTooltip"]`).click();
+ });
+
+ await waitFor(() => {
+ expect(wrapper.findTooltip()).toBe(null);
+ });
+ });
+
+ test("dismissTooltip callback works when tooltip is pinned", async () => {
+ let dismissCallback: (() => void) | undefined;
+ const { wrapper } = renderChart({
+ highcharts,
+ options: { series: pieSeries },
+ getTooltipContent: () => ({
+ header: ({ hideTooltip }) => {
+ dismissCallback = hideTooltip;
+ return "Header";
+ },
+ body: () => "Body",
+ }),
+ });
+
+ act(() => hc.highlightChartPoint(0, 0));
+
+ await waitFor(() => {
+ expect(wrapper.findTooltip()).not.toBe(null);
+ expect(wrapper.findTooltip()!.findDismissButton()).toBe(null);
+ expect(dismissCallback).toBeDefined();
+ });
+
+ // Pin tooltip
+ act(() => hc.clickChartPoint(0, 0));
+
+ await waitFor(() => {
+ expect(wrapper.findTooltip()).not.toBe(null);
+ expect(wrapper.findTooltip()!.findDismissButton()).not.toBe(null);
+ });
+
+ act(() => {
+ dismissCallback!();
+ });
+
+ await waitFor(() => {
+ expect(wrapper.findTooltip()).toBe(null);
+ });
+ });
+ });
});
diff --git a/src/core/chart-api/index.tsx b/src/core/chart-api/index.tsx
index f0056750..95a0cbde 100644
--- a/src/core/chart-api/index.tsx
+++ b/src/core/chart-api/index.tsx
@@ -184,6 +184,14 @@ export class ChartAPI {
}
};
+ // Hide the tooltip from an action initiated by the tooltip's content
+ public hideTooltip = () => {
+ this.chartExtraTooltip.hideTooltip();
+ // The chart highlight is preserved while the tooltip is pinned. We need to clear it manually here, for the case
+ // when the pointer lands outside the chart after the tooltip is dismissed, so that the mouse-out event won't fire.
+ this.clearChartHighlight({ isApiCall: false });
+ };
+
// Reference to the role="application" element used for navigation.
public setApplication = this.chartExtraNavigation.setApplication.bind(this.chartExtraNavigation);
diff --git a/src/core/components/core-tooltip.tsx b/src/core/components/core-tooltip.tsx
index 25ce55eb..f50cb5fa 100644
--- a/src/core/components/core-tooltip.tsx
+++ b/src/core/components/core-tooltip.tsx
@@ -70,6 +70,9 @@ export function ChartTooltip({
group: tooltip.group,
expandedSeries,
setExpandedSeries,
+ hideTooltip: () => {
+ api.hideTooltip();
+ },
});
if (!content) {
return null;
@@ -117,6 +120,7 @@ function getTooltipContent(
api: ChartAPI,
props: CoreChartProps.GetTooltipContentProps & {
renderers?: CoreChartProps.TooltipContentRenderer;
+ hideTooltip: () => void;
} & ExpandedSeriesStateProps,
): null | RenderedTooltipContent {
if (props.point && props.point.series.type === "pie") {
@@ -136,8 +140,10 @@ function getTooltipContentCartesian(
expandedSeries,
renderers = {},
setExpandedSeries,
+ hideTooltip,
}: CoreChartProps.GetTooltipContentProps & {
renderers?: CoreChartProps.TooltipContentRenderer;
+ hideTooltip: () => void;
} & ExpandedSeriesStateProps,
): RenderedTooltipContent {
// The cartesian tooltip might or might not have a selected point, but it always has a non-empty group.
@@ -150,7 +156,12 @@ function getTooltipContentCartesian(
const detailItems: ChartSeriesDetailItem[] = matchedItems.map((item) => {
const valueFormatter = getFormatter(item.point.series.yAxis);
const itemY = isXThreshold(item.point.series) ? null : (item.point.y ?? null);
- const customContent = renderers.point ? renderers.point({ item }) : undefined;
+ const customContent = renderers.point
+ ? renderers.point({
+ item,
+ hideTooltip,
+ })
+ : undefined;
return {
key: customContent?.key ?? item.point.series.name,
value: customContent?.value ?? valueFormatter(itemY),
@@ -177,7 +188,11 @@ function getTooltipContentCartesian(
});
// We only support cartesian charts with a single x axis.
const titleFormatter = getFormatter(chart.xAxis[0]);
- const slotRenderProps: CoreChartProps.TooltipSlotProps = { x, items: matchedItems };
+ const slotRenderProps: CoreChartProps.TooltipSlotProps = {
+ x,
+ items: matchedItems,
+ hideTooltip: hideTooltip,
+ };
return {
header: renderers.header?.(slotRenderProps) ?? titleFormatter(x),
body: renderers.body?.(slotRenderProps) ?? (
@@ -203,9 +218,17 @@ function getTooltipContentCartesian(
function getTooltipContentPie(
api: ChartAPI,
- { point, renderers = {} }: { point: Highcharts.Point } & { renderers?: CoreChartProps.TooltipContentRenderer },
+ {
+ point,
+ renderers = {},
+ hideTooltip,
+ }: { point: Highcharts.Point } & { renderers?: CoreChartProps.TooltipContentRenderer; hideTooltip: () => void },
): RenderedTooltipContent {
- const tooltipDetails: CoreChartProps.TooltipSlotProps = { x: point.x, items: [{ point, errorRanges: [] }] };
+ const tooltipDetails: CoreChartProps.TooltipSlotProps = {
+ x: point.x,
+ items: [{ point, errorRanges: [] }],
+ hideTooltip,
+ };
return {
header: renderers.header?.(tooltipDetails) ?? (
@@ -218,7 +241,13 @@ function getTooltipContentPie(
body:
renderers.body?.(tooltipDetails) ??
(renderers.details ? (
-
+
) : (
// We expect all pie chart segments to have defined y values. We use y=0 as fallback
// because the property is optional in Highcharts types.
diff --git a/src/core/interfaces.ts b/src/core/interfaces.ts
index e299768e..0d7c028f 100644
--- a/src/core/interfaces.ts
+++ b/src/core/interfaces.ts
@@ -451,14 +451,17 @@ export namespace CoreChartProps {
}
export interface TooltipPointProps {
item: TooltipContentItem;
+ hideTooltip: () => void;
}
export interface TooltipSlotProps {
x: number;
items: TooltipContentItem[];
+ hideTooltip: () => void;
}
export interface TooltipDetailsProps {
point: Highcharts.Point;
+ hideTooltip: () => void;
}
export type TooltipDetail = BaseTooltipDetail;
diff --git a/src/pie-chart/chart-pie-internal.tsx b/src/pie-chart/chart-pie-internal.tsx
index 68c401f2..8392f71c 100644
--- a/src/pie-chart/chart-pie-internal.tsx
+++ b/src/pie-chart/chart-pie-internal.tsx
@@ -73,7 +73,7 @@ export const InternalPieChart = forwardRef(
};
const transformSlotProps = (props: CoreChartProps.TooltipSlotProps): PieChartProps.TooltipDetailsRenderProps => {
const point = props.items[0].point;
- return transformDetailsProps({ point });
+ return transformDetailsProps({ point, hideTooltip: props.hideTooltip });
};
return {
header: tooltip?.header ? (props) => tooltip.header!(transformSlotProps(props)) : undefined,