Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert isMouseOutside changes in favor of mouseleave event listener #127

Merged
merged 6 commits into from
Mar 14, 2021
Merged
Show file tree
Hide file tree
Changes from 4 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 .size-snapshot.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"react-popper-tooltip.js": {
"bundled": 13338,
"minified": 5751,
"gzipped": 1990,
"bundled": 10744,
"minified": 5095,
"gzipped": 1733,
"treeshaked": {
"rollup": {
"code": 142,
"import_statements": 142
},
"webpack": {
"code": 1369
"code": 1355
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@
z-index: 9999;
}

.tooltip-container[data-popper-interactive='false'] {
pointer-events: none;
}

.tooltip-arrow {
height: 1rem;
position: absolute;
width: 1rem;
pointer-events: none;
}

.tooltip-arrow::before {
Expand Down
94 changes: 32 additions & 62 deletions src/usePopperTooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,9 @@ import {
useControlledState,
useGetLatest,
generateBoundingClientRect,
isMouseOutside,
} from './utils';
import { Config, PopperOptions, PropsGetterArgs, TriggerType } from './types';

const { isArray } = Array;

const virtualElement = {
getBoundingClientRect: generateBoundingClientRect(),
};
Expand Down Expand Up @@ -51,7 +48,7 @@ export function usePopperTooltip(
const defaultModifiers = React.useMemo(
() => [{ name: 'offset', options: { offset: finalConfig.offset } }],
// eslint-disable-next-line react-hooks/exhaustive-deps
isArray(finalConfig.offset) ? finalConfig.offset : []
Array.isArray(finalConfig.offset) ? finalConfig.offset : []
);

const finalPopperOptions = {
Expand All @@ -69,7 +66,7 @@ export function usePopperTooltip(
});

const timer = React.useRef<number>();
React.useEffect(() => () => clearTimeout(timer.current));
React.useEffect(() => () => clearTimeout(timer.current), []);

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

const isTriggeredBy = React.useCallback(
(trigger: TriggerType) => {
return isArray(finalConfig.trigger)
return Array.isArray(finalConfig.trigger)
? finalConfig.trigger.includes(trigger)
: finalConfig.trigger === trigger;
},
// eslint-disable-next-line react-hooks/exhaustive-deps
isArray(finalConfig.trigger) ? finalConfig.trigger : [finalConfig.trigger]
Array.isArray(finalConfig.trigger)
? finalConfig.trigger
: [finalConfig.trigger]
);

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

triggerRef.addEventListener('mouseenter', showTooltip);
let stopTimer: undefined | (() => void);
if (!visible) {
stopTimer = () => clearTimeout(timer.current);
triggerRef.addEventListener('mouseleave', stopTimer);
}
triggerRef.addEventListener('mouseleave', hideTooltip);
return () => {
triggerRef.removeEventListener('mouseenter', showTooltip);
if (stopTimer) {
triggerRef.removeEventListener('mouseleave', stopTimer);
}
};
}, [isTriggeredBy, hideTooltip, showTooltip, triggerRef, visible]);
// Listen for mouse exiting the hover area &&
// handle the followCursor
React.useEffect(() => {
if (
!visible ||
triggerRef == null ||
(!isTriggeredBy('hover') && !finalConfig.followCursor)
) {
return;
}

let lastMouseOutside = false;
const handleMouseMove = (event: MouseEvent) => {
const mouseOutside = isMouseOutside(
event,
triggerRef,
!finalConfig.followCursor &&
getLatest().finalConfig.interactive &&
tooltipRef
);
if (mouseOutside && lastMouseOutside !== mouseOutside) {
hideTooltip();
}
if (!mouseOutside && finalConfig.followCursor) {
virtualElement.getBoundingClientRect = generateBoundingClientRect(
event.clientX,
event.clientY
);
update?.();
}
lastMouseOutside = mouseOutside;
triggerRef.removeEventListener('mouseleave', hideTooltip);
};
window.addEventListener('mousemove', handleMouseMove);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, [
finalConfig.followCursor,
getLatest,
hideTooltip,
isTriggeredBy,
tooltipRef,
triggerRef,
update,
visible,
]);
}, [triggerRef, isTriggeredBy, showTooltip, hideTooltip]);

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

// Handle follow cursor
React.useEffect(() => {
if (!finalConfig.followCursor || triggerRef == null) return;

function setMousePosition({
clientX,
clientY,
}: {
clientX: number;
clientY: number;
}) {
virtualElement.getBoundingClientRect = generateBoundingClientRect(
clientX,
clientY
);
update?.();
}

triggerRef.addEventListener('mousemove', setMousePosition);
return () => triggerRef.removeEventListener('mousemove', setMousePosition);
}, [finalConfig.followCursor, triggerRef, update]);

// Handle tooltip DOM mutation changes (aka mutation observer)
React.useEffect(() => {
if (
Expand All @@ -285,6 +254,7 @@ export function usePopperTooltip(
...styles.popper,
},
...attributes.popper,
'data-popper-interactive': finalConfig.interactive,
};
};

Expand Down
48 changes: 0 additions & 48 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,51 +60,3 @@ export function generateBoundingClientRect(x = 0, y = 0) {
left: x,
});
}

// pageX cannot be supplied in the tests, so we fallback to clientX
// @see https://github.com/testing-library/dom-testing-library/issues/144
const mouseOutsideRect = (
{ clientX, clientY }: MouseEvent,
{ bottom, left, right, top }: DOMRect
) => {
// DOMRect contains fractional pixel values but MouseEvent reports integers,
// so we round DOMRect boundaries to make DOMRect slightly bigger.
// Also exceed the DOMRect by 1 pixel to fix Chromium reporting MouseEvent's
// `clientX` and `clientY` by the whole integer away from the DOMRect.
// see https://github.com/mohsinulhaq/react-popper-tooltip/issues/118#issuecomment-782698921
return (
clientX < Math.floor(left) - 1 ||
clientX > Math.ceil(right) + 1 ||
clientY < Math.floor(top) - 1 ||
clientY > Math.ceil(bottom) + 1
);
};

/**
* Checks if mouseevent is triggered outside triggerRef and tooltipRef.
* Counts with potential offset between them.
* @param {MouseEvent} mouseEvent
* @param {HTMLElement} triggerRef
* @param {HTMLElement} tooltipRef - provide only when prop `interactive` is on
*/
export function isMouseOutside(
mouseEvent: MouseEvent,
triggerRef: HTMLElement,
tooltipRef?: HTMLElement | false | null
): boolean {
const triggerRect = triggerRef.getBoundingClientRect();
if (!tooltipRef) return mouseOutsideRect(mouseEvent, triggerRect);
const tooltipRect = tooltipRef.getBoundingClientRect();
// triggerRect extended to the tooltipRect boundary, thus will contain cursor
// moving from triggerRect to tooltipRect over some non zero offset.
const triggerRectExtendedToTooltip = {
bottom: Math.max(triggerRect.bottom, tooltipRect.top),
left: Math.min(triggerRect.left, tooltipRect.right),
right: Math.max(triggerRect.right, tooltipRect.left),
top: Math.min(triggerRect.top, tooltipRect.bottom),
};
return (
mouseOutsideRect(mouseEvent, triggerRectExtendedToTooltip as DOMRect) &&
mouseOutsideRect(mouseEvent, tooltipRect)
);
}
15 changes: 3 additions & 12 deletions tests/usePopperTooltip.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,7 @@ describe('trigger option', () => {
expect(await screen.findByText(TooltipText)).toBeInTheDocument();

// tooltip hidden on hover out
userEvent.unhover(screen.getByText(TriggerText), {
clientX: 100,
clientY: 100,
});
userEvent.unhover(screen.getByText(TriggerText));
await waitFor(() => {
expect(screen.queryByText(TooltipText)).not.toBeInTheDocument();
});
Expand Down Expand Up @@ -200,10 +197,7 @@ test('delayHide option removes tooltip after specified delay', async () => {
});
expect(await screen.findByText(TooltipText)).toBeInTheDocument();

userEvent.unhover(screen.getByText(TriggerText), {
clientX: 100,
clientY: 100,
});
userEvent.unhover(screen.getByText(TriggerText));
// Still present after 2000ms
act(() => {
jest.advanceTimersByTime(2000);
Expand Down Expand Up @@ -241,10 +235,7 @@ test('onVisibleChange option called when state changes', async () => {
expect(onVisibleChange).toHaveBeenLastCalledWith(true);

// Now visible, change visible to false when unhover
userEvent.unhover(screen.getByText(TriggerText), {
clientX: 100,
clientY: 100,
});
userEvent.unhover(screen.getByText(TriggerText));
await waitFor(() => {
expect(screen.queryByText(TooltipText)).not.toBeInTheDocument();
});
Expand Down
94 changes: 0 additions & 94 deletions tests/utils.spec.ts

This file was deleted.