Skip to content

Commit 00b00b5

Browse files
authored
fix: Close tooltip when cursor leaves chart via y-axis (#4350)
1 parent a494b56 commit 00b00b5

File tree

2 files changed

+67
-40
lines changed

2 files changed

+67
-40
lines changed

src/mixed-line-bar-chart/__tests__/use-mouse-hover.test.ts

Lines changed: 53 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -107,18 +107,21 @@ describe('Mouse hover hook', () => {
107107
expect(customProps.highlightX).toHaveBeenCalledTimes(1);
108108
});
109109

110-
test('does not clear highlighted X on mouseOut if moving within popover', () => {
110+
test('does not clear highlighted X on mouseOut if moving into popover', () => {
111111
const SvgElementDummy = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
112112
const lineElement = document.createElementNS('http://www.w3.org/2000/svg', 'line');
113113

114114
SvgElementDummy.appendChild(lineElement);
115-
const popoverWrapper = document.createElement('div');
116-
const popoverDiv = document.createElement('div');
117-
popoverWrapper.appendChild(popoverDiv);
115+
// Simulate the real DOM structure: transition wrapper > popover element > popover content
116+
const transitionWrapper = document.createElement('div');
117+
const popoverElement = document.createElement('div');
118+
const popoverContent = document.createElement('div');
119+
transitionWrapper.appendChild(popoverElement);
120+
popoverElement.appendChild(popoverContent);
118121

119122
const customProps = {
120123
highlightX: jest.fn(),
121-
popoverRef: { current: popoverWrapper },
124+
popoverRef: { current: popoverElement },
122125
plotRef: { current: { svg: SvgElementDummy, focusApplication: jest.fn(), focusPlot: jest.fn() } },
123126
};
124127
const { hook } = renderMouseHoverHook(customProps);
@@ -135,57 +138,75 @@ describe('Mouse hover hook', () => {
135138
expect(customProps.highlightX).toHaveBeenCalledWith({ scaledX: 100, label: 0 });
136139
expect(customProps.highlightX).toHaveBeenCalledTimes(1);
137140

141+
// relatedTarget is inside the popover DOM tree (child of popover)
138142
const mouseOutEvent = {
139-
relatedTarget: popoverWrapper,
140-
clientX: 10,
141-
clientY: 10,
143+
relatedTarget: popoverContent,
142144
} as any;
143145

144146
act(() => hook.current.onSVGMouseOut(mouseOutEvent));
145147
expect(customProps.highlightX).toHaveBeenCalledTimes(1);
146148
});
147149

148-
test('clears highlightX when onPopoverLeave is called', () => {
150+
test('does not clear highlighted X on mouseOut if moving into popover transition wrapper', () => {
149151
const SvgElementDummy = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
150-
const lineElement = document.createElementNS('http://www.w3.org/2000/svg', 'line');
151-
SvgElementDummy.appendChild(lineElement);
152+
153+
// Simulate: transition wrapper > popover element
154+
const transitionWrapper = document.createElement('div');
155+
const popoverElement = document.createElement('div');
156+
transitionWrapper.appendChild(popoverElement);
157+
158+
const customProps = {
159+
highlightX: jest.fn(),
160+
popoverRef: { current: popoverElement },
161+
plotRef: { current: { svg: SvgElementDummy, focusApplication: jest.fn(), focusPlot: jest.fn() } },
162+
};
163+
const { hook } = renderMouseHoverHook(customProps);
164+
165+
// relatedTarget is the transition wrapper (parent of popoverRef)
166+
const mouseOutEvent = {
167+
relatedTarget: transitionWrapper,
168+
} as any;
169+
170+
act(() => hook.current.onSVGMouseOut(mouseOutEvent));
171+
expect(customProps.highlightX).not.toHaveBeenCalled();
172+
});
173+
174+
test('clears highlighted X on mouseOut when leaving SVG near popover but not into it', () => {
175+
const SvgElementDummy = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
176+
177+
// Simulate: transition wrapper > popover element
178+
const transitionWrapper = document.createElement('div');
179+
const popoverElement = document.createElement('div');
180+
transitionWrapper.appendChild(popoverElement);
152181

153182
const customProps = {
154183
highlightX: jest.fn(),
155184
clearHighlightedSeries: jest.fn(),
185+
popoverRef: { current: popoverElement },
156186
plotRef: { current: { svg: SvgElementDummy, focusApplication: jest.fn(), focusPlot: jest.fn() } },
157187
};
158188
const { hook } = renderMouseHoverHook(customProps);
159-
const event = {
160-
relatedTarget: lineElement,
189+
190+
// relatedTarget is an element outside both SVG and popover (e.g. y-axis area, page body)
191+
const outsideElement = document.createElement('div');
192+
const mouseOutEvent = {
193+
relatedTarget: outsideElement,
161194
} as any;
162-
act(() => hook.current.onPopoverLeave(event));
195+
196+
act(() => hook.current.onSVGMouseOut(mouseOutEvent));
163197
expect(customProps.highlightX).toHaveBeenCalledWith(null);
164198
expect(customProps.clearHighlightedSeries).toHaveBeenCalledTimes(1);
165199
});
166200

167-
test('does not clear highlightX when onPopoverLeave is called (pointing device exited from the svg)', () => {
168-
const svgMock = { contains: () => true } as unknown as SVGSVGElement;
201+
test('clears highlightX when onPopoverLeave is called', () => {
169202
const customProps = {
170203
highlightX: jest.fn(),
171204
clearHighlightedSeries: jest.fn(),
172-
isHandlersDisabled: true,
173-
plotRef: {
174-
current: {
175-
svg: svgMock,
176-
focusApplication: jest.fn(),
177-
focusPlot: jest.fn(),
178-
},
179-
},
180205
};
181-
182206
const { hook } = renderMouseHoverHook(customProps);
183-
const event = {
184-
relatedTarget: null,
185-
} as any;
186-
act(() => hook.current.onPopoverLeave(event));
187-
expect(customProps.highlightX).not.toHaveBeenCalled();
188-
expect(customProps.clearHighlightedSeries).not.toHaveBeenCalled();
207+
act(() => hook.current.onPopoverLeave());
208+
expect(customProps.highlightX).toHaveBeenCalledWith(null);
209+
expect(customProps.clearHighlightedSeries).toHaveBeenCalledTimes(1);
189210
});
190211

191212
test('does not clear highlightX when onPopoverLeave is called if isHandlersDisabled is true', () => {
@@ -195,10 +216,7 @@ describe('Mouse hover hook', () => {
195216
isHandlersDisabled: true,
196217
};
197218
const { hook } = renderMouseHoverHook(customProps);
198-
const event = {
199-
relatedTarget: null,
200-
} as any;
201-
act(() => hook.current.onPopoverLeave(event));
219+
act(() => hook.current.onPopoverLeave());
202220
expect(customProps.highlightX).not.toHaveBeenCalled();
203221
expect(customProps.clearHighlightedSeries).not.toHaveBeenCalled();
204222
});

src/mixed-line-bar-chart/hooks/use-mouse-hover.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,17 @@ export function useMouseHover<T>({
121121
};
122122

123123
const onSVGMouseOut = (event: React.MouseEvent<SVGElement, MouseEvent>) => {
124-
if (isHandlersDisabled || isMouseOverPopover(event)) {
124+
if (isHandlersDisabled) {
125125
return;
126126
}
127+
128+
// If the mouse is moving into the popover or its container (transition wrapper),
129+
// let onPopoverLeave handle cleanup.
130+
const popoverContainer = popoverRef.current?.parentElement;
131+
if (event.relatedTarget && popoverContainer && nodeContains(popoverContainer, event.relatedTarget)) {
132+
return;
133+
}
134+
127135
if (
128136
!nodeContains(plotRef.current!.svg, event.relatedTarget) ||
129137
(event.relatedTarget && (event.relatedTarget as Element).classList.contains(styles.series))
@@ -133,11 +141,12 @@ export function useMouseHover<T>({
133141
}
134142
};
135143

136-
const onPopoverLeave = (event: React.MouseEvent) => {
137-
if (!isHandlersDisabled && nodeContains(plotRef.current!.svg, event.relatedTarget)) {
138-
highlightX(null);
139-
clearHighlightedSeries();
144+
const onPopoverLeave = () => {
145+
if (isHandlersDisabled) {
146+
return;
140147
}
148+
highlightX(null);
149+
clearHighlightedSeries();
141150
};
142151

143152
return { onSVGMouseMove, onSVGMouseOut, onPopoverLeave };

0 commit comments

Comments
 (0)