Skip to content

Commit e81af39

Browse files
authored
Better Select focus behavior and keyboard navigation (#172)
* Better Select focus behavior and keyboard navigation * Fix placeholder overflow
1 parent 6ce8a79 commit e81af39

File tree

6 files changed

+196
-57
lines changed

6 files changed

+196
-57
lines changed

docs/Examples/Form.example.purs

+10-5
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import Data.Foldable (foldMap)
1111
import Data.Int as Int
1212
import Data.Lens (iso)
1313
import Data.Lens.Record (prop)
14-
import Data.Maybe (Maybe(..), maybe)
14+
import Data.Maybe (Maybe(..), isNothing, maybe)
1515
import Data.Monoid as Monoid
1616
import Data.Newtype (class Newtype, un)
1717
import Data.Nullable as Nullable
@@ -225,7 +225,7 @@ type Pet =
225225
, lastName :: Validated String
226226
, animal :: Validated (Maybe String)
227227
, age :: Validated String
228-
, color :: Maybe String
228+
, color :: Validated (Maybe String)
229229
}
230230

231231
type ValidatedPet =
@@ -314,9 +314,9 @@ userForm = ado
314314
, editor: addressForm
315315
}
316316
leastFavoriteColors <-
317-
F.indent "Least Favorite Colors" Neither
317+
F.indent "Least Favorite Colors" Required
318318
$ F.focus (prop (SProxy :: SProxy "leastFavoriteColors"))
319-
$ F.validated (F.nonEmptyArray "Required")
319+
$ F.validated (F.nonEmptyArray' "This is absolutely essential. Don't skip it!!")
320320
$ F.multiSelect show
321321
$ map (\x -> { label: x, value: x })
322322
$ [ "Beige"
@@ -348,7 +348,7 @@ userForm = ado
348348
, lastName: F.Fresh ""
349349
, animal: F.Fresh Nothing
350350
, age: F.Fresh "1"
351-
, color: Nothing
351+
, color: F.Fresh Nothing
352352
}
353353
, maxRows: top
354354
, rowMenu: FT.defaultRowMenu
@@ -401,6 +401,11 @@ userForm = ado
401401
FT.column_ "Color"
402402
$ F.withProps \props ->
403403
F.focus (prop (SProxy :: SProxy "color"))
404+
$ F.warn (\x ->
405+
Monoid.guard
406+
(isNothing x)
407+
(pure "Your pet has no color??")
408+
)
404409
$ F.asyncSelectByKey
405410
(loadColor props.simulatePauses)
406411
(loadColors props.simulatePauses)

src/Lumi/Components/Form.purs

+4-4
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ import Lumi.Components.FetchCache as FetchCache
8383
import Lumi.Components.Form.Defaults (formDefaults) as Defaults
8484
import Lumi.Components.Form.Internal (Forest, FormBuilder'(..), FormBuilder, SeqFormBuilder, Tree(..), formBuilder, formBuilder_, invalidate, pruneTree, sequential)
8585
import Lumi.Components.Form.Internal (Forest, FormBuilder', FormBuilder, SeqFormBuilder', SeqFormBuilder, Tree(..), formBuilder, formBuilder_, invalidate, listen, parallel, revalidate, sequential) as Internal
86-
import Lumi.Components.Form.Validation (ModifyValidated(..), Validated(..), Validator, _Validated, fromValidated, mustBe, mustEqual, nonEmpty, nonEmptyArray, nonNull, optional, setFresh, setModified, validDate, validInt, validNumber, validated, warn) as Validation
86+
import Lumi.Components.Form.Validation (ModifyValidated(..), Validated(..), Validator, _Validated, fromValidated, mustBe, mustEqual, nonEmpty, nonEmptyArray, nonNull, nonEmpty', nonEmptyArray', nonNull', optional, setFresh, setModified, validDate, validInt, validNumber, validDate', validInt', validNumber', validated, warn) as Validation
8787
import Lumi.Components.Input (alignToInput)
8888
import Lumi.Components.Input as Input
8989
import Lumi.Components.LabeledField (RequiredField(..), labeledField, labeledFieldValidationErrorStyles, labeledFieldValidationWarningStyles)
@@ -609,7 +609,7 @@ multiSelect encode opts = formBuilder_ \{ readonly } selected onChange ->
609609
, id: ""
610610
, name: ""
611611
, noResultsText: "No results"
612-
, placeholder: "Select an option ..."
612+
, placeholder: "Select an option..."
613613
, disabled: false
614614
, loading: false
615615
, optionRenderer: R.text <<< _.label
@@ -646,7 +646,7 @@ asyncSelect loadOptions toSelectOption optionRenderer =
646646
, disabled: false
647647
, loading: false
648648
, noResultsText: "No results"
649-
, placeholder: "Search ..."
649+
, placeholder: "Search..."
650650
, optionRenderer
651651
, optionSort: Nothing
652652
, toSelectOption
@@ -693,7 +693,7 @@ asyncSelectByKey getData loadOptions fromId toId toSelectOption optionRenderer =
693693
, disabled: false
694694
, loading: isJust value && isNothing data_
695695
, noResultsText: "No results"
696-
, placeholder: "Search ..."
696+
, placeholder: "Search..."
697697
, optionRenderer
698698
, optionSort: Nothing
699699
, toSelectOption

src/Lumi/Components/Form/Validation.purs

+34-7
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
module Lumi.Components.Form.Validation
22
( Validator
33
, nonEmpty, nonEmptyArray, nonNull
4+
, nonEmpty', nonEmptyArray', nonNull'
45
, mustEqual, mustBe
56
, validNumber, validInt, validDate
7+
, validNumber', validInt', validDate'
68
, optional
79
, Validated(..)
810
, _Validated, _Fresh, _Modified
@@ -58,15 +60,27 @@ type WarningValidator result =
5860

5961
-- | A `Validator` which verifies that an input string is non-empty.
6062
nonEmpty :: String -> Validator String NonEmptyString
61-
nonEmpty name = note (name <> " is required") <<< NES.fromString
63+
nonEmpty name = nonEmpty' (name <> " is required")
64+
65+
-- | `nonEmpty`, but the argument is the entire validation message.
66+
nonEmpty' :: String -> Validator String NonEmptyString
67+
nonEmpty' msg = note msg <<< NES.fromString
6268

6369
-- | A `Validator` which verifies that an input array is non-empty.
6470
nonEmptyArray :: forall a. String -> Validator (Array a) (NonEmptyArray a)
65-
nonEmptyArray name = note (name <> " cannot be empty") <<< NEA.fromArray
71+
nonEmptyArray name = nonEmptyArray' (name <> " cannot be empty")
72+
73+
-- | `nonEmptyArray`, but the argument is the entire validation message.
74+
nonEmptyArray' :: forall a. String -> Validator (Array a) (NonEmptyArray a)
75+
nonEmptyArray' msg = note msg <<< NEA.fromArray
6676

6777
-- | A `Validator` which verifies that an optional field is specified.
6878
nonNull :: forall a. String -> Validator (Maybe a) a
69-
nonNull name = note (name <> " is required")
79+
nonNull name = nonNull' (name <> " is required")
80+
81+
-- | `nonNull`, but the argument is the entire validation message.
82+
nonNull' :: forall a. String -> Validator (Maybe a) a
83+
nonNull' msg = note msg
7084

7185
-- | A `Validator` which verifies that its input equals some value.
7286
mustEqual :: forall a. Eq a => a -> String -> Validator a a
@@ -80,17 +94,30 @@ mustBe cond error value
8094

8195
-- | A `Validator` which verifies that its input can be parsed as a number.
8296
validNumber :: String -> Validator String Number
83-
validNumber name = note (name <> " must be a number") <<< Number.fromString
97+
validNumber name = validNumber' (name <> " must be a number")
98+
99+
-- | `validNumber`, but the argument is the entire validation message.
100+
validNumber' :: String -> Validator String Number
101+
validNumber' msg = note msg <<< Number.fromString
84102

85103
-- | A `Validator` which verifies that its input can be parsed as an integer.
86104
validInt :: String -> Validator String Int
87-
validInt name = note (name <> " must be a whole number") <<< Int.fromString
105+
validInt name = validInt' (name <> " must be a whole number")
106+
107+
-- | `validInt`, but the argument is the entire validation message.
108+
validInt' :: String -> Validator String Int
109+
validInt' msg = note msg <<< Int.fromString
88110

89111
-- | A `Validator` which verifies that its input can be parsed as a date.
90112
-- | Dates are of the format "YYYY-MM-DD".
91113
validDate :: String -> Validator String Date.Date
92-
validDate name input =
93-
note (name <> " must be a date") result
114+
validDate name =
115+
validDate' (name <> " must be a date")
116+
117+
-- | `validDate`, but the argument is the entire validation message.
118+
validDate' :: String -> Validator String Date.Date
119+
validDate' msg input =
120+
note msg result
94121
where
95122
result = case traverse Int.fromString $ split (Pattern "-") input of
96123
Just [y, m, d] -> join $ Date.exactDate <$> toEnum y <*> toEnum m <*> toEnum d

src/Lumi/Components/Select.purs

+16-4
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ import Effect (Effect)
2727
import Effect.Aff (Aff)
2828
import Effect.Unsafe (unsafePerformEffect)
2929
import JSS (JSS, important, jss)
30+
import Lumi.Components (($$$))
3031
import Lumi.Components.Color (colors)
3132
import Lumi.Components.Icon as Icon
3233
import Lumi.Components.Input (lumiInputDisabledStyles, lumiInputFocusStyles, lumiInputHoverStyles, lumiInputStyles)
3334
import Lumi.Components.Loader (loader)
3435
import Lumi.Components.Select.Backend (SelectBackendProps, SelectOption, SelectOptions(..), selectBackend)
3536
import Lumi.Components.ZIndex (ziSelect)
37+
import Lumi.Components2.Text as T
3638
import React.Basic.Classic (Component, JSX, createComponent, element, elementKeyed, empty, makeStateless)
3739
import React.Basic.DOM (CSS, css)
3840
import React.Basic.DOM as R
@@ -226,7 +228,7 @@ select = makeStateless component render
226228
, value: props.value
227229
, render: \selectState ->
228230
lumiSelectInnerElement
229-
{ "data-focus": selectState.isOpen
231+
{ "data-focus": selectState.isActive
230232
, children:
231233
[ renderInput props selectState
232234
, if selectState.isOpen
@@ -262,7 +264,11 @@ select = makeStateless component render
262264
then
263265
[ lumiSelectInputPlaceholder
264266
{ key: "lumi-select-placeholder"
265-
, children: R.text props.placeholder
267+
, title: props.placeholder
268+
, children:
269+
T.text
270+
$ T.truncate
271+
$$$ props.placeholder
266272
}
267273
]
268274
else
@@ -342,7 +348,7 @@ select = makeStateless component render
342348
, children: R.text props.noResultsText
343349
}
344350
]
345-
renderOptions options_ = options_ `flip Array.mapWithIndex` \index option ->
351+
renderOptions options_ = options_ # Array.mapWithIndex \index option ->
346352
let
347353
{ label, value } = props.toSelectOption option
348354
in
@@ -437,6 +443,7 @@ styles = jss
437443
{ margin: "-2px" -- (gap)
438444

439445
, display: "flex"
446+
, position: "relative"
440447
, flexFlow: "row wrap"
441448
, flex: "1 1 auto"
442449
, minWidth: 0
@@ -451,6 +458,9 @@ styles = jss
451458
, lineHeight: "32px"
452459
, flex: "1 1 0%"
453460
, display: "block"
461+
, position: "absolute"
462+
, width: "100%"
463+
, maxWidth: "calc(100% - " <> clearIconWidth <> ")"
454464
, overflow: "hidden"
455465
, whiteSpace: "nowrap"
456466
, textOverflow: "ellipsis"
@@ -502,7 +512,7 @@ styles = jss
502512
, fontSize: "8px"
503513
, cursor: "pointer"
504514
, height: "26px"
505-
, width: "20px"
515+
, width: clearIconWidth
506516
, flex: "0 0 auto"
507517
, paddingTop: "3px"
508518
, "@media (min-width: 860px)":
@@ -630,3 +640,5 @@ styles = jss
630640
, overflow: "hidden"
631641
, textOverflow: "ellipsis"
632642
}
643+
644+
clearIconWidth = "20px"

0 commit comments

Comments
 (0)