Skip to content

Commit

Permalink
Revert isMouseOutside changes in favor of mouseleave event listener (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
denisborovikov authored Mar 14, 2021
1 parent 2e6f325 commit b342cde
Show file tree
Hide file tree
Showing 7 changed files with 47 additions and 221 deletions.
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,16 @@ Import `react-popper-tooltip/dist/styles.css` to import it into your project. Ad
While the tooltip is being displayed, you have access to some attributes on the tooltip container. You can use them
in your CSS in specific scenarios.

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

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

- `data-popper-escaped`: set to true when the tooltip escapes the trigger element's boundary (and so it appears
detached). Set to false otherwise.

- `data-popper-interactive`: contains the current `interactive` option value.

## API reference

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.

0 comments on commit b342cde

Please sign in to comment.