ensolabs/designsystemet-elm/1.0.0

Designsystemet.Tabs

Content split into panels, one shown at a time.

Mirrors Tabs from @digdir/designsystemet-react.

type Section
    = Overview
    | Documents
    | History


sectionId : Section -> String
sectionId section =
    case section of
        Overview ->
            "case-overview"


        Documents ->
            "case-documents"


        History ->
            "case-history"


view : Model -> Html Msg
view model =
    Tabs.controlled { toId = sectionId, selected = model.section, onSelect = SectionSelected }
        []
        [ Tabs.tab { value = Overview, label = [ Html.text "Oversikt" ], panel = [ overview model ] }
        , Tabs.tab { value = Documents, label = [ Html.text "Dokumenter" ], panel = [ documents model ] }
        , Tabs.tab { value = History, label = [ Html.text "Historikk" ], panel = [ history model ] }
        ]

Each tab carries its panel, so neither can exist without the other.

The markup is designsystemet-web's <ds-tabs>, which handles the keyboard (arrow keys, Home, End), focus and showing the right panel.

Controlled or uncontrolled

  • uncontrolled: the browser tracks the open tab and nothing goes in your model. Use it when the rest of the page doesn't care which tab is open.
  • controlled: your model says which tab is open, and clicking a tab sends a message instead of switching. Use it when the open tab is in the URL or something else depends on it.

Ids

toId turns a value into the DOM id of its panel, so it must be unique on the page. Prefix it if you have more than one set of tabs.

React → Elm

  • <Tabs defaultValue="a"> → Tabs.uncontrolled { toId = …, initial = A } []
  • <Tabs value={v} onChange={f}> → Tabs.controlled { toId = …, selected = v, onSelect = F } []
  • <Tabs.List> + <Tabs.Tab value="a"> → Tabs.tab { value = A, label = […], panel = […] }
  • <Tabs.Panel value="a"> → the panel of that Tabs.tab
  • <Tooltip content="…"><Tabs.Tab> + icon → Tabs.iconTab { value = A, label = "…", icon = …, panel = […] }
  • <Tabs onChange={f}> without value → use controlled

Differences from React

React puts the raw value in each tab's data-value and derives the panel ids from it with useId. Here both are toId value, since Elm has no useId and toId already gives a unique, readable string.

Managed by designsystemet-web

Never set these yourself: tabindex on tabs and panels; hidden, aria-hidden, aria-labelledby and role on panels; role on the list. In uncontrolled tabs, aria-selected also belongs to the web component after the first render. Elm writes it again only when initial changes. Elm renders the tabs' role="tab" and aria-controls as React does, with the values the web component would write.

type Tab a msg

A tab and the panel it shows.

tab :
{ value : a
, label : List (Html msg)
, panel : List (Html msg)
}
-> Tab a msg

A tab with a label (what's on the tab itself) and the panel it shows.

Tabs.tab
    { value = Documents
    , label = [ documentIcon, Html.text "Dokumenter" ]
    , panel = [ documentList model.documents ]
    }
{ value : a
, label : String
, icon : Html msg
, panel : List (Html msg)
}
-> Tab a msg

A tab showing only an icon. The label is required, since an icon means nothing to a screen reader or to someone who doesn't recognise it. It names the tab and shows as a tooltip on hover and focus.

Tabs.iconTab
    { value = Cars
    , label = "Dine biler"
    , icon = carIcon
    , panel = [ Html.text "Du har ingen av denne typen registrert hos oss" ]
    }
{ toId : a -> String
, initial : a
}
-> List (Attribute msg)
-> List (Tab a msg)
-> Html msg

Tabs the browser switches between. initial is the tab open on first render.

Tabs.uncontrolled { toId = sectionId, initial = Overview } [] tabs

If initial changes later, that tab is opened. Otherwise Elm leaves the tabs alone.

{ toId : a -> String
, selected : a
, onSelect : a -> msg
}
-> List (Attribute msg)
-> List (Tab a msg)
-> Html msg

Tabs your model switches between. Clicking a tab (or pressing Enter or Space on it) sends onSelect with its value. The open tab changes only when selected does.

Tabs.controlled
    { toId = sectionId
    , selected = model.section
    , onSelect = SectionSelected
    }
    []
    tabs