diff --git a/assets/js/hooks/combobox.js b/assets/js/hooks/combobox.js index 14c436e..9b213c0 100644 --- a/assets/js/hooks/combobox.js +++ b/assets/js/hooks/combobox.js @@ -165,11 +165,7 @@ export default { } const focusedOption = options.find(option => option.dataset.value === this.focusedOptionBeforeUpdate) || options[0] - if (focusedOption) { - this.setFocus(focusedOption) - } else { - this.js().removeAttribute(this.refs.searchInput, 'aria-activedescendant') - } + this.setFocus(focusedOption) }, sendQuery(query) { @@ -263,12 +259,18 @@ export default { this.notifyFormChange() }, - setFocus(el) { + clearFocus() { const focused = this.getCurrentFocusedOption() if (focused) this.js().removeAttribute(focused, 'data-focus') + this.js().removeAttribute(this.refs.searchInput, 'aria-activedescendant') + }, + + setFocus(el) { + this.clearFocus() + if (!el) return + this.js().setAttribute(el, 'data-focus', 'true') - // Update aria-activedescendant to point to the focused option if (el.id) { this.js().setAttribute(this.refs.searchInput, 'aria-activedescendant', el.id) } @@ -278,11 +280,7 @@ export default { focusFirstOption() { const firstOption = this.refs.optionsContainer?.querySelector(SELECTORS.VISIBLE_OPTION) - if (firstOption) { - this.setFocus(firstOption) - } else { - this.js().removeAttribute(this.refs.searchInput, 'aria-activedescendant') - } + this.setFocus(firstOption) }, getCurrentFocusedOption() { @@ -324,7 +322,7 @@ export default { }, selectOption(el) { - if (!el) return + if (!el || el.hasAttribute('data-hidden')) return let value = el.getAttribute('data-value') let displayValue = el.getAttribute('data-display') @@ -611,11 +609,10 @@ export default { close() { clearTimeout(this.searchTimer) - this.js().removeAttribute(this.refs.searchInput, 'aria-activedescendant') + this.clearFocus() if (this.refs.createOption) { this.hideOption(this.refs.createOption) this.refs.createOption.textContent = '' - this.js().removeAttribute(this.refs.createOption, 'data-focus') } if (!this.refs.optionsContainer || !this.isOpen) return 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..5ac4a2b 100644 --- a/demo/test/wallaby/demo_web/combobox_keyboard_navigation_test.exs +++ b/demo/test/wallaby/demo_web/combobox_keyboard_navigation_test.exs @@ -57,6 +57,26 @@ defmodule DemoWeb.ComboboxKeyboardNavigationTest do |> assert_combobox_selection("#demo-combobox", "demo-combobox[fruit]", "Pear", "Pear") end + for key <- [:enter, :tab] do + @selection_key key + + feature "#{@selection_key} ignores options hidden by filtering", %{session: session} do + session + |> visit_fixture("/fixtures/simple-combobox", "#demo-combobox") + |> click(@search_input) + |> assert_has(Query.css("#demo-combobox [data-value='Apple'][data-focus=true]")) + |> fill_in(@search_input, with: "zzzzzz") + |> assert_has(Query.css("#demo-combobox [role=option]:not([data-hidden])", count: 0)) + |> send_keys([@selection_key]) + |> assert_combobox_selection("#demo-combobox", "demo-combobox[fruit]", "zzzzzz", nil) + |> assert_has(Query.css("#demo-combobox [data-focus=true]", count: 0, visible: :any)) + |> assert_has(Query.css("#demo-combobox input:not([aria-activedescendant])")) + |> fill_in(@search_input, with: "Pear") + |> send_keys([:down_arrow, :enter]) + |> assert_combobox_selection("#demo-combobox", "demo-combobox[fruit]", "Pear", "Pear") + end + end + feature "selects focused option with Tab key", %{session: session} do session |> visit_fixture("/fixtures/simple-combobox", "#demo-combobox")