Search code examples
htmlcssscrollcss-position

Position relative with overflow and scroll


I have a div with a fixed height, and overflow: auto. In that div, I have a button that shows a small popup when pressed. The popup is using position: absolute, and the entire containing div has position: relative so that the popups do not get clipped by the overflow. My issue is that when I scroll, the popups with absolute position get positioned to where the button is as if there was no scrolling not to where it currently is. How can I get around this?

This is a CodePen mimicking my issue: https://codepen.io/Hunky524/pen/jOKxYby When you hover over a grey box, the blue box shows at the top, but if you scroll down, and then hover over a grey box, the blue box becomes mispositioned.

This is a screenshot of the issue in my application. You can see my mouse over the button that shows the popup. I would expect the popup to appear where my mouse is, but because I've scrolled down a bit, it gets offset. enter image description here


Solution

  • Just gimme the code... Here is a working CodePen example. Notice now when you scroll down, and hover over a grey box, the blue popup is correctly aligned: https://codepen.io/Hunky524/pen/LYrrVZE

    Further explanation...

    I was able to come up with a solution for my specific problem. Note: This is not a general solution, and will only work for setups similar to mine. The most important part is that there is only one parent in the DOM tree that has position: relative, otherwise the function I made won't calculate the correct offset.

    This is my HTML structure for reference. Written in Angular, and I left only what matters:

    <!--  Important that no elements from the root HTML element to here have relative position  -->
    <div style="position: relative; overflow-y: auto;">
        <!--  Many elements between these two elements  -->
        <!--  Also important that no elements between these have relative position  -->
        <button class="popup-parent" (focus)="onPopupFocus($event)">
            <span>{{ offset }}</span>
            <div class="popup">
                <!-- Popup content here  -->
            </div>
        </button>
    </div>
    
    .popup {
        display: none;
        position: fixed;
    }
    
    .popup-parent:focus-within > .popup {
        display: block;
    }
    

    Then this was the code ran when the button element was focused (clicked):

    public onPopupFocus(event: FocusEvent): void {
        event.stopImmediatePropagation();
    
        const target = event.currentTarget as HTMLElement;
        const targetOffsetTop = target.offsetTop;
    
        // This is the first parent with relative positioning
        // We assume there are no other relative parents
        const offsetParent = target.offsetParent as HTMLElement;
        const offsetParentScrollOffset = offsetParent?.scrollTop ?? 0;
    
        const offsetTop = offsetParent.offsetTop + (targetOffsetTop - offsetParentScrollOffset);
    
        // We assume the popup DIV card will always be after an initial SPAN
        const popup = target.children[1] as HTMLElement;
    
        popup.style.top = `${offsetTop}px`;
    }
    

    Now, in the picture you can see that even being scrolled almost all the way down, the popup is positioned correctly, while also not being clipped by the overflow, and being able to extend outside of the parent card.

    enter image description here

    A more general solution would require doing some kind of recursive calling on the parent offset elements until you reach the root HTML element, and summing up all the offsets.