hotwire_combobox 0.1.11 → 0.1.12

Sign up to get free protection for your applications and to get access to all the features.
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 +76 -26
  20. data/app/presenters/hotwire_combobox/component.rb +150 -20
  21. data/app/presenters/hotwire_combobox/listbox/option.rb +5 -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: f21dd202e0a59116a7d505c6b79df8ba287db06dfa66fd6c9159cbaad15f119d
4
+ data.tar.gz: 9eb4d23df658d14948200e88ee39f9dad471a61782cde5f4132767a6c81763e1
5
5
  SHA512:
6
- metadata.gz: 4165ae08d560f369125e5542e26619fd2ccaea559232670d4dd1511cbf8972d7068b560ada69abb2ee61c0fad22826040bb9ced88a791192f0442e049a7c2124
7
- data.tar.gz: 004c8565c5c9ba15950bda9e242315d2ad21f7ee3e0a28fff5c6fe6ee0b54fd495b9caed719df2ee5714cea096362e129be6339e19dd74ec82d9baf91da54672
6
+ metadata.gz: 4013316a7bcbaf7080f25987c32f0f2dc4b2a38f11e8cfb62292155d626ab2bbc7d46f5116125b351eacabeb40856e50fb03335f69c95c7e09a919a0a0ed26ca
7
+ data.tar.gz: 6ca62cf9479f2d6951823affeb07320e572ff02dbb34681cf3ef33dabb579103410f7ad6123ee02cd21252c2cdcbb0e06c2efb564913c52b289beaae57e1fb36
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
+ }