Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions pages/03-core/core-line-chart.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: (
<div>
{numberFormatter(value)} <Button variant="inline-icon" iconName="settings" />
{numberFormatter(value)} <Button variant="inline-icon" iconName="settings" onClick={hideTooltip} />
</div>
),
};
},
footer() {
return <Button>Footer action</Button>;
footer({ hideTooltip }) {
return <Button onClick={hideTooltip}>Footer action</Button>;
},
})}
getLegendTooltipContent={({ legendItem }) => ({
Expand Down
130 changes: 129 additions & 1 deletion src/core/__tests__/chart-core-tooltip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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 (
<button data-testid="hideTooltip" onClick={hideTooltip}>
hideTooltip
</button>
);
},
}),
},
{
name: `body renderer - ${type} chart`,
series,
getTooltipContent: () => ({
header: () => "Header",
footer: () => "Footer",
body: ({ hideTooltip }) => {
return (
<button data-testid="hideTooltip" onClick={hideTooltip}>
hideTooltip
</button>
);
},
}),
},
{
name: `footer renderer - ${type} chart`,
series,
getTooltipContent: () => ({
header: () => "Header",
body: () => "Body",
footer: ({ hideTooltip }) => {
return (
<button data-testid="hideTooltip" onClick={hideTooltip}>
hideTooltip
</button>
);
},
}),
},
];
}),
)("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);
});
});
});
});
8 changes: 8 additions & 0 deletions src/core/chart-api/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
39 changes: 34 additions & 5 deletions src/core/components/core-tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ export function ChartTooltip({
group: tooltip.group,
expandedSeries,
setExpandedSeries,
hideTooltip: () => {
api.hideTooltip();
},
});
if (!content) {
return null;
Expand Down Expand Up @@ -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") {
Expand All @@ -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.
Expand All @@ -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),
Expand All @@ -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) ?? (
Expand All @@ -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) ?? (
<div className={styles["tooltip-default-header"]}>
Expand All @@ -218,7 +241,13 @@ function getTooltipContentPie(
body:
renderers.body?.(tooltipDetails) ??
(renderers.details ? (
<ChartSeriesDetails details={renderers.details({ point })} compactList={true} />
<ChartSeriesDetails
details={renderers.details({
point,
hideTooltip,
})}
compactList={true}
/>
) : (
// 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.
Expand Down
3 changes: 3 additions & 0 deletions src/core/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/pie-chart/chart-pie-internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading