hotwire_combobox 0.3.2 → 0.4.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/MIT-LICENSE +1 -1
- data/README.md +19 -15
- data/app/assets/config/hw_combobox_manifest.js +1 -1
- data/app/assets/javascripts/controllers/hw_combobox_controller.js +5 -13
- data/app/assets/javascripts/hotwire_combobox.esm.js +67 -66
- data/app/assets/javascripts/hw_combobox/helpers.js +9 -0
- data/app/assets/javascripts/hw_combobox/models/combobox/autocomplete.js +14 -4
- data/app/assets/javascripts/hw_combobox/models/combobox/callbacks.js +2 -1
- data/app/assets/javascripts/hw_combobox/models/combobox/dialog.js +1 -4
- data/app/assets/javascripts/hw_combobox/models/combobox/filtering.js +10 -6
- data/app/assets/javascripts/hw_combobox/models/combobox/form_field.js +1 -2
- data/app/assets/javascripts/hw_combobox/models/combobox/multiselect.js +6 -3
- data/app/assets/javascripts/hw_combobox/models/combobox/navigation.js +2 -2
- data/app/assets/javascripts/hw_combobox/models/combobox/selection.js +4 -12
- data/app/assets/javascripts/hw_combobox/models/combobox/toggle.js +13 -18
- data/app/assets/stylesheets/hotwire_combobox.css +12 -2
- data/app/presenters/hotwire_combobox/component/announced.rb +5 -0
- data/app/presenters/hotwire_combobox/component/associations.rb +18 -0
- data/app/presenters/hotwire_combobox/component/async.rb +6 -0
- data/app/presenters/hotwire_combobox/component/customizable.rb +8 -20
- data/app/presenters/hotwire_combobox/component/freetext.rb +26 -0
- data/app/presenters/hotwire_combobox/component/markup/dialog.rb +57 -0
- data/app/presenters/hotwire_combobox/component/markup/fieldset.rb +46 -0
- data/app/presenters/hotwire_combobox/component/markup/form.rb +6 -0
- data/app/presenters/hotwire_combobox/component/markup/handle.rb +7 -0
- data/app/presenters/hotwire_combobox/component/markup/hidden_field.rb +28 -0
- data/app/presenters/hotwire_combobox/component/markup/input.rb +44 -0
- data/app/presenters/hotwire_combobox/component/markup/label.rb +5 -0
- data/app/presenters/hotwire_combobox/component/markup/listbox.rb +14 -0
- data/app/presenters/hotwire_combobox/component/markup/wrapper.rb +7 -0
- data/app/presenters/hotwire_combobox/component/multiselect.rb +6 -0
- data/app/presenters/hotwire_combobox/component/paginated.rb +18 -0
- data/app/presenters/hotwire_combobox/component.rb +32 -398
- data/app/presenters/hotwire_combobox/listbox/group.rb +3 -15
- data/app/presenters/hotwire_combobox/listbox/item.rb +5 -12
- data/app/presenters/hotwire_combobox/listbox/option.rb +3 -20
- data/app/views/hotwire_combobox/_component.html.erb +28 -6
- data/app/views/hotwire_combobox/_pagination.html.erb +2 -2
- data/config/hw_importmap.rb +1 -1
- data/lib/hotwire_combobox/helper.rb +24 -65
- data/lib/hotwire_combobox/platform.rb +15 -0
- data/lib/hotwire_combobox/version.rb +1 -1
- data/lib/hotwire_combobox.rb +1 -0
- metadata +33 -11
- data/app/assets/javascripts/hotwire_combobox.umd.js +0 -1843
- data/app/views/hotwire_combobox/combobox/_dialog.html.erb +0 -9
- data/app/views/hotwire_combobox/combobox/_hidden_field.html.erb +0 -4
- data/app/views/hotwire_combobox/combobox/_input.html.erb +0 -2
- data/app/views/hotwire_combobox/combobox/_paginated_listbox.html.erb +0 -9
@@ -31,11 +31,11 @@ Combobox.Navigation = Base => class extends Base {
|
|
31
31
|
cancel(event)
|
32
32
|
},
|
33
33
|
Enter: (event) => {
|
34
|
-
this.
|
34
|
+
this.close("hw:keyHandler:enter")
|
35
35
|
cancel(event)
|
36
36
|
},
|
37
37
|
Escape: (event) => {
|
38
|
-
this.
|
38
|
+
this._isOpen ? this.close("hw:keyHandler:escape") : this._clearQuery()
|
39
39
|
cancel(event)
|
40
40
|
},
|
41
41
|
Backspace: (event) => {
|
@@ -4,7 +4,7 @@ import { wrapAroundAccess, isDeleteEvent } from "hw_combobox/helpers"
|
|
4
4
|
Combobox.Selection = Base => class extends Base {
|
5
5
|
selectOnClick({ currentTarget, inputType }) {
|
6
6
|
this._forceSelectionAndFilter(currentTarget, inputType)
|
7
|
-
this.
|
7
|
+
this.close("hw:optionRoleClick")
|
8
8
|
}
|
9
9
|
|
10
10
|
_connectSelection() {
|
@@ -20,9 +20,9 @@ Combobox.Selection = Base => class extends Base {
|
|
20
20
|
} else if (isDeleteEvent({ inputType: inputType })) {
|
21
21
|
this._deselect()
|
22
22
|
} else if (inputType === "hw:lockInSelection" && this._ensurableOption) {
|
23
|
-
this.
|
23
|
+
this._select(this._ensurableOption, this._softAutocomplete.bind(this))
|
24
24
|
} else if (this._isOpen && this._visibleOptionElements[0]) {
|
25
|
-
this.
|
25
|
+
this._select(this._visibleOptionElements[0], this._softAutocomplete.bind(this))
|
26
26
|
} else if (this._isOpen) {
|
27
27
|
this._resetOptionsAndNotify()
|
28
28
|
this._markInvalid()
|
@@ -98,21 +98,13 @@ Combobox.Selection = Base => class extends Base {
|
|
98
98
|
}
|
99
99
|
}
|
100
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
101
|
_forceSelectionAndFilter(option, inputType) {
|
110
102
|
this._forceSelectionWithoutFiltering(option)
|
111
103
|
this._filter(inputType)
|
112
104
|
}
|
113
105
|
|
114
106
|
_forceSelectionWithoutFiltering(option) {
|
115
|
-
this.
|
107
|
+
this._select(option, this._hardAutocomplete.bind(this))
|
116
108
|
}
|
117
109
|
|
118
110
|
_lockInSelection() {
|
@@ -6,10 +6,6 @@ Combobox.Toggle = Base => class extends Base {
|
|
6
6
|
this.expandedValue = true
|
7
7
|
}
|
8
8
|
|
9
|
-
openByFocusing() {
|
10
|
-
this._actingCombobox.focus()
|
11
|
-
}
|
12
|
-
|
13
9
|
close(inputType) {
|
14
10
|
if (this._isOpen) {
|
15
11
|
const shouldReopen = this._isMultiselect &&
|
@@ -24,9 +20,8 @@ Combobox.Toggle = Base => class extends Base {
|
|
24
20
|
|
25
21
|
this.expandedValue = false
|
26
22
|
|
27
|
-
this._dispatchSelectionEvent()
|
28
|
-
|
29
23
|
if (inputType != "hw:keyHandler:escape") {
|
24
|
+
this._dispatchSelectionEvent()
|
30
25
|
this._createChip(shouldReopen)
|
31
26
|
}
|
32
27
|
|
@@ -38,43 +33,38 @@ Combobox.Toggle = Base => class extends Base {
|
|
38
33
|
|
39
34
|
toggle() {
|
40
35
|
if (this.expandedValue) {
|
41
|
-
this.
|
36
|
+
this.close("hw:toggle")
|
42
37
|
} else {
|
43
|
-
this.
|
38
|
+
this.open()
|
44
39
|
}
|
45
40
|
}
|
46
41
|
|
47
42
|
closeOnClickOutside(event) {
|
48
43
|
const target = event.target
|
49
44
|
|
50
|
-
if (
|
45
|
+
if (this._isClosed) return
|
51
46
|
if (this.mainWrapperTarget.contains(target) && !this._isDialogDismisser(target)) return
|
52
47
|
if (this._withinElementBounds(event)) return
|
53
48
|
|
54
|
-
this.
|
49
|
+
this.close("hw:clickOutside")
|
55
50
|
}
|
56
51
|
|
57
52
|
closeOnFocusOutside({ target }) {
|
58
|
-
if (
|
53
|
+
if (this._isClosed) return
|
59
54
|
if (this.element.contains(target)) return
|
60
55
|
|
61
|
-
this.
|
56
|
+
this.close("hw:focusOutside")
|
62
57
|
}
|
63
58
|
|
64
59
|
clearOrToggleOnHandleClick() {
|
65
60
|
if (this._isQueried) {
|
66
61
|
this._clearQuery()
|
67
|
-
this.
|
62
|
+
this.open()
|
68
63
|
} else {
|
69
64
|
this.toggle()
|
70
65
|
}
|
71
66
|
}
|
72
67
|
|
73
|
-
_closeAndBlur(inputType) {
|
74
|
-
this.close(inputType)
|
75
|
-
this._actingCombobox.blur()
|
76
|
-
}
|
77
|
-
|
78
68
|
// Some browser extensions like 1Password overlay elements on top of the combobox.
|
79
69
|
// Hovering over these elements emits a click event for some reason.
|
80
70
|
// These events don't contain any telling information, so we use `_withinElementBounds`
|
@@ -121,6 +111,7 @@ Combobox.Toggle = Base => class extends Base {
|
|
121
111
|
this._preventFocusingComboboxAfterClosingDialog()
|
122
112
|
this._preventBodyScroll()
|
123
113
|
this.dialogTarget.showModal()
|
114
|
+
this._resizeDialog()
|
124
115
|
}
|
125
116
|
|
126
117
|
_openInline() {
|
@@ -156,4 +147,8 @@ Combobox.Toggle = Base => class extends Base {
|
|
156
147
|
get _isOpen() {
|
157
148
|
return this.expandedValue
|
158
149
|
}
|
150
|
+
|
151
|
+
get _isClosed() {
|
152
|
+
return !this._isOpen
|
153
|
+
}
|
159
154
|
}
|
@@ -1,6 +1,7 @@
|
|
1
1
|
:root {
|
2
2
|
--hw-active-bg-color: #F3F4F6;
|
3
3
|
--hw-border-color: #D1D5DB;
|
4
|
+
--hw-component-bg-color: #FFFFFF;
|
4
5
|
--hw-group-color: #57595C;
|
5
6
|
--hw-group-bg-color: #FFFFFF;
|
6
7
|
--hw-invalid-color: #EF4444;
|
@@ -22,7 +23,7 @@
|
|
22
23
|
--hw-dialog-label-size: 1.05rem;
|
23
24
|
--hw-dialog-listbox-margin: 1.25rem 0 0;
|
24
25
|
--hw-dialog-padding: 1rem 1rem 0;
|
25
|
-
--hw-dialog-top-offset:
|
26
|
+
--hw-dialog-top-offset: 18vh;
|
26
27
|
|
27
28
|
--hw-font-size: 1rem;
|
28
29
|
|
@@ -63,6 +64,7 @@
|
|
63
64
|
}
|
64
65
|
|
65
66
|
.hw-combobox__main__wrapper {
|
67
|
+
background-color: var(--hw-component-bg-color);
|
66
68
|
border: var(--hw-border-width--slim) solid var(--hw-border-color);
|
67
69
|
border-radius: var(--hw-border-radius);
|
68
70
|
padding: var(--hw-padding--slim) calc(var(--hw-handle-width) + var(--hw-padding--slimmer)) var(--hw-padding--slim) var(--hw-padding--thick);
|
@@ -201,7 +203,7 @@
|
|
201
203
|
overflow: hidden;
|
202
204
|
padding: var(--hw-dialog-padding);
|
203
205
|
pointer-events: auto;
|
204
|
-
position:
|
206
|
+
position: fixed;
|
205
207
|
top: var(--hw-dialog-top-offset);
|
206
208
|
width: auto;
|
207
209
|
|
@@ -215,6 +217,10 @@
|
|
215
217
|
&::backdrop {
|
216
218
|
background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.6) 50%, white 50%);
|
217
219
|
}
|
220
|
+
|
221
|
+
.hw-combobox--ios & {
|
222
|
+
position: absolute;
|
223
|
+
}
|
218
224
|
}
|
219
225
|
|
220
226
|
.hw-combobox__dialog__label {
|
@@ -291,4 +297,8 @@
|
|
291
297
|
|
292
298
|
.hw_combobox__pagination__wrapper {
|
293
299
|
background-color: var(--hw-option-bg-color);
|
300
|
+
|
301
|
+
&:only-child {
|
302
|
+
background-color: transparent;
|
303
|
+
}
|
294
304
|
}
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module HotwireCombobox::Component::Associations
|
2
|
+
private
|
3
|
+
def infer_association_name
|
4
|
+
if name.end_with?("_id")
|
5
|
+
name.sub(/_id\z/, "")
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def associated_object
|
10
|
+
@associated_object ||= if association_exists?
|
11
|
+
form_object&.public_send association_name
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def association_exists?
|
16
|
+
association_name && form_object&.respond_to?(association_name)
|
17
|
+
end
|
18
|
+
end
|
@@ -1,32 +1,20 @@
|
|
1
1
|
module HotwireCombobox::Component::Customizable
|
2
2
|
CUSTOMIZABLE_ELEMENTS = %i[
|
3
|
+
dialog dialog_input dialog_label dialog_listbox dialog_wrapper
|
3
4
|
fieldset
|
4
|
-
|
5
|
+
handle
|
5
6
|
hidden_field
|
6
|
-
main_wrapper
|
7
7
|
input
|
8
|
-
|
8
|
+
label
|
9
9
|
listbox
|
10
|
-
|
11
|
-
dialog_wrapper
|
12
|
-
dialog_label
|
13
|
-
dialog_input
|
14
|
-
dialog_listbox
|
10
|
+
main_wrapper
|
15
11
|
].freeze
|
16
12
|
|
17
|
-
PROTECTED_ATTRS = %i[
|
18
|
-
id
|
19
|
-
name
|
20
|
-
value
|
21
|
-
open
|
22
|
-
role
|
23
|
-
hidden
|
24
|
-
for
|
25
|
-
].freeze
|
13
|
+
PROTECTED_ATTRS = %i[ for hidden id name open role value ].freeze
|
26
14
|
|
27
15
|
CUSTOMIZABLE_ELEMENTS.each do |element|
|
28
16
|
define_method "customize_#{element}" do |**attrs|
|
29
|
-
|
17
|
+
store_customizations element, **attrs
|
30
18
|
end
|
31
19
|
end
|
32
20
|
|
@@ -35,14 +23,14 @@ module HotwireCombobox::Component::Customizable
|
|
35
23
|
@custom_attrs ||= Hash.new { |h, k| h[k] = {} }
|
36
24
|
end
|
37
25
|
|
38
|
-
def
|
26
|
+
def store_customizations(element, **attrs)
|
39
27
|
element = element.to_sym.presence_in(CUSTOMIZABLE_ELEMENTS)
|
40
28
|
sanitized_attrs = attrs.deep_symbolize_keys.except(*PROTECTED_ATTRS)
|
41
29
|
|
42
30
|
custom_attrs.store element, sanitized_attrs
|
43
31
|
end
|
44
32
|
|
45
|
-
def
|
33
|
+
def customize(element, base: {})
|
46
34
|
custom = custom_attrs[element]
|
47
35
|
|
48
36
|
coalesce = ->(key, value) do
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module HotwireCombobox::Component::Freetext
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
include ActiveModel::Validations
|
6
|
+
validate :name_when_new_on_multiselect_must_match_original_name
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
def name_when_new
|
11
|
+
if free_text && @name_when_new.blank?
|
12
|
+
hidden_field_name
|
13
|
+
else
|
14
|
+
@name_when_new
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def name_when_new_on_multiselect_must_match_original_name
|
19
|
+
return unless multiselect? && name_when_new.present?
|
20
|
+
|
21
|
+
unless name_when_new.to_s == hidden_field_name
|
22
|
+
errors.add :name_when_new, :must_match_original_name,
|
23
|
+
message: "must match the regular name ('#{hidden_field_name}', in this case) on multiselect comboboxes."
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module HotwireCombobox::Component::Markup::Dialog
|
2
|
+
def dialog_wrapper_attrs
|
3
|
+
customize :dialog_wrapper, base: { class: "hw-combobox__dialog__wrapper" }
|
4
|
+
end
|
5
|
+
|
6
|
+
def dialog_attrs
|
7
|
+
customize :dialog, base: {
|
8
|
+
class: "hw-combobox__dialog", role: :dialog, data: {
|
9
|
+
action: "keydown->hw-combobox#navigate", hw_combobox_target: "dialog" } }
|
10
|
+
end
|
11
|
+
|
12
|
+
def dialog_label
|
13
|
+
@dialog_label || label
|
14
|
+
end
|
15
|
+
|
16
|
+
def dialog_label_attrs
|
17
|
+
customize :dialog_label, base: { class: "hw-combobox__dialog__label", for: dialog_input_id }
|
18
|
+
end
|
19
|
+
|
20
|
+
def dialog_input_attrs
|
21
|
+
customize :dialog_input, base: {
|
22
|
+
id: dialog_input_id, role: :combobox, autofocus: "", type: input_type,
|
23
|
+
class: "hw-combobox__dialog__input", data: dialog_input_data, aria: dialog_input_aria }
|
24
|
+
end
|
25
|
+
|
26
|
+
def dialog_listbox_attrs
|
27
|
+
customize :dialog_listbox, base: {
|
28
|
+
id: dialog_listbox_id, role: :listbox, class: "hw-combobox__dialog__listbox",
|
29
|
+
data: { hw_combobox_target: "dialogListbox" }, aria: { multiselectable: multiselect? } }
|
30
|
+
end
|
31
|
+
|
32
|
+
def dialog_focus_trap_attrs
|
33
|
+
{ tabindex: "-1", data: { hw_combobox_target: "dialogFocusTrap" } }
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
def dialog_input_id
|
38
|
+
"#{canonical_id}-hw-dialog-combobox"
|
39
|
+
end
|
40
|
+
|
41
|
+
def dialog_input_data
|
42
|
+
{ action: "
|
43
|
+
keydown->hw-combobox#prepareToFilter
|
44
|
+
input->hw-combobox#filterAndSelect
|
45
|
+
keydown->hw-combobox#navigate
|
46
|
+
click@window->hw-combobox#closeOnClickOutside".squish,
|
47
|
+
hw_combobox_target: "dialogCombobox" }
|
48
|
+
end
|
49
|
+
|
50
|
+
def dialog_input_aria
|
51
|
+
{ controls: dialog_listbox_id, owns: dialog_listbox_id, autocomplete: autocomplete, activedescendant: "" }
|
52
|
+
end
|
53
|
+
|
54
|
+
def dialog_listbox_id
|
55
|
+
"#{canonical_id}-hw-dialog-listbox"
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module HotwireCombobox::Component::Markup::Fieldset
|
2
|
+
def fieldset_attrs
|
3
|
+
customize :fieldset, base: {
|
4
|
+
class: [ "hw-combobox", platform_classes, { "hw-combobox--multiple": multiselect? } ], data: fieldset_data }
|
5
|
+
end
|
6
|
+
|
7
|
+
private
|
8
|
+
def platform_classes
|
9
|
+
platform = HotwireCombobox::Platform.new request&.user_agent
|
10
|
+
|
11
|
+
view.token_list \
|
12
|
+
"hw-combobox--ios": platform.ios?,
|
13
|
+
"hw-combobox--android": platform.android?,
|
14
|
+
"hw-combobox--mobile-webkit": platform.mobile_webkit?
|
15
|
+
end
|
16
|
+
|
17
|
+
def fieldset_data
|
18
|
+
data.merge \
|
19
|
+
async_id: canonical_id,
|
20
|
+
controller: view.token_list("hw-combobox", data[:controller]),
|
21
|
+
hw_combobox_expanded_value: open,
|
22
|
+
hw_combobox_name_when_new_value: name_when_new,
|
23
|
+
hw_combobox_original_name_value: hidden_field_name,
|
24
|
+
hw_combobox_autocomplete_value: autocomplete,
|
25
|
+
hw_combobox_small_viewport_max_width_value: mobile_at,
|
26
|
+
hw_combobox_async_src_value: async_src,
|
27
|
+
hw_combobox_prefilled_display_value: prefilled_display,
|
28
|
+
hw_combobox_selection_chip_src_value: multiselect_chip_src,
|
29
|
+
hw_combobox_filterable_attribute_value: "data-filterable-as",
|
30
|
+
hw_combobox_autocompletable_attribute_value: "data-autocompletable-as",
|
31
|
+
hw_combobox_selected_class: "hw-combobox__option--selected",
|
32
|
+
hw_combobox_invalid_class: "hw-combobox__input--invalid"
|
33
|
+
end
|
34
|
+
|
35
|
+
def prefilled_display
|
36
|
+
return if multiselect? || !hidden_field_value
|
37
|
+
|
38
|
+
if async_src && associated_object
|
39
|
+
associated_object.to_combobox_display
|
40
|
+
elsif async_src && form_object&.respond_to?(name)
|
41
|
+
form_object.public_send name
|
42
|
+
else
|
43
|
+
options.find_by_value(hidden_field_value)&.autocompletable_as || hidden_field_value
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module HotwireCombobox::Component::Markup::HiddenField
|
2
|
+
def hidden_field_attrs
|
3
|
+
customize :hidden_field, base: {
|
4
|
+
id: hidden_field_id, name: hidden_field_name, value: hidden_field_value,
|
5
|
+
data: { hw_combobox_target: "hiddenField" } }
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
def hidden_field_id
|
10
|
+
"#{canonical_id}-hw-hidden-field"
|
11
|
+
end
|
12
|
+
|
13
|
+
def hidden_field_name
|
14
|
+
form&.field_name(name) || name
|
15
|
+
end
|
16
|
+
|
17
|
+
def hidden_field_value
|
18
|
+
return value if value
|
19
|
+
|
20
|
+
if form_object&.try(:defined_enums)&.try(:[], name)
|
21
|
+
form_object.public_send "#{name}_before_type_cast"
|
22
|
+
else
|
23
|
+
form_object&.try(name).then do |value|
|
24
|
+
value.respond_to?(:map) ? value.join(",") : value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module HotwireCombobox::Component::Markup::Input
|
2
|
+
def input_attrs
|
3
|
+
customize :input, base: {
|
4
|
+
id: input_id, role: :combobox, type: input_type,
|
5
|
+
class: "hw-combobox__input", autocomplete: :off,
|
6
|
+
data: input_data, aria: input_aria
|
7
|
+
}.merge(combobox_attrs.except(:data, :aria))
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
def input_id
|
12
|
+
canonical_id
|
13
|
+
end
|
14
|
+
|
15
|
+
def input_type
|
16
|
+
combobox_attrs[:type].to_s.presence_in(%w[ text search ]) || "text"
|
17
|
+
end
|
18
|
+
|
19
|
+
def input_data
|
20
|
+
data = combobox_attrs.fetch(:data, {}).dup
|
21
|
+
action = %w[
|
22
|
+
click->hw-combobox#toggle
|
23
|
+
keydown->hw-combobox#prepareToFilter
|
24
|
+
input->hw-combobox#filterAndSelect
|
25
|
+
keydown->hw-combobox#navigate
|
26
|
+
click@window->hw-combobox#closeOnClickOutside
|
27
|
+
focusin@window->hw-combobox#closeOnFocusOutside
|
28
|
+
turbo:before-stream-render@document->hw-combobox#rerouteListboxStreamToDialog
|
29
|
+
turbo:before-cache@document->hw-combobox#hideChipsForCache
|
30
|
+
turbo:morph-element->hw-combobox#idempotentConnect
|
31
|
+
].append(data.delete(:action)).compact.join(" ")
|
32
|
+
|
33
|
+
data.merge action: action, hw_combobox_target: "combobox", async_id: canonical_id
|
34
|
+
end
|
35
|
+
|
36
|
+
def input_aria
|
37
|
+
combobox_attrs.fetch(:aria, {}).merge \
|
38
|
+
controls: listbox_id,
|
39
|
+
owns: listbox_id,
|
40
|
+
haspopup: "listbox",
|
41
|
+
autocomplete: autocomplete,
|
42
|
+
activedescendant: ""
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module HotwireCombobox::Component::Markup::Listbox
|
2
|
+
def listbox_attrs
|
3
|
+
customize :listbox, base: {
|
4
|
+
id: listbox_id, role: :listbox, hidden: "",
|
5
|
+
class: "hw-combobox__listbox",
|
6
|
+
data: { hw_combobox_target: "listbox" },
|
7
|
+
aria: { multiselectable: multiselect? } }
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
def listbox_id
|
12
|
+
"#{canonical_id}-hw-listbox"
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module HotwireCombobox::Component::Paginated
|
2
|
+
def paginated?
|
3
|
+
async_src.present?
|
4
|
+
end
|
5
|
+
|
6
|
+
def pagination_attrs
|
7
|
+
{ for_id: canonical_id, src: async_src, loading: preload_next_page? ? :eager : :lazy }
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
def preload_next_page?
|
12
|
+
view.hw_first_page? && preload?
|
13
|
+
end
|
14
|
+
|
15
|
+
def preload?
|
16
|
+
preload.present?
|
17
|
+
end
|
18
|
+
end
|