hotwire_combobox 0.1.11 → 0.1.13

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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +1 -1
  3. data/app/assets/javascripts/controllers/hw_combobox_controller.js +49 -242
  4. data/app/assets/javascripts/helpers.js +52 -0
  5. data/app/assets/javascripts/models/combobox/actors.js +24 -0
  6. data/app/assets/javascripts/models/combobox/async_loading.js +7 -0
  7. data/app/assets/javascripts/models/combobox/autocomplete.js +39 -0
  8. data/app/assets/javascripts/models/combobox/base.js +3 -0
  9. data/app/assets/javascripts/models/combobox/dialog.js +50 -0
  10. data/app/assets/javascripts/models/combobox/filtering.js +57 -0
  11. data/app/assets/javascripts/models/combobox/navigation.js +39 -0
  12. data/app/assets/javascripts/models/combobox/options.js +41 -0
  13. data/app/assets/javascripts/models/combobox/selection.js +62 -0
  14. data/app/assets/javascripts/models/combobox/toggle.js +104 -0
  15. data/app/assets/javascripts/models/combobox/validity.js +34 -0
  16. data/app/assets/javascripts/models/combobox.js +14 -0
  17. data/app/assets/javascripts/vendor/bodyScrollLock.js +299 -0
  18. data/app/assets/stylesheets/hotwire_combobox.css +181 -0
  19. data/app/helpers/hotwire_combobox/helper.rb +81 -26
  20. data/app/presenters/hotwire_combobox/component.rb +150 -20
  21. data/app/presenters/hotwire_combobox/listbox/option.rb +4 -2
  22. data/app/views/hotwire_combobox/_combobox.html.erb +4 -13
  23. data/app/views/hotwire_combobox/_next_page.turbo_stream.erb +5 -0
  24. data/app/views/hotwire_combobox/_paginated_options.turbo_stream.erb +7 -0
  25. data/app/views/hotwire_combobox/_pagination.html.erb +3 -0
  26. data/app/views/hotwire_combobox/combobox/_dialog.html.erb +7 -0
  27. data/app/views/hotwire_combobox/combobox/_hidden_field.html.erb +4 -0
  28. data/app/views/hotwire_combobox/combobox/_input.html.erb +2 -0
  29. data/app/views/hotwire_combobox/combobox/_paginated_listbox.html.erb +11 -0
  30. data/lib/hotwire_combobox/version.rb +1 -1
  31. data/lib/hotwire_combobox.rb +4 -2
  32. metadata +52 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0fabe58275ffd3010bca3cc350f94ebd673af65d7411156998418b7cd09c262e
4
- data.tar.gz: 223c6a8e14566bc49cc900732ca234f559741b06f07c3e5d4724d9bf78b33732
3
+ metadata.gz: 4f868770ad8adc907437ea9d7c0f2a9550b848a1ab0ca3dddb3592aacd4ce75a
4
+ data.tar.gz: 6d1932f507fd650da412d2bacc1d6dc01faed6b8d3f2230fe9df1251b66baaf9
5
5
  SHA512:
6
- metadata.gz: 4165ae08d560f369125e5542e26619fd2ccaea559232670d4dd1511cbf8972d7068b560ada69abb2ee61c0fad22826040bb9ced88a791192f0442e049a7c2124
7
- data.tar.gz: 004c8565c5c9ba15950bda9e242315d2ad21f7ee3e0a28fff5c6fe6ee0b54fd495b9caed719df2ee5714cea096362e129be6339e19dd74ec82d9baf91da54672
6
+ metadata.gz: 31127e0fa02f231b604b10b25f5ff514b33065814a3283ae4d6c338ab59835ef8696e19496f867bec8ddc564066a1b5af2c6c7fa1a88e88507c1943bb741f6ce
7
+ data.tar.gz: 77070143486b82fa1b0d3179e1fb74fe43300988a6a6c2c3654dc883a06d1b4b3a9e069e10d8e71f543ee72ee6a242cba42ef7e595e6ba377a530e73954a1aca
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2023 Jose Farias
1
+ Copyright 2024 Jose Farias
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
@@ -1,262 +1,69 @@
1
+ import Combobox from "models/combobox"
2
+ import { Concerns } from "helpers"
1
3
  import { Controller } from "@hotwired/stimulus"
2
4
 
3
- export default class extends Controller {
4
- static classes = [ "selected", "invalid" ]
5
- static targets = [ "combobox", "listbox", "hiddenField" ]
5
+ const concerns = [
6
+ Controller,
7
+ Combobox.Actors,
8
+ Combobox.AsyncLoading,
9
+ Combobox.Autocomplete,
10
+ Combobox.Dialog,
11
+ Combobox.Filtering,
12
+ Combobox.Navigation,
13
+ Combobox.Options,
14
+ Combobox.Selection,
15
+ Combobox.Toggle,
16
+ Combobox.Validity
17
+ ]
18
+
19
+ export default class HwComboboxController extends Concerns(...concerns) {
20
+ static classes = [
21
+ "invalid",
22
+ "selected"
23
+ ]
24
+
25
+ static targets = [
26
+ "combobox",
27
+ "dialog",
28
+ "dialogCombobox",
29
+ "dialogFocusTrap",
30
+ "dialogListbox",
31
+ "handle",
32
+ "hiddenField",
33
+ "listbox"
34
+ ]
35
+
6
36
  static values = {
37
+ asyncSrc: String,
38
+ autocompletableAttribute: String,
39
+ autocomplete: String,
7
40
  expanded: Boolean,
41
+ filterableAttribute: String,
8
42
  nameWhenNew: String,
9
43
  originalName: String,
10
- filterableAttribute: String,
11
- autocompletableAttribute: String }
12
-
13
- connect() {
14
- if (this.hiddenFieldTarget.value) {
15
- this.selectOptionByValue(this.hiddenFieldTarget.value)
16
- }
17
- }
18
-
19
- open() {
20
- this.expandedValue = true
21
- }
22
-
23
- close() {
24
- if (!this.isOpen) return
25
- this.commitSelection()
26
- this.expandedValue = false
27
- }
28
-
29
- selectOption(event) {
30
- this.select(event.currentTarget)
31
- this.close()
44
+ smallViewportMaxWidth: String
32
45
  }
33
46
 
34
- filter(event) {
35
- const isDeleting = event.inputType === "deleteContentBackward"
36
- const query = this.comboboxTarget.value.trim()
37
-
38
- this.open()
39
-
40
- this.allOptionElements.forEach(applyFilter(query, { matching: this.filterableAttributeValue }))
41
-
42
- if (this.isValidNewOption(query, { ignoreAutocomplete: isDeleting })) {
43
- this.selectNew(query)
44
- } else if (isDeleting) {
45
- this.deselect(this.selectedOptionElement)
46
- } else {
47
- this.select(this.visibleOptionElements[0])
48
- }
49
- }
50
-
51
- navigate(event) {
52
- this.keyHandlers[event.key]?.call(this, event)
53
- }
54
-
55
- closeOnClickOutside({ target }) {
56
- if (this.element.contains(target)) return
57
-
58
- this.close()
47
+ initialize() {
48
+ this._initializeActors()
49
+ this._initializeFiltering()
59
50
  }
60
51
 
61
- closeOnFocusOutside({ target }) {
62
- if (!this.isOpen) return
63
- if (this.element.contains(target)) return
64
- if (target.matches("main")) return
65
-
66
- this.close()
67
- }
68
-
69
- // private
70
-
71
- keyHandlers = {
72
- ArrowUp(event) {
73
- this.selectIndex(this.selectedOptionIndex - 1)
74
- cancel(event)
75
- },
76
- ArrowDown(event) {
77
- this.selectIndex(this.selectedOptionIndex + 1)
78
- cancel(event)
79
- },
80
- Home(event) {
81
- this.selectIndex(0)
82
- cancel(event)
83
- },
84
- End(event) {
85
- this.selectIndex(this.visibleOptionElements.length - 1)
86
- cancel(event)
87
- },
88
- Enter(event) {
89
- this.close()
90
- cancel(event)
91
- }
52
+ connect() {
53
+ this._connectSelection()
54
+ this._connectListAutocomplete()
55
+ this._connectDialog()
92
56
  }
93
57
 
94
- commitSelection() {
95
- if (!this.isValidNewOption(this.comboboxTarget.value, { ignoreAutocomplete: true })) {
96
- this.select(this.selectedOptionElement, { force: true })
97
- }
58
+ disconnect() {
59
+ this._disconnectDialog()
98
60
  }
99
61
 
100
62
  expandedValueChanged() {
101
63
  if (this.expandedValue) {
102
- this.expand()
64
+ this._expand()
103
65
  } else {
104
- this.collapse()
66
+ this._collapse()
105
67
  }
106
68
  }
107
-
108
- expand() {
109
- this.listboxTarget.hidden = false
110
- this.comboboxTarget.setAttribute("aria-expanded", true)
111
- }
112
-
113
- collapse() {
114
- this.listboxTarget.hidden = true
115
- this.comboboxTarget.setAttribute("aria-expanded", false)
116
- }
117
-
118
- select(option, { force = false } = {}) {
119
- this.resetOptions()
120
-
121
- if (option) {
122
- if (this.hasSelectedClass) option.classList.add(this.selectedClass)
123
- if (this.hasInvalidClass) this.comboboxTarget.classList.remove(this.invalidClass)
124
-
125
- this.maybeAutocompleteWith(option, { force })
126
- this.executeSelect(option, { selected: true })
127
- } else {
128
- if (this.valueIsInvalid) {
129
- if (this.hasInvalidClass) this.comboboxTarget.classList.add(this.invalidClass)
130
-
131
- this.comboboxTarget.setAttribute("aria-invalid", true)
132
- this.comboboxTarget.setAttribute("aria-errormessage", `Please select a valid option for ${this.comboboxTarget.name}`)
133
- }
134
- }
135
- }
136
-
137
- selectNew(query) {
138
- this.resetOptions()
139
- this.hiddenFieldTarget.value = query
140
- this.hiddenFieldTarget.name = this.nameWhenNewValue
141
- }
142
-
143
- resetOptions() {
144
- this.allOptionElements.forEach(option => this.deselect(option))
145
- this.hiddenFieldTarget.name = this.originalNameValue
146
- }
147
-
148
- selectIndex(index) {
149
- const option = wrapAroundAccess(this.visibleOptionElements, index)
150
- this.select(option, { force: true })
151
- }
152
-
153
- selectOptionByValue(value) {
154
- this.allOptions.find(option => option.dataset.value === value)?.click()
155
- }
156
-
157
- deselect(option) {
158
- if (option) {
159
- if (this.hasSelectedClass) option.classList.remove(this.selectedClass)
160
- this.executeSelect(option, { selected: false })
161
- }
162
- }
163
-
164
- executeSelect(option, { selected }) {
165
- if (selected) {
166
- option.setAttribute("aria-selected", true)
167
- this.hiddenFieldTarget.value = option.dataset.value
168
- } else {
169
- option.setAttribute("aria-selected", false)
170
- this.hiddenFieldTarget.value = null
171
- }
172
- }
173
-
174
- maybeAutocompleteWith(option, { force }) {
175
- const typedValue = this.comboboxTarget.value
176
- const autocompletedValue = option.getAttribute(this.autocompletableAttributeValue)
177
-
178
- if (force) {
179
- this.comboboxTarget.value = autocompletedValue
180
- this.comboboxTarget.setSelectionRange(autocompletedValue.length, autocompletedValue.length)
181
- } else if (startsWith(autocompletedValue, typedValue)) {
182
- this.comboboxTarget.value = autocompletedValue
183
- this.comboboxTarget.setSelectionRange(typedValue.length, autocompletedValue.length)
184
- }
185
- }
186
-
187
- isValidNewOption(query, { ignoreAutocomplete = false } = {}) {
188
- const typedValue = this.comboboxTarget.value
189
- const autocompletedValue = this.visibleOptionElements[0]?.getAttribute(this.autocompletableAttributeValue)
190
- const insufficentAutocomplete = !autocompletedValue || !startsWith(autocompletedValue, typedValue)
191
-
192
- return query.length > 0 && this.allowNew && (ignoreAutocomplete || insufficentAutocomplete)
193
- }
194
-
195
- get allOptions() {
196
- return Array.from(this.allOptionElements)
197
- }
198
-
199
- get allOptionElements() {
200
- return this.listboxTarget.querySelectorAll(`[${this.filterableAttributeValue}]`)
201
- }
202
-
203
- get visibleOptionElements() {
204
- return [ ...this.allOptionElements ].filter(visible)
205
- }
206
-
207
- get selectedOptionElement() {
208
- return this.listboxTarget.querySelector("[role=option][aria-selected=true]")
209
- }
210
-
211
- get selectedOptionIndex() {
212
- return [ ...this.visibleOptionElements ].indexOf(this.selectedOptionElement)
213
- }
214
-
215
- get isOpen() {
216
- return this.expandedValue
217
- }
218
-
219
- get valueIsInvalid() {
220
- const isRequiredAndEmpty = this.comboboxTarget.required && !this.hiddenFieldTarget.value
221
- return isRequiredAndEmpty
222
- }
223
-
224
- get allowNew() {
225
- return !!this.nameWhenNewValue
226
- }
227
- }
228
-
229
- function applyFilter(query, { matching }) {
230
- return (target) => {
231
- if (query) {
232
- const value = target.getAttribute(matching) || ""
233
- const match = value.toLowerCase().includes(query.toLowerCase())
234
-
235
- target.hidden = !match
236
- } else {
237
- target.hidden = false
238
- }
239
- }
240
- }
241
-
242
- function visible(target) {
243
- return !(target.hidden || target.closest("[hidden]"))
244
- }
245
-
246
- function wrapAroundAccess(array, index) {
247
- const first = 0
248
- const last = array.length - 1
249
-
250
- if (index < first) return array[last]
251
- if (index > last) return array[first]
252
- return array[index]
253
- }
254
-
255
- function cancel(event) {
256
- event.stopPropagation()
257
- event.preventDefault()
258
- }
259
-
260
- function startsWith(string, substring) {
261
- return string.toLowerCase().startsWith(substring.toLowerCase())
262
69
  }
@@ -0,0 +1,52 @@
1
+ export function Concerns(Base, ...mixins) {
2
+ return mixins.reduce((accumulator, current) => current(accumulator), Base)
3
+ }
4
+
5
+ export function visible(target) {
6
+ return !(target.hidden || target.closest("[hidden]"))
7
+ }
8
+
9
+ export function wrapAroundAccess(array, index) {
10
+ const first = 0
11
+ const last = array.length - 1
12
+
13
+ if (index < first) return array[last]
14
+ if (index > last) return array[first]
15
+ return array[index]
16
+ }
17
+
18
+ export function applyFilter(query, { matching }) {
19
+ return (target) => {
20
+ if (query) {
21
+ const value = target.getAttribute(matching) || ""
22
+ const match = value.toLowerCase().includes(query.toLowerCase())
23
+
24
+ target.hidden = !match
25
+ } else {
26
+ target.hidden = false
27
+ }
28
+ }
29
+ }
30
+
31
+ export function cancel(event) {
32
+ event.stopPropagation()
33
+ event.preventDefault()
34
+ }
35
+
36
+ export function startsWith(string, substring) {
37
+ return string.toLowerCase().startsWith(substring.toLowerCase())
38
+ }
39
+
40
+ export function nextFrame() {
41
+ return new Promise(requestAnimationFrame)
42
+ }
43
+
44
+ export function debounce(fn, delay = 150) {
45
+ let timeoutId = null
46
+
47
+ return (...args) => {
48
+ const callback = () => fn.apply(this, args)
49
+ clearTimeout(timeoutId)
50
+ timeoutId = setTimeout(callback, delay)
51
+ }
52
+ }
@@ -0,0 +1,24 @@
1
+ import Combobox from "models/combobox/base"
2
+
3
+ Combobox.Actors = Base => class extends Base {
4
+ _initializeActors() {
5
+ this._actingListbox = this.listboxTarget
6
+ this._actingCombobox = this.comboboxTarget
7
+ }
8
+
9
+ get _actingListbox() {
10
+ return this.actingListbox
11
+ }
12
+
13
+ set _actingListbox(listbox) {
14
+ this.actingListbox = listbox
15
+ }
16
+
17
+ get _actingCombobox() {
18
+ return this.actingCombobox
19
+ }
20
+
21
+ set _actingCombobox(combobox) {
22
+ this.actingCombobox = combobox
23
+ }
24
+ }
@@ -0,0 +1,7 @@
1
+ import Combobox from "models/combobox/base"
2
+
3
+ Combobox.AsyncLoading = Base => class extends Base {
4
+ get _isAsync() {
5
+ return this.hasAsyncSrcValue
6
+ }
7
+ }
@@ -0,0 +1,39 @@
1
+ import Combobox from "models/combobox/base"
2
+ import { startsWith } from "helpers"
3
+
4
+ Combobox.Autocomplete = Base => class extends Base {
5
+ _connectListAutocomplete() {
6
+ if (!this._autocompletesList) {
7
+ this._visuallyHideListbox()
8
+ }
9
+ }
10
+
11
+ _maybeAutocompleteWith(option, { force }) {
12
+ if (!this._autocompletesInline && !force) return
13
+
14
+ const typedValue = this._actingCombobox.value
15
+ const autocompletedValue = option.getAttribute(this.autocompletableAttributeValue)
16
+
17
+ if (force) {
18
+ this._actingCombobox.value = autocompletedValue
19
+ this._actingCombobox.setSelectionRange(autocompletedValue.length, autocompletedValue.length)
20
+ } else if (startsWith(autocompletedValue, typedValue)) {
21
+ this._actingCombobox.value = autocompletedValue
22
+ this._actingCombobox.setSelectionRange(typedValue.length, autocompletedValue.length)
23
+ }
24
+ }
25
+
26
+ // +visuallyHideListbox+ hides the listbox from the user,
27
+ // but makes it still searchable by JS.
28
+ _visuallyHideListbox() {
29
+ this.listboxTarget.style.display = "none"
30
+ }
31
+
32
+ get _autocompletesList() {
33
+ return this.autocompleteValue === "both" || this.autocompleteValue === "list"
34
+ }
35
+
36
+ get _autocompletesInline() {
37
+ return this.autocompleteValue === "both" || this.autocompleteValue === "inline"
38
+ }
39
+ }
@@ -0,0 +1,3 @@
1
+ const Combobox = {}
2
+
3
+ export default Combobox
@@ -0,0 +1,50 @@
1
+ import Combobox from "models/combobox/base"
2
+
3
+ Combobox.Dialog = Base => class extends Base {
4
+ _connectDialog() {
5
+ if (window.visualViewport) {
6
+ window.visualViewport.addEventListener("resize", this._resizeDialog)
7
+ }
8
+ }
9
+
10
+ _disconnectDialog() {
11
+ if (window.visualViewport) {
12
+ window.visualViewport.removeEventListener("resize", this._resizeDialog)
13
+ }
14
+ }
15
+
16
+ _moveArtifactsToDialog() {
17
+ this.dialogComboboxTarget.value = this.actingCombobox.value
18
+
19
+ this._actingCombobox = this.dialogComboboxTarget
20
+ this._actingListbox = this.dialogListboxTarget
21
+
22
+ this.dialogListboxTarget.append(...this.listboxTarget.children)
23
+ }
24
+
25
+ _moveArtifactsInline() {
26
+ this.comboboxTarget.value = this.actingCombobox.value
27
+
28
+ this._actingCombobox = this.comboboxTarget
29
+ this._actingListbox = this.listboxTarget
30
+
31
+ this.listboxTarget.append(...this.dialogListboxTarget.children)
32
+ }
33
+
34
+ _resizeDialog = () => {
35
+ const fullHeight = window.innerHeight
36
+ const viewportHeight = window.visualViewport.height
37
+ this.dialogTarget.style.setProperty("--hw-dialog-bottom-padding", `${fullHeight - viewportHeight}px`)
38
+ }
39
+
40
+ // After closing a dialog, focus returns to the last focused element.
41
+ // +preventFocusingComboboxAfterClosingDialog+ focuses a placeholder element before opening
42
+ // the dialog, so that the combobox is not focused again after closing, which would reopen.
43
+ _preventFocusingComboboxAfterClosingDialog() {
44
+ this.dialogFocusTrapTarget.focus()
45
+ }
46
+
47
+ get _smallViewport() {
48
+ return window.matchMedia(`(max-width: ${this.smallViewportMaxWidthValue})`).matches
49
+ }
50
+ }
@@ -0,0 +1,57 @@
1
+
2
+ import Combobox from "models/combobox/base"
3
+ import { applyFilter, nextFrame, debounce } from "helpers"
4
+ import { get } from "@rails/request.js"
5
+
6
+ Combobox.Filtering = Base => class extends Base {
7
+ filter(event) {
8
+ if (this._isAsync) {
9
+ this._debouncedFilterAsync(event)
10
+ } else {
11
+ this._filterSync(event)
12
+ }
13
+ }
14
+
15
+ _initializeFiltering() {
16
+ this._debouncedFilterAsync = debounce(this._debouncedFilterAsync.bind(this))
17
+ }
18
+
19
+ _debouncedFilterAsync(event) {
20
+ this._filterAsync(event)
21
+ }
22
+
23
+ async _filterAsync(event) {
24
+ const q = this._actingCombobox.value.trim()
25
+
26
+ await get(this.asyncSrcValue, { responseKind: "turbo-stream", query: { q } })
27
+
28
+ this._afterTurboStreamRender(() => this._commitFilter(q, event))
29
+ }
30
+
31
+ _filterSync(event) {
32
+ const query = this._actingCombobox.value.trim()
33
+
34
+ this.open()
35
+
36
+ this._allOptionElements.forEach(applyFilter(query, { matching: this.filterableAttributeValue }))
37
+
38
+ this._commitFilter(query, event)
39
+ }
40
+
41
+ _commitFilter(query, event) {
42
+ const isDeleting = event.inputType === "deleteContentBackward"
43
+
44
+ if (this._isValidNewOption(query, { ignoreAutocomplete: isDeleting })) {
45
+ this._selectNew(query)
46
+ } else if (isDeleting) {
47
+ this._deselect()
48
+ } else {
49
+ this._select(this._visibleOptionElements[0])
50
+ }
51
+ }
52
+
53
+ async _afterTurboStreamRender(callback) {
54
+ await nextFrame()
55
+ callback()
56
+ }
57
+ }
@@ -0,0 +1,39 @@
1
+ import Combobox from "models/combobox/base"
2
+ import { cancel } from "helpers"
3
+
4
+ Combobox.Navigation = Base => class extends Base {
5
+ navigate(event) {
6
+ if (this._autocompletesList) {
7
+ this._keyHandlers[event.key]?.call(this, event)
8
+ }
9
+ }
10
+
11
+ _keyHandlers = {
12
+ ArrowUp: (event) => {
13
+ this._selectIndex(this._selectedOptionIndex - 1)
14
+ cancel(event)
15
+ },
16
+ ArrowDown: (event) => {
17
+ this._selectIndex(this._selectedOptionIndex + 1)
18
+ cancel(event)
19
+ },
20
+ Home: (event) => {
21
+ this._selectIndex(0)
22
+ cancel(event)
23
+ },
24
+ End: (event) => {
25
+ this._selectIndex(this._visibleOptionElements.length - 1)
26
+ cancel(event)
27
+ },
28
+ Enter: (event) => {
29
+ this.close()
30
+ this._actingCombobox.blur()
31
+ cancel(event)
32
+ },
33
+ Escape: (event) => {
34
+ this.close()
35
+ this._actingCombobox.blur()
36
+ cancel(event)
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,41 @@
1
+ import Combobox from "models/combobox/base"
2
+ import { visible, startsWith } from "helpers"
3
+
4
+ Combobox.Options = Base => class extends Base {
5
+ _resetOptions() {
6
+ this._deselect()
7
+ this.hiddenFieldTarget.name = this.originalNameValue
8
+ }
9
+
10
+ _isValidNewOption(query, { ignoreAutocomplete = false } = {}) {
11
+ const typedValue = this._actingCombobox.value
12
+ const autocompletedValue = this._visibleOptionElements[0]?.getAttribute(this.autocompletableAttributeValue)
13
+ const insufficentAutocomplete = !autocompletedValue || !startsWith(autocompletedValue, typedValue)
14
+
15
+ return query.length > 0 && this._allowNew && (ignoreAutocomplete || insufficentAutocomplete)
16
+ }
17
+
18
+ get _allowNew() {
19
+ return !!this.nameWhenNewValue
20
+ }
21
+
22
+ get _allOptions() {
23
+ return Array.from(this._allOptionElements)
24
+ }
25
+
26
+ get _allOptionElements() {
27
+ return this._actingListbox.querySelectorAll(`[${this.filterableAttributeValue}]`)
28
+ }
29
+
30
+ get _visibleOptionElements() {
31
+ return [ ...this._allOptionElements ].filter(visible)
32
+ }
33
+
34
+ get _selectedOptionElement() {
35
+ return this._actingListbox.querySelector("[role=option][aria-selected=true]")
36
+ }
37
+
38
+ get _selectedOptionIndex() {
39
+ return [ ...this._visibleOptionElements ].indexOf(this._selectedOptionElement)
40
+ }
41
+ }