hotwire_combobox 0.1.10 → 0.1.12

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +1 -1
  3. data/README.md +1 -4
  4. data/app/assets/javascripts/controllers/hw_combobox_controller.js +55 -211
  5. data/app/assets/javascripts/helpers.js +52 -0
  6. data/app/assets/javascripts/models/combobox/actors.js +24 -0
  7. data/app/assets/javascripts/models/combobox/async_loading.js +7 -0
  8. data/app/assets/javascripts/models/combobox/autocomplete.js +39 -0
  9. data/app/assets/javascripts/models/combobox/base.js +3 -0
  10. data/app/assets/javascripts/models/combobox/dialog.js +50 -0
  11. data/app/assets/javascripts/models/combobox/filtering.js +57 -0
  12. data/app/assets/javascripts/models/combobox/navigation.js +39 -0
  13. data/app/assets/javascripts/models/combobox/options.js +41 -0
  14. data/app/assets/javascripts/models/combobox/selection.js +62 -0
  15. data/app/assets/javascripts/models/combobox/toggle.js +104 -0
  16. data/app/assets/javascripts/models/combobox/validity.js +34 -0
  17. data/app/assets/javascripts/models/combobox.js +14 -0
  18. data/app/assets/javascripts/vendor/bodyScrollLock.js +299 -0
  19. data/app/assets/stylesheets/hotwire_combobox.css +181 -0
  20. data/app/helpers/hotwire_combobox/helper.rb +62 -73
  21. data/app/presenters/hotwire_combobox/component.rb +257 -0
  22. data/app/presenters/hotwire_combobox/listbox/option.rb +53 -0
  23. data/app/views/hotwire_combobox/_combobox.html.erb +5 -20
  24. data/app/views/hotwire_combobox/_next_page.turbo_stream.erb +5 -0
  25. data/app/views/hotwire_combobox/_paginated_options.turbo_stream.erb +7 -0
  26. data/app/views/hotwire_combobox/_pagination.html.erb +3 -0
  27. data/app/views/hotwire_combobox/combobox/_dialog.html.erb +7 -0
  28. data/app/views/hotwire_combobox/combobox/_hidden_field.html.erb +4 -0
  29. data/app/views/hotwire_combobox/combobox/_input.html.erb +2 -0
  30. data/app/views/hotwire_combobox/combobox/_paginated_listbox.html.erb +11 -0
  31. data/lib/hotwire_combobox/engine.rb +12 -2
  32. data/lib/hotwire_combobox/version.rb +1 -1
  33. data/lib/hotwire_combobox.rb +4 -2
  34. metadata +54 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 115007b81c7f19287376a49f41aa34c43e2fb331a18459b9f47877bf282d15cf
4
- data.tar.gz: b5eb9f21bd0f46f39a434ef8dea3d82a639e00b0dfb7f131c8264c77a55ac2ae
3
+ metadata.gz: f21dd202e0a59116a7d505c6b79df8ba287db06dfa66fd6c9159cbaad15f119d
4
+ data.tar.gz: 9eb4d23df658d14948200e88ee39f9dad471a61782cde5f4132767a6c81763e1
5
5
  SHA512:
6
- metadata.gz: fd39e53b94270d271b0b397d4e983346ea3b99a8054d0f2ab59756a38e440b23d7bf53d63cd34e1422530aaa1fa2fe0fec57aa5d0f693778d52f0f3a2e722b18
7
- data.tar.gz: b8ef213bc60a3b722d25bf4532e562151b4c660ebac0c5c75283cb045532787ff51bcb12ecafdeac28af54dc20262646d4953d5a7f306bdd43950442160831bc
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
data/README.md CHANGED
@@ -59,9 +59,6 @@ The `options` argument takes an array of any objects which respond to:
59
59
  | autocompletable_as | Used to autocomplete the input element when the user types into it. Falls back to calling `display` on the object if not provided. |
60
60
  | display | Used as a short-hand for other attributes. See the rest of the list for details. |
61
61
 
62
- > [!NOTE]
63
- > The `id` attribute is required only if `value` is not provided.
64
-
65
62
  You can use the `combobox_options` helper to create an array of option objects which respond to the above methods:
66
63
 
67
64
  ```ruby
@@ -89,7 +86,7 @@ Additionally, you can pass the following [Stimulus class values](https://stimulu
89
86
 
90
87
  ### Validity
91
88
 
92
- The hidden input can't have a value that's not in the list of options.
89
+ Unless `name_when_new` is passed, the hidden input can't have a value that's not in the list of options.
93
90
 
94
91
  If a nonexistent value is typed into the combobox, the value of the hidden input will be set empty.
95
92
 
@@ -1,225 +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", "valueField" ]
6
- static values = { expanded: Boolean, filterableAttribute: String, autocompletableAttribute: String }
7
-
8
- connect() {
9
- if (this.valueFieldTarget.value) {
10
- this.selectOptionByValue(this.valueFieldTarget.value)
11
- }
12
- }
13
-
14
- open() {
15
- this.expandedValue = true
16
- }
17
-
18
- close() {
19
- if (!this.isOpen) return
20
- this.commitSelection()
21
- this.expandedValue = false
22
- }
23
-
24
- selectOption(event) {
25
- this.select(event.currentTarget)
26
- this.close()
27
- }
28
-
29
- filter(event) {
30
- const query = this.comboboxTarget.value.trim()
31
-
32
- this.open()
33
-
34
- this.allOptionElements.forEach(applyFilter(query, { matching: this.filterableAttributeValue }))
35
-
36
- if (event.inputType === "deleteContentBackward") {
37
- this.deselect(this.selectedOptionElement)
38
- } else {
39
- this.select(this.visibleOptionElements[0])
40
- }
41
- }
42
-
43
- navigate(event) {
44
- this.keyHandlers[event.key]?.call(this, event)
45
- }
46
-
47
- closeOnClickOutside({ target }) {
48
- if (this.element.contains(target)) return
49
-
50
- this.close()
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
+
36
+ static values = {
37
+ asyncSrc: String,
38
+ autocompletableAttribute: String,
39
+ autocomplete: String,
40
+ expanded: Boolean,
41
+ filterableAttribute: String,
42
+ nameWhenNew: String,
43
+ originalName: String,
44
+ smallViewportMaxWidth: String
45
+ }
46
+
47
+ initialize() {
48
+ this._initializeActors()
49
+ this._initializeFiltering()
51
50
  }
52
51
 
53
- closeOnFocusOutside({ target }) {
54
- if (!this.isOpen) return
55
- if (this.element.contains(target)) return
56
- if (target.matches("main")) return
57
-
58
- this.close()
59
- }
60
-
61
- // private
62
-
63
- keyHandlers = {
64
- ArrowUp(event) {
65
- this.selectIndex(this.selectedOptionIndex - 1)
66
- cancel(event)
67
- },
68
- ArrowDown(event) {
69
- this.selectIndex(this.selectedOptionIndex + 1)
70
- cancel(event)
71
- },
72
- Home(event) {
73
- this.selectIndex(0)
74
- cancel(event)
75
- },
76
- End(event) {
77
- this.selectIndex(this.visibleOptionElements.length - 1)
78
- cancel(event)
79
- },
80
- Enter(event) {
81
- this.close()
82
- cancel(event)
83
- }
52
+ connect() {
53
+ this._connectSelection()
54
+ this._connectListAutocomplete()
55
+ this._connectDialog()
84
56
  }
85
57
 
86
- commitSelection() {
87
- this.select(this.selectedOptionElement, { force: true })
58
+ disconnect() {
59
+ this._disconnectDialog()
88
60
  }
89
61
 
90
62
  expandedValueChanged() {
91
63
  if (this.expandedValue) {
92
- this.expand()
93
- } else {
94
- this.collapse()
95
- }
96
- }
97
-
98
- expand() {
99
- this.listboxTarget.hidden = false
100
- this.comboboxTarget.setAttribute("aria-expanded", true)
101
- }
102
-
103
- collapse() {
104
- this.listboxTarget.hidden = true
105
- this.comboboxTarget.setAttribute("aria-expanded", false)
106
- }
107
-
108
- select(option, { force = false } = {}) {
109
- this.allOptionElements.forEach(option => this.deselect(option))
110
-
111
- if (option) {
112
- if (this.hasSelectedClass) option.classList.add(this.selectedClass)
113
- if (this.hasInvalidClass) this.comboboxTarget.classList.remove(this.invalidClass)
114
-
115
- this.maybeAutocompleteWith(option, { force })
116
- this.executeSelect(option, { selected: true })
117
- } else {
118
- if (this.valueIsInvalid) {
119
- if (this.hasInvalidClass) this.comboboxTarget.classList.add(this.invalidClass)
120
-
121
- this.comboboxTarget.setAttribute("aria-invalid", true)
122
- this.comboboxTarget.setAttribute("aria-errormessage", `Please select a valid option for ${this.comboboxTarget.name}`)
123
- }
124
- }
125
- }
126
-
127
- selectIndex(index) {
128
- const option = wrapAroundAccess(this.visibleOptionElements, index)
129
- this.select(option, { force: true })
130
- }
131
-
132
- selectOptionByValue(value) {
133
- this.allOptions.find(option => option.dataset.value === value)?.click()
134
- }
135
-
136
- deselect(option) {
137
- if (option) {
138
- if (this.hasSelectedClass) option.classList.remove(this.selectedClass)
139
- this.executeSelect(option, { selected: false })
140
- }
141
- }
142
-
143
- executeSelect(option, { selected }) {
144
- if (selected) {
145
- option.setAttribute("aria-selected", true)
146
- this.valueFieldTarget.value = option.dataset.value
147
- } else {
148
- option.setAttribute("aria-selected", false)
149
- this.valueFieldTarget.value = null
150
- }
151
- }
152
-
153
- maybeAutocompleteWith(option, { force }) {
154
- const typedValue = this.comboboxTarget.value
155
- const autocompletedValue = option.dataset.autocompletableAs
156
-
157
- if (force) {
158
- this.comboboxTarget.value = autocompletedValue
159
- this.comboboxTarget.setSelectionRange(autocompletedValue.length, autocompletedValue.length)
160
- } else if (autocompletedValue.toLowerCase().startsWith(typedValue.toLowerCase())) {
161
- this.comboboxTarget.value = autocompletedValue
162
- this.comboboxTarget.setSelectionRange(typedValue.length, autocompletedValue.length)
163
- }
164
- }
165
-
166
- get allOptions() {
167
- return Array.from(this.allOptionElements)
168
- }
169
-
170
- get allOptionElements() {
171
- return this.listboxTarget.querySelectorAll(`[${this.filterableAttributeValue}]`)
172
- }
173
-
174
- get visibleOptionElements() {
175
- return [ ...this.allOptionElements ].filter(visible)
176
- }
177
-
178
- get selectedOptionElement() {
179
- return this.listboxTarget.querySelector("[role=option][aria-selected=true]")
180
- }
181
-
182
- get selectedOptionIndex() {
183
- return [ ...this.visibleOptionElements ].indexOf(this.selectedOptionElement)
184
- }
185
-
186
- get isOpen() {
187
- return this.expandedValue
188
- }
189
-
190
- get valueIsInvalid() {
191
- const isRequiredAndEmpty = this.comboboxTarget.required && !this.valueFieldTarget.value
192
- return isRequiredAndEmpty
193
- }
194
- }
195
-
196
- function applyFilter(query, { matching }) {
197
- return (target) => {
198
- if (query) {
199
- const value = target.getAttribute(matching) || ""
200
- const match = value.toLowerCase().includes(query.toLowerCase())
201
-
202
- target.hidden = !match
64
+ this._expand()
203
65
  } else {
204
- target.hidden = false
66
+ this._collapse()
205
67
  }
206
68
  }
207
69
  }
208
-
209
- function visible(target) {
210
- return !(target.hidden || target.closest("[hidden]"))
211
- }
212
-
213
- function wrapAroundAccess(array, index) {
214
- const first = 0
215
- const last = array.length - 1
216
-
217
- if (index < first) return array[last]
218
- if (index > last) return array[first]
219
- return array[index]
220
- }
221
-
222
- function cancel(event) {
223
- event.stopPropagation()
224
- event.preventDefault()
225
- }
@@ -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
+ }