diff --git a/assets/js/hooks/combobox.js b/assets/js/hooks/combobox.js index 14c436e..22ad5ba 100644 --- a/assets/js/hooks/combobox.js +++ b/assets/js/hooks/combobox.js @@ -442,8 +442,15 @@ export default { if (!this.isOpen) { return } + + const focusedOption = this.getCurrentFocusedOption() + if (e.key === KEYS.TAB && !this.getVisibleOptions().includes(focusedOption)) { + this.handleBlur() + return + } + e.preventDefault() - this.selectOption(this.getCurrentFocusedOption()) + this.selectOption(focusedOption) }, handleBackspace(e) { diff --git a/demo/lib/demo_web/live/fixtures_live/async_combobox_fixture.html.heex b/demo/lib/demo_web/live/fixtures_live/async_combobox_fixture.html.heex index a37dd3a..6ce2e46 100644 --- a/demo/lib/demo_web/live/fixtures_live/async_combobox_fixture.html.heex +++ b/demo/lib/demo_web/live/fixtures_live/async_combobox_fixture.html.heex @@ -1,3 +1,5 @@ + + <.combobox id="demo-async-combobox" name="user[favourite_fruit]"> <.combobox_input on_search="async_combobox_search" @@ -19,3 +21,5 @@ <% end %> + + diff --git a/demo/test/wallaby/demo_web/combobox_keyboard_navigation_test.exs b/demo/test/wallaby/demo_web/combobox_keyboard_navigation_test.exs index d8d6592..2193dfd 100644 --- a/demo/test/wallaby/demo_web/combobox_keyboard_navigation_test.exs +++ b/demo/test/wallaby/demo_web/combobox_keyboard_navigation_test.exs @@ -97,6 +97,42 @@ defmodule DemoWeb.ComboboxKeyboardNavigationTest do |> assert_has(@options_container |> Query.visible(false)) end + for {key, keys, target} <- [ + {"Tab", [:tab], "next-input"}, + {"Shift+Tab", [:shift, :tab], "previous-input"} + ] do + @tag keys: keys, target: target + @feature_name "#{key} leaves an async combobox with no results" + feature @feature_name, %{ + session: session, + keys: keys, + target: target + } do + input = Query.css("#demo-async-combobox input[data-prima-ref=search_input]") + + session + |> visit_fixture("/fixtures/async-combobox", "#demo-async-combobox") + |> click(input) + |> click(Query.css("#demo-async-combobox [role=option][data-value=Cherry]")) + |> fill_in(input, with: "no matching fruit") + |> assert_has(Query.css("#demo-async-combobox-options", visible: true)) + |> assert_has(Query.css("#demo-async-combobox [role=option]", count: 0)) + |> send_keys(keys) + |> assert_has(Query.css("##{target}:focus")) + |> assert_has(Query.css("#demo-async-combobox-options", visible: false)) + |> execute_script( + """ + const root = document.querySelector('#demo-async-combobox'); + return { + search: root.querySelector('[data-prima-ref=search_input]').value, + selection: root.querySelector('[data-prima-ref=submit_input]').value + }; + """, + fn values -> assert values == %{"search" => "Cherry", "selection" => "Cherry"} end + ) + end + end + feature "ArrowDown opens combobox when closed and input is focused", %{session: session} do session |> visit_fixture("/fixtures/simple-combobox", "#demo-combobox")