admin管理员组

文章数量:1386760

I have an absolutely positioned tooltip within an ancestor element. The ancestor has overflow: auto so that if its content overflows, it will provide a scrollbar. But I don't want the tooltip to be included in this content - I want the tooltip to break out of this box and overflow it. I can achieve this by setting it to position: fixed, but then it won't stay with its immediate parent (the thing I'm hovering over) if the user has scrolled. Absolute positioning stays with the parent, but the tooltip gets clipped.

How can I have the tooltip stay where it belongs, anchored to the hovered element even while scrolling, and yet allow the tooltip to go beyond the boundaries of the scrollable ancestor?

With fixed positioning (tooltip is not aligned iwth the TS$ element my mouse is over, due to the scrollbar offset:

With absolute positioning (tooltip is correctly positioned even with scroll, but the tooltip is clipped):

Update:

I was able to get the behavior I wanted with some inelegant calculations to set left and top with values while using position: fixed. It required recursively traversing up the DOM until hitting a new containing box, and aggregating all the scroll values, and traversing up the offset chain to aggregate the offsets, then subtracting.

export const isFixedContainingBlock = (el) => {
  const style = getComputedStyle(el)

  return ([style.filter,style.backdropFilter,style.transform,style.perspective].some(v => v !== "none") ||
          ["layout","paint","strict","content"].includes(style.contain) ||
          style.containerType !== "normal" ||
          ["filter","transform"].includes(style.willChange) ||
          style.contentVisibility === "auto")
}

then:

  import { isFixedContainingBlock } from ...

  let ot=0, ol=0, st=0, sl=0

  for (let n=e.target;!isFixedContainingBlock(n); n=n.parentElement) {
    sl += n.scrollLeft
    st += n.scrollTop
  }

  for (let n=e.target;!isFixedContainingBlock(n); n=n.offsetParent) {
    ot += n.offsetTop
    ol += n.offsetLeft
  }

  let posLeft=ol-sl, posTop=ot-st
 
  node.style.setProperty('left', posLeft+"px")
  node.style.setProperty('top', posTop+"px")

This is working, but it shouldn't be this hard to do something so simple. It sure seems as if CSS ought to provide a better way to do this.

I have an absolutely positioned tooltip within an ancestor element. The ancestor has overflow: auto so that if its content overflows, it will provide a scrollbar. But I don't want the tooltip to be included in this content - I want the tooltip to break out of this box and overflow it. I can achieve this by setting it to position: fixed, but then it won't stay with its immediate parent (the thing I'm hovering over) if the user has scrolled. Absolute positioning stays with the parent, but the tooltip gets clipped.

How can I have the tooltip stay where it belongs, anchored to the hovered element even while scrolling, and yet allow the tooltip to go beyond the boundaries of the scrollable ancestor?

With fixed positioning (tooltip is not aligned iwth the TS$ element my mouse is over, due to the scrollbar offset:

With absolute positioning (tooltip is correctly positioned even with scroll, but the tooltip is clipped):

Update:

I was able to get the behavior I wanted with some inelegant calculations to set left and top with values while using position: fixed. It required recursively traversing up the DOM until hitting a new containing box, and aggregating all the scroll values, and traversing up the offset chain to aggregate the offsets, then subtracting.

export const isFixedContainingBlock = (el) => {
  const style = getComputedStyle(el)

  return ([style.filter,style.backdropFilter,style.transform,style.perspective].some(v => v !== "none") ||
          ["layout","paint","strict","content"].includes(style.contain) ||
          style.containerType !== "normal" ||
          ["filter","transform"].includes(style.willChange) ||
          style.contentVisibility === "auto")
}

then:

  import { isFixedContainingBlock } from ...

  let ot=0, ol=0, st=0, sl=0

  for (let n=e.target;!isFixedContainingBlock(n); n=n.parentElement) {
    sl += n.scrollLeft
    st += n.scrollTop
  }

  for (let n=e.target;!isFixedContainingBlock(n); n=n.offsetParent) {
    ot += n.offsetTop
    ol += n.offsetLeft
  }

  let posLeft=ol-sl, posTop=ot-st
 
  node.style.setProperty('left', posLeft+"px")
  node.style.setProperty('top', posTop+"px")

This is working, but it shouldn't be this hard to do something so simple. It sure seems as if CSS ought to provide a better way to do this.

Share Improve this question edited Mar 18 at 20:30 Paul W asked Mar 17 at 16:55 Paul WPaul W 12.2k2 gold badges7 silver badges24 bronze badges 2
  • 1 I don't think this is possible because if it is absolute, then it will be within the overflow container, which makes it get clipped. The best you can do is use the fixed position but calculate the position using the offset of the hovered element – Pete Commented Mar 17 at 17:13
  • Use some library, for example Tippy.js – imhvost Commented Mar 17 at 21:25
Add a comment  | 

1 Answer 1

Reset to default 0

One approach you can take in pure css is wrapping the parent with a "container". First you use position: fixed; to overcome the overflow and then move the container from the viewport to the parent by setting the contain property. This keeps the overflowed content parked relatively:

ul {
    width: 200px;
    max-height: 250px;
    overflow: auto;
  margin: 1em;
    color: white;
    font-family: sans-serif;
    font-size: 16px;
}
li {
    position: relative;
    padding: 1em;
}
li ul {
    position: absolute;
    z-index: 10;
    display: none;
    margin: 0;
    cursor: auto;
}
li:hover > ul {
    display: block;
}
li:nth-child(2n) {
    background: #0E8CE0;
}
li:nth-child(2n+1) {
    background: #0064B3;
}
li.parent {
    background: #00B99B;
    cursor: pointer;
}

.ancestor {
    height: 200px;
    overflow: auto; /*  captures everything not having position:fixed  */
  border: dashed red;
}
.tooltip {
    position: fixed; /*  escape the overflow on ancestor  */
    top: 0;
    left: 50px;
}
.contain {
    height: fit-content;
    background-color: amber;
    contain: layout; /*  fixed position will be contained here  */
}
.buffer {
    height: 10rem;
    background-color: aqua;
}
<div class="buffer">Some page content</div>
<div class="contain">
<div class="ancestor">
    <ul>
      <li>Abc</li>
      <li>Def</li>
      <li>Ghi</li>
      <li>Jkl</li>
      <li class="parent">TooltipHere >
        <ul class="tooltip">
          <li>Abc</li>
          <li>Def</li>
          <li>Ghi</li>
          <li>Jkl</li>
          <li>Mno</li>
          <li>Pqr</li>
          <li>Stu</li>
          <li>Vw</li>
          <li>Xyz</li>
        </ul>
      </li>
      <li>Pqr</li>
      <li>Stu</li>
      <li>Vw</li>
      <li>Xyz</li>
      <li>Def</li>
      <li>Ghi</li>
      <li>Jkl</li>
      <li>Mno</li>
      <li>Pqr</li>
      <li>Stu</li>
      <li>Vw</li>
      <li>Xyz</li>
    </ul>
  <div class="buffer">Some page content</div>
  </div>
</div>

Hopefully this helps. Nevertheless, it's nice to get minimum reproducible examples so that nuances can be taken into account. Depending on your setup a pure css solution might not be possible.

本文标签: