There were a couple of posts about how to close dialogs by clicking outside them, but these post were about custom dialogs, not the native HTML <dialog>
element and its modal opening that can also be closed by the Escape key.
There was a blog post by Lindsay Wardell about how to use native <dialog>
with ports. I tried it and it works well but:
- It uses one port.
- You cannot close the modal by clicking outside it.
- You cannot replay the modal opening in the debugger (opening does not depend on the model, although you could easily fix that).
So I ended up using the following custom element:
/* index.html */
<style>
dialog {
padding: 1rem;
}
/* .modal-filler first dialog child needed to prevent closing on dialog padding */
dialog:modal > .modal-filler {
opacity: 0;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -10;
}
/* scrim */
dialog::backdrop {
background: rgb(0 0 0 / 0.5);
}
}
</style>
<script>
customElements.define('modal-dialog',
class extends HTMLElement {
static observedAttributes = [ 'open']
#isConnected = false
connectedCallback() {
this.#setContent()
this.#isConnected = true
}
attributeChangedCallback() { if (this.#isConnected) this.#setContent() }
#setContent() {
const isOpen = this.getAttribute('open') !== null
const dialog = this.querySelector('dialog')
if (dialog) {
if (isOpen) {
dialog.onclick = (event) => {
// closing only if clicked on scrim, not on the filler
if (event.currentTarget === event.target) dialog.close()
}
dialog.showModal()
} else {
dialog.close()
}
}
}
}
)
</script>
along with the following Elm helper:
import Html exposing (Html, Attribute, div, node)
import Html.Attributes exposing (class, attribute)
import Html.Events exposing (on)
import Json.Decode as D
onClose : msg -> Attribute msg
onClose message =
on "close" (D.succeed message)
modalDialog : Bool -> msg -> List (Attribute msg) -> List (Html msg) -> Html msg
modalDialog isOpen closeMsg attributes children =
node "modal-dialog" (if isOpen then [ attribute "open" ""] else [])
[ node "dialog" ((onClose closeMsg) :: attributes) <|
(div [ class "modal-filler"] []) :: children
]
Now I can simply use native HTML <dialog>
modals in Elm like this:
modalDialog (model.isMyModalOpen) ToggleIsMyModalOpen []
[ p []
[ text "My modal!" ]
, button [ onClick ToggleIsMyModalOpen ]
[ text "Close" ]
]
Let me know if you have another way to use native dialog elements as modals!