diff --git a/elm.json b/elm.json index 48ddb811..37619342 100644 --- a/elm.json +++ b/elm.json @@ -73,6 +73,7 @@ "Nri.Ui.SegmentedControl.V8", "Nri.Ui.Select.V5", "Nri.Ui.Select.V6", + "Nri.Ui.Select.V7", "Nri.Ui.Slide.V1", "Nri.Ui.SlideModal.V1", "Nri.Ui.SlideModal.V2", @@ -115,4 +116,4 @@ "avh4/elm-program-test": "3.1.0 <= v < 4.0.0", "elm-explorations/test": "1.2.0 <= v < 2.0.0" } -} \ No newline at end of file +} diff --git a/src/Nri/Ui/Select/V7.elm b/src/Nri/Ui/Select/V7.elm new file mode 100644 index 00000000..fae53113 --- /dev/null +++ b/src/Nri/Ui/Select/V7.elm @@ -0,0 +1,171 @@ +module Nri.Ui.Select.V7 exposing (Choice, view) + +{-| Build a select input. + +@docs Choice, view + +-} + +import Color +import Css +import Dict +import Html.Styled as Html exposing (Html) +import Html.Styled.Attributes as Attributes +import Html.Styled.Events as Events +import Json.Decode exposing (Decoder) +import Nri.Ui +import Nri.Ui.Colors.Extra as ColorsExtra +import Nri.Ui.Colors.V1 as Colors +import Nri.Ui.Css.VendorPrefixed as VendorPrefixed +import Nri.Ui.Fonts.V1 as Fonts +import Nri.Ui.Util + + +{-| A single possible choice. +-} +type alias Choice a = + { label : String, value : a } + + +{-| A select dropdown. Remember to add a label! +-} +view : + { choices : List (Choice a) + , current : Maybe a + , id : String + , valueToString : a -> String + , defaultDisplayText : Maybe String + , isInError : Bool + } + -> Html a +view config = + let + valueLookup = + config.choices + |> List.map (\x -> ( niceId (config.valueToString x.value), x.value )) + |> Dict.fromList + + decodeValue string = + Dict.get string valueLookup + |> Maybe.map Json.Decode.succeed + |> Maybe.withDefault + -- At present, elm/virtual-dom throws this failure away. + (Json.Decode.fail + ("Nri.Select: could not decode the value: " + ++ string + ++ "\nexpected one of: " + ++ String.join ", " (Dict.keys valueLookup) + ) + ) + + onSelectHandler = + Events.on "change" (Events.targetValue |> Json.Decode.andThen decodeValue) + + defaultOption = + config.defaultDisplayText + |> Maybe.map (viewDefaultChoice config.current >> List.singleton) + |> Maybe.withDefault [] + + currentVal = + if config.current == Nothing && config.defaultDisplayText == Nothing then + config.choices + |> List.head + |> Maybe.map .value + + else + config.current + in + config.choices + |> List.map (viewChoice currentVal config.valueToString) + |> (++) defaultOption + |> Nri.Ui.styled Html.select + "nri-select-menu" + [ -- border + Css.border3 (Css.px 1) + Css.solid + (if config.isInError then + Colors.purple + + else + Colors.gray75 + ) + , Css.borderBottomWidth (Css.px 4) + , Css.borderRadius (Css.px 8) + , Css.focus [ Css.borderColor Colors.azure ] + + -- Font and color + , Css.color Colors.gray20 + , Fonts.baseFont + , Css.fontSize (Css.px 15) + + -- Interaction + , Css.cursor Css.pointer + + -- Size and spacing + , Css.height (Css.px 45) + , Css.width (Css.pct 100) + , Css.paddingLeft (Css.px 20) + + -- Icons + , selectArrowsCss + ] + [ onSelectHandler + , Attributes.id config.id + ] + + +viewDefaultChoice : Maybe a -> String -> Html a +viewDefaultChoice current displayText = + Html.option + [ Attributes.selected (current == Nothing) + , Attributes.disabled True + ] + [ Html.text displayText ] + + +viewChoice : Maybe a -> (a -> String) -> Choice a -> Html a +viewChoice current toString choice = + let + isSelected = + current + |> Maybe.map ((==) choice.value) + |> Maybe.withDefault False + in + Html.option + [ Attributes.id (niceId (toString choice.value)) + , Attributes.value (niceId (toString choice.value)) + , Attributes.selected isSelected + ] + [ Html.text choice.label ] + + +niceId : String -> String +niceId x = + "nri-select-" ++ Nri.Ui.Util.dashify (Nri.Ui.Util.removePunctuation x) + + +selectArrowsCss : Css.Style +selectArrowsCss = + let + color = + Color.toRGBString (ColorsExtra.fromCssColor Colors.azure) + in + Css.batch + [ """ """ + |> urlUtf8 + |> Css.property "background" + , Css.backgroundColor Colors.white + + -- "appearance: none" removes the default dropdown arrows + , VendorPrefixed.property "appearance" "none" + , Css.backgroundRepeat Css.noRepeat + , Css.property "background-position" "center right 10px" + , Css.backgroundOrigin Css.contentBox + ] + + +urlUtf8 : String -> String +urlUtf8 content = + """url('data:image/svg+xml;utf8,""" ++ content ++ """')""" diff --git a/styleguide-app/Examples/Select.elm b/styleguide-app/Examples/Select.elm index 37584b50..783674bc 100644 --- a/styleguide-app/Examples/Select.elm +++ b/styleguide-app/Examples/Select.elm @@ -1,7 +1,6 @@ module Examples.Select exposing ( Msg , State - , Value , example , init , update @@ -11,7 +10,6 @@ module Examples.Select exposing @docs Msg @docs State -@docs Value @docs example @docs init @docs update @@ -22,12 +20,56 @@ import Html.Styled import Html.Styled.Attributes import ModuleExample exposing (Category(..), ModuleExample) import Nri.Ui.Heading.V2 as Heading -import Nri.Ui.Select.V6 as Select +import Nri.Ui.Select.V7 as Select {-| -} -type alias Value = - String +example : (Msg -> msg) -> State -> ModuleExample msg +example parentMessage state = + { name = "Nri.Ui.Select.V7" + , category = Inputs + , content = + [ Html.Styled.label + [ Html.Styled.Attributes.for "tortilla-selector" ] + [ Heading.h3 [] [ Html.Styled.text "Tortilla Selector" ] ] + , Select.view + { current = Nothing + , choices = + [ { label = "Tacos", value = "Tacos" } + , { label = "Burritos", value = "Burritos" } + , { label = "Enchiladas", value = "Enchiladas" } + ] + , id = "tortilla-selector" + , valueToString = identity + , defaultDisplayText = Just "Select a tasty tortilla based treat!" + , isInError = False + } + |> Html.Styled.map (parentMessage << ConsoleLog) + , Html.Styled.label + [ Html.Styled.Attributes.for "errored-selector" ] + [ Heading.h3 [] [ Html.Styled.text "Errored Selector" ] ] + , Select.view + { current = Nothing + , choices = [] + , id = "errored-selector" + , valueToString = identity + , defaultDisplayText = Just "Please select an option" + , isInError = True + } + |> Html.Styled.map (parentMessage << ConsoleLog) + ] + } + + +{-| -} +init : State +init = + Nothing + + +{-| -} +type alias State = + Maybe String {-| -} @@ -36,41 +78,7 @@ type Msg {-| -} -type alias State value = - Select.Config value - - -{-| -} -example : (Msg -> msg) -> State Value -> ModuleExample msg -example parentMessage state = - { name = "Nri.Ui.Select.V6" - , category = Inputs - , content = - [ Html.Styled.label - [ Html.Styled.Attributes.for "tortilla-selector" ] - [ Heading.h3 [] [ Html.Styled.text "Tortilla Selector" ] ] - , Html.Styled.map (parentMessage << ConsoleLog) (Select.view state) - ] - } - - -{-| -} -init : State Value -init = - { current = Nothing - , choices = - [ { label = "Tacos", value = "Tacos" } - , { label = "Burritos", value = "Burritos" } - , { label = "Enchiladas", value = "Enchiladas" } - ] - , id = Just "tortilla-selector" - , valueToString = identity - , defaultDisplayText = Just "Select a tasty tortilla based treat!" - } - - -{-| -} -update : Msg -> State Value -> ( State Value, Cmd Msg ) +update : Msg -> State -> ( State, Cmd Msg ) update msg state = case msg of ConsoleLog message -> @@ -78,4 +86,4 @@ update msg state = _ = Debug.log "SelectExample" message in - ( state, Cmd.none ) + ( Just message, Cmd.none ) diff --git a/styleguide-app/NriModules.elm b/styleguide-app/NriModules.elm index d40215d6..8fb96c0e 100644 --- a/styleguide-app/NriModules.elm +++ b/styleguide-app/NriModules.elm @@ -47,7 +47,7 @@ type alias ModuleStates = , checkboxExampleState : Examples.Checkbox.State , dropdownState : Examples.Dropdown.State Examples.Dropdown.Value , segmentedControlState : Examples.SegmentedControl.State - , selectState : Examples.Select.State Examples.Select.Value + , selectState : Examples.Select.State , tableExampleState : Examples.Table.State , textAreaExampleState : TextAreaExample.State , textInputExampleState : TextInputExample.State diff --git a/tests/Spec/Nri/Ui/Select.elm b/tests/Spec/Nri/Ui/Select.elm index 6c7ecf05..77b59ffe 100644 --- a/tests/Spec/Nri/Ui/Select.elm +++ b/tests/Spec/Nri/Ui/Select.elm @@ -6,6 +6,7 @@ import Html.Attributes as Attr import Html.Styled import Nri.Ui.Select.V5 import Nri.Ui.Select.V6 +import Nri.Ui.Select.V7 import Test exposing (..) import Test.Html.Query as Query import Test.Html.Selector exposing (..) @@ -39,6 +40,20 @@ spec = |> Html.Styled.toUnstyled ) ) + , describe "V7" + (viewSuiteV7 + (\config -> + { choices = config.choices + , current = config.current + , id = "fake-id" + , valueToString = identity + , defaultDisplayText = config.defaultDisplayText + , isInError = False + } + |> Nri.Ui.Select.V7.view + |> Html.Styled.toUnstyled + ) + ) ] @@ -181,3 +196,101 @@ viewTestV6 view defaultDisplayText selected items = } ] ) + + +viewSuiteV7 : + ({ choices : List { label : String, value : String }, current : Maybe String, defaultDisplayText : Maybe String } -> Html.Html msg) + -> List Test +viewSuiteV7 view = + [ describe "without a default option" + [ test "shows all options" <| + \() -> + viewTestV7 + view + Nothing + Nothing + [ "Tacos", "Burritos", "Enchiladas" ] + |> Query.find [ tag "select" ] + |> Query.findAll [ tag "option" ] + |> Query.count (Expect.equal 3) + , test "selects the first option if nothing is selected and there's no default" <| + \() -> + viewTestV7 + view + Nothing + Nothing + [ "Tacos", "Burritos", "Enchiladas" ] + |> Query.find + [ tag "option" + , attribute <| Attr.selected True + ] + |> Query.has [ text "Tacos" ] + , test "selects the current option" <| + \() -> + viewTestV7 + view + Nothing + (Just "Burritos") + [ "Tacos", "Burritos", "Enchiladas" ] + |> Query.find + [ tag "option" + , attribute <| Attr.selected True + ] + |> Query.has [ text "Burritos" ] + ] + , describe "with a default option" + [ test "shows all options" <| + \() -> + viewTestV7 + view + (Just "Tasty tortilla'd foods") + Nothing + [ "Tacos", "Burritos", "Enchiladas" ] + |> Query.find [ tag "select" ] + |> Query.findAll [ tag "option" ] + |> Query.count (Expect.equal 4) + , test "selects the disabled default option if nothing is currently selected" <| + \() -> + viewTestV7 + view + (Just "Tasty tortilla'd foods") + Nothing + [ "Tacos", "Burritos", "Enchiladas" ] + |> Query.find + [ tag "option" + , attribute <| Attr.selected True + , attribute <| Attr.disabled True + ] + |> Query.has [ text "Tasty tortilla'd foods" ] + , test "selects the current option" <| + \() -> + viewTestV7 + view + (Just "Tasty tortilla'd foods") + (Just "Burritos") + [ "Tacos", "Burritos", "Enchiladas" ] + |> Query.find + [ tag "option" + , attribute <| Attr.selected True + ] + |> Query.has [ text "Burritos" ] + ] + ] + + +viewTestV7 : + ({ choices : List { label : a, value : a }, current : Maybe b, defaultDisplayText : Maybe String } -> Html.Html msg) + -> Maybe String + -> Maybe b + -> List a + -> Query.Single msg +viewTestV7 view defaultDisplayText selected items = + Query.fromHtml + (Html.div [] + [ view + { choices = List.map (\x -> { label = x, value = x }) items + , current = selected + , defaultDisplayText = defaultDisplayText + } + ] + )