hotwire_combobox 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/MIT-LICENSE +1 -1
- data/README.md +19 -15
- data/app/assets/config/hw_combobox_manifest.js +1 -1
- data/app/assets/javascripts/controllers/hw_combobox_controller.js +34 -18
- data/app/assets/javascripts/hotwire_combobox.esm.js +147 -72
- data/app/assets/javascripts/hw_combobox/helpers.js +9 -0
- data/app/assets/javascripts/hw_combobox/models/combobox/autocomplete.js +14 -4
- data/app/assets/javascripts/hw_combobox/models/combobox/callbacks.js +46 -0
- data/app/assets/javascripts/hw_combobox/models/combobox/dialog.js +1 -4
- data/app/assets/javascripts/hw_combobox/models/combobox/filtering.js +19 -8
- data/app/assets/javascripts/hw_combobox/models/combobox/form_field.js +1 -2
- data/app/assets/javascripts/hw_combobox/models/combobox/multiselect.js +6 -3
- data/app/assets/javascripts/hw_combobox/models/combobox/navigation.js +2 -2
- data/app/assets/javascripts/hw_combobox/models/combobox/selection.js +4 -12
- data/app/assets/javascripts/hw_combobox/models/combobox/toggle.js +13 -18
- data/app/assets/javascripts/hw_combobox/models/combobox.js +1 -0
- data/app/assets/stylesheets/hotwire_combobox.css +13 -3
- data/app/presenters/hotwire_combobox/component/announced.rb +5 -0
- data/app/presenters/hotwire_combobox/component/associations.rb +18 -0
- data/app/presenters/hotwire_combobox/component/async.rb +6 -0
- data/app/presenters/hotwire_combobox/component/customizable.rb +8 -20
- data/app/presenters/hotwire_combobox/component/freetext.rb +26 -0
- data/app/presenters/hotwire_combobox/component/markup/dialog.rb +57 -0
- data/app/presenters/hotwire_combobox/component/markup/fieldset.rb +46 -0
- data/app/presenters/hotwire_combobox/component/markup/form.rb +6 -0
- data/app/presenters/hotwire_combobox/component/markup/handle.rb +7 -0
- data/app/presenters/hotwire_combobox/component/markup/hidden_field.rb +28 -0
- data/app/presenters/hotwire_combobox/component/markup/input.rb +44 -0
- data/app/presenters/hotwire_combobox/component/markup/label.rb +5 -0
- data/app/presenters/hotwire_combobox/component/markup/listbox.rb +14 -0
- data/app/presenters/hotwire_combobox/component/markup/wrapper.rb +7 -0
- data/app/presenters/hotwire_combobox/component/multiselect.rb +6 -0
- data/app/presenters/hotwire_combobox/component/paginated.rb +18 -0
- data/app/presenters/hotwire_combobox/component.rb +32 -398
- data/app/presenters/hotwire_combobox/listbox/group.rb +3 -15
- data/app/presenters/hotwire_combobox/listbox/item.rb +5 -12
- data/app/presenters/hotwire_combobox/listbox/option.rb +3 -19
- data/app/views/hotwire_combobox/_component.html.erb +28 -6
- data/app/views/hotwire_combobox/_pagination.html.erb +3 -3
- data/config/hw_importmap.rb +1 -1
- data/lib/hotwire_combobox/helper.rb +24 -65
- data/lib/hotwire_combobox/platform.rb +15 -0
- data/lib/hotwire_combobox/version.rb +1 -1
- data/lib/hotwire_combobox.rb +1 -0
- metadata +34 -11
- data/app/assets/javascripts/hotwire_combobox.umd.js +0 -1769
- data/app/views/hotwire_combobox/combobox/_dialog.html.erb +0 -9
- data/app/views/hotwire_combobox/combobox/_hidden_field.html.erb +0 -4
- data/app/views/hotwire_combobox/combobox/_input.html.erb +0 -2
- data/app/views/hotwire_combobox/combobox/_paginated_listbox.html.erb +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 01df7da888bb6651172b141161fcf58610b9c1e1e28e8f20a3bee7d880a9f6cb
|
4
|
+
data.tar.gz: 3ae59ccce1a2782b49759d2797244d8b8084996a8f115b46ac32f10fc22eab39
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 11e979427daf0c338006be0ab69732fcd4ca7f4860319b54e003730341419b1845069638acab2adaae1c45ecc961cc5216c30791e567cb0abb1dd6fc326ad794
|
7
|
+
data.tar.gz: 9f05dafd9e46f000641a86f11d9d223df4264d4f53b131f6f9be347bdb8356e4800eae22719d99c4b20bb665ce437efac27941ada324e725087801cd6fa5414e
|
data/MIT-LICENSE
CHANGED
data/README.md
CHANGED
@@ -24,7 +24,7 @@ Finally, configure your assets:
|
|
24
24
|
|
25
25
|
### Configuring JS
|
26
26
|
|
27
|
-
Before continuing, you should know whether your app is using importmaps or JS bundling
|
27
|
+
Before continuing, you should know whether your app is using importmaps or JS bundling.
|
28
28
|
|
29
29
|
#### Importmaps
|
30
30
|
|
@@ -35,28 +35,30 @@ In `app/javascript/controllers/index.js` you should have one of the following:
|
|
35
35
|
Either,
|
36
36
|
|
37
37
|
```js
|
38
|
-
import { application } from "controllers/application"
|
38
|
+
import { application } from "controllers/application"
|
39
39
|
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
|
40
|
-
|
41
40
|
eagerLoadControllersFrom("controllers", application)
|
42
41
|
```
|
43
42
|
|
44
43
|
Or,
|
45
44
|
|
46
45
|
```js
|
47
|
-
import { application } from "controllers/application"
|
46
|
+
import { application } from "controllers/application"
|
48
47
|
import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
|
49
|
-
|
50
48
|
lazyLoadControllersFrom("controllers", application)
|
51
49
|
```
|
52
50
|
|
53
|
-
|
51
|
+
Alternatively, modify `app/javascript/controllers/application.js` as follows:
|
54
52
|
|
55
53
|
```js
|
56
|
-
import {
|
54
|
+
import { Application } from "@hotwired/stimulus"
|
55
|
+
const application = Application.start()
|
57
56
|
|
57
|
+
// Add the following two lines:
|
58
58
|
import HwComboboxController from "controllers/hw_combobox_controller"
|
59
59
|
application.register("hw-combobox", HwComboboxController)
|
60
|
+
|
61
|
+
export { application }
|
60
62
|
```
|
61
63
|
|
62
64
|
#### JS bundling (esbuild, rollup, etc)
|
@@ -71,13 +73,17 @@ yarn add @josefarias/hotwire_combobox
|
|
71
73
|
npm install @josefarias/hotwire_combobox
|
72
74
|
```
|
73
75
|
|
74
|
-
Then, register the library's stimulus controller in `app/javascript/controllers/
|
76
|
+
Then, register the library's stimulus controller in `app/javascript/controllers/application.js` as follows:
|
75
77
|
|
76
78
|
```js
|
77
|
-
import {
|
79
|
+
import { Application } from "@hotwired/stimulus"
|
80
|
+
const application = Application.start()
|
78
81
|
|
82
|
+
// Add the following two lines:
|
79
83
|
import HwComboboxController from "@josefarias/hotwire_combobox"
|
80
84
|
application.register("hw-combobox", HwComboboxController)
|
85
|
+
|
86
|
+
export { application }
|
81
87
|
```
|
82
88
|
|
83
89
|
> [!WARNING]
|
@@ -85,7 +91,7 @@ application.register("hw-combobox", HwComboboxController)
|
|
85
91
|
|
86
92
|
### Configuring CSS
|
87
93
|
|
88
|
-
This library comes with
|
94
|
+
This library comes with customizable default styles. Follow the instructions below to include them in your app.
|
89
95
|
|
90
96
|
Read the [docs section](#Docs) for instructions on styling the combobox yourself.
|
91
97
|
|
@@ -118,11 +124,9 @@ This gem follows the [APG combobox pattern guidelines](https://www.w3.org/WAI/AR
|
|
118
124
|
|
119
125
|
These are the exceptions:
|
120
126
|
|
121
|
-
1.
|
122
|
-
2.
|
123
|
-
3.
|
124
|
-
4. It is possible to have an unlabled combobox, as that responsibility is delegated to the implementing user.
|
125
|
-
5. There are currently [no APG guidelines](https://github.com/w3c/aria-practices/issues/1512) for a multiselect combobox. We've introduced some mechanisms to make the experience accessible, like announcing multi-selections via a live region. But we'd welcome feedback on how to make it better until official guidelines are available.
|
127
|
+
1. The listbox has wrap-around selection. That is, pressing `Up Arrow` when the user is on the first option will select the last option. And pressing `Down Arrow` when on the last option will select the first option. In paginated comboboxes, the first and last options refer to the currently available options. More options may be loaded after navigating to the last currently available option.
|
128
|
+
2. It is possible to have an unlabeled combobox, as that responsibility is delegated to the implementing user.
|
129
|
+
3. There are currently [no APG guidelines](https://github.com/w3c/aria-practices/issues/1512) for a multiselect combobox. We've introduced some mechanisms to make the experience accessible, like announcing multi-selections via a live region. But we'd welcome feedback on how to make it better until official guidelines are available.
|
126
130
|
|
127
131
|
It should be noted none of the maintainers use assistive technologies in their daily lives. If you do, and you feel these exceptions are detrimental to your ability to use the component, or if you find an undocumented exception, please [open a GitHub issue](https://github.com/josefarias/hotwire_combobox/issues). We'll get it sorted.
|
128
132
|
|
@@ -1,2 +1,2 @@
|
|
1
1
|
//= link_directory ../stylesheets .css
|
2
|
-
//=
|
2
|
+
//= link hotwire_combobox.esm.js
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import Combobox from "hw_combobox/models/combobox"
|
2
2
|
import { Concerns, sleep } from "hw_combobox/helpers"
|
3
|
+
import { nextRepaint } from "hw_combobox/helpers"
|
3
4
|
import { Controller } from "@hotwired/stimulus"
|
4
5
|
|
5
6
|
window.HOTWIRE_COMBOBOX_STREAM_DELAY = 0 // ms, for testing purposes
|
@@ -10,6 +11,7 @@ const concerns = [
|
|
10
11
|
Combobox.Announcements,
|
11
12
|
Combobox.AsyncLoading,
|
12
13
|
Combobox.Autocomplete,
|
14
|
+
Combobox.Callbacks,
|
13
15
|
Combobox.Dialog,
|
14
16
|
Combobox.Events,
|
15
17
|
Combobox.Filtering,
|
@@ -24,20 +26,13 @@ const concerns = [
|
|
24
26
|
]
|
25
27
|
|
26
28
|
export default class HwComboboxController extends Concerns(...concerns) {
|
27
|
-
static classes = [
|
28
|
-
"invalid",
|
29
|
-
"selected"
|
30
|
-
]
|
31
|
-
|
29
|
+
static classes = [ "invalid", "selected" ]
|
32
30
|
static targets = [
|
33
31
|
"announcer",
|
34
32
|
"combobox",
|
35
33
|
"chipDismisser",
|
36
34
|
"closer",
|
37
|
-
"dialog",
|
38
|
-
"dialogCombobox",
|
39
|
-
"dialogFocusTrap",
|
40
|
-
"dialogListbox",
|
35
|
+
"dialog", "dialogCombobox", "dialogFocusTrap", "dialogListbox",
|
41
36
|
"endOfOptionsStream",
|
42
37
|
"handle",
|
43
38
|
"hiddenField",
|
@@ -61,6 +56,7 @@ export default class HwComboboxController extends Concerns(...concerns) {
|
|
61
56
|
initialize() {
|
62
57
|
this._initializeActors()
|
63
58
|
this._initializeFiltering()
|
59
|
+
this._initializeCallbacks()
|
64
60
|
}
|
65
61
|
|
66
62
|
connect() {
|
@@ -87,23 +83,43 @@ export default class HwComboboxController extends Concerns(...concerns) {
|
|
87
83
|
}
|
88
84
|
|
89
85
|
async endOfOptionsStreamTargetConnected(element) {
|
90
|
-
|
91
|
-
|
86
|
+
if (element.dataset.callbackId) {
|
87
|
+
this._runCallback(element)
|
88
|
+
} else {
|
89
|
+
this._preselectSingle()
|
90
|
+
}
|
91
|
+
}
|
92
92
|
|
93
|
-
|
93
|
+
async _runCallback(element) {
|
94
|
+
const callbackId = element.dataset.callbackId
|
95
|
+
|
96
|
+
if (this._callbackAttemptsExceeded(callbackId)) {
|
97
|
+
return this._dequeueCallback(callbackId)
|
98
|
+
} else {
|
99
|
+
this._recordCallbackAttempt(callbackId)
|
100
|
+
}
|
101
|
+
|
102
|
+
if (this._isNextCallback(callbackId)) {
|
103
|
+
const inputType = element.dataset.inputType
|
104
|
+
const delay = window.HOTWIRE_COMBOBOX_STREAM_DELAY
|
94
105
|
|
95
|
-
if (inputType === "hw:multiselectSync") {
|
96
|
-
this.openByFocusing()
|
97
|
-
} else if (inputType && inputType !== "hw:lockInSelection") {
|
98
106
|
if (delay) await sleep(delay)
|
99
|
-
this.
|
107
|
+
this._dequeueCallback(callbackId)
|
108
|
+
this._resetMultiselectionMarks()
|
109
|
+
|
110
|
+
if (inputType === "hw:multiselectSync") {
|
111
|
+
this.open()
|
112
|
+
} else if (inputType !== "hw:lockInSelection") {
|
113
|
+
this._selectOnQuery(inputType)
|
114
|
+
}
|
100
115
|
} else {
|
101
|
-
|
116
|
+
await nextRepaint()
|
117
|
+
this._runCallback(element)
|
102
118
|
}
|
103
119
|
}
|
104
120
|
|
105
121
|
closerTargetConnected() {
|
106
|
-
this.
|
122
|
+
this.close("hw:asyncCloser")
|
107
123
|
}
|
108
124
|
|
109
125
|
// Use +_printStack+ for debugging purposes
|
@@ -1,5 +1,5 @@
|
|
1
1
|
/*!
|
2
|
-
HotwireCombobox 0.
|
2
|
+
HotwireCombobox 0.4.0
|
3
3
|
*/
|
4
4
|
import { Controller } from '@hotwired/stimulus';
|
5
5
|
|
@@ -150,6 +150,15 @@ function nextEventLoopTick() {
|
|
150
150
|
return new Promise((resolve) => setTimeout(() => resolve(), 0))
|
151
151
|
}
|
152
152
|
|
153
|
+
function randomUUID() {
|
154
|
+
const uuidPattern = "10000000-1000-4000-8000-100000000000";
|
155
|
+
|
156
|
+
return uuidPattern.replace(/[018]/g, (match) => {
|
157
|
+
const randomByte = crypto.getRandomValues(new Uint8Array(1))[0];
|
158
|
+
return (match ^ (randomByte & 15) >> (match / 4)).toString(16)
|
159
|
+
})
|
160
|
+
}
|
161
|
+
|
153
162
|
Combobox.Autocomplete = Base => class extends Base {
|
154
163
|
_connectListAutocomplete() {
|
155
164
|
if (!this._autocompletesList) {
|
@@ -157,23 +166,33 @@ Combobox.Autocomplete = Base => class extends Base {
|
|
157
166
|
}
|
158
167
|
}
|
159
168
|
|
160
|
-
|
169
|
+
_hardAutocomplete(option) {
|
170
|
+
const typedValue = this._typedQuery;
|
161
171
|
const autocompletedValue = option.getAttribute(this.autocompletableAttributeValue);
|
162
172
|
|
163
173
|
this._fullQuery = autocompletedValue;
|
164
|
-
|
174
|
+
|
175
|
+
if (this._isAutocompletableWith(typedValue, autocompletedValue)) {
|
176
|
+
this._actingCombobox.setSelectionRange(typedValue.length, autocompletedValue.length);
|
177
|
+
} else {
|
178
|
+
this._actingCombobox.setSelectionRange(autocompletedValue.length, autocompletedValue.length);
|
179
|
+
}
|
165
180
|
}
|
166
181
|
|
167
|
-
|
182
|
+
_softAutocomplete(option) {
|
168
183
|
const typedValue = this._typedQuery;
|
169
184
|
const autocompletedValue = option.getAttribute(this.autocompletableAttributeValue);
|
170
185
|
|
171
|
-
if (this.
|
186
|
+
if (this._isAutocompletableWith(typedValue, autocompletedValue)) {
|
172
187
|
this._fullQuery = autocompletedValue;
|
173
188
|
this._actingCombobox.setSelectionRange(typedValue.length, autocompletedValue.length);
|
174
189
|
}
|
175
190
|
}
|
176
191
|
|
192
|
+
_isAutocompletableWith(typedValue, autocompletedValue) {
|
193
|
+
return this._autocompletesInline && startsWith(autocompletedValue, typedValue)
|
194
|
+
}
|
195
|
+
|
177
196
|
// +visuallyHideListbox+ hides the listbox from the user,
|
178
197
|
// but makes it still searchable by JS.
|
179
198
|
_visuallyHideListbox() {
|
@@ -204,6 +223,50 @@ Combobox.Autocomplete = Base => class extends Base {
|
|
204
223
|
}
|
205
224
|
};
|
206
225
|
|
226
|
+
const MAX_CALLBACK_ATTEMPTS = 3;
|
227
|
+
|
228
|
+
Combobox.Callbacks = Base => class extends Base {
|
229
|
+
_initializeCallbacks() {
|
230
|
+
this.callbackQueue = [];
|
231
|
+
this.callbackExecutionAttempts = {};
|
232
|
+
}
|
233
|
+
|
234
|
+
_enqueueCallback() {
|
235
|
+
const callbackId = randomUUID();
|
236
|
+
this.callbackQueue.push(callbackId);
|
237
|
+
return callbackId
|
238
|
+
}
|
239
|
+
|
240
|
+
_isNextCallback(callbackId) {
|
241
|
+
return this._nextCallback === callbackId
|
242
|
+
}
|
243
|
+
|
244
|
+
_callbackAttemptsExceeded(callbackId) {
|
245
|
+
return this._callbackAttempts(callbackId) > MAX_CALLBACK_ATTEMPTS
|
246
|
+
}
|
247
|
+
|
248
|
+
_callbackAttempts(callbackId) {
|
249
|
+
return this.callbackExecutionAttempts[callbackId] || 0
|
250
|
+
}
|
251
|
+
|
252
|
+
_recordCallbackAttempt(callbackId) {
|
253
|
+
this.callbackExecutionAttempts[callbackId] = this._callbackAttempts(callbackId) + 1;
|
254
|
+
}
|
255
|
+
|
256
|
+
_dequeueCallback(callbackId) {
|
257
|
+
this.callbackQueue = this.callbackQueue.filter(id => id !== callbackId);
|
258
|
+
this._forgetCallbackExecutionAttempts(callbackId);
|
259
|
+
}
|
260
|
+
|
261
|
+
_forgetCallbackExecutionAttempts(callbackId) {
|
262
|
+
delete this.callbackExecutionAttempts[callbackId];
|
263
|
+
}
|
264
|
+
|
265
|
+
get _nextCallback() {
|
266
|
+
return this.callbackQueue[0]
|
267
|
+
}
|
268
|
+
};
|
269
|
+
|
207
270
|
Combobox.Dialog = Base => class extends Base {
|
208
271
|
rerouteListboxStreamToDialog({ detail: { newStream } }) {
|
209
272
|
if (newStream.target == this.listboxTarget.id && this._dialogIsOpen) {
|
@@ -243,10 +306,7 @@ Combobox.Dialog = Base => class extends Base {
|
|
243
306
|
|
244
307
|
_resizeDialog = () => {
|
245
308
|
if (window.visualViewport) {
|
246
|
-
this.dialogTarget.style.setProperty(
|
247
|
-
"--hw-visual-viewport-height",
|
248
|
-
`${window.visualViewport.height}px`
|
249
|
-
);
|
309
|
+
this.dialogTarget.style.setProperty("--hw-visual-viewport-height", `${window.visualViewport.height}px`);
|
250
310
|
}
|
251
311
|
}
|
252
312
|
|
@@ -584,6 +644,15 @@ async function post(url, options) {
|
|
584
644
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
585
645
|
|
586
646
|
Combobox.Filtering = Base => class extends Base {
|
647
|
+
prepareToFilter({ key }) {
|
648
|
+
const intendsToFilter = key.match(/^[a-zA-Z0-9]$|^ArrowDown$/);
|
649
|
+
|
650
|
+
if (this._isClosed && intendsToFilter) {
|
651
|
+
this.open(); // `.open()` sets the appropriate state so the combobox knows it’s open.
|
652
|
+
this._expand(); // `.open()` will call `._expand()` via stimulus callbacks, but we’re calling it inline so it happens immediately.
|
653
|
+
}
|
654
|
+
}
|
655
|
+
|
587
656
|
filterAndSelect({ inputType }) {
|
588
657
|
this._filter(inputType);
|
589
658
|
|
@@ -592,6 +661,12 @@ Combobox.Filtering = Base => class extends Base {
|
|
592
661
|
}
|
593
662
|
}
|
594
663
|
|
664
|
+
clear(event) {
|
665
|
+
this._clearQuery();
|
666
|
+
this.chipDismisserTargets.forEach(el => el.click());
|
667
|
+
if (event && !event.defaultPrevented) event.target.focus();
|
668
|
+
}
|
669
|
+
|
595
670
|
_initializeFiltering() {
|
596
671
|
this._debouncedFilterAsync = debounce(this._debouncedFilterAsync.bind(this));
|
597
672
|
}
|
@@ -614,19 +689,15 @@ Combobox.Filtering = Base => class extends Base {
|
|
614
689
|
const query = {
|
615
690
|
q: this._fullQuery,
|
616
691
|
input_type: inputType,
|
617
|
-
for_id: this.element.dataset.asyncId
|
692
|
+
for_id: this.element.dataset.asyncId,
|
693
|
+
callback_id: this._enqueueCallback()
|
618
694
|
};
|
619
695
|
|
620
696
|
await get(this.asyncSrcValue, { responseKind: "turbo-stream", query });
|
621
697
|
}
|
622
698
|
|
623
699
|
_filterSync() {
|
624
|
-
this._allFilterableOptionElements.forEach(
|
625
|
-
applyFilter(
|
626
|
-
this._fullQuery,
|
627
|
-
{ matching: this.filterableAttributeValue }
|
628
|
-
)
|
629
|
-
);
|
700
|
+
this._allFilterableOptionElements.forEach(applyFilter(this._fullQuery, { matching: this.filterableAttributeValue }));
|
630
701
|
}
|
631
702
|
|
632
703
|
_clearQuery() {
|
@@ -635,7 +706,7 @@ Combobox.Filtering = Base => class extends Base {
|
|
635
706
|
}
|
636
707
|
|
637
708
|
_markQueried() {
|
638
|
-
this.
|
709
|
+
this._forAllComboboxes(el => el.toggleAttribute("data-queried", this._isQueried));
|
639
710
|
}
|
640
711
|
|
641
712
|
get _isQueried() {
|
@@ -708,8 +779,7 @@ Combobox.FormField = Base => class extends Base {
|
|
708
779
|
|
709
780
|
get _hasEmptyFieldValue() {
|
710
781
|
if (this._isMultiselect) {
|
711
|
-
return this.hiddenFieldTarget.dataset.valueForMultiselect == "" ||
|
712
|
-
this.hiddenFieldTarget.dataset.valueForMultiselect == "undefined"
|
782
|
+
return this.hiddenFieldTarget.dataset.valueForMultiselect == "" || this.hiddenFieldTarget.dataset.valueForMultiselect == "undefined"
|
713
783
|
} else {
|
714
784
|
return this.hiddenFieldTarget.value === ""
|
715
785
|
}
|
@@ -751,7 +821,7 @@ Combobox.Multiselect = Base => class extends Base {
|
|
751
821
|
currentTarget.closest("[data-hw-combobox-chip]").remove();
|
752
822
|
|
753
823
|
if (!this._isSmallViewport) {
|
754
|
-
this.
|
824
|
+
this.open();
|
755
825
|
}
|
756
826
|
|
757
827
|
this._announceToScreenReader(display, "removed");
|
@@ -776,7 +846,7 @@ Combobox.Multiselect = Base => class extends Base {
|
|
776
846
|
cancel(event);
|
777
847
|
},
|
778
848
|
Escape: (event) => {
|
779
|
-
this.
|
849
|
+
this.open();
|
780
850
|
cancel(event);
|
781
851
|
}
|
782
852
|
}
|
@@ -793,13 +863,16 @@ Combobox.Multiselect = Base => class extends Base {
|
|
793
863
|
|
794
864
|
this._beforeClearingMultiselectQuery(async (display, value) => {
|
795
865
|
this._fullQuery = "";
|
866
|
+
|
796
867
|
this._filter("hw:multiselectSync");
|
797
868
|
this._requestChips(value);
|
798
869
|
this._addToFieldValue(value);
|
870
|
+
|
799
871
|
if (shouldReopen) {
|
800
872
|
await nextRepaint();
|
801
|
-
this.
|
873
|
+
this.open();
|
802
874
|
}
|
875
|
+
|
803
876
|
this._announceToScreenReader(display, "multi-selected. Press Shift + Tab, then Enter to remove.");
|
804
877
|
});
|
805
878
|
}
|
@@ -915,11 +988,11 @@ Combobox.Navigation = Base => class extends Base {
|
|
915
988
|
cancel(event);
|
916
989
|
},
|
917
990
|
Enter: (event) => {
|
918
|
-
this.
|
991
|
+
this.close("hw:keyHandler:enter");
|
919
992
|
cancel(event);
|
920
993
|
},
|
921
994
|
Escape: (event) => {
|
922
|
-
this.
|
995
|
+
this._isOpen ? this.close("hw:keyHandler:escape") : this._clearQuery();
|
923
996
|
cancel(event);
|
924
997
|
},
|
925
998
|
Backspace: (event) => {
|
@@ -1028,7 +1101,7 @@ Combobox.Options = Base => class extends Base {
|
|
1028
1101
|
Combobox.Selection = Base => class extends Base {
|
1029
1102
|
selectOnClick({ currentTarget, inputType }) {
|
1030
1103
|
this._forceSelectionAndFilter(currentTarget, inputType);
|
1031
|
-
this.
|
1104
|
+
this.close("hw:optionRoleClick");
|
1032
1105
|
}
|
1033
1106
|
|
1034
1107
|
_connectSelection() {
|
@@ -1044,9 +1117,9 @@ Combobox.Selection = Base => class extends Base {
|
|
1044
1117
|
} else if (isDeleteEvent({ inputType: inputType })) {
|
1045
1118
|
this._deselect();
|
1046
1119
|
} else if (inputType === "hw:lockInSelection" && this._ensurableOption) {
|
1047
|
-
this.
|
1120
|
+
this._select(this._ensurableOption, this._softAutocomplete.bind(this));
|
1048
1121
|
} else if (this._isOpen && this._visibleOptionElements[0]) {
|
1049
|
-
this.
|
1122
|
+
this._select(this._visibleOptionElements[0], this._softAutocomplete.bind(this));
|
1050
1123
|
} else if (this._isOpen) {
|
1051
1124
|
this._resetOptionsAndNotify();
|
1052
1125
|
this._markInvalid();
|
@@ -1115,21 +1188,13 @@ Combobox.Selection = Base => class extends Base {
|
|
1115
1188
|
}
|
1116
1189
|
}
|
1117
1190
|
|
1118
|
-
_selectAndAutocompleteMissingPortion(option) {
|
1119
|
-
this._select(option, this._autocompleteMissingPortion.bind(this));
|
1120
|
-
}
|
1121
|
-
|
1122
|
-
_selectAndAutocompleteFullQuery(option) {
|
1123
|
-
this._select(option, this._replaceFullQueryWithAutocompletedValue.bind(this));
|
1124
|
-
}
|
1125
|
-
|
1126
1191
|
_forceSelectionAndFilter(option, inputType) {
|
1127
1192
|
this._forceSelectionWithoutFiltering(option);
|
1128
1193
|
this._filter(inputType);
|
1129
1194
|
}
|
1130
1195
|
|
1131
1196
|
_forceSelectionWithoutFiltering(option) {
|
1132
|
-
this.
|
1197
|
+
this._select(option, this._hardAutocomplete.bind(this));
|
1133
1198
|
}
|
1134
1199
|
|
1135
1200
|
_lockInSelection() {
|
@@ -1458,10 +1523,6 @@ Combobox.Toggle = Base => class extends Base {
|
|
1458
1523
|
this.expandedValue = true;
|
1459
1524
|
}
|
1460
1525
|
|
1461
|
-
openByFocusing() {
|
1462
|
-
this._actingCombobox.focus();
|
1463
|
-
}
|
1464
|
-
|
1465
1526
|
close(inputType) {
|
1466
1527
|
if (this._isOpen) {
|
1467
1528
|
const shouldReopen = this._isMultiselect &&
|
@@ -1476,9 +1537,8 @@ Combobox.Toggle = Base => class extends Base {
|
|
1476
1537
|
|
1477
1538
|
this.expandedValue = false;
|
1478
1539
|
|
1479
|
-
this._dispatchSelectionEvent();
|
1480
|
-
|
1481
1540
|
if (inputType != "hw:keyHandler:escape") {
|
1541
|
+
this._dispatchSelectionEvent();
|
1482
1542
|
this._createChip(shouldReopen);
|
1483
1543
|
}
|
1484
1544
|
|
@@ -1490,43 +1550,38 @@ Combobox.Toggle = Base => class extends Base {
|
|
1490
1550
|
|
1491
1551
|
toggle() {
|
1492
1552
|
if (this.expandedValue) {
|
1493
|
-
this.
|
1553
|
+
this.close("hw:toggle");
|
1494
1554
|
} else {
|
1495
|
-
this.
|
1555
|
+
this.open();
|
1496
1556
|
}
|
1497
1557
|
}
|
1498
1558
|
|
1499
1559
|
closeOnClickOutside(event) {
|
1500
1560
|
const target = event.target;
|
1501
1561
|
|
1502
|
-
if (
|
1562
|
+
if (this._isClosed) return
|
1503
1563
|
if (this.mainWrapperTarget.contains(target) && !this._isDialogDismisser(target)) return
|
1504
1564
|
if (this._withinElementBounds(event)) return
|
1505
1565
|
|
1506
|
-
this.
|
1566
|
+
this.close("hw:clickOutside");
|
1507
1567
|
}
|
1508
1568
|
|
1509
1569
|
closeOnFocusOutside({ target }) {
|
1510
|
-
if (
|
1570
|
+
if (this._isClosed) return
|
1511
1571
|
if (this.element.contains(target)) return
|
1512
1572
|
|
1513
|
-
this.
|
1573
|
+
this.close("hw:focusOutside");
|
1514
1574
|
}
|
1515
1575
|
|
1516
1576
|
clearOrToggleOnHandleClick() {
|
1517
1577
|
if (this._isQueried) {
|
1518
1578
|
this._clearQuery();
|
1519
|
-
this.
|
1579
|
+
this.open();
|
1520
1580
|
} else {
|
1521
1581
|
this.toggle();
|
1522
1582
|
}
|
1523
1583
|
}
|
1524
1584
|
|
1525
|
-
_closeAndBlur(inputType) {
|
1526
|
-
this.close(inputType);
|
1527
|
-
this._actingCombobox.blur();
|
1528
|
-
}
|
1529
|
-
|
1530
1585
|
// Some browser extensions like 1Password overlay elements on top of the combobox.
|
1531
1586
|
// Hovering over these elements emits a click event for some reason.
|
1532
1587
|
// These events don't contain any telling information, so we use `_withinElementBounds`
|
@@ -1573,6 +1628,7 @@ Combobox.Toggle = Base => class extends Base {
|
|
1573
1628
|
this._preventFocusingComboboxAfterClosingDialog();
|
1574
1629
|
this._preventBodyScroll();
|
1575
1630
|
this.dialogTarget.showModal();
|
1631
|
+
this._resizeDialog();
|
1576
1632
|
}
|
1577
1633
|
|
1578
1634
|
_openInline() {
|
@@ -1608,6 +1664,10 @@ Combobox.Toggle = Base => class extends Base {
|
|
1608
1664
|
get _isOpen() {
|
1609
1665
|
return this.expandedValue
|
1610
1666
|
}
|
1667
|
+
|
1668
|
+
get _isClosed() {
|
1669
|
+
return !this._isOpen
|
1670
|
+
}
|
1611
1671
|
};
|
1612
1672
|
|
1613
1673
|
Combobox.Validity = Base => class extends Base {
|
@@ -1657,6 +1717,7 @@ const concerns = [
|
|
1657
1717
|
Combobox.Announcements,
|
1658
1718
|
Combobox.AsyncLoading,
|
1659
1719
|
Combobox.Autocomplete,
|
1720
|
+
Combobox.Callbacks,
|
1660
1721
|
Combobox.Dialog,
|
1661
1722
|
Combobox.Events,
|
1662
1723
|
Combobox.Filtering,
|
@@ -1671,20 +1732,13 @@ const concerns = [
|
|
1671
1732
|
];
|
1672
1733
|
|
1673
1734
|
class HwComboboxController extends Concerns(...concerns) {
|
1674
|
-
static classes = [
|
1675
|
-
"invalid",
|
1676
|
-
"selected"
|
1677
|
-
]
|
1678
|
-
|
1735
|
+
static classes = [ "invalid", "selected" ]
|
1679
1736
|
static targets = [
|
1680
1737
|
"announcer",
|
1681
1738
|
"combobox",
|
1682
1739
|
"chipDismisser",
|
1683
1740
|
"closer",
|
1684
|
-
"dialog",
|
1685
|
-
"dialogCombobox",
|
1686
|
-
"dialogFocusTrap",
|
1687
|
-
"dialogListbox",
|
1741
|
+
"dialog", "dialogCombobox", "dialogFocusTrap", "dialogListbox",
|
1688
1742
|
"endOfOptionsStream",
|
1689
1743
|
"handle",
|
1690
1744
|
"hiddenField",
|
@@ -1708,6 +1762,7 @@ class HwComboboxController extends Concerns(...concerns) {
|
|
1708
1762
|
initialize() {
|
1709
1763
|
this._initializeActors();
|
1710
1764
|
this._initializeFiltering();
|
1765
|
+
this._initializeCallbacks();
|
1711
1766
|
}
|
1712
1767
|
|
1713
1768
|
connect() {
|
@@ -1734,23 +1789,43 @@ class HwComboboxController extends Concerns(...concerns) {
|
|
1734
1789
|
}
|
1735
1790
|
|
1736
1791
|
async endOfOptionsStreamTargetConnected(element) {
|
1737
|
-
|
1738
|
-
|
1792
|
+
if (element.dataset.callbackId) {
|
1793
|
+
this._runCallback(element);
|
1794
|
+
} else {
|
1795
|
+
this._preselectSingle();
|
1796
|
+
}
|
1797
|
+
}
|
1798
|
+
|
1799
|
+
async _runCallback(element) {
|
1800
|
+
const callbackId = element.dataset.callbackId;
|
1739
1801
|
|
1740
|
-
this.
|
1802
|
+
if (this._callbackAttemptsExceeded(callbackId)) {
|
1803
|
+
return this._dequeueCallback(callbackId)
|
1804
|
+
} else {
|
1805
|
+
this._recordCallbackAttempt(callbackId);
|
1806
|
+
}
|
1807
|
+
|
1808
|
+
if (this._isNextCallback(callbackId)) {
|
1809
|
+
const inputType = element.dataset.inputType;
|
1810
|
+
const delay = window.HOTWIRE_COMBOBOX_STREAM_DELAY;
|
1741
1811
|
|
1742
|
-
if (inputType === "hw:multiselectSync") {
|
1743
|
-
this.openByFocusing();
|
1744
|
-
} else if (inputType && inputType !== "hw:lockInSelection") {
|
1745
1812
|
if (delay) await sleep(delay);
|
1746
|
-
this.
|
1813
|
+
this._dequeueCallback(callbackId);
|
1814
|
+
this._resetMultiselectionMarks();
|
1815
|
+
|
1816
|
+
if (inputType === "hw:multiselectSync") {
|
1817
|
+
this.open();
|
1818
|
+
} else if (inputType !== "hw:lockInSelection") {
|
1819
|
+
this._selectOnQuery(inputType);
|
1820
|
+
}
|
1747
1821
|
} else {
|
1748
|
-
|
1822
|
+
await nextRepaint();
|
1823
|
+
this._runCallback(element);
|
1749
1824
|
}
|
1750
1825
|
}
|
1751
1826
|
|
1752
1827
|
closerTargetConnected() {
|
1753
|
-
this.
|
1828
|
+
this.close("hw:asyncCloser");
|
1754
1829
|
}
|
1755
1830
|
|
1756
1831
|
// Use +_printStack+ for debugging purposes
|