Skip to content

Commit bdb4047

Browse files
committed
Enhance the search combobox open/close behavior
This changeset improves the interactions for openingn and closing the search combobox within the dialog. - Adds open/close methods to change a new stimulus value "expandedValue" - On change of "expandedValue", the aria-expanded value is changed and one of expand/collapse is called - expand/collapse displays or hides the listbox, respectively - adds cross-controller communication so when the dialog is opened or closed, the combobox open or close methods are called. More: Some browsers clear the focused search element when the ESC key is pressed. now the search element won’t be cleared by the dialog will close. Now, when the dialog is closed, the current query is retained, so when the dialog opens again, the search query is still present. I kind of like this behavior, but it may not be the behavior some users expect. Let’s see, maybe no one cares enough.
1 parent a6ea836 commit bdb4047

File tree

5 files changed

+108
-16
lines changed

5 files changed

+108
-16
lines changed

app/javascript/controllers/dialog.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,13 @@ export default class extends Controller {
3232
open() {
3333
console.log('Opening dialog');
3434
this.element.showModal();
35+
this.dispatch('open');
3536
}
3637

3738
close() {
3839
console.log('Closing dialog');
3940
this.element.close();
41+
this.dispatch('close');
4042
}
4143

4244
tryClose(event) {

app/javascript/controllers/searches/combobox.js

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,26 @@ function cyclingValueAt(array, index) {
2020
}
2121

2222
export default class extends Controller {
23+
static values = {
24+
expanded: Boolean,
25+
};
26+
2327
connect() {
2428
console.log('Connected');
29+
30+
this.tryOpen();
31+
}
32+
33+
// Many Comboboxes act like fancy select inputs for forms. The Search Combobox
34+
// is currently hard-coded for for navigation: when an option is selected and
35+
// enabled with Enter key, we expect it to have a anchor tag and follow its link.
36+
go(event) {
37+
let anchor = this.selectedItem?.querySelector('a');
38+
39+
if (anchor) {
40+
this.close();
41+
anchor.click();
42+
}
2543
}
2644

2745
selectIndex(index) {
@@ -55,13 +73,61 @@ export default class extends Controller {
5573
this.combobox.setAttribute('aria-activedescendant', id);
5674
}
5775

58-
listboxOpen({ detail }) {
59-
console.log('Listbox open', this.options.length > 0);
60-
this.combobox.setAttribute('aria-expanded', this.options.length > 0);
76+
expandedValueChanged() {
77+
console.log('Expanded value changed', this.expandedValue);
78+
if (this.expandedValue) {
79+
this.expand();
80+
} else {
81+
this.collapse();
82+
}
6183
}
6284

63-
navigate(event) {
64-
this.navigationKeyHandlers[event.key]?.call(this, event);
85+
tryOpen() {
86+
if (this.options.length > 0) {
87+
this.open();
88+
} else {
89+
this.close();
90+
}
91+
}
92+
93+
open() {
94+
this.expandedValue = true;
95+
}
96+
97+
expand() {
98+
this.listbox.classList.remove('hidden');
99+
this.combobox.setAttribute('aria-expanded', true);
100+
}
101+
102+
close() {
103+
this.expandedValue = false;
104+
}
105+
106+
openInTarget(event) {
107+
if (!event.target) return;
108+
109+
if (event.target.contains(this.element)) {
110+
this.tryOpen();
111+
}
112+
}
113+
114+
closeInTarget(event) {
115+
if (!event.target) return;
116+
117+
if (event.target.contains(this.element)) {
118+
this.close();
119+
}
120+
}
121+
122+
collapse() {
123+
this.combobox.setAttribute('aria-expanded', false);
124+
this.listbox.classList.add('hidden');
125+
}
126+
127+
closeAndBlur() {
128+
this.close();
129+
this.combobox.blur();
130+
this.dispatch('close');
65131
}
66132

67133
get options() {
@@ -84,6 +150,10 @@ export default class extends Controller {
84150
return this.element.querySelector('[role="listbox"]');
85151
}
86152

153+
navigate(event) {
154+
this.navigationKeyHandlers[event.key]?.call(this, event);
155+
}
156+
87157
get navigationKeyHandlers() {
88158
return {
89159
ArrowDown: (event) => {
@@ -103,9 +173,13 @@ export default class extends Controller {
103173
cancel(event);
104174
},
105175
Enter: (event) => {
106-
this.selectedItem?.querySelector('a')?.click();
176+
this.go(event);
107177
cancel(event);
108178
},
179+
Escape: (event) => {
180+
this.closeAndBlur();
181+
cancel(event); // Prevent ESC from clearing the search input as is the behavior in some browsers
182+
},
109183
};
110184
}
111185
}

app/views/searches/combobox.rb

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ def view_template
1818
controller: "search-combobox",
1919
action: "
2020
keydown->search-combobox#navigate
21-
search-listbox:connected->search-combobox#listboxOpen
21+
search-listbox:connected->search-combobox#tryOpen
22+
dialog:close@window->search-combobox#closeInTarget
23+
dialog:open@window->search-combobox#openInTarget
2224
"
2325
}
2426
) do
@@ -38,14 +40,12 @@ def view_template
3840
value: query,
3941
autofocus: true,
4042
role: "combobox",
41-
aria: {
42-
expanded: false,
43-
autocomplete: "none",
44-
controls: "search-listbox",
45-
activedescendant: nil
46-
},
43+
aria: input_aria,
4744
data: {
48-
action: "autosubmit-form#submit"
45+
action: "
46+
focus->combobox#tryOpen
47+
input->autosubmit-form#submit
48+
"
4949
},
5050
placeholder: "Search Joy of Rails",
5151
class: "w-full step-1"
@@ -55,5 +55,20 @@ def view_template
5555
render Searches::Listbox.new(pages: pages, query: query)
5656
end
5757
end
58+
59+
def input_aria
60+
{
61+
expanded: false,
62+
autocomplete: "none",
63+
controls: "search-listbox",
64+
owns: "search-listbox",
65+
haspopup: "listbox",
66+
activedescendant: ""
67+
}
68+
end
69+
70+
def listbox_id
71+
"search-listbox"
72+
end
5873
end
5974
end

app/views/searches/dialog.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ class Dialog < ApplicationComponent
55
def view_template
66
render ::Dialog::Layout.new(
77
id: "search-dialog",
8-
"aria-label": "Search",
8+
aria: {label: "Search Dialog"},
99
class: "max-w-xl p-2 mt-32 mx-auto",
1010
data: {
1111
controller: "dialog",
1212
action: "
13+
search-combobox:close->dialog#close
1314
keydown.meta+k@window->dialog#open
1415
click->dialog#tryClose
1516
"

app/views/searches/listbox.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def view_template
1313
ul(**mix(
1414
id: "search-listbox",
1515
role: "listbox",
16-
class: ["grid", ("hidden" unless pages.any? || query_long_enough?)],
16+
class: ["grid"],
1717
data: {
1818
controller: "search-listbox"
1919
}

0 commit comments

Comments
 (0)