'How to make cursor tooltip not go offscreen
I'm working on a popup box that shows some text some position away from an element when the cursor hovers over it. You can see a simplified demo here: https://jsfiddle.net/xsvm2Lur/61/
Right now the popup box will squash up when it is near the bounding box. I want the popup to appear to the bottom-left (if the popup will overflow to the right) or top-left (if the popup will overflow to the bottom and to the right) of the hovered element's position if the popup will overflow.
The position and text that will be shown are dynamically generated (e.g. I don't know before rendering).
Right now I have a working version using Javascript. The way I go about it is:
- Get the text that will be displayed. Count the number of characters that is going to be displayed x 0.25em for each character to get the width of the text.
- Calculate the width of the displayed string + padding (left and right). Let's call it
textLength. This will be set as the popup's width so all the text goes into 1 line. - If
textLength+ x position of the cursor > box width, "invert" the popup box on the x-axis by deducting the popup's "left" value bytextLengthand some distance away from the element. - Repeat the same check for the y position, i.e. if cursor position + line height (1em) + bottom padding > box height, invert y.
The solution works, but I'm wondering if there's a better way to do this without character counting, or if there is another way to do it elegantly, maybe CSS only without Javascript?
Solution 1:[1]
Sadly, I don't believe there is a way to do it with CSS only. However, by working on your fiddle, I've managed to add the functionality you wanted.
The way I went about it was just to include a reference to the container and check whether the popup position and size were inside the container BoundingClientRect.
This is the updated code for the popupShow function:
const showPopup = (top, left, text, container) => {
popup.textContent = text;
const containerBCR = container.getBoundingClientRect();
const popupBCR = popup.getBoundingClientRect();
const popupWidth = popupBCR.width,
popupHeight = popupBCR.height;
let popupTop = top + 20,
popupLeft = left + 20,
newPopupWidth;
console.log("height: ", popupHeight);
console.log("top: ", top);
console.log("bottomPopup: ", top + 20 + popupHeight);
console.log("bottomBoundary", containerBCR.bottom);
if (left + 20 + popupWidth > containerBCR.right) {
popupLeft = left - popupWidth;
if (popupLeft < containerBCR.left) {
popupLeft = containerBCR.left;
newPopupWidth = left - containerBCR.left;
}
}
if (top + 20 + popupHeight > containerBCR.bottom) {
popupTop = top - popupHeight;
if (popupTop < containerBCR.top) {
popupTop = containerBCR.top;
}
}
popup.style.top = popupTop + "px";
popup.style.left = popupLeft + "px";
popup.style.width = newPopupWidth;
popup.style.visibility = 'visible';
}
As you can see, I've also edited the popup to use "visibility: hidden" instead of "display: none". This is because if the display is set to "none", we won't be able to get his size (there might be workarounds for this, though).
Try checking out the updated fiddle and tell me what you think.
I've pushed one circle a little bit further down because the code doesn't currently check for the padding of the popup, so it was overflowing a little (few pixels).
Solution 2:[2]
This is based on quadrants, simple calculates if we are over 50% width and/or height and swaps the style to use the right or bottom instead. This doesn't care about the content of the popup, no measuring required.
const popup = document.getElementById("pop-up")
const parsePx = (px) => parseFloat(px.slice(0, -2))
const showPopup = (text, position) => {
popup.textContent = text;
popup.style.top = position.top;
popup.style.left = position.left;
popup.style.right = position.right;
popup.style.bottom = position.bottom;
popup.style.display = 'inline-block';
}
const hidePopup = () => {
popup.style.display = 'none';
}
const circles = document.querySelectorAll(".red-circle")
circles.forEach(el => el.addEventListener('mouseover', (e) => {
const hoveredEl = e.target;
const textContent = hoveredEl.getAttribute('data-content');
//get absolute position of elements
let elBounds = hoveredEl.getBoundingClientRect();
//get absolute position of parent;
let ctBounds = popup.parentElement.getBoundingClientRect();
//calculate relative positions
let left = elBounds.left - ctBounds.left + (elBounds.width / 2),
top = elBounds.top - ctBounds.top + (elBounds.height / 2),
width = ctBounds.width,
height = ctBounds.height
//prepare position settings
let position = { left: "auto", top: "auto", bottom: "auto", right: "auto" }
//calculate if we're over 50% of box size
if(top>ctBounds.height/2) position.bottom = ctBounds.height - top + 20 + 'px'; else position.top = top + 20 + 'px';
if(left>ctBounds.width/2) position.right = ctBounds.width - left + 20 + 'px'; else position.left = left + 20 + 'px';
showPopup(textContent, position);
}))
circles.forEach(el => el.addEventListener('mouseout', (e) => { hidePopup() }))
.container { width: 200px; height: 200px; border: 1px solid black; position: relative;}
.red-circle { border-radius: 50%; background: red; width: 20px; height: 20px; position: absolute;}
#pop-up { background-color: #EFEFEF; padding: 0.25em; position: absolute;}
<div class="container">
<div style="top:20px;left:20px;" class="red-circle" data-content="This is a red circle"></div>
<div style="top:10px;left:150px;" class="red-circle" data-content="This is the top-right red circle"></div>
<div style="top:140px;left:150px;" class="red-circle" data-content="This is the bottom-right red circle"></div>
<div style="top:140px;left:15px;" class="red-circle" data-content="This is the bottom-left red circle"></div>
<span style="display:hidden" id="pop-up"></span>
</div>
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 |

