'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 |