A few months ago I ported our modals at work to use <dialog>
. This was a huge win for a11y and it broke our usage of elm-portal . You see, the browser native modals render on a separate layer from the rest of the DOM and elm-portal was only target the normal DOM layer, causing all of our dropdowns and tooltips to render behind our new modals!
Quick aside, for those wondering what portals I’m talking about I have a blog post about building a web component called elm-portal for building things like tooltips, dropdowns, at the time modals, and more.
Back to the portalling problem, resolving this involved 2 changes. The first is very simple. We already wrap all of our modals in a single module, so I duplicated our various target elements into our modal module. Roughly that means our code went from
module Main exposing (..)
view model =
{ title = ...
, body =
[ -- our regular content
, Html.div [ Html.Attributes.id "elm-portal-tooltip" ] []
]
to
module Main exposing (..)
view model =
{ title = ...
, body =
[ -- our regular content
, Html.div [ Html.Attributes.id "elm-portal-tooltip" ] []
]
module Modal exposing (..)
view ... =
Html.node "dialog"
[]
[ -- modal content
, Html.div [ Html.Attributes.id "elm-portal-tooltip" ] []
]
You might notice that this breaks using document.getElementById()
because now there are 2 elements on the page with the same id! We could change to another selector, and maybe there is a better way to do this*, but the route we went with is writing out own “nearest node” function
function nearestNodeWithId(el: HTMLElement, id: string): HTMLElement | null {
for (const sibling of getAllSiblings(el)) {
if (sibling.id === id) {
return sibling
}
}
if (el.parentElement) {
return nearestNodeWithId(el.parentElement, id)
}
return null
}
function getAllSiblings(el: HTMLElement): HTMLElement[] {
const siblings = []
let sibling = el?.parentNode?.firstChild
if (sibling) {
do {
// text node
if (sibling.nodeType !== 3) {
siblings.push(sibling as HTMLElement)
}
} while ((sibling = sibling.nextSibling))
}
return siblings
}
This allows our elm-portal component to look up the DOM tree for the nearest node with the specified ID.
* a better way might be to use a data-
attribute and a combination of query selectors, though I’m not sure what that’d look like. If anyone has suggestions for how to improve upon nearestNodeWithId
I’d love to hear about it!