'Custom edge detection for React tooltip causes page to "flash" occasionally

So I created simple edge detection for a tooltip that is part of a charting library. I can't use tooltips that already have implemented edge detection (like MUI tooltip) because the charting software only allows HTML tooltips, so I was left to create a React component that has edge detection for RIGHT side of screen only. Since the tooltip always opens to the right, I just have to take care of the right side.

So when the hover is close to the edge, the tooltip checks how close it is to the edge, then I use CSS offset to place the tooltip at a point away from the right side of the screen.

The component looks like this:

export const MyTooltip = ({ children }) => {

  useEffect(() => {
    const container =
      document.getElementById('tooltip');

    if (container) {
      const x = container.getBoundingClientRect().x;

      if (x > window.outerWidth - 200) {
    container.style.left = 'auto';
        container.style.right = '0';
        container.style.transform = `translateX(${
          window.outerWidth - x - 40
        }px)`;
      }
    }
  }, []);

  return (
    <TooltipStyle>
      <div id="tooltip" className="custom-tooltip">
        {children}
      </div>
    </TooltipStyle>
  );
};

TooltipStyle is a styled component that has CSS for the .custom-tooltip class for the background, padding, etc. 200px is the width of the tooltip in all instances and 40px is the padding.

The problem: Occasionally, when the tooltip renders in the DOM, the page "flashes" for a split second. It's very unsightly. I know it's because of the code in the useEffect hook that actually sets the offset of the tooltip, but I'm not certain about what a good solution would be.

Any help is much appreciated! Thanks.



Solution 1:[1]

Another thing: It might be a better idea to use a ref instead of useEffect

export const MyTooltip = ({ children }) => {

  const tooltipRef = React.useCallback((container) => {
    if (container) {
      const x = container.getBoundingClientRect().x;

      if (x > window.outerWidth - 200) {
        container.style.left = 'auto';
        container.style.right = '0';
        container.style.transform = `translateX(${window.outerWidth - x - 40}px)`;
      }
    }
  }, []);

  return (
    <TooltipStyle>
      <div ref={tooltipRef} className="custom-tooltip">
        {children}
      </div>
    </TooltipStyle>
  );
};

Solution 2:[2]

It would be great if you could provide a minimal reproducible on Codesandbox. For example, https://codesandbox.io/s/me6kg

In your code when a user hovers over the container you create a new tooltip every time. It gets put on DOM then you change CSS props on the element using the hook.
If you avoid rerendering tooltips, then it may solve the issue. We can use shouldComponentUpdate lifecycle for this.

body {
  padding: 2rem;
  white-space: nowrap;
}

div { 
  display: inline-block;
}
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/[email protected]/babel.min.js" crossorigin="anonymous"></script>

<div id="root">App will go here</div>

<script type="text/babel">
   const root = ReactDOM.createRoot(document.getElementById('root'));

   class Tooltip extends React.Component {
    constructor(props) {
     super(props);
     this.shown = false;
     this.self = React.createRef(null);
    }

    componentDidMount() {
     this.shown = true;
    }

    shouldComponentUpdate(nextProps, nextState) {
     let ref = this.self.current;
     let portWidth = 0;
     if (document.documentElement) 
       portWidth = document.documentElement.clientWidth;   //iframe
     else 
       portWidth = window.outerWidth;

     let left = '0px';
     if (ref) {
      const x = ref.getBoundingClientRect().x;
      if (x > portWidth - 200 - 40) {
       left = `${portWidth - x - 40 - 200}px`;
      }
     }

     ref.style.left = left;
     ref.style.visibility = nextProps.show ? 'visible' : 'hidden';
     if (!this.shown) {
      return true;
     }
     // prevent rerenders
     return false;
    }

    render() {
     console.log('Render a tooltip.');
     const styles = {
      position: 'absolute',
      width: '200px',
      color: 'white',
      backgroundColor: 'black',
      top: '-2rem',
      left: this.props.left ? this.props.left : '0px',
      visibility: this.props.show ? 'visible' : 'hidden',
     };
     return (
      <div ref={this.self} style={styles}>
       {this.props.children}
      </div>
     );
    }
   }

   class Box extends React.Component {
    constructor(props) {
     super(props);
     this.state = {show: false};
     this.handleEnter = this.handleEnter.bind(this);
     this.handleLeave = this.handleLeave.bind(this);
    }

    handleEnter() {
     this.setState({ show: true });
    }

    handleLeave() {
     this.setState({ show: false });
    }

    render() {
     const styles = {
      border: '1px solid gray',
      height: '100px',
      width: '300px',
      position: 'relative',
      left: this.props.left ? this.props.left : '0px',
     };

     return (
      <div
       style={styles}
       onMouseEnter={this.handleEnter}
       onMouseLeave={this.handleLeave}
      >
       {this.props.content}
       <Tooltip show={this.state.show}>
          I am<em><b>{this.props.content}</b></em>
       </Tooltip>
      </div>
     );
    }
   }

   root.render(
    <React.Fragment>
     <Box content="Box 1" />
     <Box content="Box 2" left="calc(100vw - 300px)" />
    </React.Fragment>
   );
</script>

Play with the horizontal scroll and check tooltip positions.

Note: If you don't see a horizontal scrollbar in the output then reduce the browser width.


This is just a bare minimum code. Many improvements can be done.

Solution 3:[3]

it's hard to tell what is the problem, because you didn't supply the code that hides/shows the tooltip.

anyway there is a very nice library for tooltips: https://floating-ui.com

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 Alissa