DEV Community

Cover image for Breaking out of overflow hidden
Simon
Simon

Posted on • Edited on

Breaking out of overflow hidden

Have you ever tried to create a dropdown for your button, select but then getting blocked by overflow hidden?

Then what do you do, well then you reach for javascript to throw the element to the root of the DOM and then position the element based on the trigger elements rect, recalulating everytime layout changes, scrolling og resize window happens, not very effective.

I wanna start by saying it dosn't have full support yet, but there is a polyfill to solve that for now.

The two main features we're gonna rely on are

anchor-positioning

anchor positioning compatability matrix

popovers

popover compatability matrix

So my initial idea came when I saw the dialog are sent to the root but with DOM layer. I stumpled across the popover api where you basically get the same logic but just acts slightly different.

I also remember seeing a post about anchoring an element to a thumb on a range slider.

Then i thought basically what you wanna do for any dropdown/tooltip etc that needs to break overflow hidden if you mix the two you didn't need all the excessive javascript.

So here is a working demo in the browsers that support it

  • Chrome, Edge v125+
  • Opera v111+
  • Chrome for Andriod, Andriod browser v129+

Okay so what if you wanna use it today. Well there is a polyfill for that that we can use.

The oddbird/css-anchor-positioning polyfill

Which has great Browser Support

  • Firefox 54+
  • Chrome 51+
  • Edge 79+
  • Safari 10+

Try visiting a browser not mentioned in the non polyfill example and this example below works, I have personally tested the latest versions of safari and firefox

This means that yes in most browsers we're going to have the extra computed overhead but its easy to use and toggle off when we have sufficient compatability.

There is one caviat to this approach and that is that the polyfill is ~92kb in size so that will have impact on load, but you could lazy load it after the application/website is done doing anything important


Say you dont wanna add another 92kb to your gzipped bundle which is a tall ask what you could do would be to do something like this

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Popover</title>
     

    <style>
      /* Basic popover styles */
      .popover-trigger {
        position: relative;
        display: inline-block;
        cursor: pointer; /* Added */
      }

      .popover {
        position: absolute;
        display: none;
        background-color: white;
        box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
        z-index: 1000; /* Ensure popover is on top */
        min-width: 180px; /* Added */
      }

      .popover.show {
        display: block;
      }

      /* Add more styles as needed */

      /* Added styles from Angular component */
      spk-popover {
        display: inline-flex;
      }

      .popover-trigger {
        display: inline-flex;
      }

      .popover-trigger [spk-button] {
        display: none;
      }

      .popover-trigger-wrapper {
        display: inline-flex;
      }

      .popover-trigger-wrapper:empty + * {
        display: inherit;
      }

      .popover-overlay {
        position: fixed;
        inset: 0;
        opacity: 0;
        z-index: 1;
        pointer-events: none;
      }

      .popover {
        margin: 0;
        z-index: 5000;
        background-color: var(--base-level-10);
        border: 1px solid var(--base-level-40);
        border-radius: var(--shape-2);
        font: var(--paragraph-30);
        color: var(--base-level-60);
        transition: opacity 125ms linear, visibility 125ms linear;
        opacity: 0;
        visibility: hidden;
      }

      @supports (left: anchor(left)) and (top: anchor(bottom)) {
        .popover {
          left: anchor(left);
          top: anchor(bottom);
          margin-top: 4px;
        }
      }

      .popover.show {
        /* Use .show instead of :popover-open */
        opacity: 1;
        visibility: visible;
        transition: opacity 125ms linear, visibility 125ms linear;
      }
    </style>
  </head>
  <body>
    <div class="popover-trigger" id="myPopoverTrigger">
      <button>Open Popover</button>
      <div class="popover" id="myPopover">
        <p>This is the popover content.</p>
      </div>
    </div>

    <script>
      const BASE_SPACE = 4;
      const SUPPORTS_ANCHOR =
        CSS.supports("position-anchor", "--abc") &&
        CSS.supports("anchor-name", "--abc");

      document
        .getElementById("myPopoverTrigger")
        .addEventListener("click", function () {
          const popover = document.getElementById("myPopover");
          popover.classList.toggle("show");

          if (popover.classList.contains("show")) {
            calculateMenuPosition(this, popover);

            const scrollableParent = findScrollableParent(popover);

            const scrollHandler = () => calculateMenuPosition(this, popover);
            const resizeHandler = () => calculateMenuPosition(this, popover);

            scrollableParent.addEventListener("scroll", scrollHandler);
            window.addEventListener("resize", resizeHandler);

            // Remove event listeners when popover is hidden (optional)
            popover.addEventListener("transitionend", () => {
              if (!popover.classList.contains("show")) {
                scrollableParent.removeEventListener("scroll", scrollHandler);
                window.removeEventListener("resize", resizeHandler);
              }
            });
          }
        });

      function findScrollableParent(element) {
        const scrollableStyles = ["scroll", "auto"];
        let parent = element.parentElement;

        while (parent) {
          const style = window.getComputedStyle(parent);
          if (
            scrollableStyles.includes(style.overflowY) &&
            parent.scrollHeight > parent.clientHeight
          ) {
            return parent;
          }
          parent = parent.parentElement;
        }

        return document.documentElement;
      }

      function calculateMenuPosition(trigger, popover) {
        const triggerRect = trigger.getBoundingClientRect();
        const menuRect = popover.getBoundingClientRect();

        let newLeft = triggerRect.left;
        let newTop = triggerRect.bottom + BASE_SPACE;

        const outOfBoundsRight = newLeft + menuRect.width > window.innerWidth;
        const outOfBoundsBottom = newTop + menuRect.height > window.innerHeight;

        // Basic positioning logic (you'll need to expand this based on your requirements)
        if (outOfBoundsBottom) {
          newTop = triggerRect.top - menuRect.height - BASE_SPACE;
        }
        if (outOfBoundsRight) {
          newLeft = triggerRect.left - menuRect.width - BASE_SPACE;
        }

        popover.style.left = `${newLeft}px`;
        popover.style.top = `${newTop}px`;
      }
    </script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

So the idea here is that we calculate the position on the popover relative to the popoverTrigger if and only if anchor-name and position-anchor is not supported by our browser. This way we dont ship excessive code to our browser but still support all the way back till the versions mention about the popover compatability chart

What more to add, well in my mind these should auto detect when out of the view and this is not supported by the browser position handles left: anchor(left); so this would be something i would add in my example above.

Top comments (0)