Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
132 changes: 131 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,134 @@ 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, getByTestId } = renderChart({
highcharts,
options: { series },
getTooltipContent: getTooltipContent,
});

act(() => hc.highlightChartPoint(0, 0));

await waitFor(() => {
expect(wrapper.findTooltip()).not.toBe(null);
});

act(() => {
hoverTooltip();
hc.leaveChartPoint(0, 0);
});

await waitFor(() => {
expect(wrapper.findTooltip()).not.toBe(null);
});

act(() => {
getByTestId("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(() => {
hc.leaveChartPoint(0, 0);
dismissCallback!();
});

await waitFor(() => {
expect(wrapper.findTooltip()).toBe(null);
});
});
});
});
3 changes: 2 additions & 1 deletion src/core/__tests__/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,10 @@ export function renderChart({ i18nProvider, ...props }: CoreChartTestProps, Comp
<Component options={{}} {...props} />
);
};
const { rerender } = render(<ComponentWrapper {...props} />);
const { rerender, getByTestId } = render(<ComponentWrapper {...props} />);
return {
wrapper: createChartWrapper(),
getByTestId,
rerender: (props: CoreChartTestProps) => rerender(<ComponentWrapper {...props} />),
};
}
Expand Down
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