Merge pull request #441 from NoRedInk/lab/customizable-select

Lab/customizable select
This commit is contained in:
Brian Hicks 2020-01-22 17:25:00 -06:00 committed by GitHub
commit 074c8c0dc1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 336 additions and 43 deletions

View File

@ -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"
}
}
}

171
src/Nri/Ui/Select/V7.elm Normal file
View File

@ -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
[ """<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12px" height="16px" viewBox="0 0 12 16"><g fill=" """
++ color
++ """ "><path d="M2.10847,9.341803 C1.65347,8.886103 0.91427,8.886103 0.45857,9.341803 C0.23107,9.570003 0.11697,9.868203 0.11697,10.167103 C0.11697,10.465303 0.23107,10.763503 0.45857,10.991703 L5.12547,15.657903 C5.57977,16.114303 6.31897,16.114303 6.77537,15.657903 L11.44157,10.991703 C11.89727,10.536003 11.89727,9.797503 11.44157,9.341803 C10.98657,8.886103 10.24667,8.886103 9.79167,9.341803 L5.95007,13.182703 L2.10847,9.341803 Z"/><path d="M1.991556,6.658179 C1.536659,7.11394 0.797279,7.11394 0.3416911,6.658179 C0.1140698,6.43004 0,6.13173 0,5.83325 C0,5.53476 0.1140698,5.23645 0.3416911,5.00831 L5.008185,0.34182 C5.463081,-0.11394 6.202461,-0.11394 6.65805,0.34182 L11.32454,5.00831 C11.78031,5.4639 11.78031,6.202592 11.32454,6.658179 C10.86965,7.11394 10.13027,7.11394 9.674679,6.658179 L5.833118,2.81679 L1.991556,6.658179 Z"/></g></svg> """
|> 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 ++ """')"""

View File

@ -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 )

View File

@ -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

View File

@ -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
}
]
)