New attributes-style API for TextInput.V6

This commit is contained in:
Aaron VonderHaar 2020-04-13 16:43:07 -07:00
parent fc61d0bfe6
commit a2d305b6e4
5 changed files with 369 additions and 163 deletions

3
elm-refactor/README.md Normal file
View File

@ -0,0 +1,3 @@
# elm-refactor scripts
See NoRedInk slack: `#eng-elm-refactor-alpha` for how to use the upgrade scripts here.

View File

@ -0,0 +1,101 @@
module Main exposing (upgrade_Nri_Ui_TextInput_V5_view)
import ElmFix
import Nri.Ui.TextInput.V6 as TextInput
upgrade_Nri_Ui_TextInput_V5_text =
TextInput.text
upgrade_Nri_Ui_TextInput_V5_number =
TextInput.number
upgrade_Nri_Ui_TextInput_V5_float =
TextInput.float
upgrade_Nri_Ui_TextInput_V5_password =
TextInput.password
upgrade_Nri_Ui_TextInput_V5_email =
TextInput.email
upgrade_Nri_Ui_TextInput_V5_view model =
TextInput.view model.label
(model.type_ model.onInput)
[ case model.isInError of
False ->
ElmFix.remove
_ ->
TextInput.errorIf model.isInError
, case model.showLabel of
True ->
ElmFix.remove
False ->
TextInput.hiddenLabel
, if model.placeholder == model.label then
ElmFix.remove
else
TextInput.placeholder model.placeholder
, case model.onBlur of
Nothing ->
ElmFix.remove
Just msg ->
TextInput.onBlur msg
, case model.autofocus of
True ->
TextInput.autofocus
False ->
ElmFix.remove
]
model.value
upgrade_Nri_Ui_TextInput_V5_writing model =
TextInput.view model.label
(model.type_ model.onInput)
[ TextInput.writing
, case model.isInError of
False ->
ElmFix.remove
_ ->
TextInput.errorIf model.isInError
, case model.showLabel of
True ->
ElmFix.remove
False ->
TextInput.hiddenLabel
, if model.placeholder == model.label then
ElmFix.remove
else
TextInput.placeholder model.placeholder
, case model.onBlur of
Nothing ->
ElmFix.remove
Just msg ->
TextInput.onBlur msg
, case model.autofocus of
True ->
TextInput.autofocus
False ->
ElmFix.remove
]
model.value
upgrade_Nri_Ui_TextInput_V5_generateId labelText =
TextInput.generateId labelText

View File

@ -1,8 +1,8 @@
module Nri.Ui.TextInput.V6 exposing module Nri.Ui.TextInput.V6 exposing
( Model ( view, generateId
, view, writing
, generateId
, InputType, number, float, text, password, email , InputType, number, float, text, password, email
, Attribute, placeholder, errorIf, hiddenLabel, onBlur, autofocus
, writing
) )
{-| {-|
@ -10,17 +10,21 @@ module Nri.Ui.TextInput.V6 exposing
# Changes from V5 # Changes from V5
- nothing yet - new Attributes-style API
@docs Model @docs view, generateId
@docs view, writing
@docs generateId
## Input types ### Input types
@docs InputType, number, float, text, password, email @docs InputType, number, float, text, password, email
## Attributes
@docs Attribute, placeholder, errorIf, hiddenLabel, onBlur, autofocus
@docs writing
-} -}
import Accessibility.Styled.Style as Accessibility import Accessibility.Styled.Style as Accessibility
@ -30,29 +34,15 @@ import Html.Styled as Html exposing (..)
import Html.Styled.Attributes as Attributes exposing (..) import Html.Styled.Attributes as Attributes exposing (..)
import Html.Styled.Events as Events exposing (onInput) import Html.Styled.Events as Events exposing (onInput)
import Nri.Ui.Html.Attributes.V2 as Extra import Nri.Ui.Html.Attributes.V2 as Extra
import Nri.Ui.InputStyles.V2 as InputStyles exposing (Theme) import Nri.Ui.InputStyles.V2 as InputStyles
import Nri.Ui.Util exposing (dashify) import Nri.Ui.Util exposing (dashify)
{-| -} {-| -}
type alias Model value msg = type InputType value msg
{ label : String
, isInError : Bool
, onInput : value -> msg
, onBlur : Maybe msg
, placeholder : String
, value : value
, autofocus : Bool
, showLabel : Bool
, type_ : InputType value
}
{-| -}
type InputType value
= InputType = InputType
{ toString : value -> String { toString : value -> String
, fromString : String -> value , fromString : String -> msg
, fieldType : String , fieldType : String
, inputMode : Maybe String , inputMode : Maybe String
, autocomplete : Maybe String , autocomplete : Maybe String
@ -61,11 +51,11 @@ type InputType value
{-| An input that allows text entry {-| An input that allows text entry
-} -}
text : InputType String text : (String -> msg) -> InputType String msg
text = text toMsg =
InputType InputType
{ toString = identity { toString = identity
, fromString = identity , fromString = toMsg
, fieldType = "text" , fieldType = "text"
, inputMode = Nothing , inputMode = Nothing
, autocomplete = Nothing , autocomplete = Nothing
@ -74,11 +64,11 @@ text =
{-| An input that allows integer entry {-| An input that allows integer entry
-} -}
number : InputType (Maybe Int) number : (Maybe Int -> msg) -> InputType (Maybe Int) msg
number = number toMsg =
InputType InputType
{ toString = Maybe.map String.fromInt >> Maybe.withDefault "" { toString = Maybe.map String.fromInt >> Maybe.withDefault ""
, fromString = String.toInt , fromString = String.toInt >> toMsg
, fieldType = "number" , fieldType = "number"
, inputMode = Nothing , inputMode = Nothing
, autocomplete = Nothing , autocomplete = Nothing
@ -87,11 +77,11 @@ number =
{-| An input that allows float entry {-| An input that allows float entry
-} -}
float : InputType (Maybe Float) float : (Maybe Float -> msg) -> InputType (Maybe Float) msg
float = float toMsg =
InputType InputType
{ toString = Maybe.map String.fromFloat >> Maybe.withDefault "" { toString = Maybe.map String.fromFloat >> Maybe.withDefault ""
, fromString = String.toFloat , fromString = String.toFloat >> toMsg
, fieldType = "number" , fieldType = "number"
, inputMode = Nothing , inputMode = Nothing
, autocomplete = Nothing , autocomplete = Nothing
@ -100,11 +90,11 @@ float =
{-| An input that allows password entry {-| An input that allows password entry
-} -}
password : InputType String password : (String -> msg) -> InputType String msg
password = password toMsg =
InputType InputType
{ toString = identity { toString = identity
, fromString = identity , fromString = toMsg
, fieldType = "password" , fieldType = "password"
, inputMode = Nothing , inputMode = Nothing
, autocomplete = Just "current-password" , autocomplete = Just "current-password"
@ -118,37 +108,137 @@ but not `type="email"` because that would enable browser-provided validation whi
with our validation UI. with our validation UI.
-} -}
email : InputType String email : (String -> msg) -> InputType String msg
email = email toMsg =
InputType InputType
{ toString = identity { toString = identity
, fromString = identity , fromString = toMsg
, fieldType = "text" , fieldType = "text"
, inputMode = Just "email" , inputMode = Just "email"
, autocomplete = Just "email" , autocomplete = Just "email"
} }
{-| -} {-| An optional customization of a TextInput.
view : Model value msg -> Html msg -}
view model = type Attribute value msg
view_ InputStyles.Standard model = InputStyleAttribute InputStyles.Theme
| ErrorAttribute Bool
| HideLabelAttribute Bool
| PlaceholderAttribute String
| OnBlurAttribute msg
| AutofocusAttribute Bool
{-| -} {-| If not explicit placeholder is given, the input label will be used as the placeholder.
writing : Model value msg -> Html msg -}
writing model = placeholder : String -> Attribute value msg
view_ InputStyles.Writing model placeholder text_ =
PlaceholderAttribute text_
view_ : Theme -> Model value msg -> Html msg {-| Sets whether or not the field will be highlighted as having a validation error.
view_ theme model = If you are always passing `True`, then you don't need to use this attribute.
-}
errorIf : Bool -> Attribute value msg
errorIf isInError =
ErrorAttribute isInError
{-| Hides the visible label. (There will still be an invisible label for screen readers.
-}
hiddenLabel : Attribute value msg
hiddenLabel =
HideLabelAttribute True
{-| Causes the TextInput to produce the given `msg` when the field is blurred.
-}
onBlur : msg -> Attribute value msg
onBlur msg =
OnBlurAttribute msg
{-| Sets the `autofocus` attribute of the resulting HTML input.
-}
autofocus : Attribute value msg
autofocus =
AutofocusAttribute True
{-| This is private. The public API only exposes `Attribute`.
-}
type alias Config msg =
{ inputStyle : InputStyles.Theme
, isInError : Bool
, hideLabel : Bool
, placeholder : Maybe String
, onBlur : Maybe msg
, autofocus : Bool
}
emptyConfig : Config msg
emptyConfig =
{ inputStyle = InputStyles.Standard
, isInError = False
, hideLabel = False
, placeholder = Nothing
, onBlur = Nothing
, autofocus = False
}
updateConfig : Attribute value msg -> Config msg -> Config msg
updateConfig attribute config =
case attribute of
InputStyleAttribute theme ->
{ config | inputStyle = theme }
ErrorAttribute isInError ->
{ config | isInError = isInError }
HideLabelAttribute hideLabel ->
{ config | hideLabel = hideLabel }
PlaceholderAttribute text_ ->
{ config | placeholder = Just text_ }
OnBlurAttribute msg ->
{ config | onBlur = Just msg }
AutofocusAttribute autofocus_ ->
{ config | autofocus = autofocus_ }
{-| Render the TextInput as HTML.
The input's label, InputType, and current value are all required. Other attributes are all optional.
-}
view : String -> InputType value msg -> List (Attribute value msg) -> value -> Html msg
view label inputType attributes currentValue =
let
config =
List.foldl updateConfig emptyConfig attributes
in
view_ label inputType config currentValue
{-| Uses the "Writing" input style. See [`Nri.Ui.InputStyles.V2.Theme`](Nri-Ui-InputStyles-V2#Theme).
-}
writing : Attribute value msg
writing =
InputStyleAttribute InputStyles.Writing
view_ : String -> InputType value msg -> Config msg -> value -> Html msg
view_ label (InputType inputType) config currentValue =
let let
idValue = idValue =
generateId model.label generateId label
(InputType inputType) = placeholder_ =
model.type_ config.placeholder
|> Maybe.withDefault label
maybeStep = maybeStep =
if inputType.fieldType == "number" then if inputType.fieldType == "number" then
@ -169,8 +259,8 @@ view_ theme model =
(maybeStep (maybeStep
++ [ Attributes.id idValue ++ [ Attributes.id idValue
, css , css
[ InputStyles.input theme model.isInError [ InputStyles.input config.inputStyle config.isInError
, if theme == InputStyles.Writing then , if config.inputStyle == InputStyles.Writing then
Css.Global.withClass "override-sass-styles" Css.Global.withClass "override-sass-styles"
[ textAlign center [ textAlign center
, Css.height Css.auto , Css.height Css.auto
@ -181,17 +271,17 @@ view_ theme model =
[ Css.height (px 45) [ Css.height (px 45)
] ]
] ]
, placeholder model.placeholder , Attributes.placeholder placeholder_
, value (inputType.toString model.value) , value (inputType.toString currentValue)
, onInput (inputType.fromString >> model.onInput) , onInput inputType.fromString
, maybeAttr Events.onBlur model.onBlur , maybeAttr Events.onBlur config.onBlur
, autofocus model.autofocus , Attributes.autofocus config.autofocus
, type_ inputType.fieldType , type_ inputType.fieldType
, maybeAttr (attribute "inputmode") inputType.inputMode , maybeAttr (attribute "inputmode") inputType.inputMode
, maybeAttr (attribute "autocomplete") inputType.autocomplete , maybeAttr (attribute "autocomplete") inputType.autocomplete
, class "override-sass-styles" , class "override-sass-styles"
, Attributes.attribute "aria-invalid" <| , Attributes.attribute "aria-invalid" <|
if model.isInError then if config.isInError then
"true" "true"
else else
@ -199,21 +289,21 @@ view_ theme model =
] ]
) )
[] []
, if model.showLabel then , let
Html.label extraStyles =
[ for idValue if config.hideLabel then
, css [ InputStyles.label theme model.isInError ] Accessibility.invisible
]
[ Html.text model.label ]
else else
Html.label []
([ for idValue in
, css [ InputStyles.label theme model.isInError ] Html.label
] ([ for idValue
++ Accessibility.invisible , css [ InputStyles.label config.inputStyle config.isInError ]
) ]
[ Html.text model.label ] ++ extraStyles
)
[ Html.text label ]
] ]

View File

@ -60,89 +60,106 @@ example =
[ Control.view UpdateControl state.control [ Control.view UpdateControl state.control
|> Html.fromUnstyled |> Html.fromUnstyled
, Heading.h3 [] [ text "TextInput.view { type_ = TextInput.text }" ] , Heading.h3 [] [ text "TextInput.view { type_ = TextInput.text }" ]
, TextInput.view , TextInput.view (exampleConfig.label ++ " (text)")
{ label = exampleConfig.label ++ " (text)" (TextInput.text (SetTextInput 1))
, isInError = exampleConfig.isInError ([ TextInput.errorIf exampleConfig.isInError
, placeholder = exampleConfig.placeholder , TextInput.placeholder exampleConfig.placeholder
, showLabel = exampleConfig.showLabel ]
, value = Maybe.withDefault "" <| Dict.get 1 state.stringInputValues ++ (if exampleConfig.showLabel then
, onInput = SetTextInput 1 []
, onBlur = Nothing
, autofocus = False else
, type_ = TextInput.text [ TextInput.hiddenLabel ]
} )
)
(Maybe.withDefault "" <| Dict.get 1 state.stringInputValues)
, Heading.h3 [] [ text "... type_ = TextInput.number" ] , Heading.h3 [] [ text "... type_ = TextInput.number" ]
, TextInput.view , TextInput.view (exampleConfig.label ++ " (number)")
{ label = exampleConfig.label ++ " (number)" (TextInput.number SetNumberInput)
, isInError = exampleConfig.isInError ([ TextInput.errorIf exampleConfig.isInError
, placeholder = exampleConfig.placeholder , TextInput.placeholder exampleConfig.placeholder
, showLabel = exampleConfig.showLabel ]
, value = state.numberInputValue ++ (if exampleConfig.showLabel then
, onInput = SetNumberInput []
, onBlur = Nothing
, autofocus = False else
, type_ = TextInput.number [ TextInput.hiddenLabel ]
} )
)
state.numberInputValue
, Heading.h3 [] [ text "... type_ = TextInput.float" ] , Heading.h3 [] [ text "... type_ = TextInput.float" ]
, TextInput.view , TextInput.view (exampleConfig.label ++ " (float)")
{ label = exampleConfig.label ++ " (float)" (TextInput.float SetFloatInput)
, isInError = exampleConfig.isInError ([ TextInput.errorIf exampleConfig.isInError
, placeholder = exampleConfig.placeholder , TextInput.placeholder exampleConfig.placeholder
, showLabel = exampleConfig.showLabel ]
, value = state.floatInputValue ++ (if exampleConfig.showLabel then
, onInput = SetFloatInput []
, onBlur = Nothing
, autofocus = False else
, type_ = TextInput.float [ TextInput.hiddenLabel ]
} )
)
state.floatInputValue
, Heading.h3 [] [ text "... type_ = TextInput.password" ] , Heading.h3 [] [ text "... type_ = TextInput.password" ]
, TextInput.view , TextInput.view (exampleConfig.label ++ " (password)")
{ label = exampleConfig.label ++ " (password)" (TextInput.password SetPassword)
, isInError = exampleConfig.isInError ([ TextInput.errorIf exampleConfig.isInError
, placeholder = exampleConfig.placeholder , TextInput.placeholder exampleConfig.placeholder
, showLabel = exampleConfig.showLabel ]
, value = state.passwordInputValue ++ (if exampleConfig.showLabel then
, onInput = SetPassword []
, onBlur = Nothing
, autofocus = False else
, type_ = TextInput.password [ TextInput.hiddenLabel ]
} )
)
state.passwordInputValue
, Heading.h3 [] [ text "... type_ = TextInput.email" ] , Heading.h3 [] [ text "... type_ = TextInput.email" ]
, TextInput.view , TextInput.view (exampleConfig.label ++ " (email)")
{ label = exampleConfig.label ++ " (email)" (TextInput.email (SetTextInput 2))
, isInError = exampleConfig.isInError ([ TextInput.errorIf exampleConfig.isInError
, placeholder = exampleConfig.placeholder , TextInput.placeholder exampleConfig.placeholder
, showLabel = exampleConfig.showLabel ]
, value = Maybe.withDefault "" <| Dict.get 2 state.stringInputValues ++ (if exampleConfig.showLabel then
, onInput = SetTextInput 2 []
, onBlur = Nothing
, autofocus = False else
, type_ = TextInput.email [ TextInput.hiddenLabel ]
} )
)
(Maybe.withDefault "" <| Dict.get 2 state.stringInputValues)
, Heading.h3 [] [ Html.text "TextInput.writing { type_ = TextInput.text }" ] , Heading.h3 [] [ Html.text "TextInput.writing { type_ = TextInput.text }" ]
, TextInput.writing , TextInput.view (exampleConfig.label ++ " (writing)")
{ label = exampleConfig.label ++ " (writing)" (TextInput.text (SetTextInput 4))
, isInError = exampleConfig.isInError ([ TextInput.writing
, placeholder = exampleConfig.placeholder , TextInput.errorIf exampleConfig.isInError
, value = Maybe.withDefault "" <| Dict.get 4 state.stringInputValues , TextInput.placeholder exampleConfig.placeholder
, onInput = SetTextInput 4 ]
, onBlur = Nothing ++ (if exampleConfig.showLabel then
, autofocus = False []
, type_ = TextInput.text
, showLabel = exampleConfig.showLabel else
} [ TextInput.hiddenLabel ]
)
)
(Maybe.withDefault "" <| Dict.get 4 state.stringInputValues)
, Heading.h3 [] [ text "onBlur demonstration" ] , Heading.h3 [] [ text "onBlur demonstration" ]
, TextInput.writing , TextInput.view (exampleConfig.label ++ " (onBlur)")
{ label = exampleConfig.label ++ " (onBlur)" (TextInput.text (SetTextInput 7))
, isInError = exampleConfig.isInError ([ TextInput.writing
, placeholder = exampleConfig.placeholder , TextInput.errorIf exampleConfig.isInError
, value = Maybe.withDefault "" <| Dict.get 7 state.stringInputValues , TextInput.placeholder exampleConfig.placeholder
, onInput = SetTextInput 7 , TextInput.onBlur (SetTextInput 7 "Blurred!")
, onBlur = Just (SetTextInput 7 "Blurred!") ]
, autofocus = False ++ (if exampleConfig.showLabel then
, type_ = TextInput.text []
, showLabel = exampleConfig.showLabel
} else
[ TextInput.hiddenLabel ]
)
)
(Maybe.withDefault "" <| Dict.get 7 state.stringInputValues)
] ]
] ]
} }

View File

@ -12,17 +12,12 @@ all =
describe "Nri.Ui.TextInput.V6" describe "Nri.Ui.TextInput.V6"
[ test "it uses the same DOM id that generateId produces" <| [ test "it uses the same DOM id that generateId produces" <|
\() -> \() ->
TextInput.view TextInput.view "myLabel"
{ label = "myLabel" (TextInput.text identity)
, isInError = False [ TextInput.hiddenLabel
, onInput = identity , TextInput.placeholder "placeholder"
, onBlur = Nothing ]
, placeholder = "placeholder" "value"
, value = "value"
, autofocus = False
, showLabel = False
, type_ = TextInput.text
}
|> Html.Styled.toUnstyled |> Html.Styled.toUnstyled
|> Query.fromHtml |> Query.fromHtml
|> Query.has |> Query.has