ensolabs/designsystemet-elm/1.0.0

Designsystemet.Suggestion

Experimental. A combobox: a text input that suggests values from a list as you type, for picking one or several.

Mirrors EXPERIMENTAL_Suggestion from @digdir/designsystemet-react. As there, the API and markup may change in any release.

Field.view []
    [ Label.view [] [ Html.text "Kommune" ]
    , Suggestion.single
        { options = Municipality.all
        , selected = model.municipality
        , toValue = Municipality.toCode
        , toLabel = Municipality.toName
        , onSelect = PickedMunicipality
        }
        |> Suggestion.withClear { label = "Tøm" }
        |> Suggestion.withEmptyMessage [ Html.text "Ingen treff" ]
        |> Suggestion.view []
    ]


Suggestion.multiple
    { options = Tag.all
    , selected = model.tags
    , toValue = Tag.toId
    , toLabel = Tag.toName
    , onChange = ChangedTags
    }
    |> Suggestion.withToggle { label = "Vis alle" }
    |> Suggestion.view [ Attr.attribute "aria-label" "Stikkord" ]

The attributes you give view go on the text <input />. Add Events.onInput there to get the typed text, e.g. to fetch options from a server.

Controlled only

Elm owns the selection. When the user picks or removes a value, the web component fires comboboxbeforeselect. The module cancels it and sends onSelect or onChange with the new selection, so nothing changes on screen until your model does.

React also has an uncontrolled mode (defaultSelected) where the web component adds and removes selected items itself. That would insert nodes among Elm's, so it isn't ported.

If a value isn't in options (or in selected, when removing), you get your current selection back unchanged and the event is still cancelled.

React → Elm

  • <Suggestion selected onSelectedChange> → Suggestion.single { … }
  • <Suggestion multiple selected onSelectedChange> → Suggestion.multiple { … }
  • <Suggestion.Input> → the attributes of Suggestion.view
  • <Suggestion.Option value> → options, toValue, toLabel
  • <Suggestion.Toggle aria-label> → Suggestion.withToggle { label = "…" }
  • <Suggestion.Clear> + data-sr-clear → Suggestion.withClear { label = "…" }
  • <Suggestion.Empty> → Suggestion.withEmptyMessage [ … ]
  • <Suggestion.Option label> with markup in it → Suggestion.withOptionContent (\a -> [ … ])
  • filter={false} → Suggestion.withoutFiltering
  • filter={fn} → filter options yourself + withoutFiltering
  • creatable + <Suggestion.Empty data-empty="…"> → Suggestion.withCreatable { … }
  • name → Suggestion.withName "…"
  • renderSelected → toLabel
  • defaultSelected → not ported: controlled only
  • onBeforeMatch → not ported

Filtering

By default the web component shows the options whose label contains the typed text, ignoring case, like React's default filter. With withoutFiltering every option you pass is shown, so you can filter options yourself or show what a server sent back.

Managed by designsystemet-web

<ds-suggestion> (built on u-combobox and u-datalist from u-elements) writes these, so the module never renders them:

  • on the input: role, list, popovertarget, autocomplete, enterkeyhint, aria-autocomplete, aria-controls, aria-expanded, aria-activedescendant, aria-description, and its value in single mode (it shows the selected label)
  • on the selected <data> items: aria-label, role, slot, tabindex, hidden
  • on the options: selected, aria-selected, aria-disabled, aria-hidden, role, id
  • on the list: hidden, tabindex, aria-label, data-sr-*, data-is-floating
  • on the empty option: hidden, label, value, disabled, data-create
  • on the clear and toggle buttons: hidden, aria-hidden, tabindex, and aria-expanded on the toggle (Elm renders the initial hidden and aria-expanded="false", like React, and never changes them)
  • the <option>s of the hidden <select> from withName, and its multiple (set from data-multiple)
  • the screen reader texts (data-sr-*) other than the clear label, from the CSS custom properties --dsc-suggestion-sr-*
type Suggestion a msg

A suggestion field being configured. Start with single or multiple, end with view.

{ options : List a
, selected : Maybe a
, toValue : a -> String
, toLabel : a -> String
, onSelect : Maybe a -> msg
}
-> Suggestion a msg

Pick at most one value. The input shows the selected label.

  • toValue must give every option a different string.
  • toLabel is what the user sees and types to find it.
  • onSelect gets Nothing when the user clears the field.
{ options : List a
, selected : List a
, toValue : a -> String
, toLabel : a -> String
, onChange : List a -> msg
}
-> Suggestion a msg

Pick any number of values. They are shown as removable chips before the input, in the order of selected.

view : List (Attribute msg) -> Suggestion a msg -> Html msg

Render the field. The attributes go on the text <input />.

It has no visible label. Put it in a Field with a Label, or give the input an aria-label.

Buttons and messages

withToggle : { label : String } -> Suggestion a msg -> Suggestion a msg

A button, shown as an arrow, that opens and closes the list. label is its accessible name.

withClear : { label : String } -> Suggestion a msg -> Suggestion a msg

A button, shown as an ×, that empties the input (and in single mode the selection). label is its accessible name.

withEmptyMessage : List (Html msg) -> Suggestion a msg -> Suggestion a msg

What the list shows when nothing matches, like [ Html.text "Ingen treff" ]. While fetching options from a server, show [ Spinner.decorative [], Html.text "Laster..." ] instead.

withOptionContent : (a -> List (Html msg)) -> Suggestion a msg -> Suggestion a msg

Show more than the label in each option, like a second line of detail. toLabel is still what the user types to find it, what the input shows once picked, and what a chip says.

|> Suggestion.withOptionContent
    (\place -> [ Html.text place.name, Html.div [] [ Html.text place.county ] ])

Behaviour

Show every option you pass, instead of only those matching the typed text.

{ template : String
, onCreate : String -> msg
}
-> Suggestion a msg
-> Suggestion a msg

Let the user add a value that isn't among the options.

  • template is the text of the "add" option in the list; %s is replaced by what the user typed: "Legg til «%s»".
  • onCreate gets the typed text when the user picks it. Turn it into one of your values, add it to options and to the selection.

The "add" option replaces the withEmptyMessage message.

withName : String -> Suggestion a msg -> Suggestion a msg

Submit the selected values with a surrounding <form>, under this name. A hidden <select> holds them.