You can notice that the message options menu overflows the scrollable “Channel” messages panel and goes over the “Thread” one.

The Problem

As far as I know, this is not solvable only with CSS except for some very simple cases (no scrolling and each “popout” positioned manually relative to an ancestor outside its container, see next section).

I made a simple Ellie to illustrate the problem with a naive approach using position: absolute relative to an ancestor inside the scrollable container:https://ellie-app.com/4M6xfxfvqkBa1

The options menu extends its scrollable container instead of “poping-out” of it.

Known solutions

I am aware of two techniques to fix this:

Set the “popout” div with position: absolute and relative to an ancestor of the scrollable (or overflow: hidden) div, as explained in css-tricks. It then needs to be positioned dynamically because its container may have been scrolled.

Change the parent of the popout div but keep its position. I believe that this is what React portals are for (I think that portals also keep bubbling events to its original parent).

In addition, to avoid updating the position on scrolling, scrolling is often disabled when the “popout” is displayed, like in slack.

An imperfect implementation in Elm

Solution 2. would require some javascript and would most likely not play well with the Virtual Dom, so I implemented solution 1. using the following techniques:

When the "popout’ is enabled, Browser.Dom.getElement is used to get the position of an element that will be used as an anchor for positioning.

The “popout” has position: absolute and is displayed only once its anchor element position is known in order to compute the final position. Because getElement will wait for the next frame, this means that we often miss one frame before displaying the “popout” but I don’t think that there is a better solution.

When the “popout” is displayed, scrolling is disabled by intercepting the scrollable container wheel and touchmove events, because the “popout” would not move inside the scrolled viewport

Unfortunately scrolling is still possible with the keyboard, which leads to an incorrect position of the popout. I guess we could also ignore some keyboard events, but TAB can cause scrolling, and it does not seem reasonable to block it.

I’m also not convinced that blocking the wheel and touchmove events works everywhere and does not have unwanted side effects.

A better solution

As scrolling seems to be the main pain point and an events handling rabbit hole, a reverse thinking solution is to close the “popout” on any scroll event. It seems reasonable, a lot more robust, and does not break any keyboard/mouse/touchscreen navigation:

This seems like something elm-ui could do with some effort. For instance with the slack layout example you have the “channel” pane and the “thread” pane. These would be wrapped in some sort of “content” element. In elm-ui you can have the message options menu inFront of the “content”. This popout would be present upon some msg being passed to update, so passing the position info along with the msg would be reasonable.

To fix the scrolling behavior you could store something like scrollable: bool in the model and when you pass the msg for the “popout” to launch or hide you toggle the scrollable field. Then make the scrollable elements attributes dependent on the scrollable state.

To fix the scrolling behavior you could store something like scrollable: bool in the model and when you pass the msg for the “popout” to launch or hide you toggle the scrollable field. Then make the scrollable elements attributes dependent on the scrollable state.

I tried something like this by setting overflow: hidden instead of overflow: auto on the scrollable container when the popout is displayed:

Ah, that is tricky. I suppose you could use some javascript to have a custom scrollbar with a fixed width and you can replace it with a transparent element the same width when switching to overflow: hidden. Probably more effort than it’s worth and would be tough to maintain.

Yes, also this would not prevent scrolling with TAB, and I’m not convinced anyway that this is a better solution overall than closing the “popout” when scroll occurs. It might be a more common behaviour though (so less surprising for users), I’m not sure.

I think closing the popout on scroll is pretty good. It think it might even be better UX than blocking it, because if the user wants to scroll, they should be allowed to, without first having to make sure any popout is closed. This is how the context menu and tooltips in Chrome behave too. Although in Firefox, the context menu blocks scrolling.

It behaves really nice! And “close-on-scroll” approach is indeed concise in implementation AND intuitive!
Thanks for writing this up
I had the very similar situation in my dropdown impl and was looking for a good solution. Now I will definitely adopt your approach.

I think this is a requirement found commonly enough. If we can find a good way to define generic APIs, it deserves to be a package!