diff --git a/elm.json b/elm.json index 449bd410..eb48b5aa 100644 --- a/elm.json +++ b/elm.json @@ -50,6 +50,7 @@ "Nri.Ui.PremiumCheckbox.V3", "Nri.Ui.PremiumCheckbox.V4", "Nri.Ui.SegmentedControl.V6", + "Nri.Ui.SegmentedControl.V7", "Nri.Ui.Select.V5", "Nri.Ui.Select.V6", "Nri.Ui.Svg.V1", diff --git a/src/Nri/Ui/SegmentedControl/V7.elm b/src/Nri/Ui/SegmentedControl/V7.elm new file mode 100644 index 00000000..57da7fd4 --- /dev/null +++ b/src/Nri/Ui/SegmentedControl/V7.elm @@ -0,0 +1,232 @@ +module Nri.Ui.SegmentedControl.V7 exposing (Config, Icon, Option, Width(..), view, viewSpa) + +{-| + +@docs Config, Icon, Option, Width, view, viewSpa + +-} + +import Accessibility.Styled exposing (..) +import Accessibility.Styled.Aria as Aria +import Accessibility.Styled.Role as Role +import Css exposing (..) +import EventExtras.Styled as EventExtras +import Html.Styled as Html exposing (Html) +import Html.Styled.Attributes as Attr exposing (css, href) +import Html.Styled.Events as Events +import Nri.Ui +import Nri.Ui.Colors.Extra exposing (withAlpha) +import Nri.Ui.Colors.V1 as Colors +import Nri.Ui.Fonts.V1 as Fonts +import Nri.Ui.Icon.V5 as Icon +import Nri.Ui.Util exposing (dashify) + + +{-| + + - `onClick` : the message to produce when an option is selected (clicked) by the user + - `options`: the list of options available + - `selected`: the value of the currently-selected option + - `width`: how to size the segmented control + - `content`: the panel content for the selected option + +-} +type alias Config a msg = + { onClick : a -> msg + , options : List (Option a) + , selected : a + , width : Width + , content : Html msg + } + + +{-| -} +type alias Option a = + { value : a + , icon : Maybe Icon + , label : String + } + + +{-| -} +type Width + = FitContent + | FillContainer + + +{-| -} +type alias Icon = + { alt : String + , icon : Icon.IconType + } + + +{-| -} +view : Config a msg -> Html.Html msg +view config = + viewHelper Nothing config + + +{-| Creates a segmented control that supports SPA navigation. +You should always use this instead of `view` when building a SPA +and the segmented control options correspond to routes in the SPA. + +The first parameter is a function that takes a `route` and returns the URL of that route. + +-} +viewSpa : (route -> String) -> Config route msg -> Html msg +viewSpa toUrl config = + viewHelper (Just toUrl) config + + +viewHelper : Maybe (a -> String) -> Config a msg -> Html msg +viewHelper maybeToUrl config = + let + selected = + config.options + |> List.filter (\o -> o.value == config.selected) + |> List.head + in + div [] + [ tabList + [ css + [ displayFlex + , cursor pointer + ] + ] + (List.map (viewTab maybeToUrl config) config.options) + , tabPanel + (List.filterMap identity + [ Maybe.map (Attr.id << panelIdFor) selected + , Maybe.map (Aria.labelledBy << tabIdFor) selected + , Just <| css [ paddingTop (px 10) ] + ] + ) + [ config.content + ] + ] + + +tabIdFor : Option a -> String +tabIdFor option = + "Nri-Ui-SegmentedControl-Tab-" ++ dashify option.label + + +panelIdFor : Option a -> String +panelIdFor option = + "Nri-Ui-SegmentedControl-Panel-" ++ dashify option.label + + +viewTab : Maybe (a -> String) -> Config a msg -> Option a -> Html.Html msg +viewTab maybeToUrl config option = + let + idValue = + tabIdFor option + + element attrs children = + case maybeToUrl of + Nothing -> + -- This is for a non-SPA view + Html.button + (Events.onClick (config.onClick option.value) + :: attrs + ) + children + + Just toUrl -> + -- This is a for a SPA view + Html.a + (href (toUrl option.value) + :: EventExtras.onClickPreventDefaultForLinkWithHref + (config.onClick option.value) + :: attrs + ) + children + in + element + (List.concat + [ [ Attr.id idValue + , Role.tab + , Aria.controls (panelIdFor option) + , css sharedTabStyles + ] + , if option.value == config.selected then + [ css focusedTabStyles + , Aria.currentPage + ] + + else + [ css unFocusedTabStyles ] + , case config.width of + FitContent -> + [] + + FillContainer -> + [ css expandingTabStyles ] + ] + ) + [ case option.icon of + Nothing -> + Html.text "" + + Just icon -> + viewIcon icon + , Html.text option.label + ] + + +viewIcon : Icon -> Html.Html msg +viewIcon icon = + Html.span + [ css [ marginRight (px 10) ] ] + [ Icon.icon icon ] + + +sharedTabStyles : List Style +sharedTabStyles = + [ padding2 (px 6) (px 20) + , height (px 45) + , Fonts.baseFont + , fontSize (px 15) + , fontWeight bold + , lineHeight (px 30) + , firstOfType + [ borderTopLeftRadius (px 8) + , borderBottomLeftRadius (px 8) + , borderLeft3 (px 1) solid Colors.azure + ] + , lastOfType + [ borderTopRightRadius (px 8) + , borderBottomRightRadius (px 8) + ] + , border3 (px 1) solid Colors.azure + , borderLeft (px 0) + , boxSizing borderBox + , cursor pointer + , property "transition" "background-color 0.2s, color 0.2s, box-shadow 0.2s, border 0.2s, border-width 0s" + , textDecoration none + ] + + +focusedTabStyles : List Style +focusedTabStyles = + [ backgroundColor Colors.glacier + , boxShadow5 inset zero (px 3) zero (withAlpha 0.2 Colors.gray20) + , color Colors.navy + ] + + +unFocusedTabStyles : List Style +unFocusedTabStyles = + [ backgroundColor Colors.white + , boxShadow5 inset zero (px -2) zero Colors.azure + , color Colors.azure + , hover [ backgroundColor Colors.glacier ] + ] + + +expandingTabStyles : List Style +expandingTabStyles = + [ flexGrow (int 1) + , textAlign center + ] diff --git a/styleguide-app/Examples/SegmentedControl.elm b/styleguide-app/Examples/SegmentedControl.elm index e23e6adc..f3463b0f 100644 --- a/styleguide-app/Examples/SegmentedControl.elm +++ b/styleguide-app/Examples/SegmentedControl.elm @@ -17,108 +17,111 @@ module Examples.SegmentedControl exposing -} import Accessibility.Styled +import Debug.Control as Control exposing (Control) import Html.Styled as Html exposing (Html) import Html.Styled.Attributes as Attr import Html.Styled.Events as Events import ModuleExample exposing (Category(..), ModuleExample) -import Nri.Ui.SegmentedControl.V6 exposing (Width(..)) +import Nri.Ui.Icon.V5 as Icon +import Nri.Ui.SegmentedControl.V7 as SegmentedControl {-| -} type Msg - = Select Id - | SetFillContainer Bool + = Select ExampleOption + | ChangeOptions (Control Options) + + +type ExampleOption + = A + | B + | C {-| -} type alias State = - Nri.Ui.SegmentedControl.V6.Config Id Msg + { selected : ExampleOption + , optionsControl : Control Options + } + + +type alias Options = + { width : SegmentedControl.Width + , icon : Maybe SegmentedControl.Icon + , useSpa : Bool + } {-| -} example : (Msg -> msg) -> State -> ModuleExample msg example parentMessage state = - { name = "Nri.Ui.SegmentedControl.V6" + { name = "Nri.Ui.SegmentedControl.V7" , category = Widgets , content = - List.map (Html.map parentMessage) - [ fillContainerCheckbox state.width - , Nri.Ui.SegmentedControl.V6.view state - ] + [ Control.view ChangeOptions state.optionsControl + |> Html.fromUnstyled + , let + options = + Control.currentValue state.optionsControl + + viewFn = + if options.useSpa then + SegmentedControl.viewSpa Debug.toString + + else + SegmentedControl.view + in + viewFn + { onClick = Select + , options = + [ A, B, C ] + |> List.map + (\i -> + { icon = options.icon + , label = "Option " ++ Debug.toString i + , value = i + } + ) + , selected = state.selected + , width = options.width + , content = Html.text ("[Content for " ++ Debug.toString state.selected ++ "]") + } + ] + |> List.map (Html.map parentMessage) } {-| -} -init : State -init = - { onClick = Select - , options = - [ { icon = Nothing - , id = "a" - , label = "Option A" - , value = "a" - } - , { icon = Nothing - , id = "b" - , label = "Option B" - , value = "b" - } - ] - , selected = "a" - , width = FitContent +init : { r | help : String } -> State +init assets = + { selected = A + , optionsControl = + Control.record Options + |> Control.field "width" + (Control.choice + ( "FitContent", Control.value SegmentedControl.FitContent ) + [ ( "FillContainer", Control.value SegmentedControl.FillContainer ) ] + ) + |> Control.field "icon" + (Control.maybe False (Control.value { alt = "Help", icon = Icon.helpSvg assets })) + |> Control.field "which view function" + (Control.choice + ( "view", Control.value False ) + [ ( "viewSpa", Control.value True ) ] + ) } -fillContainerCheckbox : Width -> Html Msg -fillContainerCheckbox currentOption = - let - id = - "SegmentedControl-fill-container-checkbox" - - isChecked = - case currentOption of - FitContent -> - Just False - - FillContainer -> - Just True - in - Html.div [] - [ Accessibility.Styled.checkbox "Fill container" - isChecked - [ Attr.id id - , Events.onCheck SetFillContainer - ] - , Html.label - [ Attr.for id - ] - [ Html.text "Fill Container" ] - ] - - {-| -} update : Msg -> State -> ( State, Cmd Msg ) update msg state = case msg of Select id -> - ( { state | selected = id }, Cmd.none ) - - SetFillContainer fillContainer -> - ( { state - | width = - if fillContainer then - FillContainer - - else - FitContent - } + ( { state | selected = id } , Cmd.none ) - - --- INTERNAL - - -type alias Id = - String + ChangeOptions newOptions -> + ( { state | optionsControl = newOptions } + , Cmd.none + ) diff --git a/styleguide-app/NriModules.elm b/styleguide-app/NriModules.elm index d2d3eacd..deabe76f 100644 --- a/styleguide-app/NriModules.elm +++ b/styleguide-app/NriModules.elm @@ -55,7 +55,7 @@ init = , clickableTextExampleState = Examples.ClickableText.init assets , checkboxExampleState = Examples.Checkbox.init , dropdownState = Examples.Dropdown.init - , segmentedControlState = Examples.SegmentedControl.init + , segmentedControlState = Examples.SegmentedControl.init assets , selectState = Examples.Select.init , tableExampleState = Examples.Table.init , textAreaExampleState = TextAreaExample.init