recharts icon indicating copy to clipboard operation
recharts copied to clipboard

Render Tooltip to React Portal to Prevent Clipping

Open Eric24 opened this issue 3 years ago • 8 comments

  • [x] I have searched the issues of this repository and believe that this is not a duplicate.

What problem does this feature solve?

Currently, if there are enough data series shown in the Tooltip to result in a Tooltip taller than the height of the chart and it's "chrome" (axes, legend, etc.), the tooltip is often clipped by the DOM element that contains the chart (note: it is not the chart itself that is clipping the Tooltip, but rarely if ever will a chart be the only element on a page by itself). By using a React Portal (https://reactjs.org/docs/portals.html), the Tooltip can be rendered independently of the chart and other elements in the DOM hierarchy, thus preventing the clipping.

What does the proposed API look like?

No change to the API would be necessary. This should be a transparent, non-breaking change.

Eric24 avatar Feb 22 '21 15:02 Eric24

For people who are looking for a baseline implementation (with react-popper):

import type { VirtualElement as IVirtualElement } from "@popperjs/core";
import { ReactNode, useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { usePopper } from "react-popper";
import { useEvent } from "react-use";

import useLazyRef from "~/utils/useLazyRef";

export interface PopperPortalProps {
  active?: boolean;
  children: ReactNode;
}

export default function PopperPortal({
  active = true,
  children,
}: PopperPortalProps) {
  const [portalElement, setPortalElement] = useState<HTMLDivElement>();
  const [popperElement, setPopperElement] = useState<HTMLDivElement | null>();
  const virtualElementRef = useLazyRef(() => new VirtualElement());

  const { styles, attributes, update } = usePopper(
    virtualElementRef.current,
    popperElement,
    POPPER_OPTIONS
  );

  useEffect(() => {
    const el = document.createElement("div");
    document.body.appendChild(el);
    setPortalElement(el);
    return () => el.remove();
  }, []);

  useEvent("mousemove", ({ clientX: x, clientY: y }) => {
    virtualElementRef.current.update(x, y);
    if (!active) return;
    update?.();
  });

  useEffect(() => {
    if (!active) return;
    update?.();
  }, [active, update]);

  if (!portalElement) return null;

  return createPortal(
    <div
      ref={setPopperElement}
      {...attributes.popper}
      style={{
        ...styles.popper,
        zIndex: 1000,
        display: active ? "block" : "none",
      }}
    >
      {children}
    </div>,
    portalElement
  );
}

class VirtualElement implements IVirtualElement {
  private rect = {
    width: 0,
    height: 0,
    top: 0,
    right: 0,
    bottom: 0,
    left: 0,
    x: 0,
    y: 0,
    toJSON() {
      return this;
    },
  };

  update(x: number, y: number) {
    this.rect.y = y;
    this.rect.top = y;
    this.rect.bottom = y;

    this.rect.x = x;
    this.rect.left = x;
    this.rect.right = x;
  }

  getBoundingClientRect(): DOMRect {
    return this.rect;
  }
}

const POPPER_OPTIONS: Parameters<typeof usePopper>[2] = {
  placement: "right-start",
  modifiers: [
    {
      name: "offset",
      options: {
        offset: [8, 8],
      },
    },
  ],
};

Usage

function MyComponent() {
  return (
    <BarChart>
      <Tooltip content={<TooltipContent />} />
    </BarChart>
  }
}

function TooltipContent({ active }: TooltipProps) {
  return (
    <PopperPortal active={active}>
      CUSTOM TOOLTIP RENDERER
    </PopperPortal>
  );
}

mrcljx avatar Mar 09 '22 22:03 mrcljx

As an alternative, <Tooltip /> could add a portal prop taking a DOM element, and render its contents inside that element if it is given.

reify-thomas-smith avatar Mar 10 '22 16:03 reify-thomas-smith

We need Portals! Please support it!

Sin9k avatar Dec 21 '22 12:12 Sin9k

Hello, Has this feature request development been shipped ? Thanks in advance for your answer.

CyrilQuandalle avatar Nov 16 '23 10:11 CyrilQuandalle

No, no work on portals yet. Refactoring needs to be done first in order to make it viable

ckifer avatar Nov 16 '23 14:11 ckifer

ok thanks for the answer, then external custom Tooltip it is ;-)

CyrilQuandalle avatar Nov 16 '23 15:11 CyrilQuandalle

@CyrilQuandalle could you share how you've solved it? (your custom implementation)

I'm facing this issue as well as another issue where the tooltip hides the entire chart. It happens if it's big enough, the chart is small enough and is located near the edge of the viewport. I would expect it to go above the chart in that case and not hide it.

All in all, seems like the tooltip's implementation is very lacking.

dlvhdr avatar Jan 15 '24 18:01 dlvhdr

This should be available in 3.0 release

PavelVanecek avatar Apr 19 '24 00:04 PavelVanecek