hotwire_combobox 0.1.42 → 0.2.0
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.
- 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
|