Prevent more unnecessary filtering

This commit is contained in:
Tom Nunn 2023-03-11 21:24:47 +00:00
parent a75b080360
commit 559129d85e
6 changed files with 130 additions and 75 deletions

View File

@ -25,6 +25,7 @@ exampleProgramTest =
[ Test.test "Filter for United Kingdom produces one result" <|
\() ->
programTest
|> focusInput
|> ProgramTest.fillIn "" "Choose a country" "United Kingdom"
|> ProgramTest.ensureView
(Query.find [ Selector.id (Select.toMenuElementId countrySelect) ]
@ -38,12 +39,14 @@ exampleProgramTest =
, Test.test "Click United Kingdom selects it" <|
\() ->
programTest
|> focusInput
|> ProgramTest.fillIn "" "Choose a country" "United"
|> Select.Effect.simulateClickOption simulateInputConfig "country-select" "🇬🇧 United Kingdom of Great Britain and Northern Ireland"
|> ProgramTest.expectViewHas [ Selector.text "You chose United Kingdom of Great Britain and Northern Ireland" ]
, Test.test "Keyboard select United Kingdom" <|
\() ->
programTest
|> focusInput
|> ProgramTest.fillIn "" "Choose a country" "United"
|> SimulateInput.arrowDown "country-select"
|> SimulateInput.enter "country-select"
@ -51,12 +54,12 @@ exampleProgramTest =
, Test.test "Focusing on the input triggers the onFocus msg" <|
\() ->
programTest
|> ProgramTest.simulateDomEvent (Query.find [ Selector.id (Select.toInputElementId countrySelect) ]) Test.Html.Event.focus
|> focusInput
|> ProgramTest.expectModel (.inputIsFocused >> Expect.equal (Just True))
, Test.test "Input losing focus triggers the onLoseFocus msg" <|
\() ->
programTest
|> ProgramTest.simulateDomEvent (Query.find [ Selector.id (Select.toInputElementId countrySelect) ]) Test.Html.Event.focus
|> focusInput
|> ProgramTest.simulateDomEvent (Query.find [ Selector.id (Select.toInputElementId countrySelect) ]) Test.Html.Event.blur
|> ProgramTest.expectModel (.inputIsFocused >> Expect.equal (Just False))
, Test.test "Filling in the input triggers the onInput msg" <|
@ -67,26 +70,31 @@ exampleProgramTest =
, Test.test "Typing 2 chars with withMinInputLength (Just 3) does not show any items" <|
\() ->
programTestWith (Select.withMinInputLength (Just 3))
|> focusInput
|> ProgramTest.fillIn "" "Choose a country" "un"
|> ProgramTest.expectViewHasNot [ Selector.text "🇬🇧 United Kingdom of Great Britain and Northern Ireland" ]
, Test.test "Typing 3 chars with withMinInputLength (Just 3) does shows items" <|
\() ->
programTestWith (Select.withMinInputLength (Just 3))
|> focusInput
|> ProgramTest.fillIn "" "Choose a country" "uni"
|> ProgramTest.expectViewHas [ Selector.text "🇬🇧 United Kingdom of Great Britain and Northern Ireland" ]
, Test.test "Typing less than minInputLength does not show no matches even if nothing matched" <|
\() ->
programTestWith (Select.withMinInputLength (Just 5))
|> focusInput
|> ProgramTest.fillIn "" "Choose a country" "zzzz"
|> ProgramTest.expectViewHasNot [ Selector.text "No matches" ]
, Test.test "Typing up to the minInputLength shows no matches if nothing matched" <|
\() ->
programTestWith (Select.withMinInputLength (Just 3))
|> focusInput
|> ProgramTest.fillIn "" "Choose a country" "zzzz"
|> ProgramTest.expectViewHas [ Selector.text "No matches" ]
, Test.test "Choosing an option and then focusing back on the input shows all the options again" <|
\() ->
programTest
|> focusInput
|> ProgramTest.fillIn "" "Choose a country" "United"
|> Select.Effect.simulateClickOption simulateInputConfig "country-select" "🇬🇧 United Kingdom of Great Britain and Northern Ireland"
|> ProgramTest.simulateDomEvent (Query.find [ Selector.id (Select.toInputElementId countrySelect) ]) Test.Html.Event.focus
@ -155,3 +163,8 @@ simulateEffect effect =
, sleep = SimulatedProcess.sleep
}
selectEffect
focusInput : ProgramTest model msg effect -> ProgramTest model msg effect
focusInput =
ProgramTest.simulateDomEvent (Query.find [ Selector.id (Select.toInputElementId countrySelect) ]) Test.Html.Event.focus

View File

@ -1,10 +1,10 @@
module RequestTest exposing (exampleProgramTest)
import EffectRequestExample as App
import EffectRequestExample as App exposing (Cocktail)
import Http
import Json.Decode as Decode
import ProgramTest exposing (ProgramTest, SimulatedEffect)
import Select
import Select exposing (Select)
import Select.Effect
import SimulateInput
import SimulatedEffect.Cmd as SimulatedCmd
@ -12,6 +12,7 @@ import SimulatedEffect.Http as SimulateHttp
import SimulatedEffect.Process as SimulatedProcess
import SimulatedEffect.Task as SimulatedTask
import Test exposing (Test)
import Test.Html.Event
import Test.Html.Query as Query exposing (Single)
import Test.Html.Selector as Selector exposing (Selector)
@ -22,6 +23,7 @@ exampleProgramTest =
[ Test.test "Type in Chocolate, and choose second option with keyboard navigation" <|
\() ->
programTest
|> focusInput
|> ProgramTest.fillIn "" "Find a cocktail" "Chocolate"
|> ProgramTest.advanceTime 500
|> ProgramTest.simulateHttpOk "GET"
@ -33,6 +35,7 @@ exampleProgramTest =
, Test.test "Type in Chocolate, and choose \"Chocolate Drink\" with mouse click" <|
\() ->
programTest
|> focusInput
|> ProgramTest.fillIn "" "Find a cocktail" "Chocolate"
|> ProgramTest.advanceTime 300
|> ProgramTest.simulateHttpOk "GET"
@ -97,6 +100,18 @@ simulateConfig =
}
drinkSelect : Select Cocktail
drinkSelect =
App.init ()
|> Tuple.first
|> .select
focusInput : ProgramTest model msg effect -> ProgramTest model msg effect
focusInput =
ProgramTest.simulateDomEvent (Query.find [ Selector.id (Select.toInputElementId drinkSelect) ]) Test.Html.Event.focus
cocktailsResponse : String
cocktailsResponse =
"""{

View File

@ -10,6 +10,7 @@ module Internal.Model exposing
, isOpen
, isRequestFailed
, openMenu
, requiresNewFilteredOptions
, selectOption
, setElements
, setFilteredOptions
@ -128,20 +129,24 @@ wasHighlightedByMouse (Model { highlightedByMouse }) =
toFilteredOptions : Maybe Int -> (a -> String) -> Maybe (Filter a) -> Model a -> List (Option a)
toFilteredOptions minInputLength itemToString filter (Model model) =
case minInputLength of
Just chars ->
if String.length model.inputValue >= chars then
toFilteredOptions_ itemToString filter (Model model)
if not model.focused then
[]
else
[]
else
case minInputLength of
Just chars ->
if String.length model.inputValue >= chars then
toFilteredOptions_ itemToString filter (Model model)
Nothing ->
if model.applyFilter then
toFilteredOptions_ itemToString filter (Model model)
else
[]
else
List.map (Option.init itemToString) model.items
Nothing ->
if model.applyFilter then
toFilteredOptions_ itemToString filter (Model model)
else
List.map (Option.init itemToString) model.items
toFilteredOptions_ : (a -> String) -> Maybe (Filter a) -> Model a -> List (Option a)
@ -238,6 +243,11 @@ isRequestFailed (Model { requestState }) =
requestState == Just Failed
requiresNewFilteredOptions : Model a -> Bool
requiresNewFilteredOptions (Model { filteredOptions }) =
filteredOptions == Nothing
-- UPDATE

View File

@ -8,6 +8,7 @@ type Msg a
= InputChanged String (List (Option a))
| OptionClicked (Option a)
| InputFocused (Maybe Int)
| GotNewFilteredOptions (List (Option a))
| InputClicked (Maybe Int)
| InputLostFocus
{ clearInputValue : Bool

View File

@ -85,13 +85,19 @@ update_ { request, requestMinInputLength, debounceRequest, onFocus, onLoseFocus,
, Effect.emitJust onLoseFocus
)
GotNewFilteredOptions options ->
( Model.setFilteredOptions options model
, Effect.None
)
MouseEnteredOption i ->
( Model.highlightIndex (Just i) True model
, Effect.none
)
KeyDown selectOnTab filteredOptions key ->
handleKey selectOnTab tagger model key filteredOptions
Model.setFilteredOptions filteredOptions model
|> handleKey selectOnTab tagger key filteredOptions
GotContainerAndMenuElements maybeIdx result ->
( model
@ -176,8 +182,8 @@ onFocusMenu tagger maybeOptionIdx hasRequest model =
)
handleKey : Bool -> (Msg a -> msg) -> Model a -> String -> List (Option a) -> ( Model a, Effect effect msg )
handleKey selectOnTab tagger model key filteredOptions =
handleKey : Bool -> (Msg a -> msg) -> String -> List (Option a) -> Model a -> ( Model a, Effect effect msg )
handleKey selectOnTab tagger key filteredOptions model =
let
selectHighlighted =
case Model.toHighlighted model |> Maybe.andThen (\idx -> getAt idx filteredOptions) of

View File

@ -89,58 +89,66 @@ toElement model config =
toElement_ : Placement -> List (Option a) -> Model a -> ViewConfigInternal a msg -> Element msg
toElement_ placement filteredOptions model config =
Element.el
([ Element.htmlAttribute (Html.Attributes.id <| Model.toContainerElementId model)
, Element.width Element.fill
, Element.below <|
if
List.length filteredOptions
== 0
&& Model.isOpen model
&& (String.length (Model.toInputValue model) >= Maybe.withDefault 1 config.minInputLength)
&& (Model.toRequestState model == Nothing || Model.toRequestState model == Just Success)
then
config.noMatchElement
(List.concat
[ [ Element.htmlAttribute (Html.Attributes.id <| Model.toContainerElementId model)
, Element.width Element.fill
, Element.below <|
if
List.length filteredOptions
== 0
&& Model.isOpen model
&& (String.length (Model.toInputValue model) >= Maybe.withDefault 1 config.minInputLength)
&& (Model.toRequestState model == Nothing || Model.toRequestState model == Just Success)
then
config.noMatchElement
else
Element.none
, Placement.toAttribute
(if config.positionFixed then
Placement.Below
else
Element.none
, Placement.toAttribute
(if config.positionFixed then
Placement.Below
else
placement
)
<|
(if config.positionFixed then
positionFixedEl placement (Model.toContainerElement model)
else
identity
)
<|
menuView
(defaultMenuAttrs
{ menuWidth = Model.toMenuMinWidth model
, maxWidth = config.menuMaxWidth
, menuHeight = Model.toMenuMaxHeight config.menuMaxHeight config.menuPlacement model
}
++ List.concatMap (\toAttrs -> toAttrs (Model.toMenuPlacement config.menuMaxHeight config.menuPlacement model)) config.menuAttributes
else
placement
)
{ menuId = Model.toMenuElementId model
, toOptionId = Model.toOptionElementId model
, toOptionState = Model.toOptionState model
, onChange = config.onChange
, menuOpen = Model.isOpen model
, options = filteredOptions
, optionElement = config.optionElement
}
]
++ (if Model.isOpen model then
[ Element.htmlAttribute <| Html.Attributes.style "z-index" "21" ]
<|
(if config.positionFixed then
positionFixedEl placement (Model.toContainerElement model)
else
[]
)
else
identity
)
<|
menuView
(defaultMenuAttrs
{ menuWidth = Model.toMenuMinWidth model
, maxWidth = config.menuMaxWidth
, menuHeight = Model.toMenuMaxHeight config.menuMaxHeight config.menuPlacement model
}
++ List.concatMap (\toAttrs -> toAttrs (Model.toMenuPlacement config.menuMaxHeight config.menuPlacement model)) config.menuAttributes
)
{ menuId = Model.toMenuElementId model
, toOptionId = Model.toOptionElementId model
, toOptionState = Model.toOptionState model
, onChange = config.onChange
, menuOpen = Model.isOpen model
, options = filteredOptions
, optionElement = config.optionElement
}
]
, if Model.isOpen model then
[ Element.htmlAttribute <| Html.Attributes.style "z-index" "21" ]
else
[]
, if Model.isFocused model && Model.requiresNewFilteredOptions model then
[ Element.htmlAttribute (Html.Events.on "focusin" (Decode.succeed (GotNewFilteredOptions filteredOptions |> config.onChange)))
, Element.htmlAttribute (Html.Events.on "mousemove" (Decode.succeed (GotNewFilteredOptions filteredOptions |> config.onChange)))
]
else
[]
]
)
(inputView filteredOptions model config)
@ -153,10 +161,11 @@ inputView filteredOptions model config =
|> Maybe.andThen (Option.findIndex filteredOptions)
in
Input.text
(config.inputAttribs
++ [ Events.onFocus (InputFocused selectedIdx |> config.onChange)
, Events.onClick (InputClicked selectedIdx |> config.onChange)
, Events.onLoseFocus
(List.concat
[ config.inputAttribs
, [ Events.onFocus (InputFocused selectedIdx |> config.onChange)
, Events.onClick (InputClicked selectedIdx |> config.onChange)
, Events.onLoseFocus
(config.onChange
(InputLostFocus
{ clearInputValue = config.clearInputValueOnBlur
@ -165,16 +174,17 @@ inputView filteredOptions model config =
filteredOptions
)
)
, onKeyDown (Model.isOpen model) (KeyDown config.selectOnTab filteredOptions >> config.onChange)
, Element.htmlAttribute (Html.Attributes.id <| Model.toInputElementId model)
, Element.inFront <|
, onKeyDown (Model.isOpen model) (KeyDown config.selectOnTab filteredOptions >> config.onChange)
, Element.htmlAttribute (Html.Attributes.id <| Model.toInputElementId model)
, Element.inFront <|
if Model.toValue model /= Nothing || Model.toInputValue model /= "" then
Maybe.withDefault Element.none config.clearButton
else
Element.none
]
++ inputAccessibilityAttributes filteredOptions model
]
, inputAccessibilityAttributes filteredOptions model
]
)
{ onChange =
\v ->