hotwire_combobox 0.1.43 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +2 -1
- data/app/assets/javascripts/controllers/hw_combobox_controller.js +29 -3
- data/app/assets/javascripts/hotwire_combobox.esm.js +442 -104
- data/app/assets/javascripts/hotwire_combobox.umd.js +442 -104
- 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/dialog.js +1 -1
- data/app/assets/javascripts/hw_combobox/models/combobox/events.js +21 -11
- data/app/assets/javascripts/hw_combobox/models/combobox/filtering.js +20 -12
- 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 +19 -7
- data/app/assets/javascripts/hw_combobox/models/combobox/selection.js +50 -49
- data/app/assets/javascripts/hw_combobox/models/combobox/toggle.js +33 -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 +90 -19
- data/app/presenters/hotwire_combobox/component/customizable.rb +9 -1
- data/app/presenters/hotwire_combobox/component.rb +106 -32
- data/app/presenters/hotwire_combobox/listbox/group.rb +47 -0
- data/app/presenters/hotwire_combobox/listbox/item/collection.rb +14 -0
- data/app/presenters/hotwire_combobox/listbox/item.rb +111 -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 +112 -91
- data/lib/hotwire_combobox/version.rb +1 -1
- metadata +11 -3
@@ -6,22 +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()
|
12
22
|
this._clearInvalidQuery()
|
13
23
|
|
14
24
|
this.expandedValue = false
|
15
25
|
|
16
|
-
this.
|
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
|
+
}
|
17
35
|
}
|
18
36
|
}
|
19
37
|
|
20
38
|
toggle() {
|
21
39
|
if (this.expandedValue) {
|
22
|
-
this.
|
40
|
+
this._closeAndBlur("hw:toggle")
|
23
41
|
} else {
|
24
|
-
this.
|
42
|
+
this.openByFocusing()
|
25
43
|
}
|
26
44
|
}
|
27
45
|
|
@@ -32,14 +50,14 @@ Combobox.Toggle = Base => class extends Base {
|
|
32
50
|
if (this.mainWrapperTarget.contains(target) && !this._isDialogDismisser(target)) return
|
33
51
|
if (this._withinElementBounds(event)) return
|
34
52
|
|
35
|
-
this.
|
53
|
+
this._closeAndBlur("hw:clickOutside")
|
36
54
|
}
|
37
55
|
|
38
56
|
closeOnFocusOutside({ target }) {
|
39
57
|
if (!this._isOpen) return
|
40
58
|
if (this.element.contains(target)) return
|
41
59
|
|
42
|
-
this.
|
60
|
+
this._closeAndBlur("hw:focusOutside")
|
43
61
|
}
|
44
62
|
|
45
63
|
clearOrToggleOnHandleClick() {
|
@@ -51,6 +69,11 @@ Combobox.Toggle = Base => class extends Base {
|
|
51
69
|
}
|
52
70
|
}
|
53
71
|
|
72
|
+
_closeAndBlur(inputType) {
|
73
|
+
this.close(inputType)
|
74
|
+
this._actingCombobox.blur()
|
75
|
+
}
|
76
|
+
|
54
77
|
// Some browser extensions like 1Password overlay elements on top of the combobox.
|
55
78
|
// Hovering over these elements emits a click event for some reason.
|
56
79
|
// These events don't contain any telling information, so we use `_withinElementBounds`
|
@@ -62,18 +85,16 @@ Combobox.Toggle = Base => class extends Base {
|
|
62
85
|
return clientX >= left && clientX <= right && clientY >= top && clientY <= bottom
|
63
86
|
}
|
64
87
|
|
65
|
-
_openByFocusing() {
|
66
|
-
this._actingCombobox.focus()
|
67
|
-
}
|
68
|
-
|
69
88
|
_isDialogDismisser(target) {
|
70
89
|
return target.closest("dialog") && target.role != "combobox"
|
71
90
|
}
|
72
91
|
|
73
92
|
_expand() {
|
74
|
-
if (this.
|
93
|
+
if (this._isSync) {
|
94
|
+
this._preselectSingle()
|
95
|
+
}
|
75
96
|
|
76
|
-
if (this._autocompletesList && this.
|
97
|
+
if (this._autocompletesList && this._isSmallViewport) {
|
77
98
|
this._openInDialog()
|
78
99
|
} else {
|
79
100
|
this._openInline()
|
@@ -134,8 +155,4 @@ Combobox.Toggle = Base => class extends Base {
|
|
134
155
|
get _isOpen() {
|
135
156
|
return this.expandedValue
|
136
157
|
}
|
137
|
-
|
138
|
-
get _preselectOnExpansion() {
|
139
|
-
return !this._isAsync // async comboboxes preselect based on callbacks
|
140
|
-
}
|
141
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,8 @@
|
|
1
1
|
:root {
|
2
2
|
--hw-active-bg-color: #F3F4F6;
|
3
3
|
--hw-border-color: #D1D5DB;
|
4
|
+
--hw-group-color: #57595C;
|
5
|
+
--hw-group-bg-color: #FFFFFF;
|
4
6
|
--hw-invalid-color: #EF4444;
|
5
7
|
--hw-dialog-label-color: #1D1D1D;
|
6
8
|
--hw-focus-color: #2563EB;
|
@@ -10,6 +12,9 @@
|
|
10
12
|
--hw-border-width--slim: 1px;
|
11
13
|
--hw-border-width--thick: 2px;
|
12
14
|
|
15
|
+
--hw-combobox-width: 10rem;
|
16
|
+
--hw-combobox-width--multiple: 30rem;
|
17
|
+
|
13
18
|
--hw-dialog-font-size: 1.25rem;
|
14
19
|
--hw-dialog-input-height: 2.5rem;
|
15
20
|
--hw-dialog-label-alignment: center;
|
@@ -24,20 +29,21 @@
|
|
24
29
|
--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
30
|
--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
31
|
--hw-handle-offset-right: 0.375rem;
|
27
|
-
--hw-handle-width: 1.
|
28
|
-
--hw-handle-width--queried:
|
29
|
-
|
30
|
-
--hw-combobox-width: 10rem;
|
32
|
+
--hw-handle-width: 1.5rem;
|
33
|
+
--hw-handle-width--queried: 1rem;
|
31
34
|
|
32
35
|
--hw-line-height: 1.5rem;
|
36
|
+
--hw-line-height--multiple: 2.125rem;
|
33
37
|
|
34
38
|
--hw-listbox-height: calc(var(--hw-line-height) * 10);
|
35
|
-
--hw-listbox-offset-top: calc(var(--hw-line-height) * 1.625);
|
36
39
|
--hw-listbox-z-index: 10;
|
37
40
|
|
41
|
+
--hw-padding--slimmer: 0.25rem;
|
38
42
|
--hw-padding--slim: 0.375rem;
|
39
43
|
--hw-padding--thick: 0.75rem;
|
40
44
|
|
45
|
+
--hw-selection-chip-font-size: 0.875rem;
|
46
|
+
|
41
47
|
--hw-visual-viewport-height: 100vh;
|
42
48
|
}
|
43
49
|
|
@@ -57,30 +63,38 @@
|
|
57
63
|
}
|
58
64
|
|
59
65
|
.hw-combobox__main__wrapper {
|
66
|
+
border: var(--hw-border-width--slim) solid var(--hw-border-color);
|
67
|
+
border-radius: var(--hw-border-radius);
|
68
|
+
padding: var(--hw-padding--slim) calc(var(--hw-handle-width) + var(--hw-padding--slimmer)) var(--hw-padding--slim) var(--hw-padding--thick);
|
60
69
|
position: relative;
|
61
|
-
width:
|
70
|
+
width: var(--hw-combobox-width);
|
71
|
+
|
72
|
+
&:focus-within {
|
73
|
+
box-shadow: 0 0 0 var(--hw-border-width--thick) var(--hw-focus-color);
|
74
|
+
}
|
75
|
+
|
76
|
+
&:has(.hw-combobox__input--invalid) {
|
77
|
+
box-shadow: 0 0 0 var(--hw-border-width--thick) var(--hw-invalid-color);
|
78
|
+
}
|
62
79
|
}
|
63
80
|
|
64
81
|
.hw-combobox__input {
|
65
|
-
border:
|
66
|
-
border-radius: var(--hw-border-radius);
|
82
|
+
border: none;
|
67
83
|
font-size: inherit;
|
68
84
|
line-height: var(--hw-line-height);
|
69
|
-
|
70
|
-
|
71
|
-
width: var(--hw-combobox-width);
|
85
|
+
min-width: 0;
|
86
|
+
padding: 0;
|
72
87
|
text-overflow: ellipsis;
|
88
|
+
width: 100%;
|
73
89
|
}
|
74
90
|
|
75
|
-
.hw-combobox__input:focus
|
76
|
-
|
91
|
+
.hw-combobox__input:focus,
|
92
|
+
.hw-combobox__input:focus-visible,
|
93
|
+
.hw-combobox__input:focus-within {
|
94
|
+
box-shadow: none;
|
77
95
|
outline: none;
|
78
96
|
}
|
79
97
|
|
80
|
-
.hw-combobox__input--invalid {
|
81
|
-
border: var(--hw-border-width--slim) solid var(--hw-invalid-color);
|
82
|
-
}
|
83
|
-
|
84
98
|
.hw-combobox__handle {
|
85
99
|
height: 100%;
|
86
100
|
position: absolute;
|
@@ -105,7 +119,6 @@
|
|
105
119
|
.hw-combobox__input[data-queried] + .hw-combobox__handle::before {
|
106
120
|
background-image: var(--hw-handle-image--queried);
|
107
121
|
background-size: var(--hw-handle-width--queried);
|
108
|
-
cursor: pointer;
|
109
122
|
}
|
110
123
|
|
111
124
|
.hw-combobox__listbox {
|
@@ -121,7 +134,7 @@
|
|
121
134
|
overflow: auto;
|
122
135
|
padding: 0;
|
123
136
|
position: absolute;
|
124
|
-
top:
|
137
|
+
top: calc(100% + 0.2rem);
|
125
138
|
width: 100%;
|
126
139
|
z-index: var(--hw-listbox-z-index);
|
127
140
|
|
@@ -130,6 +143,21 @@
|
|
130
143
|
}
|
131
144
|
}
|
132
145
|
|
146
|
+
.hw-combobox__group {
|
147
|
+
display: none;
|
148
|
+
padding: 0;
|
149
|
+
}
|
150
|
+
|
151
|
+
.hw-combobox__group__label {
|
152
|
+
background-color: var(--hw-group-bg-color);
|
153
|
+
color: var(--hw-group-color);
|
154
|
+
padding: var(--hw-padding--slim);
|
155
|
+
}
|
156
|
+
|
157
|
+
.hw-combobox__group:has(.hw-combobox__option:not([hidden])) {
|
158
|
+
display: block;
|
159
|
+
}
|
160
|
+
|
133
161
|
.hw-combobox__option {
|
134
162
|
background-color: var(--hw-option-bg-color);
|
135
163
|
padding: var(--hw-padding--slim) var(--hw-padding--thick);
|
@@ -144,6 +172,7 @@
|
|
144
172
|
}
|
145
173
|
|
146
174
|
.hw-combobox__option:hover,
|
175
|
+
.hw-combobox__option--navigated,
|
147
176
|
.hw-combobox__option--selected {
|
148
177
|
background-color: var(--hw-active-bg-color);
|
149
178
|
}
|
@@ -217,3 +246,45 @@
|
|
217
246
|
padding: var(--hw-padding--thick);
|
218
247
|
}
|
219
248
|
}
|
249
|
+
|
250
|
+
.hw-combobox__chip {
|
251
|
+
align-items: center;
|
252
|
+
border: var(--hw-border-width--slim) solid var(--hw-border-color);
|
253
|
+
border-radius: var(--hw-border-radius);
|
254
|
+
display: flex;
|
255
|
+
font-size: var(--hw-selection-chip-font-size);
|
256
|
+
line-height: var(--hw-line-height);
|
257
|
+
padding: var(--hw-padding--slimmer);
|
258
|
+
padding-left: var(--hw-padding--slim);
|
259
|
+
}
|
260
|
+
|
261
|
+
.hw-combobox__chip__remover {
|
262
|
+
background-image: var(--hw-handle-image--queried);
|
263
|
+
background-size: var(--hw-handle-width--queried);
|
264
|
+
background-repeat: no-repeat;
|
265
|
+
margin-left: var(--hw-padding--slimmer);
|
266
|
+
min-height: var(--hw-handle-width--queried);
|
267
|
+
min-width: var(--hw-handle-width--queried);
|
268
|
+
}
|
269
|
+
|
270
|
+
.hw-combobox--multiple {
|
271
|
+
.hw-combobox__main__wrapper {
|
272
|
+
align-items: center;
|
273
|
+
display: flex;
|
274
|
+
flex-wrap: wrap;
|
275
|
+
gap: var(--hw-padding--slimmer);
|
276
|
+
width: var(--hw-combobox-width--multiple);
|
277
|
+
|
278
|
+
&:has([data-hw-combobox-chip]) .hw-combobox__input::placeholder {
|
279
|
+
color: transparent;
|
280
|
+
}
|
281
|
+
}
|
282
|
+
|
283
|
+
.hw-combobox__input {
|
284
|
+
min-width: calc(var(--hw-combobox-width) / 2);
|
285
|
+
flex-grow: 1;
|
286
|
+
line-height: var(--hw-line-height--multiple);
|
287
|
+
max-width: 100%;
|
288
|
+
width: 1rem;
|
289
|
+
}
|
290
|
+
}
|
@@ -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
|
@@ -2,33 +2,39 @@ require "securerandom"
|
|
2
2
|
|
3
3
|
class HotwireCombobox::Component
|
4
4
|
include Customizable
|
5
|
+
include ActiveModel::Validations
|
5
6
|
|
6
7
|
attr_reader :options, :label
|
7
8
|
|
9
|
+
validate :name_when_new_on_multiselect_must_match_original_name
|
10
|
+
|
8
11
|
def initialize \
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
12
|
+
view, name,
|
13
|
+
association_name: nil,
|
14
|
+
async_src: nil,
|
15
|
+
autocomplete: :both,
|
16
|
+
data: {},
|
17
|
+
dialog_label: nil,
|
18
|
+
form: nil,
|
19
|
+
id: nil,
|
20
|
+
input: {},
|
21
|
+
label: nil,
|
22
|
+
mobile_at: "640px",
|
23
|
+
multiselect_chip_src: nil,
|
24
|
+
name_when_new: nil,
|
25
|
+
open: false,
|
26
|
+
options: [],
|
27
|
+
value: nil,
|
28
|
+
**rest
|
25
29
|
@view, @autocomplete, @id, @name, @value, @form, @async_src, @label,
|
26
|
-
@name_when_new, @open, @data, @mobile_at, @options, @dialog_label =
|
30
|
+
@name_when_new, @open, @data, @mobile_at, @multiselect_chip_src, @options, @dialog_label =
|
27
31
|
view, autocomplete, id, name.to_s, value, form, async_src, label,
|
28
|
-
name_when_new, open, data, mobile_at, options, dialog_label
|
32
|
+
name_when_new, open, data, mobile_at, multiselect_chip_src, options, dialog_label
|
29
33
|
|
30
34
|
@combobox_attrs = input.reverse_merge(rest).deep_symbolize_keys
|
31
35
|
@association_name = association_name || infer_association_name
|
36
|
+
|
37
|
+
validate!
|
32
38
|
end
|
33
39
|
|
34
40
|
def render_in(view_context, &block)
|
@@ -39,7 +45,7 @@ class HotwireCombobox::Component
|
|
39
45
|
|
40
46
|
def fieldset_attrs
|
41
47
|
apply_customizations_to :fieldset, base: {
|
42
|
-
class: "hw-combobox",
|
48
|
+
class: [ "hw-combobox", { "hw-combobox--multiple": multiselect? } ],
|
43
49
|
data: fieldset_data
|
44
50
|
}
|
45
51
|
end
|
@@ -72,6 +78,23 @@ class HotwireCombobox::Component
|
|
72
78
|
end
|
73
79
|
|
74
80
|
|
81
|
+
def announcer_attrs
|
82
|
+
{
|
83
|
+
style: "
|
84
|
+
position: absolute;
|
85
|
+
width: 1px;
|
86
|
+
height: 1px;
|
87
|
+
margin: -1px;
|
88
|
+
padding: 0;
|
89
|
+
overflow: hidden;
|
90
|
+
clip: rect(0, 0, 0, 0);
|
91
|
+
border: 0;".squish,
|
92
|
+
aria: announcer_aria,
|
93
|
+
data: announcer_data
|
94
|
+
}
|
95
|
+
end
|
96
|
+
|
97
|
+
|
75
98
|
def input_attrs
|
76
99
|
nested_attrs = %i[ data aria ]
|
77
100
|
|
@@ -103,7 +126,8 @@ class HotwireCombobox::Component
|
|
103
126
|
role: :listbox,
|
104
127
|
class: "hw-combobox__listbox",
|
105
128
|
hidden: "",
|
106
|
-
data: listbox_data
|
129
|
+
data: listbox_data,
|
130
|
+
aria: listbox_aria
|
107
131
|
}
|
108
132
|
end
|
109
133
|
|
@@ -150,7 +174,8 @@ class HotwireCombobox::Component
|
|
150
174
|
id: dialog_listbox_id,
|
151
175
|
class: "hw-combobox__dialog__listbox",
|
152
176
|
role: :listbox,
|
153
|
-
data: dialog_listbox_data
|
177
|
+
data: dialog_listbox_data,
|
178
|
+
aria: dialog_listbox_aria
|
154
179
|
}
|
155
180
|
end
|
156
181
|
|
@@ -173,10 +198,23 @@ class HotwireCombobox::Component
|
|
173
198
|
private
|
174
199
|
attr_reader :view, :autocomplete, :id, :name, :value, :form,
|
175
200
|
:name_when_new, :open, :data, :combobox_attrs, :mobile_at,
|
176
|
-
:association_name
|
201
|
+
:association_name, :multiselect_chip_src
|
202
|
+
|
203
|
+
def name_when_new_on_multiselect_must_match_original_name
|
204
|
+
return unless multiselect? && name_when_new.present?
|
205
|
+
|
206
|
+
unless name_when_new.to_s == name
|
207
|
+
errors.add :name_when_new, :must_match_original_name,
|
208
|
+
message: "must match the regular name ('#{name}', in this case) on multiselect comboboxes."
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def multiselect?
|
213
|
+
multiselect_chip_src.present?
|
214
|
+
end
|
177
215
|
|
178
216
|
def infer_association_name
|
179
|
-
if name.
|
217
|
+
if name.end_with?("_id")
|
180
218
|
name.sub(/_id\z/, "")
|
181
219
|
end
|
182
220
|
end
|
@@ -192,6 +230,7 @@ class HotwireCombobox::Component
|
|
192
230
|
hw_combobox_small_viewport_max_width_value: mobile_at,
|
193
231
|
hw_combobox_async_src_value: async_src,
|
194
232
|
hw_combobox_prefilled_display_value: prefilled_display,
|
233
|
+
hw_combobox_selection_chip_src_value: multiselect_chip_src,
|
195
234
|
hw_combobox_filterable_attribute_value: "data-filterable-as",
|
196
235
|
hw_combobox_autocompletable_attribute_value: "data-autocompletable-as",
|
197
236
|
hw_combobox_selected_class: "hw-combobox__option--selected",
|
@@ -199,21 +238,29 @@ class HotwireCombobox::Component
|
|
199
238
|
end
|
200
239
|
|
201
240
|
def prefilled_display
|
241
|
+
return if multiselect? || !hidden_field_value
|
242
|
+
|
202
243
|
if async_src && associated_object
|
203
244
|
associated_object.to_combobox_display
|
204
|
-
elsif
|
205
|
-
|
245
|
+
elsif async_src && form_object&.respond_to?(name)
|
246
|
+
form_object.public_send name
|
247
|
+
else
|
248
|
+
options.find_by_value(hidden_field_value)&.autocompletable_as
|
206
249
|
end
|
207
250
|
end
|
208
251
|
|
209
252
|
def associated_object
|
210
253
|
@associated_object ||= if association_exists?
|
211
|
-
|
254
|
+
form_object&.public_send association_name
|
212
255
|
end
|
213
256
|
end
|
214
257
|
|
215
258
|
def association_exists?
|
216
|
-
|
259
|
+
form_object&.class&.reflect_on_association(association_name).present?
|
260
|
+
end
|
261
|
+
|
262
|
+
def form_object
|
263
|
+
form&.object
|
217
264
|
end
|
218
265
|
|
219
266
|
def async_src
|
@@ -227,7 +274,22 @@ class HotwireCombobox::Component
|
|
227
274
|
|
228
275
|
|
229
276
|
def main_wrapper_data
|
230
|
-
{
|
277
|
+
{
|
278
|
+
action: ("click->hw-combobox#openByFocusing:self" if multiselect?),
|
279
|
+
hw_combobox_target: "mainWrapper"
|
280
|
+
}
|
281
|
+
end
|
282
|
+
|
283
|
+
|
284
|
+
def announcer_aria
|
285
|
+
{
|
286
|
+
live: :polite,
|
287
|
+
atomic: true
|
288
|
+
}
|
289
|
+
end
|
290
|
+
|
291
|
+
def announcer_data
|
292
|
+
{ hw_combobox_target: "announcer" }
|
231
293
|
end
|
232
294
|
|
233
295
|
|
@@ -246,10 +308,12 @@ class HotwireCombobox::Component
|
|
246
308
|
def hidden_field_value
|
247
309
|
return value if value
|
248
310
|
|
249
|
-
if
|
250
|
-
|
311
|
+
if form_object&.defined_enums&.try :[], name
|
312
|
+
form_object.public_send "#{name}_before_type_cast"
|
251
313
|
else
|
252
|
-
|
314
|
+
form_object&.try(name).then do |value|
|
315
|
+
value.respond_to?(:map) ? value.join(",") : value
|
316
|
+
end
|
253
317
|
end
|
254
318
|
end
|
255
319
|
|
@@ -270,7 +334,9 @@ class HotwireCombobox::Component
|
|
270
334
|
keydown->hw-combobox#navigate
|
271
335
|
click@window->hw-combobox#closeOnClickOutside
|
272
336
|
focusin@window->hw-combobox#closeOnFocusOutside
|
273
|
-
turbo:before-stream-render@document->hw-combobox#rerouteListboxStreamToDialog
|
337
|
+
turbo:before-stream-render@document->hw-combobox#rerouteListboxStreamToDialog
|
338
|
+
turbo:before-cache@document->hw-combobox#hideChipsForCache
|
339
|
+
turbo:morph-element->hw-combobox#idempotentConnect".squish,
|
274
340
|
hw_combobox_target: "combobox",
|
275
341
|
async_id: canonical_id
|
276
342
|
end
|
@@ -301,6 +367,10 @@ class HotwireCombobox::Component
|
|
301
367
|
{ hw_combobox_target: "listbox" }
|
302
368
|
end
|
303
369
|
|
370
|
+
def listbox_aria
|
371
|
+
{ multiselectable: multiselect? }
|
372
|
+
end
|
373
|
+
|
304
374
|
|
305
375
|
def dialog_data
|
306
376
|
{
|
@@ -340,6 +410,10 @@ class HotwireCombobox::Component
|
|
340
410
|
{ hw_combobox_target: "dialogListbox" }
|
341
411
|
end
|
342
412
|
|
413
|
+
def dialog_listbox_aria
|
414
|
+
{ multiselectable: multiselect? }
|
415
|
+
end
|
416
|
+
|
343
417
|
def dialog_focus_trap_data
|
344
418
|
{ hw_combobox_target: "dialogFocusTrap" }
|
345
419
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require "securerandom"
|
2
|
+
|
3
|
+
class HotwireCombobox::Listbox::Group
|
4
|
+
attr_reader :options
|
5
|
+
|
6
|
+
def initialize(name, options:)
|
7
|
+
@name = name
|
8
|
+
@options = options
|
9
|
+
end
|
10
|
+
|
11
|
+
def render_in(view)
|
12
|
+
view.tag.ul **group_attrs do
|
13
|
+
view.concat view.tag.li(name, **label_attrs)
|
14
|
+
|
15
|
+
options.map do |option|
|
16
|
+
view.concat view.render(option)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
attr_reader :name
|
23
|
+
|
24
|
+
def id
|
25
|
+
@id ||= SecureRandom.uuid
|
26
|
+
end
|
27
|
+
|
28
|
+
def group_attrs
|
29
|
+
{
|
30
|
+
class: "hw-combobox__group",
|
31
|
+
role: :group,
|
32
|
+
aria: group_aria
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def group_aria
|
37
|
+
{ labelledby: id }
|
38
|
+
end
|
39
|
+
|
40
|
+
def label_attrs
|
41
|
+
{
|
42
|
+
id: id,
|
43
|
+
class: "hw-combobox__group__label",
|
44
|
+
role: :presentation
|
45
|
+
}
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class HotwireCombobox::Listbox::Item::Collection < Array
|
2
|
+
def find_by_value(value)
|
3
|
+
if grouped?
|
4
|
+
flat_map { |item| item.options }.find { |option| option.value == value }
|
5
|
+
else
|
6
|
+
find { |option| option.value == value }
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
def grouped?
|
12
|
+
first.is_a? HotwireCombobox::Listbox::Group
|
13
|
+
end
|
14
|
+
end
|