atomiks / tippyjs

Tooltip, popover, dropdown, and menu library
https://atomiks.github.io/tippyjs/
MIT License
11.86k stars 520 forks source link

Tippy gets placed incorrectly with followCursor #1039

Open K0rhak opened 2 years ago

K0rhak commented 2 years ago

Bug description

I create a tippy upon response from the server. It works fine without setting followCursor prop but when I set it, most notably 'initial', for the first time a tippy is created this way, it gets placed incorrectly, to translate (0px, 10px). It gets fixed when I move the mouse, but for 'initial' followCursor that doesn't do much. Others get placed likewise at top left of the screen and then get placed correctly on mouse move.

Here is a video of the problem.

Reproduction

https://codepen.io/Korhak/pen/VwyLMax

Edit

I have found the issue. Listener 'addMouseCoordsListener' gets added when I create the tippy and therefore is not called once before the tippy gets created. I have partially solved the issue to at least not placing the tippy to (0, 0) to omit the followCursor property. With this code on line 2173 in onMouseMove function:

if ((isCursorOverReference || !instance.props.interactive) && (clientX != 0 || clientY != 0)) {
...
okikio commented 2 years ago

@atomiks Related to this issue, I've found that using singletons together with followCursor causes the position of the tooltip to placed improperly.

From the miniature experiments I've run, the issue stems from createSingleton not setting the instance reference properly, so the tooltip positions are off. As in the tooltips are placed relative to the reference element, but createSingleton isn't using the proper reference element.

(PS. I'm planning to use tippy.js for bundlejs.com)

okikio commented 2 years ago

@atomiks The solution I've found to this problem is to use the common ancestor of all tippy instances in a singleton, like so,

// https://stackoverflow.com/a/62759635/12140185
function closestCommonAncestor(elements) {
  const reducer = (prev, current) => current.parentElement.contains(prev) ? current.parentElement : prev;
  return elements.reduce(reducer, elements[0]);
}

const triggerTargets = Array.from(instance.props.triggerTarget as Element[]);
const reference = triggerTargets.length ? closestCommonAncestor(triggerTargets) : instance.reference;
const doc = getOwnerDocument(reference);

Everyone Else: If you've stumbled onto this issue and it is not yet fixed you can use this temporary solution as a replacement for the official followCursor plugin.

Tempory followCursor solution ```ts import { type Props, type Instance, type FollowCursor } from 'tippy.js'; export function isType(value: any, type: string): boolean { const str = {}.toString.call(value); return str.indexOf('[object') === 0 && str.indexOf(`${type}]`) > -1; } export function isMouseEvent(value: unknown): value is MouseEvent { return isType(value, 'MouseEvent'); } export function getOwnerDocument( elementOrElements: Element | Element[] ): Document { const [element] = Array.from(elementOrElements as unknown as Element[]); // Elements created via a