hotwire_combobox 0.1.42 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -0
- data/app/assets/javascripts/controllers/hw_combobox_controller.js +25 -3
- data/app/assets/javascripts/hotwire_combobox.esm.js +531 -127
- data/app/assets/javascripts/hotwire_combobox.umd.js +531 -127
- data/app/assets/javascripts/hw_combobox/helpers.js +16 -0
- data/app/assets/javascripts/hw_combobox/models/combobox/announcements.js +7 -0
- data/app/assets/javascripts/hw_combobox/models/combobox/async_loading.js +4 -0
- data/app/assets/javascripts/hw_combobox/models/combobox/autocomplete.js +8 -6
- data/app/assets/javascripts/hw_combobox/models/combobox/dialog.js +1 -1
- data/app/assets/javascripts/hw_combobox/models/combobox/events.js +21 -6
- data/app/assets/javascripts/hw_combobox/models/combobox/filtering.js +33 -28
- data/app/assets/javascripts/hw_combobox/models/combobox/form_field.js +74 -0
- data/app/assets/javascripts/hw_combobox/models/combobox/multiselect.js +160 -0
- data/app/assets/javascripts/hw_combobox/models/combobox/navigation.js +15 -6
- data/app/assets/javascripts/hw_combobox/models/combobox/options.js +29 -9
- data/app/assets/javascripts/hw_combobox/models/combobox/selection.js +103 -51
- data/app/assets/javascripts/hw_combobox/models/combobox/toggle.js +45 -16
- data/app/assets/javascripts/hw_combobox/models/combobox/validity.js +1 -1
- data/app/assets/javascripts/hw_combobox/models/combobox.js +3 -0
- data/app/assets/stylesheets/hotwire_combobox.css +84 -18
- data/app/presenters/hotwire_combobox/component/customizable.rb +9 -1
- data/app/presenters/hotwire_combobox/component.rb +95 -28
- data/app/presenters/hotwire_combobox/listbox/group.rb +45 -0
- data/app/presenters/hotwire_combobox/listbox/item.rb +104 -0
- data/app/presenters/hotwire_combobox/listbox/option.rb +9 -4
- data/app/views/hotwire_combobox/_component.html.erb +1 -0
- data/app/views/hotwire_combobox/_selection_chip.turbo_stream.erb +8 -0
- data/app/views/hotwire_combobox/layouts/_selection_chip.turbo_stream.erb +7 -0
- data/lib/hotwire_combobox/helper.rb +111 -86
- data/lib/hotwire_combobox/version.rb +1 -1
- metadata +9 -2
@@ -1,104 +1,156 @@
|
|
1
1
|
import Combobox from "hw_combobox/models/combobox/base"
|
2
|
-
import { wrapAroundAccess } from "hw_combobox/helpers"
|
2
|
+
import { wrapAroundAccess, isDeleteEvent } from "hw_combobox/helpers"
|
3
3
|
|
4
4
|
Combobox.Selection = Base => class extends Base {
|
5
|
-
|
6
|
-
this.
|
7
|
-
this.
|
8
|
-
this.close()
|
5
|
+
selectOnClick({ currentTarget, inputType }) {
|
6
|
+
this._forceSelectionAndFilter(currentTarget, inputType)
|
7
|
+
this._closeAndBlur("hw:optionRoleClick")
|
9
8
|
}
|
10
9
|
|
11
10
|
_connectSelection() {
|
12
11
|
if (this.hasPrefilledDisplayValue) {
|
13
12
|
this._fullQuery = this.prefilledDisplayValue
|
13
|
+
this._markQueried()
|
14
14
|
}
|
15
15
|
}
|
16
16
|
|
17
|
-
|
18
|
-
this.
|
19
|
-
|
20
|
-
if (
|
21
|
-
this.
|
22
|
-
|
23
|
-
this.
|
24
|
-
} else {
|
17
|
+
_selectOnQuery(inputType) {
|
18
|
+
if (this._shouldTreatAsNewOptionForFiltering(!isDeleteEvent({ inputType: inputType }))) {
|
19
|
+
this._selectNew()
|
20
|
+
} else if (isDeleteEvent({ inputType: inputType })) {
|
21
|
+
this._deselect()
|
22
|
+
} else if (inputType === "hw:lockInSelection" && this._ensurableOption) {
|
23
|
+
this._selectAndAutocompleteMissingPortion(this._ensurableOption)
|
24
|
+
} else if (this._isOpen && this._visibleOptionElements[0]) {
|
25
|
+
this._selectAndAutocompleteMissingPortion(this._visibleOptionElements[0])
|
26
|
+
} else if (this._isOpen) {
|
27
|
+
this._resetOptionsAndNotify()
|
25
28
|
this._markInvalid()
|
29
|
+
} else {
|
30
|
+
// When selecting from an async dialog listbox: selection is forced, the listbox is filtered,
|
31
|
+
// and the dialog is closed. Filtering ends with an `endOfOptionsStream` target connected
|
32
|
+
// to the now invisible combobox, which is now closed because Turbo waits for "nextRepaint"
|
33
|
+
// before rendering turbo streams. This ultimately calls +_selectOnQuery+. We do want
|
34
|
+
// to call +_selectOnQuery+ in this case to account for e.g. selection of
|
35
|
+
// new options. But we will noop here if it's none of the cases checked above.
|
26
36
|
}
|
27
37
|
}
|
28
38
|
|
29
|
-
|
30
|
-
|
39
|
+
_select(option, autocompleteStrategy) {
|
40
|
+
const previousValue = this._fieldValueString
|
31
41
|
|
32
|
-
|
33
|
-
this.hiddenFieldTarget.value = option.dataset.value
|
34
|
-
option.scrollIntoView({ block: "nearest" })
|
35
|
-
}
|
42
|
+
this._resetOptionsSilently()
|
36
43
|
|
37
|
-
|
44
|
+
autocompleteStrategy(option)
|
45
|
+
|
46
|
+
this._fieldValue = option.dataset.value
|
47
|
+
this._markSelected(option)
|
48
|
+
this._markValid()
|
49
|
+
this._dispatchPreselectionEvent({ isNewAndAllowed: false, previousValue: previousValue })
|
50
|
+
|
51
|
+
option.scrollIntoView({ block: "nearest" })
|
38
52
|
}
|
39
53
|
|
40
|
-
|
41
|
-
|
42
|
-
option.classList.toggle(this.selectedClass, selected)
|
43
|
-
}
|
54
|
+
_selectNew() {
|
55
|
+
const previousValue = this._fieldValueString
|
44
56
|
|
45
|
-
|
46
|
-
this.
|
57
|
+
this._resetOptionsSilently()
|
58
|
+
this._fieldValue = this._fullQuery
|
59
|
+
this._fieldName = this.nameWhenNewValue
|
60
|
+
this._markValid()
|
61
|
+
this._dispatchPreselectionEvent({ isNewAndAllowed: true, previousValue: previousValue })
|
47
62
|
}
|
48
63
|
|
49
64
|
_deselect() {
|
50
|
-
const
|
65
|
+
const previousValue = this._fieldValueString
|
51
66
|
|
52
|
-
if (
|
67
|
+
if (this._selectedOptionElement) {
|
68
|
+
this._markNotSelected(this._selectedOptionElement)
|
69
|
+
}
|
53
70
|
|
54
|
-
this.
|
71
|
+
this._fieldValue = ""
|
55
72
|
this._setActiveDescendant("")
|
56
73
|
|
57
|
-
|
74
|
+
return previousValue
|
58
75
|
}
|
59
76
|
|
60
|
-
|
61
|
-
this.
|
62
|
-
this.
|
63
|
-
this.hiddenFieldTarget.name = this.nameWhenNewValue
|
64
|
-
this._markValid()
|
65
|
-
|
66
|
-
this._dispatchSelectionEvent({ isNew: true })
|
77
|
+
_deselectAndNotify() {
|
78
|
+
const previousValue = this._deselect()
|
79
|
+
this._dispatchPreselectionEvent({ isNewAndAllowed: false, previousValue: previousValue })
|
67
80
|
}
|
68
81
|
|
69
82
|
_selectIndex(index) {
|
70
83
|
const option = wrapAroundAccess(this._visibleOptionElements, index)
|
71
|
-
this.
|
84
|
+
this._forceSelectionWithoutFiltering(option)
|
72
85
|
}
|
73
86
|
|
74
|
-
|
75
|
-
if (this._hasValueButNoSelection && this._allOptions.length < 100) {
|
76
|
-
const option = this.
|
77
|
-
|
78
|
-
|
87
|
+
_preselectSingle() {
|
88
|
+
if (this._isSingleSelect && this._hasValueButNoSelection && this._allOptions.length < 100) {
|
89
|
+
const option = this._optionElementWithValue(this._fieldValue)
|
90
|
+
if (option) this._markSelected(option)
|
91
|
+
}
|
92
|
+
}
|
79
93
|
|
80
|
-
|
94
|
+
_preselectMultiple() {
|
95
|
+
if (this._isMultiselect && this._hasValueButNoSelection) {
|
96
|
+
this._requestChips(this._fieldValueString)
|
97
|
+
this._resetMultiselectionMarks()
|
81
98
|
}
|
82
99
|
}
|
83
100
|
|
101
|
+
_selectAndAutocompleteMissingPortion(option) {
|
102
|
+
this._select(option, this._autocompleteMissingPortion.bind(this))
|
103
|
+
}
|
104
|
+
|
105
|
+
_selectAndAutocompleteFullQuery(option) {
|
106
|
+
this._select(option, this._replaceFullQueryWithAutocompletedValue.bind(this))
|
107
|
+
}
|
108
|
+
|
109
|
+
_forceSelectionAndFilter(option, inputType) {
|
110
|
+
this._forceSelectionWithoutFiltering(option)
|
111
|
+
this._filter(inputType)
|
112
|
+
}
|
113
|
+
|
114
|
+
_forceSelectionWithoutFiltering(option) {
|
115
|
+
this._selectAndAutocompleteFullQuery(option)
|
116
|
+
}
|
117
|
+
|
84
118
|
_lockInSelection() {
|
85
119
|
if (this._shouldLockInSelection) {
|
86
|
-
this.
|
87
|
-
this.filter({ inputType: "hw:lockInSelection" })
|
120
|
+
this._forceSelectionAndFilter(this._ensurableOption, "hw:lockInSelection")
|
88
121
|
}
|
122
|
+
}
|
89
123
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
124
|
+
_markSelected(option) {
|
125
|
+
if (this.hasSelectedClass) option.classList.add(this.selectedClass)
|
126
|
+
option.setAttribute("aria-selected", true)
|
127
|
+
this._setActiveDescendant(option.id)
|
128
|
+
}
|
129
|
+
|
130
|
+
_markNotSelected(option) {
|
131
|
+
if (this.hasSelectedClass) option.classList.remove(this.selectedClass)
|
132
|
+
option.removeAttribute("aria-selected")
|
133
|
+
this._removeActiveDescendant()
|
94
134
|
}
|
95
135
|
|
96
136
|
_setActiveDescendant(id) {
|
97
137
|
this._forAllComboboxes(el => el.setAttribute("aria-activedescendant", id))
|
98
138
|
}
|
99
139
|
|
140
|
+
_removeActiveDescendant() {
|
141
|
+
this._setActiveDescendant("")
|
142
|
+
}
|
143
|
+
|
100
144
|
get _hasValueButNoSelection() {
|
101
|
-
return this.
|
145
|
+
return this._hasFieldValue && !this._hasSelection
|
146
|
+
}
|
147
|
+
|
148
|
+
get _hasSelection() {
|
149
|
+
if (this._isSingleSelect) {
|
150
|
+
this._selectedOptionElement
|
151
|
+
} else {
|
152
|
+
this._multiselectedOptionElements.length > 0
|
153
|
+
}
|
102
154
|
}
|
103
155
|
|
104
156
|
get _shouldLockInSelection() {
|
@@ -6,19 +6,40 @@ Combobox.Toggle = Base => class extends Base {
|
|
6
6
|
this.expandedValue = true
|
7
7
|
}
|
8
8
|
|
9
|
-
|
9
|
+
openByFocusing() {
|
10
|
+
this._actingCombobox.focus()
|
11
|
+
}
|
12
|
+
|
13
|
+
close(inputType) {
|
10
14
|
if (this._isOpen) {
|
15
|
+
const shouldReopen = this._isMultiselect &&
|
16
|
+
this._isSync &&
|
17
|
+
!this._isSmallViewport &&
|
18
|
+
inputType != "hw:clickOutside" &&
|
19
|
+
inputType != "hw:focusOutside"
|
20
|
+
|
11
21
|
this._lockInSelection()
|
22
|
+
this._clearInvalidQuery()
|
23
|
+
|
12
24
|
this.expandedValue = false
|
13
|
-
|
25
|
+
|
26
|
+
this._dispatchSelectionEvent()
|
27
|
+
|
28
|
+
if (inputType != "hw:keyHandler:escape") {
|
29
|
+
this._createChip(shouldReopen)
|
30
|
+
}
|
31
|
+
|
32
|
+
if (this._isSingleSelect && this._selectedOptionElement) {
|
33
|
+
this._announceToScreenReader(this._displayForOptionElement(this._selectedOptionElement), "selected")
|
34
|
+
}
|
14
35
|
}
|
15
36
|
}
|
16
37
|
|
17
38
|
toggle() {
|
18
39
|
if (this.expandedValue) {
|
19
|
-
this.
|
40
|
+
this._closeAndBlur("hw:toggle")
|
20
41
|
} else {
|
21
|
-
this.
|
42
|
+
this.openByFocusing()
|
22
43
|
}
|
23
44
|
}
|
24
45
|
|
@@ -29,14 +50,14 @@ Combobox.Toggle = Base => class extends Base {
|
|
29
50
|
if (this.mainWrapperTarget.contains(target) && !this._isDialogDismisser(target)) return
|
30
51
|
if (this._withinElementBounds(event)) return
|
31
52
|
|
32
|
-
this.
|
53
|
+
this._closeAndBlur("hw:clickOutside")
|
33
54
|
}
|
34
55
|
|
35
56
|
closeOnFocusOutside({ target }) {
|
36
57
|
if (!this._isOpen) return
|
37
58
|
if (this.element.contains(target)) return
|
38
59
|
|
39
|
-
this.
|
60
|
+
this._closeAndBlur("hw:focusOutside")
|
40
61
|
}
|
41
62
|
|
42
63
|
clearOrToggleOnHandleClick() {
|
@@ -48,6 +69,11 @@ Combobox.Toggle = Base => class extends Base {
|
|
48
69
|
}
|
49
70
|
}
|
50
71
|
|
72
|
+
_closeAndBlur(inputType) {
|
73
|
+
this.close(inputType)
|
74
|
+
this._actingCombobox.blur()
|
75
|
+
}
|
76
|
+
|
51
77
|
// Some browser extensions like 1Password overlay elements on top of the combobox.
|
52
78
|
// Hovering over these elements emits a click event for some reason.
|
53
79
|
// These events don't contain any telling information, so we use `_withinElementBounds`
|
@@ -59,18 +85,16 @@ Combobox.Toggle = Base => class extends Base {
|
|
59
85
|
return clientX >= left && clientX <= right && clientY >= top && clientY <= bottom
|
60
86
|
}
|
61
87
|
|
62
|
-
_openByFocusing() {
|
63
|
-
this._actingCombobox.focus()
|
64
|
-
}
|
65
|
-
|
66
88
|
_isDialogDismisser(target) {
|
67
89
|
return target.closest("dialog") && target.role != "combobox"
|
68
90
|
}
|
69
91
|
|
70
92
|
_expand() {
|
71
|
-
if (this.
|
93
|
+
if (this._isSync) {
|
94
|
+
this._preselectSingle()
|
95
|
+
}
|
72
96
|
|
73
|
-
if (this._autocompletesList && this.
|
97
|
+
if (this._autocompletesList && this._isSmallViewport) {
|
74
98
|
this._openInDialog()
|
75
99
|
} else {
|
76
100
|
this._openInline()
|
@@ -79,6 +103,8 @@ Combobox.Toggle = Base => class extends Base {
|
|
79
103
|
this._actingCombobox.setAttribute("aria-expanded", true) // needs to happen after setting acting combobox
|
80
104
|
}
|
81
105
|
|
106
|
+
// +._collapse()+ differs from `.close()` in that it might be called by stimulus on connect because
|
107
|
+
// it interprets a change in `expandedValue` — whereas `.close()` is only called internally by us.
|
82
108
|
_collapse() {
|
83
109
|
this._actingCombobox.setAttribute("aria-expanded", false) // needs to happen before resetting acting combobox
|
84
110
|
|
@@ -119,11 +145,14 @@ Combobox.Toggle = Base => class extends Base {
|
|
119
145
|
enableBodyScroll(this.dialogListboxTarget)
|
120
146
|
}
|
121
147
|
|
122
|
-
|
123
|
-
|
148
|
+
_clearInvalidQuery() {
|
149
|
+
if (this._isUnjustifiablyBlank) {
|
150
|
+
this._deselect()
|
151
|
+
this._clearQuery()
|
152
|
+
}
|
124
153
|
}
|
125
154
|
|
126
|
-
get
|
127
|
-
return
|
155
|
+
get _isOpen() {
|
156
|
+
return this.expandedValue
|
128
157
|
}
|
129
158
|
}
|
@@ -34,7 +34,7 @@ Combobox.Validity = Base => class extends Base {
|
|
34
34
|
// +_valueIsInvalid+ only checks if `comboboxTarget` (and not `_actingCombobox`) is required
|
35
35
|
// because the `required` attribute is only forwarded to the `comboboxTarget` element
|
36
36
|
get _valueIsInvalid() {
|
37
|
-
const isRequiredAndEmpty = this.comboboxTarget.required &&
|
37
|
+
const isRequiredAndEmpty = this.comboboxTarget.required && this._hasEmptyFieldValue
|
38
38
|
return isRequiredAndEmpty
|
39
39
|
}
|
40
40
|
}
|
@@ -1,11 +1,14 @@
|
|
1
1
|
import Combobox from "hw_combobox/models/combobox/base"
|
2
2
|
|
3
3
|
import "hw_combobox/models/combobox/actors"
|
4
|
+
import "hw_combobox/models/combobox/announcements"
|
4
5
|
import "hw_combobox/models/combobox/async_loading"
|
5
6
|
import "hw_combobox/models/combobox/autocomplete"
|
6
7
|
import "hw_combobox/models/combobox/dialog"
|
7
8
|
import "hw_combobox/models/combobox/events"
|
8
9
|
import "hw_combobox/models/combobox/filtering"
|
10
|
+
import "hw_combobox/models/combobox/form_field"
|
11
|
+
import "hw_combobox/models/combobox/multiselect"
|
9
12
|
import "hw_combobox/models/combobox/navigation"
|
10
13
|
import "hw_combobox/models/combobox/new_options"
|
11
14
|
import "hw_combobox/models/combobox/options"
|
@@ -1,6 +1,7 @@
|
|
1
1
|
:root {
|
2
2
|
--hw-active-bg-color: #F3F4F6;
|
3
3
|
--hw-border-color: #D1D5DB;
|
4
|
+
--hw-group-color: #57595C;
|
4
5
|
--hw-invalid-color: #EF4444;
|
5
6
|
--hw-dialog-label-color: #1D1D1D;
|
6
7
|
--hw-focus-color: #2563EB;
|
@@ -10,6 +11,9 @@
|
|
10
11
|
--hw-border-width--slim: 1px;
|
11
12
|
--hw-border-width--thick: 2px;
|
12
13
|
|
14
|
+
--hw-combobox-width: 10rem;
|
15
|
+
--hw-combobox-width--multiple: 30rem;
|
16
|
+
|
13
17
|
--hw-dialog-font-size: 1.25rem;
|
14
18
|
--hw-dialog-input-height: 2.5rem;
|
15
19
|
--hw-dialog-label-alignment: center;
|
@@ -24,20 +28,21 @@
|
|
24
28
|
--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");
|
25
29
|
--hw-handle-image--queried: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M6 18 18 6M6 6l12 12'/%3E%3C/svg%3E");
|
26
30
|
--hw-handle-offset-right: 0.375rem;
|
27
|
-
--hw-handle-width: 1.
|
28
|
-
--hw-handle-width--queried:
|
29
|
-
|
30
|
-
--hw-combobox-width: 10rem;
|
31
|
+
--hw-handle-width: 1.5rem;
|
32
|
+
--hw-handle-width--queried: 1rem;
|
31
33
|
|
32
34
|
--hw-line-height: 1.5rem;
|
35
|
+
--hw-line-height--multiple: 2.125rem;
|
33
36
|
|
34
37
|
--hw-listbox-height: calc(var(--hw-line-height) * 10);
|
35
|
-
--hw-listbox-offset-top: calc(var(--hw-line-height) * 1.625);
|
36
38
|
--hw-listbox-z-index: 10;
|
37
39
|
|
40
|
+
--hw-padding--slimmer: 0.25rem;
|
38
41
|
--hw-padding--slim: 0.375rem;
|
39
42
|
--hw-padding--thick: 0.75rem;
|
40
43
|
|
44
|
+
--hw-selection-chip-font-size: 0.875rem;
|
45
|
+
|
41
46
|
--hw-visual-viewport-height: 100vh;
|
42
47
|
}
|
43
48
|
|
@@ -57,30 +62,35 @@
|
|
57
62
|
}
|
58
63
|
|
59
64
|
.hw-combobox__main__wrapper {
|
65
|
+
border: var(--hw-border-width--slim) solid var(--hw-border-color);
|
66
|
+
border-radius: var(--hw-border-radius);
|
67
|
+
padding: var(--hw-padding--slim) calc(var(--hw-handle-width) + var(--hw-padding--slimmer)) var(--hw-padding--slim) var(--hw-padding--thick);
|
60
68
|
position: relative;
|
61
|
-
width:
|
69
|
+
width: var(--hw-combobox-width);
|
70
|
+
|
71
|
+
&:focus-within {
|
72
|
+
box-shadow: 0 0 0 var(--hw-border-width--thick) var(--hw-focus-color);
|
73
|
+
}
|
74
|
+
|
75
|
+
&:has(.hw-combobox__input--invalid) {
|
76
|
+
box-shadow: 0 0 0 var(--hw-border-width--thick) var(--hw-invalid-color);
|
77
|
+
}
|
62
78
|
}
|
63
79
|
|
64
80
|
.hw-combobox__input {
|
65
|
-
border:
|
66
|
-
border-radius: var(--hw-border-radius);
|
81
|
+
border: 0;
|
67
82
|
font-size: inherit;
|
68
83
|
line-height: var(--hw-line-height);
|
69
|
-
|
70
|
-
|
71
|
-
width: var(--hw-combobox-width);
|
84
|
+
min-width: 0;
|
85
|
+
padding: 0;
|
72
86
|
text-overflow: ellipsis;
|
87
|
+
width: 100%;
|
73
88
|
}
|
74
89
|
|
75
90
|
.hw-combobox__input:focus-visible {
|
76
|
-
box-shadow: 0 0 0 var(--hw-border-width--thick) var(--hw-focus-color);
|
77
91
|
outline: none;
|
78
92
|
}
|
79
93
|
|
80
|
-
.hw-combobox__input--invalid {
|
81
|
-
border: var(--hw-border-width--slim) solid var(--hw-invalid-color);
|
82
|
-
}
|
83
|
-
|
84
94
|
.hw-combobox__handle {
|
85
95
|
height: 100%;
|
86
96
|
position: absolute;
|
@@ -105,7 +115,6 @@
|
|
105
115
|
.hw-combobox__input[data-queried] + .hw-combobox__handle::before {
|
106
116
|
background-image: var(--hw-handle-image--queried);
|
107
117
|
background-size: var(--hw-handle-width--queried);
|
108
|
-
cursor: pointer;
|
109
118
|
}
|
110
119
|
|
111
120
|
.hw-combobox__listbox {
|
@@ -121,7 +130,7 @@
|
|
121
130
|
overflow: auto;
|
122
131
|
padding: 0;
|
123
132
|
position: absolute;
|
124
|
-
top:
|
133
|
+
top: calc(100% + 0.2rem);
|
125
134
|
width: 100%;
|
126
135
|
z-index: var(--hw-listbox-z-index);
|
127
136
|
|
@@ -130,6 +139,20 @@
|
|
130
139
|
}
|
131
140
|
}
|
132
141
|
|
142
|
+
.hw-combobox__group {
|
143
|
+
display: none;
|
144
|
+
padding: 0;
|
145
|
+
}
|
146
|
+
|
147
|
+
.hw-combobox__group__label {
|
148
|
+
color: var(--hw-group-color);
|
149
|
+
padding: var(--hw-padding--slim);
|
150
|
+
}
|
151
|
+
|
152
|
+
.hw-combobox__group:has(.hw-combobox__option:not([hidden])) {
|
153
|
+
display: block;
|
154
|
+
}
|
155
|
+
|
133
156
|
.hw-combobox__option {
|
134
157
|
background-color: var(--hw-option-bg-color);
|
135
158
|
padding: var(--hw-padding--slim) var(--hw-padding--thick);
|
@@ -144,6 +167,7 @@
|
|
144
167
|
}
|
145
168
|
|
146
169
|
.hw-combobox__option:hover,
|
170
|
+
.hw-combobox__option--navigated,
|
147
171
|
.hw-combobox__option--selected {
|
148
172
|
background-color: var(--hw-active-bg-color);
|
149
173
|
}
|
@@ -217,3 +241,45 @@
|
|
217
241
|
padding: var(--hw-padding--thick);
|
218
242
|
}
|
219
243
|
}
|
244
|
+
|
245
|
+
.hw-combobox__chip {
|
246
|
+
align-items: center;
|
247
|
+
border: var(--hw-border-width--slim) solid var(--hw-border-color);
|
248
|
+
border-radius: var(--hw-border-radius);
|
249
|
+
display: flex;
|
250
|
+
font-size: var(--hw-selection-chip-font-size);
|
251
|
+
line-height: var(--hw-line-height);
|
252
|
+
padding: var(--hw-padding--slimmer);
|
253
|
+
padding-left: var(--hw-padding--slim);
|
254
|
+
}
|
255
|
+
|
256
|
+
.hw-combobox__chip__remover {
|
257
|
+
background-image: var(--hw-handle-image--queried);
|
258
|
+
background-size: var(--hw-handle-width--queried);
|
259
|
+
background-repeat: no-repeat;
|
260
|
+
margin-left: var(--hw-padding--slimmer);
|
261
|
+
min-height: var(--hw-handle-width--queried);
|
262
|
+
min-width: var(--hw-handle-width--queried);
|
263
|
+
}
|
264
|
+
|
265
|
+
.hw-combobox--multiple {
|
266
|
+
.hw-combobox__main__wrapper {
|
267
|
+
align-items: center;
|
268
|
+
display: flex;
|
269
|
+
flex-wrap: wrap;
|
270
|
+
gap: var(--hw-padding--slimmer);
|
271
|
+
width: var(--hw-combobox-width--multiple);
|
272
|
+
|
273
|
+
&:has([data-hw-combobox-chip]) .hw-combobox__input::placeholder {
|
274
|
+
color: transparent;
|
275
|
+
}
|
276
|
+
}
|
277
|
+
|
278
|
+
.hw-combobox__input {
|
279
|
+
min-width: calc(var(--hw-combobox-width) / 2);
|
280
|
+
flex-grow: 1;
|
281
|
+
line-height: var(--hw-line-height--multiple);
|
282
|
+
max-width: 100%;
|
283
|
+
width: 1rem;
|
284
|
+
}
|
285
|
+
}
|
@@ -44,7 +44,15 @@ module HotwireCombobox::Component::Customizable
|
|
44
44
|
|
45
45
|
def apply_customizations_to(element, base: {})
|
46
46
|
custom = custom_attrs[element]
|
47
|
-
|
47
|
+
|
48
|
+
coalesce = ->(key, value) do
|
49
|
+
if value.is_a?(String) || value.is_a?(Array)
|
50
|
+
view.token_list(value, custom.delete(key))
|
51
|
+
else
|
52
|
+
value
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
48
56
|
default = base.map { |k, v| [ k, coalesce.(k, v) ] }.to_h
|
49
57
|
|
50
58
|
custom.deep_merge default
|