hotwire_combobox 0.1.10 → 0.1.12

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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
+ }