Skip to content
Draft
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
46 changes: 45 additions & 1 deletion src/core/__tests__/chart-core-tooltip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,9 @@ describe("CoreChart: tooltip", () => {
expect(wrapper.findTooltip()!.findFooter()!.getElement().textContent).toBe("Tooltip footer");
});

act(() => hc.getChart().container.dispatchEvent(new MouseEvent("mouseout", { bubbles: true, cancelable: true })));
act(() =>
hc.getChart().container.dispatchEvent(new MouseEvent("mouseleave", { bubbles: false, cancelable: false })),
);

await waitFor(() => {
expect(onClearHighlight).toHaveBeenCalled();
Expand Down Expand Up @@ -819,6 +821,48 @@ describe("CoreChart: tooltip", () => {
});
});

test("hides tooltip when mouse moves outside plot area to the left", async () => {
const onHighlight = vi.fn();
const onClearHighlight = vi.fn();
const { wrapper } = renderChart({
highcharts,
options: {
series: lineSeries,
chart: {
events: {
load() {
this.plotTop = 0;
this.plotLeft = 50;
this.plotWidth = 100;
this.plotHeight = 100;
},
},
},
},
onHighlight,
onClearHighlight,
getTooltipContent: () => ({
header: () => "Tooltip title",
body: () => "Tooltip body",
}),
});

// Move mouse inside plot area to show tooltip
act(() => hc.getChart().container.dispatchEvent(createMouseMoveEvent({ pageX: 75, pageY: 50 })));

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

// Move mouse to the left, outside the plot area (plotLeft=50, so pageX=30 is outside)
act(() => hc.getChart().container.dispatchEvent(createMouseMoveEvent({ pageX: 30, pageY: 50 })));

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

describe("Escape key dismissal", () => {
test("dismisses hover tooltip with Escape key when keyboard navigation is disabled", async () => {
const { wrapper } = renderChart({
Expand Down
36 changes: 26 additions & 10 deletions src/core/chart-api/chart-extra-pointer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ export class ChartExtraPointer {

public onChartLoad = (chart: Highcharts.Chart) => {
chart.container.addEventListener("mousemove", this.onChartMousemove);
chart.container.addEventListener("mouseout", this.onChartMouseout);
chart.container.addEventListener("mouseleave", this.onChartMouseout);
};

public onChartDestroy = () => {
this.context.chartOrNull?.container?.removeEventListener("mousemove", this.onChartMousemove);
this.context.chartOrNull?.container?.removeEventListener("mouseout", this.onChartMouseout);
this.context.chartOrNull?.container?.removeEventListener("mouseleave", this.onChartMouseout);
};

// This event is triggered by Highcharts when the cursor is over a Highcharts point. We leave this to
Expand All @@ -65,11 +65,16 @@ export class ChartExtraPointer {
this.hoveredGroup = null;
};

// When the pointer leaves the tooltip it can hover another point or group. If that does not happen,
// the on-hover-lost handler is called after a short delay.
// When the pointer leaves the tooltip, we immediately check if any point or group is still hovered.
// If not, we fire onHoverLost immediately to prevent the tooltip from staying visible when the mouse
// exits the chart area through the tooltip (e.g., moving left).
public onMouseLeaveTooltip = () => {
this.tooltipHovered = false;
this.clearHover();
this.hoverLostCall.cancelPrevious();
if (!this.hoveredPoint && !this.hoveredGroup) {
this.handlers.onHoverLost();
this.applyCursorStyle();
}
};

// The mouse-move handler takes all move events inside the chart, and its purpose is to capture hover for groups
Expand Down Expand Up @@ -110,18 +115,29 @@ export class ChartExtraPointer {
this.setHoveredGroup(matchedGroup);
}
// If the plotX, plotY are outside of the series area (e.g. if the pointer is above axis titles or ticks),
// we clear the group hover state and trigger the on-hover-lost after a short delay.
// we immediately clear all hover state. Unlike transitions between points/groups within the plot area,
// there is no need to debounce here as the cursor has definitively left the data region.
else {
this.hoveredPoint = null;
this.hoveredGroup = null;
this.clearHover();
this.hoverLostCall.cancelPrevious();
if (!this.tooltipHovered) {
this.handlers.onHoverLost();
this.applyCursorStyle();
}
}
};

// This event is triggered when the pointer leaves the chart area. Here, it is technically not necessary to add
// a delay before calling the on-hover-lost handler, but it is done for consistency in the UX.
// This event is triggered when the pointer leaves the chart container entirely.
// We immediately clear all hover state since the cursor has definitively left the chart.
private onChartMouseout = () => {
this.hoveredPoint = null;
this.hoveredGroup = null;
this.clearHover();
this.hoverLostCall.cancelPrevious();
if (!this.tooltipHovered) {
this.handlers.onHoverLost();
this.applyCursorStyle();
}
};

// This event is triggered by Highcharts when there is a click inside the chart plot. It might or might not include
Expand Down
Loading