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" <| [ Test.test "Filter for United Kingdom produces one result" <|
\() -> \() ->
programTest programTest
|> focusInput
|> ProgramTest.fillIn "" "Choose a country" "United Kingdom" |> ProgramTest.fillIn "" "Choose a country" "United Kingdom"
|> ProgramTest.ensureView |> ProgramTest.ensureView
(Query.find [ Selector.id (Select.toMenuElementId countrySelect) ] (Query.find [ Selector.id (Select.toMenuElementId countrySelect) ]
@ -38,12 +39,14 @@ exampleProgramTest =
, Test.test "Click United Kingdom selects it" <| , Test.test "Click United Kingdom selects it" <|
\() -> \() ->
programTest programTest
|> focusInput
|> ProgramTest.fillIn "" "Choose a country" "United" |> ProgramTest.fillIn "" "Choose a country" "United"
|> Select.Effect.simulateClickOption simulateInputConfig "country-select" "🇬🇧 United Kingdom of Great Britain and Northern Ireland" |> 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" ] |> ProgramTest.expectViewHas [ Selector.text "You chose United Kingdom of Great Britain and Northern Ireland" ]
, Test.test "Keyboard select United Kingdom" <| , Test.test "Keyboard select United Kingdom" <|
\() -> \() ->
programTest programTest
|> focusInput
|> ProgramTest.fillIn "" "Choose a country" "United" |> ProgramTest.fillIn "" "Choose a country" "United"
|> SimulateInput.arrowDown "country-select" |> SimulateInput.arrowDown "country-select"
|> SimulateInput.enter "country-select" |> SimulateInput.enter "country-select"
@ -51,12 +54,12 @@ exampleProgramTest =
, Test.test "Focusing on the input triggers the onFocus msg" <| , Test.test "Focusing on the input triggers the onFocus msg" <|
\() -> \() ->
programTest programTest
|> ProgramTest.simulateDomEvent (Query.find [ Selector.id (Select.toInputElementId countrySelect) ]) Test.Html.Event.focus |> focusInput
|> ProgramTest.expectModel (.inputIsFocused >> Expect.equal (Just True)) |> ProgramTest.expectModel (.inputIsFocused >> Expect.equal (Just True))
, Test.test "Input losing focus triggers the onLoseFocus msg" <| , Test.test "Input losing focus triggers the onLoseFocus msg" <|
\() -> \() ->
programTest 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.simulateDomEvent (Query.find [ Selector.id (Select.toInputElementId countrySelect) ]) Test.Html.Event.blur
|> ProgramTest.expectModel (.inputIsFocused >> Expect.equal (Just False)) |> ProgramTest.expectModel (.inputIsFocused >> Expect.equal (Just False))
, Test.test "Filling in the input triggers the onInput msg" <| , 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" <| , Test.test "Typing 2 chars with withMinInputLength (Just 3) does not show any items" <|
\() -> \() ->
programTestWith (Select.withMinInputLength (Just 3)) programTestWith (Select.withMinInputLength (Just 3))
|> focusInput
|> ProgramTest.fillIn "" "Choose a country" "un" |> ProgramTest.fillIn "" "Choose a country" "un"
|> ProgramTest.expectViewHasNot [ Selector.text "🇬🇧 United Kingdom of Great Britain and Northern Ireland" ] |> ProgramTest.expectViewHasNot [ Selector.text "🇬🇧 United Kingdom of Great Britain and Northern Ireland" ]
, Test.test "Typing 3 chars with withMinInputLength (Just 3) does shows items" <| , Test.test "Typing 3 chars with withMinInputLength (Just 3) does shows items" <|
\() -> \() ->
programTestWith (Select.withMinInputLength (Just 3)) programTestWith (Select.withMinInputLength (Just 3))
|> focusInput
|> ProgramTest.fillIn "" "Choose a country" "uni" |> ProgramTest.fillIn "" "Choose a country" "uni"
|> ProgramTest.expectViewHas [ Selector.text "🇬🇧 United Kingdom of Great Britain and Northern Ireland" ] |> 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" <| , Test.test "Typing less than minInputLength does not show no matches even if nothing matched" <|
\() -> \() ->
programTestWith (Select.withMinInputLength (Just 5)) programTestWith (Select.withMinInputLength (Just 5))
|> focusInput
|> ProgramTest.fillIn "" "Choose a country" "zzzz" |> ProgramTest.fillIn "" "Choose a country" "zzzz"
|> ProgramTest.expectViewHasNot [ Selector.text "No matches" ] |> ProgramTest.expectViewHasNot [ Selector.text "No matches" ]
, Test.test "Typing up to the minInputLength shows no matches if nothing matched" <| , Test.test "Typing up to the minInputLength shows no matches if nothing matched" <|
\() -> \() ->
programTestWith (Select.withMinInputLength (Just 3)) programTestWith (Select.withMinInputLength (Just 3))
|> focusInput
|> ProgramTest.fillIn "" "Choose a country" "zzzz" |> ProgramTest.fillIn "" "Choose a country" "zzzz"
|> ProgramTest.expectViewHas [ Selector.text "No matches" ] |> ProgramTest.expectViewHas [ Selector.text "No matches" ]
, Test.test "Choosing an option and then focusing back on the input shows all the options again" <| , Test.test "Choosing an option and then focusing back on the input shows all the options again" <|
\() -> \() ->
programTest programTest
|> focusInput
|> ProgramTest.fillIn "" "Choose a country" "United" |> ProgramTest.fillIn "" "Choose a country" "United"
|> Select.Effect.simulateClickOption simulateInputConfig "country-select" "🇬🇧 United Kingdom of Great Britain and Northern Ireland" |> 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 |> ProgramTest.simulateDomEvent (Query.find [ Selector.id (Select.toInputElementId countrySelect) ]) Test.Html.Event.focus
@ -155,3 +163,8 @@ simulateEffect effect =
, sleep = SimulatedProcess.sleep , sleep = SimulatedProcess.sleep
} }
selectEffect 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) module RequestTest exposing (exampleProgramTest)
import EffectRequestExample as App import EffectRequestExample as App exposing (Cocktail)
import Http import Http
import Json.Decode as Decode import Json.Decode as Decode
import ProgramTest exposing (ProgramTest, SimulatedEffect) import ProgramTest exposing (ProgramTest, SimulatedEffect)
import Select import Select exposing (Select)
import Select.Effect import Select.Effect
import SimulateInput import SimulateInput
import SimulatedEffect.Cmd as SimulatedCmd import SimulatedEffect.Cmd as SimulatedCmd
@ -12,6 +12,7 @@ import SimulatedEffect.Http as SimulateHttp
import SimulatedEffect.Process as SimulatedProcess import SimulatedEffect.Process as SimulatedProcess
import SimulatedEffect.Task as SimulatedTask import SimulatedEffect.Task as SimulatedTask
import Test exposing (Test) import Test exposing (Test)
import Test.Html.Event
import Test.Html.Query as Query exposing (Single) import Test.Html.Query as Query exposing (Single)
import Test.Html.Selector as Selector exposing (Selector) 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" <| [ Test.test "Type in Chocolate, and choose second option with keyboard navigation" <|
\() -> \() ->
programTest programTest
|> focusInput
|> ProgramTest.fillIn "" "Find a cocktail" "Chocolate" |> ProgramTest.fillIn "" "Find a cocktail" "Chocolate"
|> ProgramTest.advanceTime 500 |> ProgramTest.advanceTime 500
|> ProgramTest.simulateHttpOk "GET" |> ProgramTest.simulateHttpOk "GET"
@ -33,6 +35,7 @@ exampleProgramTest =
, Test.test "Type in Chocolate, and choose \"Chocolate Drink\" with mouse click" <| , Test.test "Type in Chocolate, and choose \"Chocolate Drink\" with mouse click" <|
\() -> \() ->
programTest programTest
|> focusInput
|> ProgramTest.fillIn "" "Find a cocktail" "Chocolate" |> ProgramTest.fillIn "" "Find a cocktail" "Chocolate"
|> ProgramTest.advanceTime 300 |> ProgramTest.advanceTime 300
|> ProgramTest.simulateHttpOk "GET" |> 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 : String
cocktailsResponse = cocktailsResponse =
"""{ """{

View File

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

View File

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

View File

@ -85,13 +85,19 @@ update_ { request, requestMinInputLength, debounceRequest, onFocus, onLoseFocus,
, Effect.emitJust onLoseFocus , Effect.emitJust onLoseFocus
) )
GotNewFilteredOptions options ->
( Model.setFilteredOptions options model
, Effect.None
)
MouseEnteredOption i -> MouseEnteredOption i ->
( Model.highlightIndex (Just i) True model ( Model.highlightIndex (Just i) True model
, Effect.none , Effect.none
) )
KeyDown selectOnTab filteredOptions key -> KeyDown selectOnTab filteredOptions key ->
handleKey selectOnTab tagger model key filteredOptions Model.setFilteredOptions filteredOptions model
|> handleKey selectOnTab tagger key filteredOptions
GotContainerAndMenuElements maybeIdx result -> GotContainerAndMenuElements maybeIdx result ->
( model ( 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 : Bool -> (Msg a -> msg) -> String -> List (Option a) -> Model a -> ( Model a, Effect effect msg )
handleKey selectOnTab tagger model key filteredOptions = handleKey selectOnTab tagger key filteredOptions model =
let let
selectHighlighted = selectHighlighted =
case Model.toHighlighted model |> Maybe.andThen (\idx -> getAt idx filteredOptions) of 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 -> List (Option a) -> Model a -> ViewConfigInternal a msg -> Element msg
toElement_ placement filteredOptions model config = toElement_ placement filteredOptions model config =
Element.el Element.el
([ Element.htmlAttribute (Html.Attributes.id <| Model.toContainerElementId model) (List.concat
, Element.width Element.fill [ [ Element.htmlAttribute (Html.Attributes.id <| Model.toContainerElementId model)
, Element.below <| , Element.width Element.fill
if , Element.below <|
List.length filteredOptions if
== 0 List.length filteredOptions
&& Model.isOpen model == 0
&& (String.length (Model.toInputValue model) >= Maybe.withDefault 1 config.minInputLength) && Model.isOpen model
&& (Model.toRequestState model == Nothing || Model.toRequestState model == Just Success) && (String.length (Model.toInputValue model) >= Maybe.withDefault 1 config.minInputLength)
then && (Model.toRequestState model == Nothing || Model.toRequestState model == Just Success)
config.noMatchElement then
config.noMatchElement
else else
Element.none Element.none
, Placement.toAttribute , Placement.toAttribute
(if config.positionFixed then (if config.positionFixed then
Placement.Below Placement.Below
else else
placement 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
) )
{ menuId = Model.toMenuElementId model <|
, toOptionId = Model.toOptionElementId model (if config.positionFixed then
, toOptionState = Model.toOptionState model positionFixedEl placement (Model.toContainerElement 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 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) (inputView filteredOptions model config)
@ -153,10 +161,11 @@ inputView filteredOptions model config =
|> Maybe.andThen (Option.findIndex filteredOptions) |> Maybe.andThen (Option.findIndex filteredOptions)
in in
Input.text Input.text
(config.inputAttribs (List.concat
++ [ Events.onFocus (InputFocused selectedIdx |> config.onChange) [ config.inputAttribs
, Events.onClick (InputClicked selectedIdx |> config.onChange) , [ Events.onFocus (InputFocused selectedIdx |> config.onChange)
, Events.onLoseFocus , Events.onClick (InputClicked selectedIdx |> config.onChange)
, Events.onLoseFocus
(config.onChange (config.onChange
(InputLostFocus (InputLostFocus
{ clearInputValue = config.clearInputValueOnBlur { clearInputValue = config.clearInputValueOnBlur
@ -165,16 +174,17 @@ inputView filteredOptions model config =
filteredOptions filteredOptions
) )
) )
, onKeyDown (Model.isOpen model) (KeyDown config.selectOnTab filteredOptions >> config.onChange) , onKeyDown (Model.isOpen model) (KeyDown config.selectOnTab filteredOptions >> config.onChange)
, Element.htmlAttribute (Html.Attributes.id <| Model.toInputElementId model) , Element.htmlAttribute (Html.Attributes.id <| Model.toInputElementId model)
, Element.inFront <| , Element.inFront <|
if Model.toValue model /= Nothing || Model.toInputValue model /= "" then if Model.toValue model /= Nothing || Model.toInputValue model /= "" then
Maybe.withDefault Element.none config.clearButton Maybe.withDefault Element.none config.clearButton
else else
Element.none Element.none
] ]
++ inputAccessibilityAttributes filteredOptions model , inputAccessibilityAttributes filteredOptions model
]
) )
{ onChange = { onChange =
\v -> \v ->