ensolabs/designsystemet-elm/1.0.0

Designsystemet.Dialog

A dialog over the page, for a task or message the user must deal with before continuing.

Mirrors Dialog from @digdir/designsystemet-react.

import Designsystemet.Button as Button
import Designsystemet.Dialog as Dialog


confirmDelete : Dialog.Id
confirmDelete =
    Dialog.id "confirm-delete"


view : Html Msg
view =
    Html.div []
        [ Dialog.trigger confirmDelete [] [ Html.text "Slett sak" ]
        , Dialog.new confirmDelete
            |> Dialog.withCloseButton { label = "Lukk dialogvindu" }
            |> Dialog.view [ Dialog.onClose DeleteDialogClosed ]
                [ Dialog.block [] [ Heading.h2 [] [ Html.text "Slette saken?" ] ]
                , Dialog.block []
                    [ Button.view [ Events.onClick Delete ] [ Html.text "Slett" ]
                    , Button.view (Button.secondary :: Dialog.close confirmDelete)
                        [ Html.text "Avbryt" ]
                    ]
                ]
        ]

The dialog is a native <dialog>. Open it in one of two ways:

  • From a button, with an invoker (command/commandfor, polyfilled by designsystemet-web). No message or model field needed.
  • From your model, with open and a Bool. This is React's open prop, for dialogs opened by an event (a timeout, a failed save) rather than a click.

React → Elm

  • <Dialog id="x"> → Dialog.new (Dialog.id "x") |> Dialog.view [] […]
  • closeButton="Lukk" (the default) → Dialog.withCloseButton { label = "Lukk" }
  • closeButton={false} → leave out withCloseButton
  • <Dialog.Block> → Dialog.block []
  • <Dialog.TriggerContext> → not needed: share a Dialog.Id
  • <Dialog.Trigger> → Dialog.trigger id []
  • <Button command="show-modal" commandfor="x"> → Button.view (Dialog.showModal id)
  • modal={false} → Dialog.nonModal, or open with Dialog.showNonModal id
  • placement="right" → Dialog.drawer Dialog.Right
  • closedby="any" → Dialog.closedByAny
  • onClose → Dialog.onClose msg
  • open={isOpen} + onClose → Dialog.open model.isOpen, Dialog.onClose Closed

Differences from React

  • The close button is opt-in and its label is required. There is no Norwegian default.

Managed by designsystemet-web

  • aria-haspopup="dialog" on every button that opens a dialog, set on focus. trigger also renders it up front, as React's Dialog.Trigger does. The value is the same, so they don't conflict.
  • the closedby="any" light dismiss, in browsers without native support.

Opening from your model

Opening a dialog takes a method call (showModal()), which Elm can't make. open renders the state as data-open, and the package's JavaScript syncs the element:

import "@ensolabs/designsystemet-elm"; // once, in your entry point
Dialog.new sessionExpired
    |> Dialog.view
        [ Dialog.open model.sessionExpired
        , Dialog.onClose SessionDialogClosed
        ]
        [{- … -}]

When the user closes the dialog (Esc, a close button), it closes and sends onClose. Set your state back to closed there. Until you do, open True won't reopen it, since the attribute hasn't changed.

Ids

type Id

The id that links a dialog to the buttons that open and close it.

Make one per dialog, as a top-level value, and use it in both places. A Dialog.Id can't be given to a Popover by mistake.

id : String -> Id

Name a dialog. The string becomes its DOM id, so it must be unique on the page.

confirmDelete : Dialog.Id
confirmDelete =
    Dialog.id "confirm-delete"
idToString : Id -> String

The DOM id, for ports and tests.

Dialog

type Dialog

A dialog being configured. Start with new, finish with view.

A dialog with no close button.

withCloseButton : { label : String } -> Dialog -> Dialog

Add an "×" close button in the top corner. It has no visible text, so label is what screen readers announce.

Dialog.new confirmDelete
    |> Dialog.withCloseButton { label = "Lukk dialogvindu" }
    |> Dialog.view [] [{- … -}]
view : List (Attribute msg) -> List (Html msg) -> Dialog -> Html msg

A <dialog class="ds-dialog">, closed unless you give it open.

Put content directly in it, or split it into blocks with their own padding and dividers.

The id always comes from the Id given to new, so triggers keep pointing at it. An Attr.id in the attributes is ignored.

Content

block : List (Attribute msg) -> List (Html msg) -> Html msg

A section of the dialog, like a header, the body or a footer of buttons.

Dialog.view []
    [ Dialog.block [] [ Heading.h2 [] [ Html.text "Endre adresse" ] ]
    , Dialog.block [] [ addressForm ]
    , Dialog.block [] [ saveButton, cancelButton ]
    ]
    dialog

Placement

drawer : Side -> Attribute msg

Show the dialog as a drawer along one edge of the screen instead of in the center.

Dialog.view [ Dialog.drawer Dialog.Right ] [ filterForm ] filters
type Side
= Left
| Right
| Top
| Bottom

The edge of the screen a drawer slides in from.

Open state

open : Bool -> Attribute msg

Whether the dialog is open, from your model. Needs the companion script (see Opening from your model), and an onClose that sets your state back when the user closes it.

Dialog.view [ Dialog.open model.confirming, Dialog.onClose ConfirmClosed ] [{- … -}]
nonModal : Attribute msg

No backdrop, and the page behind stays usable, when opened with open. React's modal={false}. Invoker buttons choose for themselves with showNonModal.

Light dismiss

How the user can close the dialog without a close button. The default is closedByCloseRequest for modal dialogs (Esc closes it).

closedByAny : Attribute msg

Close on Esc and on a backdrop click. Good for dialogs that only show information.

closedByCloseRequest : Attribute msg

Close on Esc (or the Android back gesture), but not on a click outside.

closedByNone : Attribute msg

Close only from a button in the dialog. Use sparingly, since the user has no other way out.

Events

onClose : msg -> Attribute msg

Sent when the dialog closes, by a close button, Esc, a backdrop click or code.

Dialog.view [ Dialog.onClose EditDialogClosed ] [{- … -}] editDialog

The native close event. It doesn't bubble, so a nested dialog never triggers it.

Triggers

trigger : Id -> List (Attribute msg) -> List (Html msg) -> Html msg

A Button that opens the dialog as a modal (React's Dialog.Trigger).

Dialog.trigger editAddress [ Button.secondary ] [ Html.text "Endre adresse" ]

For a non-modal dialog, use showNonModal on a button instead.

Invokers

command and commandfor for any <button>: a Button, a Dropdown item or a plain button. They are lists because the browser needs two attributes. Concatenate them with your own:

Button.view (Button.secondary :: Dialog.close confirmDelete) [ Html.text "Avbryt" ]
showModal : Id -> List (Attribute msg)

Open the dialog as a modal: the rest of the page is inert behind a backdrop until it closes.

showNonModal : Id -> List (Attribute msg)

Open the dialog without a backdrop, leaving the rest of the page usable.

close : Id -> List (Attribute msg)

Close the dialog immediately, as a "Cancel" button does.

requestClose : Id -> List (Attribute msg)

Ask the dialog to close, as Esc does. It fires a cancelable cancel event first, for example to warn about unsaved changes.