1module Element.Input exposing
4 , checkbox, defaultCheckbox
6 , Placeholder, placeholder
7 , username, newPassword, currentPassword, email, search, spellChecked
8 , slider, Thumb, thumb, defaultThumb
9 , radio, radioRow, Option, option, optionWith, OptionState(..)
10 , Label, labelAbove, labelBelow, labelLeft, labelRight, labelHidden
13{-| Input elements have a lot of constraints!
15We want all of our input elements to:
17 - _Always be accessible_
18 - _Behave intuitively_
19 - _Be completely restyleable_
21While these three goals may seem pretty obvious, Html and CSS have made it surprisingly difficult to achieve!
23And incredibly difficult for developers to remember all the tricks necessary to make things work. If you've every tried to make a `<textarea>` be the height of it's content or restyle a radio button while maintaining accessibility, you may be familiar!
25This module is intended to be accessible by default. You shouldn't have to wade through docs, articles, and books to find out [exactly how accessible your html actually is](https://www.powermapper.com/tests/screen-readers/aria/index.html).
30All Elements can be styled on focus by using [`Element.focusStyle`](Element#focusStyle) to set a global focus style or [`Element.focused`](Element#focused) to set a focus style individually for an element.
42A checkbox requires you to store a `Bool` in your model.
44This is also the first input element that has a [`required label`](#Label).
46 import Element exposing (text)
47 import Element.Input as Input
50 = GuacamoleChecked Bool
54 { onChange = GuacamoleChecked
55 , icon = Input.defaultCheckbox
56 , checked = model.guacamole
59 (text "Do you want Guacamole?")
62@docs checkbox, defaultCheckbox
69@docs Placeholder, placeholder
74If we want to play nicely with a browser's ability to autofill a form, we need to be able to give it a hint about what we're expecting.
76The following inputs are very similar to `Input.text`, but they give the browser a hint to allow autofill to work correctly.
78@docs username, newPassword, currentPassword, email, search, spellChecked
83A slider is great for choosing between a range of numerical values.
85 - **thumb** - The icon that you click and drag to change the value.
86 - **track** - The line behind the thumb denoting where you can slide to.
88@docs slider, Thumb, thumb, defaultThumb
93The fact that we still call this a radio selection is fascinating. I can't remember the last time I actually used an honest-to-goodness button on a radio. Chalk it up along with the floppy disk save icon or the word [Dashboard](https://en.wikipedia.org/wiki/Dashboard).
95Perhaps a better name would be `Input.chooseOne`, because this allows you to select one of a set of options!
97Nevertheless, here we are. Here's how you put one together
103 { onChange = ChooseLunch
104 , selected = Just model.lunch
105 , label = Input.labelAbove [] (text "Lunch")
107 [ Input.option Burrito (text "Burrito")
108 , Input.option Taco (text "Taco!")
109 , Input.option Gyro (text "Gyro")
113**Note** we're using `Input.option`, which will render the default radio icon you're probably used to. If you want compeltely custom styling, use `Input.optionWith`!
115@docs radio, radioRow, Option, option, optionWith, OptionState
120Every input has a required `Label`.
122@docs Label, labelAbove, labelBelow, labelLeft, labelRight, labelHidden
127You might be wondering where something like `<form>` is.
129What I've found is that most people who want `<form>` usually want it for the [implicit submission behavior](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#implicit-submission) or to be clearer, they want to do something when the `Enter` key is pressed.
131Instead of implicit submission behavior, [try making an `onEnter` event handler like in this Ellie Example](https://ellie-app.com/5X6jBKtxzdpa1). Then everything is explicit!
133And no one has to look up obtuse html documentation to understand the behavior of their code :).
138You also might be wondering how to disable an input.
140Disabled inputs can be a little problematic for user experience, and doubly so for accessibility. This is because it's now your priority to inform the user _why_ some field is disabled.
142If an input is truly disabled, meaning it's not focusable or doesn't send off a `Msg`, you actually lose your ability to help the user out! For those wary about accessibility [this is a big problem.](https://ux.stackexchange.com/questions/103239/should-disabled-elements-be-focusable-for-accessibility-purposes)
144Here are some alternatives to think about that don't involve explicitly disabling an input.
146**Disabled Buttons** - Change the `Msg` it fires, the text that is rendered, and optionally set a `Region.description` which will be available to screen readers.
148 import Element.Input as Input
149 import Element.Region as Region
154 [ Background.color blue
157 Just SaveButtonPressed
159 text "Save blog post"
164 [ Background.color grey
166 "A publish date is required before saving a blogpost."
169 Just DisabledSaveButtonPressed
174Consider showing a hint if `DisabledSaveButtonPressed` is sent.
176For other inputs such as `Input.text`, consider simply rendering it in a normal `paragraph` or `el` if it's not editable.
178Alternatively, see if it's reasonable to _not_ display an input if you'd normally disable it. Is there an option where it's only visible when it's editable?
182import Element exposing (Attribute, Color, Element)
183import Element.Background as Background
184import Element.Border as Border
185import Element.Events as Events
186import Element.Font as Font
187import Element.Region as Region
189import Html.Attributes
191import Internal.Flag as Flag
192import Internal.Model as Internal
193import Internal.Style exposing (classes)
194import Json.Decode as Json
199 = Placeholder (List (Attribute msg)) (Element msg)
207 Element.rgb (186 / 255) (189 / 255) (182 / 255)
218placeholder : List (Attribute msg) -> Element msg -> Placeholder msg
232 = Label LabelLocation (List (Attribute msg)) (Element msg)
236isStacked : Label msg -> Bool
258labelRight : List (Attribute msg) -> Element msg -> Label msg
264labelLeft : List (Attribute msg) -> Element msg -> Label msg
270labelAbove : List (Attribute msg) -> Element msg -> Label msg
276labelBelow : List (Attribute msg) -> Element msg -> Label msg
281{-| Sometimes you may need to have a label which is not visible, but is still accessible to screen readers.
283Seriously consider a visible label before using this.
285The situations where a hidden label makes sense:
287 - A searchbar with a `search` button right next to it.
288 - A `table` of inputs where the header gives the label.
290Basically, a hidden label works when there are other contextual clues that sighted people can pick up on.
293labelHidden : String -> Label msg
298hiddenLabelAttribute label =
300 HiddenLabel textLabel ->
301 Internal.Describe (Internal.Label textLabel)
307{-| A standard button.
309The `onPress` handler will be fired either `onClick` or when the element is focused and the `Enter` key has been pressed.
311 import Element exposing (rgb255, text)
312 import Element.Background as Background
313 import Element.Input as Input
316 Element.rgb255 238 238 238
320 [ Background.color blue
322 [ Background.color purple ]
324 { onPress = Just ClickMsg
325 , label = text "My Button"
332 { onPress : Maybe msg
333 , label : Element msg
336button attrs { onPress, label } =
339 -- We don't explicitly label this node as a button,
340 -- because buttons fire a bunch of times when you hold down the enter key.
341 -- We'd like to fire just once on the enter key, which means using keyup instead of keydown.
342 -- Because we have no way to disable keydown, though our messages get doubled.
344 (Element.width Element.shrink
345 :: Element.height Element.shrink
346 :: Internal.htmlClass
347 (classes.contentCenterX
349 ++ classes.contentCenterY
353 ++ classes.noTextSelection
356 :: focusDefault attrs
357 :: Internal.Describe Internal.Button
358 :: Internal.Attr (Html.Attributes.tabindex 0)
361 Internal.Attr (Html.Attributes.disabled True) :: attrs
369 (Internal.Unkeyed [ label ])
373 if List.any hasFocusStyle attrs then
377 Internal.htmlClass "focusable"
382 Internal.StyleClass _ (Internal.PseudoSelector Internal.Focus _) ->
390type alias Checkbox msg =
391 { onChange : Maybe (Bool -> msg)
392 , icon : Maybe (Element msg)
400 - **onChange** - The `Msg` to send.
401 - **icon** - The checkbox icon to show. This can be whatever you'd like, but `Input.defaultCheckbox` is included to get you started.
402 - **checked** - The current checked state.
403 - **label** - The [`Label`](#Label) for this checkbox
409 { onChange : Bool -> msg
410 , icon : Bool -> Element msg
415checkbox attrs { label, icon, checked, onChange } =
418 [ if isHiddenLabel label then
424 , Internal.Attr (Html.Events.onClick (onChange (not checked)))
428 if code == enter then
429 Just <| onChange (not checked)
431 else if code == space then
432 Just <| onChange (not checked)
439 , Element.width Element.fill
443 applyLabel attributes
449 Html.Attributes.attribute "role" "checkbox"
451 Html.Attributes.attribute "aria-checked" <|
457 , hiddenLabelAttribute label
459 , Element.height Element.fill
460 , Element.width Element.shrink
471 = Thumb (List (Attribute Never))
475thumb : List (Attribute Never) -> Thumb
484 [ Element.width (Element.px 16)
485 , Element.height (Element.px 16)
488 , Border.color (Element.rgb 0.5 0.5 0.5)
489 , Background.color (Element.rgb 1 1 1)
493{-| A slider input, good for capturing float values.
496 [ Element.height (Element.px 30)
498 -- Here is where we're creating/styling the "track"
499 , Element.behindContent
501 [ Element.width Element.fill
502 , Element.height (Element.px 2)
504 , Background.color grey
510 { onChange = AdjustValue
513 (text "My Slider Value")
517 , value = model.sliderValue
522`Element.behindContent` is used to render the track of the slider. Without it, no track would be rendered. The `thumb` is the icon that you can move around.
524The slider can be vertical or horizontal depending on the width/height of the slider.
526 - `height fill` and `width (px someWidth)` will cause the slider to be vertical.
527 - `height (px someHeight)` and `width (px someWidth)` where `someHeight` > `someWidth` will also do it.
528 - otherwise, the slider will be horizontal.
530**Note** If you want a slider for an `Int` value:
532 - set `step` to be `Just 1`, or some other whole value
533 - `value = toFloat model.myInt`
534 - And finally, round the value before making a message `onChange = round >> AdjustValue`
540 { onChange : Float -> msg
549slider attributes input =
551 (Thumb thumbAttributes) =
555 Internal.getWidth thumbAttributes
558 Internal.getHeight thumbAttributes
561 case ( trackWidth, trackHeight ) of
562 ( Nothing, Nothing ) ->
565 ( Just (Internal.Px w), Just (Internal.Px h) ) ->
568 ( Just (Internal.Px _), Just (Internal.Fill _) ) ->
575 Internal.getHeight attributes
578 Internal.getWidth attributes
580 ( spacingX, spacingY ) =
581 Internal.getSpacing attributes ( 5, 5 )
584 (input.value - input.min)
585 / (input.max - input.min)
590 - Width/Height of thumb so that the input can shadow it.
610 "thmb-" ++ thumbWidthString ++ "-" ++ thumbHeightString
617 Just (Internal.Px px) ->
618 String.fromInt px ++ "px"
628 Just (Internal.Px px) ->
629 String.fromInt px ++ "px"
635 [ Internal.Property "width"
637 , Internal.Property "height"
642 [ if isHiddenLabel input.label then
646 Element.spacingXY spacingX spacingY
653 Just (Internal.Px _) ->
664 Just (Internal.Px _) ->
674 (Maybe.withDefault Element.fill trackWidth)
676 (Maybe.withDefault (Element.px 20) trackHeight)
680 (Internal.NodeName "input")
681 [ hiddenLabelAttribute input.label
682 , Internal.StyleClass Flag.active
684 ("input[type=\"range\"]." ++ className ++ "::-moz-range-thumb")
687 , Internal.StyleClass Flag.hover
689 ("input[type=\"range\"]." ++ className ++ "::-webkit-slider-thumb")
692 , Internal.StyleClass Flag.focus
694 ("input[type=\"range\"]." ++ className ++ "::-ms-thumb")
697 , Internal.Attr (Html.Attributes.class (className ++ " focusable-parent"))
701 case String.toFloat str of
703 -- This should never happen because the browser
704 -- should always provide a Float.
712 Html.Attributes.type_ "range"
717 -- Note: If we set `any` here,
718 -- Firefox makes a single press of the arrows keys equal to 1
719 -- We could set the step manually to the effective range / 100
720 -- String.fromFloat ((input.max - input.min) / 100)
721 -- Which matches Chrome's default behavior
722 -- HOWEVER, that means manually moving a slider with the mouse will snap to that interval.
726 String.fromFloat step
729 Html.Attributes.min (String.fromFloat input.min)
731 Html.Attributes.max (String.fromFloat input.max)
733 Html.Attributes.value (String.fromFloat input.value)
736 Html.Attributes.attribute "orient" "vertical"
742 Maybe.withDefault (Element.px 20) trackHeight
745 Maybe.withDefault Element.fill trackWidth
748 Maybe.withDefault Element.fill trackWidth
751 Maybe.withDefault (Element.px 20) trackHeight
753 (Internal.Unkeyed [])
756 (Maybe.withDefault Element.fill trackWidth)
758 (Maybe.withDefault (Element.px 20) trackHeight)
760 -- This is after `attributes` because the thumb should be in front of everything.
761 ++ [ Element.behindContent <|
763 viewVerticalThumb factor thumbAttributes trackWidth
766 viewHorizontalThumb factor thumbAttributes trackHeight
774viewHorizontalThumb factor thumbAttributes trackHeight =
776 [ Element.width Element.fill
777 , Element.height (Maybe.withDefault Element.fill trackHeight)
781 [ Element.width (Element.fillPortion (round <| factor * 10000))
786 :: List.map (Internal.mapAttr Basics.never) thumbAttributes
790 [ Element.width (Element.fillPortion (round <| (abs <| 1 - factor) * 10000))
796viewVerticalThumb factor thumbAttributes trackWidth =
798 [ Element.height Element.fill
799 , Element.width (Maybe.withDefault Element.fill trackWidth)
803 [ Element.height (Element.fillPortion (round <| (abs <| 1 - factor) * 10000))
808 :: List.map (Internal.mapAttr Basics.never) thumbAttributes
812 [ Element.height (Element.fillPortion (round <| factor * 10000))
818type alias TextInput =
820 , spellchecked : Bool
821 , autofill : Maybe String
826 = TextInputNode String
832 { onChange : String -> msg
834 , placeholder : Maybe (Placeholder msg)
840textHelper : TextInput -> List (Attribute msg) -> Text msg -> Element msg
841textHelper textInput attrs textOptions =
844 defaultTextBoxStyle ++ attrs
847 redistribute (textInput.type_ == TextArea)
848 (isStacked textOptions.label)
853 Internal.StyleClass _ (Internal.SpacingStyle _ _ _) ->
867 isHeightContent attr =
869 Internal.Height Internal.Content ->
877 Internal.StyleClass cls (Internal.PaddingStyle pad t r b l) ->
878 -- The - 3 is here to prevent accidental triggering of scrollbars
879 -- when things are off by a pixel or two.
880 -- (or at least when the browser *thinks* it's off by a pixel or two)
882 { top = max 0 (floor (toFloat t - 3))
883 , right = max 0 (floor (toFloat r - 3))
884 , bottom = max 0 (floor (toFloat b - 3))
885 , left = max 0 (floor (toFloat l - 3))
892 case textInput.type_ of
893 TextInputNode inputType ->
898 |> List.filterMap getHeight
901 |> Maybe.map ((==) Internal.Content)
902 |> Maybe.withDefault False
906 |> List.filterMap getPadding
919 (case textInput.type_ of
920 TextInputNode inputType ->
921 Internal.NodeName "input"
924 Internal.NodeName "textarea"
926 ((case textInput.type_ of
927 TextInputNode inputType ->
928 -- Note: Due to a weird edgecase in...Edge...
929 -- `type` needs to come _before_ `value`
930 -- More reading: https://github.com/mdgriffith/elm-ui/pull/94/commits/4f493a27001ccc3cf1f2baa82e092c35d3811876
931 [ Internal.Attr (Html.Attributes.type_ inputType)
932 , Internal.htmlClass classes.inputText
937 , Element.height Element.fill
938 , Internal.htmlClass classes.inputMultiline
939 , calcMoveToCompensateForPadding withDefaults
940 , Element.paddingEach parentPadding
941 , Internal.Attr (Html.Attributes.style "margin" (renderBox (negateBox parentPadding)))
942 , Internal.Attr (Html.Attributes.style "box-sizing" "content-box")
945 ++ [ value textOptions.text
946 , Internal.Attr (Html.Events.onInput textOptions.onChange)
947 , hiddenLabelAttribute textOptions.label
948 , spellcheck textInput.spellchecked
949 , Maybe.map autofill textInput.autofill
950 |> Maybe.withDefault Internal.NoAttribute
952 ++ redistributed.input
954 (Internal.Unkeyed [])
957 case textInput.type_ of
959 -- textarea with height-content means that
960 -- the input element is rendered `inFront` and transparently.
961 -- Then the input text is rendered as the actual element.
965 ((if heightContent then
966 [ Element.width Element.fill
967 , Internal.htmlClass classes.focusedWithin
971 [ Element.width Element.fill
972 , Internal.htmlClass classes.focusedWithin
976 ++ redistributed.parent
982 (Element.width Element.fill
983 :: Element.height Element.fill
984 :: Element.inFront inputElement
985 :: Internal.htmlClass classes.inputMultilineParent
986 :: redistributed.wrapper
989 (if textOptions.text == "" then
990 case textOptions.placeholder of
995 [ renderPlaceholder place
997 (textOptions.text == "")
1001 [ Internal.unstyled (Html.text textOptions.text) ]
1007 TextInputNode inputType ->
1011 (Element.width Element.fill
1013 [ redistributed.parent
1014 , case textOptions.placeholder of
1019 [ Element.behindContent
1020 (renderPlaceholder place redistributed.cover (textOptions.text == ""))
1024 (Internal.Unkeyed [ inputElement ])
1027 (Internal.Class Flag.cursor classes.cursorText
1028 :: (if isHiddenLabel textOptions.label then
1029 Internal.NoAttribute
1036 :: redistributed.fullParent
1043 { top = negate box.top
1044 , right = negate box.right
1045 , bottom = negate box.bottom
1046 , left = negate box.left
1050renderBox { top, right, bottom, left } =
1053 ++ String.fromInt right
1055 ++ String.fromInt bottom
1057 ++ String.fromInt left
1061renderPlaceholder (Placeholder placeholderAttrs placeholderEl) forPlaceholder on =
1064 ++ [ Font.color charcoal
1065 , Internal.htmlClass (classes.noTextSelection ++ " " ++ classes.passPointerEvents)
1067 , Border.color (Element.rgba 0 0 0 0)
1068 , Background.color (Element.rgba 0 0 0 0)
1069 , Element.height Element.fill
1070 , Element.width Element.fill
1084{-| Because textareas are now shadowed, where they're rendered twice,
1085we to move the literal text area up because spacing is based on line height.
1087calcMoveToCompensateForPadding : List (Attribute msg) -> Attribute msg
1088calcMoveToCompensateForPadding attrs =
1090 gatherSpacing attr found =
1092 Internal.StyleClass _ (Internal.SpacingStyle _ x y) ->
1103 case List.foldr gatherSpacing Nothing attrs of
1105 Internal.NoAttribute
1108 Element.moveUp (toFloat vSpace / 2)
1111{-| Given the list of attributes provided to `Input.multiline` or `Input.text`,
1113redistribute them to the parent, the input, or the cover.
1115 - fullParent -> Wrapper around label and input
1116 - parent -> parent of wrapper
1117 - wrapper -> the element that is here to take up space.
1118 - cover -> things like placeholders or text areas which are layered on top of input.
1119 - input -> actual input element
1125 -> List (Attribute msg)
1127 { fullParent : List (Attribute msg)
1128 , parent : List (Attribute msg)
1129 , wrapper : List (Attribute msg)
1130 , input : List (Attribute msg)
1131 , cover : List (Attribute msg)
1133redistribute isMultiline stacked attrs =
1134 List.foldl (redistributeOver isMultiline stacked)
1143 { parent = List.reverse redist.parent
1144 , fullParent = List.reverse redist.fullParent
1145 , wrapper = List.reverse redist.wrapper
1146 , input = List.reverse redist.input
1147 , cover = List.reverse redist.cover
1206{-| isStacked means that the label is above or below
1208redistributeOver isMultiline stacked attr els =
1210 Internal.Nearby _ _ ->
1211 { els | parent = attr :: els.parent }
1213 Internal.Width width ->
1215 { els | fullParent = attr :: els.fullParent }
1217 else if isFill width then
1219 | fullParent = attr :: els.fullParent
1220 , parent = attr :: els.parent
1223 else if isPixel width then
1224 { els | parent = attr :: els.parent }
1228 | parent = attr :: els.parent
1231 Internal.Height height ->
1234 | fullParent = attr :: els.fullParent
1235 , parent = attr :: els.parent
1238 else if isFill height then
1240 | fullParent = attr :: els.fullParent
1241 , parent = attr :: els.parent
1244 else if isPixel height then
1245 { els | parent = attr :: els.parent }
1249 | parent = attr :: els.parent
1252 Internal.AlignX _ ->
1253 { els | fullParent = attr :: els.fullParent }
1255 Internal.AlignY _ ->
1256 { els | fullParent = attr :: els.fullParent }
1258 Internal.StyleClass _ (Internal.SpacingStyle _ _ _) ->
1260 | fullParent = attr :: els.fullParent
1261 , parent = attr :: els.parent
1262 , input = attr :: els.input
1263 , wrapper = attr :: els.wrapper
1266 Internal.StyleClass cls (Internal.PaddingStyle pad t r b l) ->
1269 | parent = attr :: els.parent
1270 , cover = attr :: els.cover
1276 Element.htmlAttribute
1277 (Html.Attributes.style
1279 ("calc(1.0em + " ++ String.fromInt (2 * min t b) ++ "px)")
1283 Element.htmlAttribute
1284 (Html.Attributes.style
1286 ("calc(1.0em + " ++ String.fromInt (2 * min t b) ++ "px)")
1289 reducedVerticalPadding =
1293 , bottom = b - min t b
1298 | parent = reducedVerticalPadding :: els.parent
1299 , input = newHeight :: newLineHeight :: els.input
1300 , cover = attr :: els.cover
1303 Internal.StyleClass _ (Internal.BorderWidth _ _ _ _ _) ->
1305 | parent = attr :: els.parent
1306 , cover = attr :: els.cover
1309 Internal.StyleClass _ (Internal.Transform _) ->
1311 | parent = attr :: els.parent
1312 , cover = attr :: els.cover
1315 Internal.StyleClass _ (Internal.FontSize _) ->
1316 { els | fullParent = attr :: els.fullParent }
1318 Internal.StyleClass _ (Internal.FontFamily _ _) ->
1319 { els | fullParent = attr :: els.fullParent }
1321 Internal.StyleClass _ _ ->
1322 { els | parent = attr :: els.parent }
1324 Internal.NoAttribute ->
1328 { els | input = attr :: els.input }
1330 Internal.Describe _ ->
1331 { els | input = attr :: els.input }
1333 Internal.Class _ _ ->
1334 { els | parent = attr :: els.parent }
1336 Internal.TransformComponent _ _ ->
1337 { els | input = attr :: els.input }
1342 List (Attribute msg)
1344 { onChange : String -> msg
1346 , placeholder : Maybe (Placeholder msg)
1352 { type_ = TextInputNode "text"
1353 , spellchecked = False
1354 , autofill = Nothing
1358{-| If spell checking is available, this input will be spellchecked.
1361 List (Attribute msg)
1363 { onChange : String -> msg
1365 , placeholder : Maybe (Placeholder msg)
1371 { type_ = TextInputNode "text"
1372 , spellchecked = True
1373 , autofill = Nothing
1379 List (Attribute msg)
1381 { onChange : String -> msg
1383 , placeholder : Maybe (Placeholder msg)
1389 { type_ = TextInputNode "search"
1390 , spellchecked = False
1391 , autofill = Nothing
1395{-| A password input that allows the browser to autofill.
1397It's `newPassword` instead of just `password` because it gives the browser a hint on what type of password input it is.
1399A password takes all the arguments a normal `Input.text` would, and also **show**, which will remove the password mask (e.g. `****` vs `pass1234`)
1403 List (Attribute msg)
1405 { onChange : String -> msg
1407 , placeholder : Maybe (Placeholder msg)
1412newPassword attrs pass =
1421 , spellchecked = False
1422 , autofill = Just "new-password"
1425 { onChange = pass.onChange
1427 , placeholder = pass.placeholder
1428 , label = pass.label
1434 List (Attribute msg)
1436 { onChange : String -> msg
1438 , placeholder : Maybe (Placeholder msg)
1443currentPassword attrs pass =
1452 , spellchecked = False
1453 , autofill = Just "current-password"
1456 { onChange = pass.onChange
1458 , placeholder = pass.placeholder
1459 , label = pass.label
1465 List (Attribute msg)
1467 { onChange : String -> msg
1469 , placeholder : Maybe (Placeholder msg)
1475 { type_ = TextInputNode "text"
1476 , spellchecked = False
1477 , autofill = Just "username"
1483 List (Attribute msg)
1485 { onChange : String -> msg
1487 , placeholder : Maybe (Placeholder msg)
1493 { type_ = TextInputNode "email"
1494 , spellchecked = False
1495 , autofill = Just "email"
1499{-| A multiline text input.
1501By default it will have a minimum height of one line and resize based on it's contents.
1505 List (Attribute msg)
1507 { onChange : String -> msg
1509 , placeholder : Maybe (Placeholder msg)
1514multiline attrs multi =
1518 , spellchecked = multi.spellcheck
1519 , autofill = Nothing
1522 { onChange = multi.onChange
1524 , placeholder = multi.placeholder
1525 , label = multi.label
1529isHiddenLabel label =
1538applyLabel : List (Attribute msg) -> Label msg -> Element msg -> Element msg
1539applyLabel attrs label input =
1541 HiddenLabel labelText ->
1542 -- NOTE: This means that the label is applied outside of this function!
1543 -- It would be nice to unify this logic, but it's a little tricky
1546 (Internal.NodeName "label")
1548 (Internal.Unkeyed [ input ])
1550 Label position labelAttrs labelChild ->
1557 (Internal.Unkeyed [ labelChild ])
1563 (Internal.NodeName "label")
1565 (Internal.Unkeyed [ labelElement, input ])
1570 (Internal.NodeName "label")
1572 (Internal.Unkeyed [ input, labelElement ])
1577 (Internal.NodeName "label")
1579 (Internal.Unkeyed [ input, labelElement ])
1584 (Internal.NodeName "label")
1586 (Internal.Unkeyed [ labelElement, input ])
1590type Option value msg
1591 = Option value (OptionState -> Element msg)
1601{-| Add a choice to your radio element. This will be rendered with the default radio icon.
1603option : value -> Element msg -> Option value msg
1605 Option val (defaultRadioOption txt)
1608{-| Customize exactly what your radio option should look like in different states.
1610optionWith : value -> (OptionState -> Element msg) -> Option value msg
1611optionWith val view =
1617 List (Attribute msg)
1619 { onChange : option -> msg
1620 , options : List (Option option msg)
1621 , selected : Maybe option
1629{-| Same as radio, but displayed as a row
1632 List (Attribute msg)
1634 { onChange : option -> msg
1635 , options : List (Option option msg)
1636 , selected : Maybe option
1644defaultRadioOption : Element msg -> OptionState -> Element msg
1645defaultRadioOption optionLabel status =
1647 [ Element.spacing 10
1649 , Element.width Element.shrink
1652 [ Element.width (Element.px 14)
1653 , Element.height (Element.px 14)
1654 , Background.color white
1658 Internal.htmlClass "focusable"
1661 Internal.NoAttribute
1663 -- , Border.shadow <|
1664 -- -- case status of
1666 -- -- { offset = ( 0, 0 )
1669 -- -- , color = Color.rgb 235 235 235
1672 -- -- { offset = ( 0, 0 )
1675 -- -- , color = Color.rgba 235 235 235 0
1678 -- { offset = ( 0, 0 )
1681 -- , color = Color.rgba 235 235 235 0
1696 Element.rgb (208 / 255) (208 / 255) (208 / 255)
1699 Element.rgb (208 / 255) (208 / 255) (208 / 255)
1702 Element.rgb (59 / 255) (153 / 255) (252 / 255)
1705 , Element.el [ Element.width Element.fill, Internal.htmlClass "unfocusable" ] optionLabel
1711 -> List (Attribute msg)
1713 { onChange : option -> msg
1714 , options : List (Option option msg)
1715 , selected : Maybe option
1719radioHelper orientation attrs input =
1721 renderOption (Option val view) =
1724 if Just val == input.selected then
1732 , case orientation of
1734 Element.width Element.shrink
1737 Element.width Element.fill
1738 , Events.onClick (input.onChange val)
1742 Html.Attributes.attribute "aria-checked"
1747 Html.Attributes.attribute "aria-checked"
1750 Html.Attributes.attribute "role" "radio"
1757 row (hiddenLabelAttribute input.label :: attrs)
1758 (List.map renderOption input.options)
1761 column (hiddenLabelAttribute input.label :: attrs)
1762 (List.map renderOption input.options)
1765 case input.options of
1769 (Option val _) :: _ ->
1770 List.foldl track ( NotFound, val, val ) input.options
1771 |> (\( found, b, a ) ->
1783 track opt ( found, prev, nxt ) =
1788 if Just val == input.selected then
1789 ( BeforeFound, prev, nxt )
1795 ( AfterFound, prev, val )
1798 ( found, prev, nxt )
1806 Internal.Width (Internal.Fill _) ->
1809 Internal.Height (Internal.Fill _) ->
1819 (List.filterMap identity
1820 [ Just Element.alignLeft
1822 , Just (Internal.htmlClass "focus")
1823 , Just Region.announce
1826 Html.Attributes.attribute "role" "radiogroup"
1831 Just ( prev, next ) ->
1835 if code == leftArrow then
1836 Just (input.onChange prev)
1838 else if code == upArrow then
1839 Just (input.onChange prev)
1841 else if code == rightArrow then
1842 Just (input.onChange next)
1844 else if code == downArrow then
1845 Just (input.onChange next)
1847 else if code == space then
1848 case input.selected of
1850 Just (input.onChange prev)
1860 -- ++ hideIfEverythingisInvisible
1877column : List (Attribute msg) -> List (Internal.Element msg) -> Internal.Element msg
1878column attributes children =
1882 (Element.height Element.shrink
1883 :: Element.width Element.fill
1886 (Internal.Unkeyed children)
1889row : List (Attribute msg) -> List (Internal.Element msg) -> Internal.Element msg
1890row attributes children =
1894 (Element.width Element.fill
1897 (Internal.Unkeyed children)
1905onEnter : msg -> Attribute msg
1911onSpace : msg -> Attribute msg
1917onUpArrow : msg -> Attribute msg
1923onRightArrow : msg -> Attribute msg
1925 onKey rightArrow msg
1929onLeftArrow : msg -> Attribute msg
1935onDownArrow : msg -> Attribute msg
1986onKey : String -> msg -> Attribute msg
1987onKey desiredCode msg =
1990 if code == desiredCode then
1994 Json.fail "Not the enter key"
1997 Json.field "key" Json.string
1998 |> Json.andThen decode
2001 Html.Events.preventDefaultOn "keyup"
2002 (Json.map (\fired -> ( fired, True )) isKey)
2006-- preventKeydown : String -> a -> Attribute a
2007-- preventKeydown desiredCode msg =
2010-- if code == desiredCode then
2013-- Json.fail "Not the enter key"
2015-- Json.field "key" Json.string
2016-- |> Json.andThen decode
2018-- Events.onWithOptions "keydown"
2019-- { stopPropagation = False
2020-- , preventDefault = True
2026onKeyLookup : (String -> Maybe msg) -> Attribute msg
2032 Json.fail "No key matched"
2038 Json.field "key" Json.string
2039 |> Json.andThen decode
2041 Internal.Attr <| Html.Events.on "keyup" isKey
2045onFocusOut : msg -> Attribute msg
2047 Internal.Attr <| Html.Events.on "focusout" (Json.succeed msg)
2051onFocusIn : msg -> Attribute msg
2053 Internal.Attr <| Html.Events.on "focusin" (Json.succeed msg)
2056selected : Bool -> Attribute msg
2058 Internal.Attr << Html.Attributes.selected
2061name : String -> Attribute msg
2063 Internal.Attr << Html.Attributes.name
2066value : String -> Attribute msg
2068 Internal.Attr << Html.Attributes.value
2071tabindex : Int -> Attribute msg
2073 Internal.Attr << Html.Attributes.tabindex
2076disabled : Bool -> Attribute msg
2078 Internal.Attr << Html.Attributes.disabled
2081spellcheck : Bool -> Attribute msg
2083 Internal.Attr << Html.Attributes.spellcheck
2086readonly : Bool -> Attribute msg
2088 Internal.Attr << Html.Attributes.readonly
2091autofill : String -> Attribute msg
2093 Internal.Attr << Html.Attributes.attribute "autocomplete"
2096{-| Attach this attribute to any `Input` that you would like to be automatically focused when the page loads.
2098You should only have a maximum of one per page.
2101focusedOnLoad : Attribute msg
2103 Internal.Attr <| Html.Attributes.autofocus True
2110defaultTextBoxStyle : List (Attribute msg)
2111defaultTextBoxStyle =
2112 [ defaultTextPadding
2114 , Border.color darkGrey
2115 , Background.color white
2118 , Element.width Element.fill
2119 , Element.height Element.shrink
2123defaultTextPadding : Attribute msg
2125 Element.paddingXY 12 12
2128{-| The blue default checked box icon.
2130You'll likely want to make your own checkbox at some point that fits your design.
2133defaultCheckbox : Bool -> Element msg
2134defaultCheckbox checked =
2136 [ Internal.htmlClass "focusable"
2137 , Element.width (Element.px 14)
2138 , Element.height (Element.px 14)
2146 Element.rgb (59 / 255) (153 / 255) (252 / 255)
2149 Element.rgb (211 / 255) (211 / 255) (211 / 255)
2156 Element.rgba (238 / 255) (238 / 255) (238 / 255) 0
2159 Element.rgb (238 / 255) (238 / 255) (238 / 255)
2161 , Background.color <|
2163 Element.rgb (59 / 255) (153 / 255) (252 / 255)
2176 [ Border.color white
2177 , Element.height (Element.px 6)
2178 , Element.width (Element.px 9)
2179 , Element.rotate (degrees -45)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list