Skip to content

Commit b342cde

Browse files
Revert isMouseOutside changes in favor of mouseleave event listener (#127)
1 parent 2e6f325 commit b342cde

7 files changed

+47
-221
lines changed

.size-snapshot.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
{
22
"react-popper-tooltip.js": {
3-
"bundled": 13338,
4-
"minified": 5751,
5-
"gzipped": 1990,
3+
"bundled": 10744,
4+
"minified": 5095,
5+
"gzipped": 1733,
66
"treeshaked": {
77
"rollup": {
88
"code": 142,
99
"import_statements": 142
1010
},
1111
"webpack": {
12-
"code": 1369
12+
"code": 1355
1313
}
1414
}
1515
}

README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,16 @@ Import `react-popper-tooltip/dist/styles.css` to import it into your project. Ad
8787
While the tooltip is being displayed, you have access to some attributes on the tooltip container. You can use them
8888
in your CSS in specific scenarios.
8989

90-
- `data-popper-placement`: contains the current tooltip placement. You can use it to properly offset and display the
90+
- `data-popper-placement`: contains the current tooltip `placement`. You can use it to properly offset and display the
9191
arrow element (e.g., if the tooltip is displayed on the right, the arrow should point to the left and vice versa).
9292

9393
- `data-popper-reference-hidden`: set to true when the trigger element is fully clipped and hidden from view, which
9494
causes the tooltip to appear to be attached to nothing. Set to false otherwise.
9595

9696
- `data-popper-escaped`: set to true when the tooltip escapes the trigger element's boundary (and so it appears
9797
detached). Set to false otherwise.
98+
99+
- `data-popper-interactive`: contains the current `interactive` option value.
98100

99101
## API reference
100102

src/styles.css

+5
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,15 @@
1414
z-index: 9999;
1515
}
1616

17+
.tooltip-container[data-popper-interactive='false'] {
18+
pointer-events: none;
19+
}
20+
1721
.tooltip-arrow {
1822
height: 1rem;
1923
position: absolute;
2024
width: 1rem;
25+
pointer-events: none;
2126
}
2227

2328
.tooltip-arrow::before {

src/usePopperTooltip.ts

+32-62
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,9 @@ import {
44
useControlledState,
55
useGetLatest,
66
generateBoundingClientRect,
7-
isMouseOutside,
87
} from './utils';
98
import { Config, PopperOptions, PropsGetterArgs, TriggerType } from './types';
109

11-
const { isArray } = Array;
12-
1310
const virtualElement = {
1411
getBoundingClientRect: generateBoundingClientRect(),
1512
};
@@ -51,7 +48,7 @@ export function usePopperTooltip(
5148
const defaultModifiers = React.useMemo(
5249
() => [{ name: 'offset', options: { offset: finalConfig.offset } }],
5350
// eslint-disable-next-line react-hooks/exhaustive-deps
54-
isArray(finalConfig.offset) ? finalConfig.offset : []
51+
Array.isArray(finalConfig.offset) ? finalConfig.offset : []
5552
);
5653

5754
const finalPopperOptions = {
@@ -69,7 +66,7 @@ export function usePopperTooltip(
6966
});
7067

7168
const timer = React.useRef<number>();
72-
React.useEffect(() => () => clearTimeout(timer.current));
69+
React.useEffect(() => () => clearTimeout(timer.current), []);
7370

7471
const { styles, attributes, ...popperProps } = usePopper(
7572
finalConfig.followCursor ? virtualElement : triggerRef,
@@ -88,12 +85,14 @@ export function usePopperTooltip(
8885

8986
const isTriggeredBy = React.useCallback(
9087
(trigger: TriggerType) => {
91-
return isArray(finalConfig.trigger)
88+
return Array.isArray(finalConfig.trigger)
9289
? finalConfig.trigger.includes(trigger)
9390
: finalConfig.trigger === trigger;
9491
},
9592
// eslint-disable-next-line react-hooks/exhaustive-deps
96-
isArray(finalConfig.trigger) ? finalConfig.trigger : [finalConfig.trigger]
93+
Array.isArray(finalConfig.trigger)
94+
? finalConfig.trigger
95+
: [finalConfig.trigger]
9796
);
9897

9998
const hideTooltip = React.useCallback(() => {
@@ -184,64 +183,12 @@ export function usePopperTooltip(
184183
if (triggerRef == null || !isTriggeredBy('hover')) return;
185184

186185
triggerRef.addEventListener('mouseenter', showTooltip);
187-
let stopTimer: undefined | (() => void);
188-
if (!visible) {
189-
stopTimer = () => clearTimeout(timer.current);
190-
triggerRef.addEventListener('mouseleave', stopTimer);
191-
}
186+
triggerRef.addEventListener('mouseleave', hideTooltip);
192187
return () => {
193188
triggerRef.removeEventListener('mouseenter', showTooltip);
194-
if (stopTimer) {
195-
triggerRef.removeEventListener('mouseleave', stopTimer);
196-
}
197-
};
198-
}, [isTriggeredBy, hideTooltip, showTooltip, triggerRef, visible]);
199-
// Listen for mouse exiting the hover area &&
200-
// handle the followCursor
201-
React.useEffect(() => {
202-
if (
203-
!visible ||
204-
triggerRef == null ||
205-
(!isTriggeredBy('hover') && !finalConfig.followCursor)
206-
) {
207-
return;
208-
}
209-
210-
let lastMouseOutside = false;
211-
const handleMouseMove = (event: MouseEvent) => {
212-
const mouseOutside = isMouseOutside(
213-
event,
214-
triggerRef,
215-
!finalConfig.followCursor &&
216-
getLatest().finalConfig.interactive &&
217-
tooltipRef
218-
);
219-
if (mouseOutside && lastMouseOutside !== mouseOutside) {
220-
hideTooltip();
221-
}
222-
if (!mouseOutside && finalConfig.followCursor) {
223-
virtualElement.getBoundingClientRect = generateBoundingClientRect(
224-
event.clientX,
225-
event.clientY
226-
);
227-
update?.();
228-
}
229-
lastMouseOutside = mouseOutside;
189+
triggerRef.removeEventListener('mouseleave', hideTooltip);
230190
};
231-
window.addEventListener('mousemove', handleMouseMove);
232-
return () => {
233-
window.removeEventListener('mousemove', handleMouseMove);
234-
};
235-
}, [
236-
finalConfig.followCursor,
237-
getLatest,
238-
hideTooltip,
239-
isTriggeredBy,
240-
tooltipRef,
241-
triggerRef,
242-
update,
243-
visible,
244-
]);
191+
}, [triggerRef, isTriggeredBy, showTooltip, hideTooltip]);
245192

246193
// Trigger: hover on tooltip, keep it open if hovered
247194
React.useEffect(() => {
@@ -262,6 +209,28 @@ export function usePopperTooltip(
262209
if (finalConfig.closeOnTriggerHidden && isReferenceHidden) hideTooltip();
263210
}, [finalConfig.closeOnTriggerHidden, hideTooltip, isReferenceHidden]);
264211

212+
// Handle follow cursor
213+
React.useEffect(() => {
214+
if (!finalConfig.followCursor || triggerRef == null) return;
215+
216+
function setMousePosition({
217+
clientX,
218+
clientY,
219+
}: {
220+
clientX: number;
221+
clientY: number;
222+
}) {
223+
virtualElement.getBoundingClientRect = generateBoundingClientRect(
224+
clientX,
225+
clientY
226+
);
227+
update?.();
228+
}
229+
230+
triggerRef.addEventListener('mousemove', setMousePosition);
231+
return () => triggerRef.removeEventListener('mousemove', setMousePosition);
232+
}, [finalConfig.followCursor, triggerRef, update]);
233+
265234
// Handle tooltip DOM mutation changes (aka mutation observer)
266235
React.useEffect(() => {
267236
if (
@@ -285,6 +254,7 @@ export function usePopperTooltip(
285254
...styles.popper,
286255
},
287256
...attributes.popper,
257+
'data-popper-interactive': finalConfig.interactive,
288258
};
289259
};
290260

src/utils.ts

-48
Original file line numberDiff line numberDiff line change
@@ -60,51 +60,3 @@ export function generateBoundingClientRect(x = 0, y = 0) {
6060
left: x,
6161
});
6262
}
63-
64-
// pageX cannot be supplied in the tests, so we fallback to clientX
65-
// @see https://github.com/testing-library/dom-testing-library/issues/144
66-
const mouseOutsideRect = (
67-
{ clientX, clientY }: MouseEvent,
68-
{ bottom, left, right, top }: DOMRect
69-
) => {
70-
// DOMRect contains fractional pixel values but MouseEvent reports integers,
71-
// so we round DOMRect boundaries to make DOMRect slightly bigger.
72-
// Also exceed the DOMRect by 1 pixel to fix Chromium reporting MouseEvent's
73-
// `clientX` and `clientY` by the whole integer away from the DOMRect.
74-
// see https://github.com/mohsinulhaq/react-popper-tooltip/issues/118#issuecomment-782698921
75-
return (
76-
clientX < Math.floor(left) - 1 ||
77-
clientX > Math.ceil(right) + 1 ||
78-
clientY < Math.floor(top) - 1 ||
79-
clientY > Math.ceil(bottom) + 1
80-
);
81-
};
82-
83-
/**
84-
* Checks if mouseevent is triggered outside triggerRef and tooltipRef.
85-
* Counts with potential offset between them.
86-
* @param {MouseEvent} mouseEvent
87-
* @param {HTMLElement} triggerRef
88-
* @param {HTMLElement} tooltipRef - provide only when prop `interactive` is on
89-
*/
90-
export function isMouseOutside(
91-
mouseEvent: MouseEvent,
92-
triggerRef: HTMLElement,
93-
tooltipRef?: HTMLElement | false | null
94-
): boolean {
95-
const triggerRect = triggerRef.getBoundingClientRect();
96-
if (!tooltipRef) return mouseOutsideRect(mouseEvent, triggerRect);
97-
const tooltipRect = tooltipRef.getBoundingClientRect();
98-
// triggerRect extended to the tooltipRect boundary, thus will contain cursor
99-
// moving from triggerRect to tooltipRect over some non zero offset.
100-
const triggerRectExtendedToTooltip = {
101-
bottom: Math.max(triggerRect.bottom, tooltipRect.top),
102-
left: Math.min(triggerRect.left, tooltipRect.right),
103-
right: Math.max(triggerRect.right, tooltipRect.left),
104-
top: Math.min(triggerRect.top, tooltipRect.bottom),
105-
};
106-
return (
107-
mouseOutsideRect(mouseEvent, triggerRectExtendedToTooltip as DOMRect) &&
108-
mouseOutsideRect(mouseEvent, tooltipRect)
109-
);
110-
}

tests/usePopperTooltip.spec.tsx

+3-12
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,7 @@ describe('trigger option', () => {
4747
expect(await screen.findByText(TooltipText)).toBeInTheDocument();
4848

4949
// tooltip hidden on hover out
50-
userEvent.unhover(screen.getByText(TriggerText), {
51-
clientX: 100,
52-
clientY: 100,
53-
});
50+
userEvent.unhover(screen.getByText(TriggerText));
5451
await waitFor(() => {
5552
expect(screen.queryByText(TooltipText)).not.toBeInTheDocument();
5653
});
@@ -200,10 +197,7 @@ test('delayHide option removes tooltip after specified delay', async () => {
200197
});
201198
expect(await screen.findByText(TooltipText)).toBeInTheDocument();
202199

203-
userEvent.unhover(screen.getByText(TriggerText), {
204-
clientX: 100,
205-
clientY: 100,
206-
});
200+
userEvent.unhover(screen.getByText(TriggerText));
207201
// Still present after 2000ms
208202
act(() => {
209203
jest.advanceTimersByTime(2000);
@@ -241,10 +235,7 @@ test('onVisibleChange option called when state changes', async () => {
241235
expect(onVisibleChange).toHaveBeenLastCalledWith(true);
242236

243237
// Now visible, change visible to false when unhover
244-
userEvent.unhover(screen.getByText(TriggerText), {
245-
clientX: 100,
246-
clientY: 100,
247-
});
238+
userEvent.unhover(screen.getByText(TriggerText));
248239
await waitFor(() => {
249240
expect(screen.queryByText(TooltipText)).not.toBeInTheDocument();
250241
});

tests/utils.spec.ts

-94
This file was deleted.

0 commit comments

Comments
 (0)