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
@@ -0,0 +1,62 @@
1
+ import Combobox from "models/combobox/base"
2
+ import { wrapAroundAccess } from "helpers"
3
+
4
+ Combobox.Selection = Base => class extends Base {
5
+ selectOption(event) {
6
+ this._select(event.currentTarget)
7
+ this.close()
8
+ }
9
+
10
+ _connectSelection() {
11
+ if (this.hiddenFieldTarget.value) {
12
+ this._selectOptionByValue(this.hiddenFieldTarget.value)
13
+ }
14
+ }
15
+
16
+ _select(option, { force = false } = {}) {
17
+ this._resetOptions()
18
+
19
+ if (option) {
20
+ if (this.hasSelectedClass) option.classList.add(this.selectedClass)
21
+
22
+ this._markValid()
23
+ this._maybeAutocompleteWith(option, { force })
24
+ this._commitSelection(option, { selected: true })
25
+ } else {
26
+ this._markInvalid()
27
+ }
28
+ }
29
+
30
+ _commitSelection(option, { selected }) {
31
+ option?.setAttribute("aria-selected", selected)
32
+ option?.scrollIntoView({ block: "nearest" })
33
+
34
+ if (selected) {
35
+ this.hiddenFieldTarget.value = option?.dataset.value
36
+ } else {
37
+ this.hiddenFieldTarget.value = null
38
+ }
39
+ }
40
+
41
+ _deselect() {
42
+ const option = this._selectedOptionElement
43
+
44
+ if (this.hasSelectedClass) option?.classList.remove(this.selectedClass)
45
+ this._commitSelection(option, { selected: false })
46
+ }
47
+
48
+ _selectNew(query) {
49
+ this._resetOptions()
50
+ this.hiddenFieldTarget.value = query
51
+ this.hiddenFieldTarget.name = this.nameWhenNewValue
52
+ }
53
+
54
+ _selectIndex(index) {
55
+ const option = wrapAroundAccess(this._visibleOptionElements, index)
56
+ this._select(option, { force: true })
57
+ }
58
+
59
+ _selectOptionByValue(value) {
60
+ this._allOptions.find(option => option.dataset.value === value)?.click()
61
+ }
62
+ }
@@ -0,0 +1,104 @@
1
+ import Combobox from "models/combobox/base"
2
+ import { disableBodyScroll, enableBodyScroll } from "vendor/bodyScrollLock"
3
+
4
+ Combobox.Toggle = Base => class extends Base {
5
+ open() {
6
+ this.expandedValue = true
7
+ }
8
+
9
+ close() {
10
+ if (this._isOpen) {
11
+ this._ensureSelection()
12
+ this.expandedValue = false
13
+ }
14
+ }
15
+
16
+ toggle() {
17
+ if (this.expandedValue) {
18
+ this.close()
19
+ } else {
20
+ this._openByFocusing()
21
+ }
22
+ }
23
+
24
+ closeOnClickOutside({ target }) {
25
+ if (this.element.contains(target) && !this._isDialogDismisser(target)) return
26
+
27
+ this.close()
28
+ }
29
+
30
+ closeOnFocusOutside({ target }) {
31
+ if (!this._isOpen) return
32
+ if (this.element.contains(target)) return
33
+ if (target.matches("main")) return
34
+
35
+ this.close()
36
+ }
37
+
38
+ _ensureSelection() {
39
+ if (!this._isValidNewOption(this._actingCombobox.value, { ignoreAutocomplete: true })) {
40
+ this._select(this._selectedOptionElement, { force: true })
41
+ }
42
+ }
43
+
44
+ _openByFocusing() {
45
+ this._actingCombobox.focus()
46
+ }
47
+
48
+ _isDialogDismisser(target) {
49
+ return target.closest("dialog") && target.role != "combobox"
50
+ }
51
+
52
+ async _expand() {
53
+ if (this._autocompletesList && this._smallViewport) {
54
+ this._openInDialog()
55
+ } else {
56
+ this._openInline()
57
+ }
58
+
59
+ this._actingCombobox.setAttribute("aria-expanded", true) // needs to happen after setting acting combobox
60
+ }
61
+
62
+ _collapse() {
63
+ this._actingCombobox.setAttribute("aria-expanded", false) // needs to happen before resetting acting combobox
64
+
65
+ if (this.dialogTarget.open) {
66
+ this._closeInDialog()
67
+ } else {
68
+ this._closeInline()
69
+ }
70
+ }
71
+
72
+ _openInDialog() {
73
+ this._moveArtifactsToDialog()
74
+ this._preventFocusingComboboxAfterClosingDialog()
75
+ this._preventBodyScroll()
76
+ this.dialogTarget.showModal()
77
+ }
78
+
79
+ _openInline() {
80
+ this.listboxTarget.hidden = false
81
+ }
82
+
83
+ _closeInDialog() {
84
+ this._moveArtifactsInline()
85
+ this.dialogTarget.close()
86
+ this._restoreBodyScroll()
87
+ }
88
+
89
+ _closeInline() {
90
+ this.listboxTarget.hidden = true
91
+ }
92
+
93
+ _preventBodyScroll() {
94
+ disableBodyScroll(this.dialogListboxTarget)
95
+ }
96
+
97
+ _restoreBodyScroll() {
98
+ enableBodyScroll(this.dialogListboxTarget)
99
+ }
100
+
101
+ get _isOpen() {
102
+ return this.expandedValue
103
+ }
104
+ }
@@ -0,0 +1,34 @@
1
+ import Combobox from "models/combobox/base"
2
+
3
+ Combobox.Validity = Base => class extends Base {
4
+ _markValid() {
5
+ if (this._valueIsInvalid) return
6
+
7
+ if (this.hasInvalidClass) {
8
+ this.comboboxTarget.classList.remove(this.invalidClass)
9
+ }
10
+
11
+ this.comboboxTarget.removeAttribute("aria-invalid")
12
+ this.comboboxTarget.removeAttribute("aria-errormessage")
13
+ }
14
+
15
+ _markInvalid() {
16
+ if (this._valueIsValid) return
17
+
18
+ if (this.hasInvalidClass) {
19
+ this.comboboxTarget.classList.add(this.invalidClass)
20
+ }
21
+
22
+ this.comboboxTarget.setAttribute("aria-invalid", true)
23
+ this.comboboxTarget.setAttribute("aria-errormessage", `Please select a valid option for ${this.comboboxTarget.name}`)
24
+ }
25
+
26
+ get _valueIsValid() {
27
+ return !this._valueIsInvalid
28
+ }
29
+
30
+ get _valueIsInvalid() {
31
+ const isRequiredAndEmpty = this.comboboxTarget.required && !this.hiddenFieldTarget.value
32
+ return isRequiredAndEmpty
33
+ }
34
+ }
@@ -0,0 +1,14 @@
1
+ import Combobox from "models/combobox/base"
2
+
3
+ import "models/combobox/actors"
4
+ import "models/combobox/async_loading"
5
+ import "models/combobox/autocomplete"
6
+ import "models/combobox/dialog"
7
+ import "models/combobox/filtering"
8
+ import "models/combobox/navigation"
9
+ import "models/combobox/options"
10
+ import "models/combobox/selection"
11
+ import "models/combobox/toggle"
12
+ import "models/combobox/validity"
13
+
14
+ export default Combobox
@@ -0,0 +1,299 @@
1
+ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
2
+
3
+ // Older browsers don't support event options, feature detect it.
4
+
5
+ // Adopted and modified solution from Bohdan Didukh (2017)
6
+ // https://stackoverflow.com/questions/41594997/ios-10-safari-prevent-scrolling-behind-a-fixed-overlay-and-maintain-scroll-posi
7
+
8
+ var hasPassiveEvents = false;
9
+ if (typeof window !== 'undefined') {
10
+ var passiveTestOptions = {
11
+ get passive() {
12
+ hasPassiveEvents = true;
13
+ return undefined;
14
+ }
15
+ };
16
+ window.addEventListener('testPassive', null, passiveTestOptions);
17
+ window.removeEventListener('testPassive', null, passiveTestOptions);
18
+ }
19
+
20
+ var isIosDevice = typeof window !== 'undefined' && window.navigator && window.navigator.platform && (/iP(ad|hone|od)/.test(window.navigator.platform) || window.navigator.platform === 'MacIntel' && window.navigator.maxTouchPoints > 1);
21
+
22
+
23
+ var locks = [];
24
+ var documentListenerAdded = false;
25
+ var initialClientY = -1;
26
+ var previousBodyOverflowSetting = void 0;
27
+ var previousBodyPosition = void 0;
28
+ var previousBodyPaddingRight = void 0;
29
+
30
+ // returns true if `el` should be allowed to receive touchmove events.
31
+ var allowTouchMove = function allowTouchMove(el) {
32
+ return locks.some(function (lock) {
33
+ if (lock.options.allowTouchMove && lock.options.allowTouchMove(el)) {
34
+ return true;
35
+ }
36
+
37
+ return false;
38
+ });
39
+ };
40
+
41
+ var preventDefault = function preventDefault(rawEvent) {
42
+ var e = rawEvent || window.event;
43
+
44
+ // For the case whereby consumers adds a touchmove event listener to document.
45
+ // Recall that we do document.addEventListener('touchmove', preventDefault, { passive: false })
46
+ // in disableBodyScroll - so if we provide this opportunity to allowTouchMove, then
47
+ // the touchmove event on document will break.
48
+ if (allowTouchMove(e.target)) {
49
+ return true;
50
+ }
51
+
52
+ // Do not prevent if the event has more than one touch (usually meaning this is a multi touch gesture like pinch to zoom).
53
+ if (e.touches.length > 1) return true;
54
+
55
+ if (e.preventDefault) e.preventDefault();
56
+
57
+ return false;
58
+ };
59
+
60
+ var setOverflowHidden = function setOverflowHidden(options) {
61
+ // If previousBodyPaddingRight is already set, don't set it again.
62
+ if (previousBodyPaddingRight === undefined) {
63
+ var _reserveScrollBarGap = !!options && options.reserveScrollBarGap === true;
64
+ var scrollBarGap = window.innerWidth - document.documentElement.clientWidth;
65
+
66
+ if (_reserveScrollBarGap && scrollBarGap > 0) {
67
+ var computedBodyPaddingRight = parseInt(window.getComputedStyle(document.body).getPropertyValue('padding-right'), 10);
68
+ previousBodyPaddingRight = document.body.style.paddingRight;
69
+ document.body.style.paddingRight = computedBodyPaddingRight + scrollBarGap + 'px';
70
+ }
71
+ }
72
+
73
+ // If previousBodyOverflowSetting is already set, don't set it again.
74
+ if (previousBodyOverflowSetting === undefined) {
75
+ previousBodyOverflowSetting = document.body.style.overflow;
76
+ document.body.style.overflow = 'hidden';
77
+ }
78
+ };
79
+
80
+ var restoreOverflowSetting = function restoreOverflowSetting() {
81
+ if (previousBodyPaddingRight !== undefined) {
82
+ document.body.style.paddingRight = previousBodyPaddingRight;
83
+
84
+ // Restore previousBodyPaddingRight to undefined so setOverflowHidden knows it
85
+ // can be set again.
86
+ previousBodyPaddingRight = undefined;
87
+ }
88
+
89
+ if (previousBodyOverflowSetting !== undefined) {
90
+ document.body.style.overflow = previousBodyOverflowSetting;
91
+
92
+ // Restore previousBodyOverflowSetting to undefined
93
+ // so setOverflowHidden knows it can be set again.
94
+ previousBodyOverflowSetting = undefined;
95
+ }
96
+ };
97
+
98
+ var setPositionFixed = function setPositionFixed() {
99
+ return window.requestAnimationFrame(function () {
100
+ // If previousBodyPosition is already set, don't set it again.
101
+ if (previousBodyPosition === undefined) {
102
+ previousBodyPosition = {
103
+ position: document.body.style.position,
104
+ top: document.body.style.top,
105
+ left: document.body.style.left
106
+ };
107
+
108
+ // Update the dom inside an animation frame
109
+ var _window = window,
110
+ scrollY = _window.scrollY,
111
+ scrollX = _window.scrollX,
112
+ innerHeight = _window.innerHeight;
113
+
114
+ document.body.style.position = 'fixed';
115
+ document.body.style.top = -scrollY + 'px';
116
+ document.body.style.left = -scrollX + 'px';
117
+
118
+ setTimeout(function () {
119
+ return window.requestAnimationFrame(function () {
120
+ // Attempt to check if the bottom bar appeared due to the position change
121
+ var bottomBarHeight = innerHeight - window.innerHeight;
122
+ if (bottomBarHeight && scrollY >= innerHeight) {
123
+ // Move the content further up so that the bottom bar doesn't hide it
124
+ document.body.style.top = -(scrollY + bottomBarHeight);
125
+ }
126
+ });
127
+ }, 300);
128
+ }
129
+ });
130
+ };
131
+
132
+ var restorePositionSetting = function restorePositionSetting() {
133
+ if (previousBodyPosition !== undefined) {
134
+ // Convert the position from "px" to Int
135
+ var y = -parseInt(document.body.style.top, 10);
136
+ var x = -parseInt(document.body.style.left, 10);
137
+
138
+ // Restore styles
139
+ document.body.style.position = previousBodyPosition.position;
140
+ document.body.style.top = previousBodyPosition.top;
141
+ document.body.style.left = previousBodyPosition.left;
142
+
143
+ // Restore scroll
144
+ window.scrollTo(x, y);
145
+
146
+ previousBodyPosition = undefined;
147
+ }
148
+ };
149
+
150
+ // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#Problems_and_solutions
151
+ var isTargetElementTotallyScrolled = function isTargetElementTotallyScrolled(targetElement) {
152
+ return targetElement ? targetElement.scrollHeight - targetElement.scrollTop <= targetElement.clientHeight : false;
153
+ };
154
+
155
+ var handleScroll = function handleScroll(event, targetElement) {
156
+ var clientY = event.targetTouches[0].clientY - initialClientY;
157
+
158
+ if (allowTouchMove(event.target)) {
159
+ return false;
160
+ }
161
+
162
+ if (targetElement && targetElement.scrollTop === 0 && clientY > 0) {
163
+ // element is at the top of its scroll.
164
+ return preventDefault(event);
165
+ }
166
+
167
+ if (isTargetElementTotallyScrolled(targetElement) && clientY < 0) {
168
+ // element is at the bottom of its scroll.
169
+ return preventDefault(event);
170
+ }
171
+
172
+ event.stopPropagation();
173
+ return true;
174
+ };
175
+
176
+ export var disableBodyScroll = function disableBodyScroll(targetElement, options) {
177
+ // targetElement must be provided
178
+ if (!targetElement) {
179
+ // eslint-disable-next-line no-console
180
+ console.error('disableBodyScroll unsuccessful - targetElement must be provided when calling disableBodyScroll on IOS devices.');
181
+ return;
182
+ }
183
+
184
+ // disableBodyScroll must not have been called on this targetElement before
185
+ if (locks.some(function (lock) {
186
+ return lock.targetElement === targetElement;
187
+ })) {
188
+ return;
189
+ }
190
+
191
+ var lock = {
192
+ targetElement: targetElement,
193
+ options: options || {}
194
+ };
195
+
196
+ locks = [].concat(_toConsumableArray(locks), [lock]);
197
+
198
+ if (isIosDevice) {
199
+ setPositionFixed();
200
+ } else {
201
+ setOverflowHidden(options);
202
+ }
203
+
204
+ if (isIosDevice) {
205
+ targetElement.ontouchstart = function (event) {
206
+ if (event.targetTouches.length === 1) {
207
+ // detect single touch.
208
+ initialClientY = event.targetTouches[0].clientY;
209
+ }
210
+ };
211
+ targetElement.ontouchmove = function (event) {
212
+ if (event.targetTouches.length === 1) {
213
+ // detect single touch.
214
+ handleScroll(event, targetElement);
215
+ }
216
+ };
217
+
218
+ if (!documentListenerAdded) {
219
+ document.addEventListener('touchmove', preventDefault, hasPassiveEvents ? { passive: false } : undefined);
220
+ documentListenerAdded = true;
221
+ }
222
+ }
223
+ };
224
+
225
+ export var clearAllBodyScrollLocks = function clearAllBodyScrollLocks() {
226
+ if (isIosDevice) {
227
+ // Clear all locks ontouchstart/ontouchmove handlers, and the references.
228
+ locks.forEach(function (lock) {
229
+ lock.targetElement.ontouchstart = null;
230
+ lock.targetElement.ontouchmove = null;
231
+ });
232
+
233
+ if (documentListenerAdded) {
234
+ document.removeEventListener('touchmove', preventDefault, hasPassiveEvents ? { passive: false } : undefined);
235
+ documentListenerAdded = false;
236
+ }
237
+
238
+ // Reset initial clientY.
239
+ initialClientY = -1;
240
+ }
241
+
242
+ if (isIosDevice) {
243
+ restorePositionSetting();
244
+ } else {
245
+ restoreOverflowSetting();
246
+ }
247
+
248
+ locks = [];
249
+ };
250
+
251
+ export var enableBodyScroll = function enableBodyScroll(targetElement) {
252
+ if (!targetElement) {
253
+ // eslint-disable-next-line no-console
254
+ console.error('enableBodyScroll unsuccessful - targetElement must be provided when calling enableBodyScroll on IOS devices.');
255
+ return;
256
+ }
257
+
258
+ locks = locks.filter(function (lock) {
259
+ return lock.targetElement !== targetElement;
260
+ });
261
+
262
+ if (isIosDevice) {
263
+ targetElement.ontouchstart = null;
264
+ targetElement.ontouchmove = null;
265
+
266
+ if (documentListenerAdded && locks.length === 0) {
267
+ document.removeEventListener('touchmove', preventDefault, hasPassiveEvents ? { passive: false } : undefined);
268
+ documentListenerAdded = false;
269
+ }
270
+ }
271
+
272
+ if (isIosDevice) {
273
+ restorePositionSetting();
274
+ } else {
275
+ restoreOverflowSetting();
276
+ }
277
+ };
278
+
279
+ // MIT License
280
+
281
+ // Copyright (c) 2018 Will Po
282
+
283
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
284
+ // of this software and associated documentation files (the "Software"), to deal
285
+ // in the Software without restriction, including without limitation the rights
286
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
287
+ // copies of the Software, and to permit persons to whom the Software is
288
+ // furnished to do so, subject to the following conditions:
289
+
290
+ // The above copyright notice and this permission notice shall be included in all
291
+ // copies or substantial portions of the Software.
292
+
293
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
294
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
295
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
296
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
297
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
298
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
299
+ // SOFTWARE.
@@ -0,0 +1,181 @@
1
+ :root {
2
+ --hw-active-bg-color: #F3F4F6;
3
+
4
+ --hw-border-color: #D1D5DB;
5
+ --hw-border-radius: 0.375rem;
6
+ --hw-border-width--slim: 1px;
7
+ --hw-border-width--thick: 2px;
8
+
9
+ --hw-dialog-bottom-padding: 0;
10
+ --hw-dialog-font-size: 1.25rem;
11
+ --hw-dialog-input-height: 2.5rem;
12
+ --hw-dialog-label-alignment: center;
13
+ --hw-dialog-label-color: #1D1D1D;
14
+ --hw-dialog-label-padding: 0.5rem 0 0.375rem;
15
+ --hw-dialog-label-size: 1.05rem;
16
+ --hw-dialog-listbox-margin: 1.25rem 0 0;
17
+ --hw-dialog-padding: 1rem 1rem 0;
18
+ --hw-dialog-top-offset: 4rem;
19
+
20
+ --hw-font-size: 1rem;
21
+
22
+ --hw-handle-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");
23
+ --hw-handle-offset-right: 0.375rem;
24
+ --hw-handle-width: 1.5em;
25
+
26
+ --hw-combobox-width: 10rem;
27
+
28
+ --hw-focus-color: #2563EB;
29
+
30
+ --hw-line-height: 1.5rem;
31
+
32
+ --hw-listbox-height: calc(var(--hw-line-height) * 10);
33
+ --hw-listbox-offset-top: var(--hw-line-height);
34
+ --hw-listbox-z-index: 10;
35
+
36
+ --hw-option-bg-color: #FFFFFF;
37
+
38
+ --hw-padding--slim: 0.375rem;
39
+ --hw-padding--thick: 0.75rem;
40
+ }
41
+
42
+ .hw-combobox {
43
+ border-width: 0;
44
+ display: inline-block;
45
+ font-size: var(--hw-font-size);
46
+ margin: 0;
47
+ padding: 0;
48
+ position: relative;
49
+ isolation: isolate;
50
+
51
+ &, * {
52
+ box-sizing: border-box;
53
+ }
54
+ }
55
+
56
+ .hw-combobox__input {
57
+ border: var(--hw-border-width--slim) solid var(--hw-border-color);
58
+ border-radius: var(--hw-border-radius);
59
+ font-size: inherit;
60
+ line-height: var(--hw-line-height);
61
+ padding: var(--hw-padding--slim) var(--hw-handle-width) var(--hw-padding--slim) var(--hw-padding--thick);
62
+ position: relative;
63
+ width: var(--hw-combobox-width);
64
+ text-overflow: ellipsis;
65
+ }
66
+
67
+ .hw-combobox__handle {
68
+ height: 100%;
69
+ position: absolute;
70
+ right: var(--hw-handle-offset-right);
71
+ top: 0;
72
+ width: var(--hw-handle-width);
73
+ }
74
+
75
+ .hw-combobox__handle::before {
76
+ background-image: var(--hw-handle-image);
77
+ background-position: center;
78
+ background-repeat: no-repeat;
79
+ background-size: var(--hw-handle-width);
80
+ bottom: 0;
81
+ content: '';
82
+ left: 0;
83
+ position: absolute;
84
+ right: 0;
85
+ top: 0;
86
+ }
87
+
88
+ .hw-combobox__input:focus-visible {
89
+ box-shadow: 0 0 0 var(--hw-border-width--thick) var(--hw-focus-color);
90
+ outline: none;
91
+ }
92
+
93
+ .hw-combobox__listbox {
94
+ border-color: transparent;
95
+ border-radius: var(--hw-border-radius);
96
+ border-style: solid;
97
+ border-width: var(--hw-border-width--slim);
98
+ left: 0;
99
+ line-height: var(--hw-line-height);
100
+ list-style: none;
101
+ max-height: var(--hw-listbox-height);
102
+ overflow: auto;
103
+ padding: 0;
104
+ position: absolute;
105
+ top: var(--hw-listbox-offset-top);
106
+ width: 100%;
107
+ z-index: var(--hw-listbox-z-index);
108
+
109
+ &:has([role="option"]:not([hidden])) {
110
+ border-color: var(--hw-border-color);
111
+ }
112
+ }
113
+
114
+ .hw-combobox__option {
115
+ background-color: var(--hw-option-bg-color);
116
+ padding: var(--hw-padding--slim) var(--hw-padding--thick);
117
+ user-select: none;
118
+ white-space: nowrap;
119
+ overflow: hidden;
120
+ text-overflow: ellipsis;
121
+ }
122
+
123
+ .hw-combobox__option:hover,
124
+ .hw-combobox__option--selected {
125
+ background-color: var(--hw-active-bg-color);
126
+ }
127
+
128
+ .hw-combobox__dialog {
129
+ border: 0;
130
+ font-size: var(--hw-dialog-font-size);
131
+ height: auto;
132
+ margin: 0;
133
+ max-height: none;
134
+ max-width: none;
135
+ overflow: hidden;
136
+ padding: var(--hw-dialog-padding) var(--hw-dialog-padding) var(--hw-dialog-bottom-padding) var(--hw-dialog-padding);
137
+ position: fixed;
138
+ top: var(--hw-dialog-top-offset);
139
+ width: auto;
140
+
141
+ &[open] {
142
+ align-items: center;
143
+ display: flex;
144
+ flex-direction: column;
145
+ justify-content: start;
146
+ }
147
+
148
+ &::backdrop {
149
+ background-color: rgba(0, 0, 0, 0.6);
150
+ }
151
+ }
152
+
153
+ .hw-combobox__dialog__label {
154
+ align-self: var(--hw-dialog-label-alignment);
155
+ color: var(--hw-dialog-label-color);
156
+ font-size: var(--hw-dialog-label-size);
157
+ padding: var(--hw-dialog-label-padding);
158
+ }
159
+
160
+ .hw-combobox__dialog__input {
161
+ border: var(--hw-border-width--slim) solid var(--hw-border-color);
162
+ border-radius: var(--hw-border-radius);
163
+ font-size: inherit;
164
+ height: var(--hw-dialog-input-height);
165
+ line-height: var(--hw-dialog-input-height);
166
+ padding: var(--hw-padding--slim) var(--hw-padding--thick);
167
+ text-overflow: ellipsis;
168
+ width: 90%;
169
+ }
170
+
171
+ .hw-combobox__dialog__listbox {
172
+ margin: var(--hw-dialog-listbox-margin);
173
+ overflow: auto;
174
+ padding: 0;
175
+ width: 100%;
176
+
177
+ [role="option"] {
178
+ border-radius: var(--hw-border-radius);
179
+ padding: var(--hw-padding--thick);
180
+ }
181
+ }