hotwire_combobox 0.1.37 → 0.1.39

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.
@@ -62,3 +62,20 @@ export function unselectedPortion(element) {
62
62
  return element.value.substring(0, element.selectionStart)
63
63
  }
64
64
  }
65
+
66
+ export function dispatch(eventName, { target, cancelable, detail } = {}) {
67
+ const event = new CustomEvent(eventName, {
68
+ cancelable,
69
+ bubbles: true,
70
+ composed: true,
71
+ detail
72
+ })
73
+
74
+ if (target && target.isConnected) {
75
+ target.dispatchEvent(event)
76
+ } else {
77
+ document.documentElement.dispatchEvent(event)
78
+ }
79
+
80
+ return event
81
+ }
@@ -1,6 +1,12 @@
1
1
  import Combobox from "hw_combobox/models/combobox/base"
2
2
 
3
3
  Combobox.Dialog = Base => class extends Base {
4
+ rerouteListboxStreamToDialog({ detail: { newStream } }) {
5
+ if (newStream.target == this.listboxTarget.id && this._dialogIsOpen) {
6
+ newStream.setAttribute("target", this.dialogListboxTarget.id)
7
+ }
8
+ }
9
+
4
10
  _connectDialog() {
5
11
  if (window.visualViewport) {
6
12
  window.visualViewport.addEventListener("resize", this._resizeDialog)
@@ -50,4 +56,8 @@ Combobox.Dialog = Base => class extends Base {
50
56
  get _smallViewport() {
51
57
  return window.matchMedia(`(max-width: ${this.smallViewportMaxWidthValue})`).matches
52
58
  }
59
+
60
+ get _dialogIsOpen() {
61
+ return this.dialogTarget.open
62
+ }
53
63
  }
@@ -0,0 +1,17 @@
1
+ import Combobox from "hw_combobox/models/combobox/base"
2
+ import { dispatch } from "hw_combobox/helpers"
3
+
4
+ Combobox.Events = Base => class extends Base {
5
+ _dispatchSelectionEvent({ isNew }) {
6
+ const detail = {
7
+ value: this.hiddenFieldTarget.value,
8
+ display: this._fullQuery,
9
+ query: this._typedQuery,
10
+ fieldName: this.hiddenFieldTarget.name,
11
+ isValid: this._valueIsValid,
12
+ isNew: isNew
13
+ }
14
+
15
+ dispatch("hw-combobox:selection", { target: this.element, detail })
16
+ }
17
+ }
@@ -21,7 +21,12 @@ Combobox.Filtering = Base => class extends Base {
21
21
  }
22
22
 
23
23
  async _filterAsync(event) {
24
- const query = { q: this._fullQuery, input_type: event.inputType }
24
+ const query = {
25
+ q: this._fullQuery,
26
+ input_type: event.inputType,
27
+ for_id: this.element.dataset.asyncId
28
+ }
29
+
25
30
  await get(this.asyncSrcValue, { responseKind: "turbo-stream", query })
26
31
  }
27
32
 
@@ -36,11 +41,16 @@ Combobox.Filtering = Base => class extends Base {
36
41
  this._selectNew()
37
42
  } else if (isDeleteEvent(event)) {
38
43
  this._deselect()
39
- } else {
44
+ } else if (this._isOpen) {
40
45
  this._select(this._visibleOptionElements[0])
41
46
  }
42
47
  }
43
48
 
49
+ _clearQuery() {
50
+ this._fullQuery = ""
51
+ this.filter({ inputType: "deleteContentBackward" })
52
+ }
53
+
44
54
  get _isQueried() {
45
55
  return this._fullQuery.length > 0
46
56
  }
@@ -30,4 +30,11 @@ Combobox.Options = Base => class extends Base {
30
30
  get _selectedOptionIndex() {
31
31
  return [ ...this._visibleOptionElements ].indexOf(this._selectedOptionElement)
32
32
  }
33
+
34
+ get _isUnjustifiablyBlank() {
35
+ const valueIsMissing = !this.hiddenFieldTarget.value
36
+ const noBlankOptionSelected = !this._selectedOptionElement
37
+
38
+ return valueIsMissing && noBlankOptionSelected
39
+ }
33
40
  }
@@ -18,9 +18,9 @@ Combobox.Selection = Base => class extends Base {
18
18
  this._resetOptions()
19
19
 
20
20
  if (option) {
21
- this._markValid()
22
21
  this._autocompleteWith(option, { force: forceAutocomplete })
23
22
  this._commitSelection(option, { selected: true })
23
+ this._markValid()
24
24
  } else {
25
25
  this._markInvalid()
26
26
  }
@@ -33,6 +33,8 @@ Combobox.Selection = Base => class extends Base {
33
33
  this.hiddenFieldTarget.value = option.dataset.value
34
34
  option.scrollIntoView({ block: "nearest" })
35
35
  }
36
+
37
+ this._dispatchSelectionEvent({ isNew: false })
36
38
  }
37
39
 
38
40
  _markSelected(option, { selected }) {
@@ -46,15 +48,22 @@ Combobox.Selection = Base => class extends Base {
46
48
 
47
49
  _deselect() {
48
50
  const option = this._selectedOptionElement
51
+
49
52
  if (option) this._commitSelection(option, { selected: false })
53
+
50
54
  this.hiddenFieldTarget.value = null
51
55
  this._setActiveDescendant("")
56
+
57
+ if (!option) this._dispatchSelectionEvent({ isNew: false })
52
58
  }
53
59
 
54
60
  _selectNew() {
55
61
  this._resetOptions()
56
62
  this.hiddenFieldTarget.value = this._fullQuery
57
63
  this.hiddenFieldTarget.name = this.nameWhenNewValue
64
+ this._markValid()
65
+
66
+ this._dispatchSelectionEvent({ isNew: true })
58
67
  }
59
68
 
60
69
  _selectIndex(index) {
@@ -77,6 +86,11 @@ Combobox.Selection = Base => class extends Base {
77
86
  this._select(this._ensurableOption, { forceAutocomplete: true })
78
87
  this.filter({ inputType: "hw:lockInSelection" })
79
88
  }
89
+
90
+ if (this._isUnjustifiablyBlank) {
91
+ this._deselect()
92
+ this._clearQuery()
93
+ }
80
94
  }
81
95
 
82
96
  _setActiveDescendant(id) {
@@ -72,7 +72,7 @@ Combobox.Toggle = Base => class extends Base {
72
72
  _collapse() {
73
73
  this._actingCombobox.setAttribute("aria-expanded", false) // needs to happen before resetting acting combobox
74
74
 
75
- if (this.dialogTarget.open) {
75
+ if (this._dialogIsOpen) {
76
76
  this._closeInDialog()
77
77
  } else {
78
78
  this._closeInline()
@@ -4,29 +4,35 @@ Combobox.Validity = Base => class extends Base {
4
4
  _markValid() {
5
5
  if (this._valueIsInvalid) return
6
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")
7
+ this._forAllComboboxes(combobox => {
8
+ if (this.hasInvalidClass) {
9
+ combobox.classList.remove(this.invalidClass)
10
+ }
11
+
12
+ combobox.removeAttribute("aria-invalid")
13
+ combobox.removeAttribute("aria-errormessage")
14
+ })
13
15
  }
14
16
 
15
17
  _markInvalid() {
16
18
  if (this._valueIsValid) return
17
19
 
18
- if (this.hasInvalidClass) {
19
- this.comboboxTarget.classList.add(this.invalidClass)
20
- }
20
+ this._forAllComboboxes(combobox => {
21
+ if (this.hasInvalidClass) {
22
+ combobox.classList.add(this.invalidClass)
23
+ }
21
24
 
22
- this.comboboxTarget.setAttribute("aria-invalid", true)
23
- this.comboboxTarget.setAttribute("aria-errormessage", `Please select a valid option for ${this.comboboxTarget.name}`)
25
+ combobox.setAttribute("aria-invalid", true)
26
+ combobox.setAttribute("aria-errormessage", `Please select a valid option for ${combobox.name}`)
27
+ })
24
28
  }
25
29
 
26
30
  get _valueIsValid() {
27
31
  return !this._valueIsInvalid
28
32
  }
29
33
 
34
+ // +_valueIsInvalid+ only checks if `comboboxTarget` (and not `_actingCombobox`) is required
35
+ // because the `required` attribute is only forwarded to the `comboboxTarget` element
30
36
  get _valueIsInvalid() {
31
37
  const isRequiredAndEmpty = this.comboboxTarget.required && !this.hiddenFieldTarget.value
32
38
  return isRequiredAndEmpty
@@ -4,6 +4,7 @@ import "hw_combobox/models/combobox/actors"
4
4
  import "hw_combobox/models/combobox/async_loading"
5
5
  import "hw_combobox/models/combobox/autocomplete"
6
6
  import "hw_combobox/models/combobox/dialog"
7
+ import "hw_combobox/models/combobox/events"
7
8
  import "hw_combobox/models/combobox/filtering"
8
9
  import "hw_combobox/models/combobox/navigation"
9
10
  import "hw_combobox/models/combobox/new_options"
@@ -121,6 +121,10 @@
121
121
  text-overflow: ellipsis;
122
122
  }
123
123
 
124
+ .hw-combobox__option--blank {
125
+ border-bottom: var(--hw-border-width--slim) solid var(--hw-border-color);
126
+ }
127
+
124
128
  .hw-combobox__option:hover,
125
129
  .hw-combobox__option--selected {
126
130
  background-color: var(--hw-active-bg-color);
@@ -1,5 +1,7 @@
1
+ require "securerandom"
2
+
1
3
  class HotwireCombobox::Component
2
- attr_reader :async_src, :options, :dialog_label
4
+ attr_reader :options, :dialog_label
3
5
 
4
6
  def initialize \
5
7
  view, name,
@@ -148,7 +150,7 @@ class HotwireCombobox::Component
148
150
  end
149
151
 
150
152
  def fieldset_data
151
- data.reverse_merge \
153
+ data.merge \
152
154
  async_id: canonical_id,
153
155
  controller: view.token_list("hw-combobox", data[:controller]),
154
156
  hw_combobox_expanded_value: open,
@@ -160,7 +162,8 @@ class HotwireCombobox::Component
160
162
  hw_combobox_prefilled_display_value: prefilled_display,
161
163
  hw_combobox_filterable_attribute_value: "data-filterable-as",
162
164
  hw_combobox_autocompletable_attribute_value: "data-autocompletable-as",
163
- hw_combobox_selected_class: "hw-combobox__option--selected"
165
+ hw_combobox_selected_class: "hw-combobox__option--selected",
166
+ hw_combobox_invalid_class: "hw-combobox__input--invalid"
164
167
  end
165
168
 
166
169
  def prefilled_display
@@ -181,9 +184,13 @@ class HotwireCombobox::Component
181
184
  form&.object&.class&.reflect_on_association(association_name).present?
182
185
  end
183
186
 
187
+ def async_src
188
+ view.hw_uri_with_params @async_src, for_id: canonical_id, format: :turbo_stream
189
+ end
190
+
184
191
 
185
192
  def canonical_id
186
- id || form&.field_id(name)
193
+ @canonical_id ||= id || form&.field_id(name) || SecureRandom.uuid
187
194
  end
188
195
 
189
196
 
@@ -219,19 +226,20 @@ class HotwireCombobox::Component
219
226
  end
220
227
 
221
228
  def input_data
222
- combobox_attrs.fetch(:data, {}).reverse_merge! \
229
+ combobox_attrs.fetch(:data, {}).merge \
223
230
  action: "
224
231
  focus->hw-combobox#open
225
232
  input->hw-combobox#filter
226
233
  keydown->hw-combobox#navigate
227
234
  click@window->hw-combobox#closeOnClickOutside
228
- focusin@window->hw-combobox#closeOnFocusOutside".squish,
235
+ focusin@window->hw-combobox#closeOnFocusOutside
236
+ turbo:before-stream-render@document->hw-combobox#rerouteListboxStreamToDialog".squish,
229
237
  hw_combobox_target: "combobox",
230
238
  async_id: canonical_id
231
239
  end
232
240
 
233
241
  def input_aria
234
- combobox_attrs.fetch(:aria, {}).reverse_merge! \
242
+ combobox_attrs.fetch(:aria, {}).merge \
235
243
  controls: listbox_id,
236
244
  owns: listbox_id,
237
245
  haspopup: "listbox",
@@ -18,7 +18,7 @@ class HotwireCombobox::Listbox::Option
18
18
  end
19
19
 
20
20
  private
21
- Data = Struct.new :id, :value, :display, :content, :filterable_as, :autocompletable_as, keyword_init: true
21
+ Data = Struct.new :id, :value, :display, :content, :blank, :filterable_as, :autocompletable_as, keyword_init: true
22
22
 
23
23
  attr_reader :option
24
24
 
@@ -26,13 +26,13 @@ class HotwireCombobox::Listbox::Option
26
26
  {
27
27
  id: id,
28
28
  role: :option,
29
- class: "hw-combobox__option",
29
+ class: [ "hw-combobox__option", { "hw-combobox__option--blank": blank? } ],
30
30
  data: data
31
31
  }
32
32
  end
33
33
 
34
34
  def id
35
- option.try(:id) || SecureRandom.uuid
35
+ @id ||= option.try(:id) || SecureRandom.uuid
36
36
  end
37
37
 
38
38
  def data
@@ -51,4 +51,8 @@ class HotwireCombobox::Listbox::Option
51
51
  def filterable_as
52
52
  option.try(:filterable_as) || option.try(:display)
53
53
  end
54
+
55
+ def blank?
56
+ option.try(:blank).present?
57
+ end
54
58
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  <%= turbo_stream.remove hw_pagination_frame_wrapper_id(for_id) %>
4
4
  <%= turbo_stream.append hw_listbox_id(for_id) do %>
5
- <%= render "hotwire_combobox/pagination", for_id: for_id, src: hw_combobox_next_page_uri(src, next_page) %>
5
+ <%= render "hotwire_combobox/pagination", for_id: for_id, src: hw_combobox_next_page_uri(src, next_page, for_id) %>
6
6
  <% end %>
@@ -10,7 +10,7 @@ module HotwireCombobox
10
10
  unless HotwireCombobox.bypass_convenience_methods?
11
11
  module FormBuilderExtensions
12
12
  def combobox(*args, **kwargs)
13
- @template.hw_combobox_tag *args, **kwargs.merge(form: self)
13
+ @template.hw_combobox_tag(*args, **kwargs.merge(form: self))
14
14
  end
15
15
  end
16
16
 
@@ -15,27 +15,32 @@ module HotwireCombobox
15
15
  end
16
16
  hw_alias :hw_combobox_style_tag
17
17
 
18
- def hw_combobox_tag(name, options_or_src = [], render_in: {}, **kwargs)
19
- options, src = hw_extract_options_and_src(options_or_src, render_in)
18
+ def hw_combobox_tag(name, options_or_src = [], render_in: {}, include_blank: nil, **kwargs)
19
+ options, src = hw_extract_options_and_src(options_or_src, render_in, include_blank)
20
20
  component = HotwireCombobox::Component.new self, name, options: options, async_src: src, **kwargs
21
21
 
22
22
  render "hotwire_combobox/combobox", component: component
23
23
  end
24
24
  hw_alias :hw_combobox_tag
25
25
 
26
- def hw_combobox_options(options, render_in: {}, display: :to_combobox_display, **methods)
26
+ def hw_combobox_options(options, render_in: {}, include_blank: nil, display: :to_combobox_display, **methods)
27
27
  if options.first.is_a? HotwireCombobox::Listbox::Option
28
28
  options
29
29
  else
30
- render_in_proc = ->(object) { render(**render_in.merge(object: object)) } if render_in.present?
31
- hw_parse_combobox_options options, render_in: render_in_proc, **methods.merge(display: display)
30
+ render_in_proc = hw_render_in_proc(render_in) if render_in.present?
31
+
32
+ hw_parse_combobox_options(options, render_in: render_in_proc, **methods.merge(display: display)).tap do |options|
33
+ options.unshift(hw_blank_option(include_blank)) if include_blank.present?
34
+ end
32
35
  end
33
36
  end
34
37
  hw_alias :hw_combobox_options
35
38
 
36
- def hw_paginated_combobox_options(options, for_id:, src: request.path, next_page: nil, render_in: {}, **methods)
37
- this_page = render("hotwire_combobox/paginated_options", for_id: for_id, options: hw_combobox_options(options, render_in: render_in, **methods))
38
- next_page = render("hotwire_combobox/next_page", for_id: for_id, src: src, next_page: next_page)
39
+ def hw_paginated_combobox_options(options, for_id: params[:for_id], src: request.path, next_page: nil, render_in: {}, include_blank: {}, **methods)
40
+ include_blank = params[:page] ? nil : include_blank
41
+ options = hw_combobox_options options, render_in: render_in, include_blank: include_blank, **methods
42
+ this_page = render "hotwire_combobox/paginated_options", for_id: for_id, options: options
43
+ next_page = render "hotwire_combobox/next_page", for_id: for_id, src: src, next_page: next_page
39
44
 
40
45
  safe_join [ this_page, next_page ]
41
46
  end
@@ -44,7 +49,7 @@ module HotwireCombobox
44
49
  alias_method :hw_async_combobox_options, :hw_paginated_combobox_options
45
50
  hw_alias :hw_async_combobox_options
46
51
 
47
- protected # library use only
52
+ # private library use only
48
53
  def hw_listbox_id(id)
49
54
  "#{id}-hw-listbox"
50
55
  end
@@ -57,9 +62,13 @@ module HotwireCombobox
57
62
  "#{id}__hw_combobox_pagination"
58
63
  end
59
64
 
60
- def hw_combobox_next_page_uri(uri, next_page)
65
+ def hw_combobox_next_page_uri(uri, next_page, for_id)
61
66
  if next_page
62
- hw_uri_with_params uri, page: next_page, q: params[:q], format: :turbo_stream
67
+ hw_uri_with_params uri,
68
+ page: next_page,
69
+ q: params[:q],
70
+ for_id: for_id,
71
+ format: :turbo_stream
63
72
  end
64
73
  end
65
74
 
@@ -67,12 +76,19 @@ module HotwireCombobox
67
76
  params[:page] ? :append : :update
68
77
  end
69
78
 
70
- private
71
- def hw_extract_options_and_src(options_or_src, render_in)
72
- if options_or_src.is_a? String
73
- [ [], hw_uri_with_params(options_or_src, format: :turbo_stream) ]
79
+ def hw_blank_option(include_blank)
80
+ display, content = hw_extract_blank_display_and_content include_blank
81
+
82
+ HotwireCombobox::Listbox::Option.new display: display, content: content, value: "", blank: true
83
+ end
84
+
85
+ def hw_extract_blank_display_and_content(include_blank)
86
+ if include_blank.is_a? Hash
87
+ text = include_blank.delete(:text)
88
+
89
+ [ text, hw_render_in_proc(include_blank).(text) ]
74
90
  else
75
- [ hw_combobox_options(options_or_src, render_in: render_in), nil ]
91
+ [ include_blank, include_blank ]
76
92
  end
77
93
  end
78
94
 
@@ -85,9 +101,23 @@ module HotwireCombobox
85
101
  url_or_path
86
102
  end
87
103
 
104
+ private
105
+ def hw_render_in_proc(render_in)
106
+ ->(object) { render(**render_in.reverse_merge(object: object)) }
107
+ end
108
+
109
+ def hw_extract_options_and_src(options_or_src, render_in, include_blank)
110
+ if options_or_src.is_a? String
111
+ [ [], options_or_src ]
112
+ else
113
+ [ hw_combobox_options(options_or_src, render_in: render_in, include_blank: include_blank), nil ]
114
+ end
115
+ end
116
+
88
117
  def hw_parse_combobox_options(options, render_in: nil, **methods)
89
118
  options.map do |option|
90
- HotwireCombobox::Listbox::Option.new **hw_option_attrs_for(option, render_in: render_in, **methods)
119
+ HotwireCombobox::Listbox::Option.new \
120
+ **hw_option_attrs_for(option, render_in: render_in, **methods)
91
121
  end
92
122
  end
93
123
 
@@ -122,8 +152,43 @@ module HotwireCombobox
122
152
  if method_or_proc.is_a? Proc
123
153
  method_or_proc.call object
124
154
  else
125
- object.public_send method_or_proc
155
+ hw_call_method object, method_or_proc
156
+ end
157
+ end
158
+
159
+ def hw_call_method(object, method)
160
+ if object.respond_to? method
161
+ object.public_send method
162
+ else
163
+ hw_raise_no_public_method_error object, method
126
164
  end
127
165
  end
166
+
167
+ def hw_raise_no_public_method_error(object, method)
168
+ if object.respond_to? method, true
169
+ header = "`#{object.class}` responds to `##{method}` but the method is not public."
170
+ else
171
+ header = "`#{object.class}` does not respond to `##{method}`."
172
+ end
173
+
174
+ if method.to_s == "to_combobox_display"
175
+ header << "\n\nThis method is used to determine how this option should appear in the combobox options list."
176
+ end
177
+
178
+ raise NoMethodError, <<~MSG
179
+ [ACTION NEEDED] – Message from HotwireCombobox:
180
+
181
+ #{header}
182
+
183
+ Please add this as a public method and return a string.
184
+
185
+ Example:
186
+ class #{object.class} < ApplicationRecord
187
+ def #{method}
188
+ name # or `title`, `to_s`, etc.
189
+ end
190
+ end
191
+ MSG
192
+ end
128
193
  end
129
194
  end
@@ -1,3 +1,3 @@
1
1
  module HotwireCombobox
2
- VERSION = "0.1.37"
2
+ VERSION = "0.1.39"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hotwire_combobox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.37
4
+ version: 0.1.39
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jose Farias
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-03 00:00:00.000000000 Z
11
+ date: 2024-03-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -52,8 +52,7 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '1.2'
55
- description: An autocomplete combobox implementation for Ruby on Rails apps using
56
- Hotwire.
55
+ description: An accessible autocomplete for Ruby on Rails apps using Hotwire.
57
56
  email:
58
57
  - jose@farias.mx
59
58
  executables: []
@@ -65,6 +64,8 @@ files:
65
64
  - Rakefile
66
65
  - app/assets/config/hw_combobox_manifest.js
67
66
  - app/assets/javascripts/controllers/hw_combobox_controller.js
67
+ - app/assets/javascripts/hotwire_combobox.esm.js
68
+ - app/assets/javascripts/hotwire_combobox.umd.js
68
69
  - app/assets/javascripts/hw_combobox/helpers.js
69
70
  - app/assets/javascripts/hw_combobox/models/combobox.js
70
71
  - app/assets/javascripts/hw_combobox/models/combobox/actors.js
@@ -72,6 +73,7 @@ files:
72
73
  - app/assets/javascripts/hw_combobox/models/combobox/autocomplete.js
73
74
  - app/assets/javascripts/hw_combobox/models/combobox/base.js
74
75
  - app/assets/javascripts/hw_combobox/models/combobox/dialog.js
76
+ - app/assets/javascripts/hw_combobox/models/combobox/events.js
75
77
  - app/assets/javascripts/hw_combobox/models/combobox/filtering.js
76
78
  - app/assets/javascripts/hw_combobox/models/combobox/navigation.js
77
79
  - app/assets/javascripts/hw_combobox/models/combobox/new_options.js
@@ -97,13 +99,13 @@ files:
97
99
  - lib/hotwire_combobox/engine.rb
98
100
  - lib/hotwire_combobox/helper.rb
99
101
  - lib/hotwire_combobox/version.rb
100
- homepage: https://github.com/josefarias/hotwire_combobox
102
+ homepage: https://hotwirecombobox.com/
101
103
  licenses:
102
104
  - MIT
103
105
  metadata:
104
- homepage_uri: https://github.com/josefarias/hotwire_combobox
106
+ homepage_uri: https://hotwirecombobox.com/
105
107
  source_code_uri: https://github.com/josefarias/hotwire_combobox
106
- changelog_uri: https://github.com/josefarias/hotwire_combobox
108
+ changelog_uri: https://github.com/josefarias/hotwire_combobox/releases
107
109
  post_install_message:
108
110
  rdoc_options: []
109
111
  require_paths:
@@ -112,7 +114,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
112
114
  requirements:
113
115
  - - ">="
114
116
  - !ruby/object:Gem::Version
115
- version: '0'
117
+ version: 2.7.0
116
118
  required_rubygems_version: !ruby/object:Gem::Requirement
117
119
  requirements:
118
120
  - - ">="
@@ -122,5 +124,5 @@ requirements: []
122
124
  rubygems_version: 3.5.6
123
125
  signing_key:
124
126
  specification_version: 4
125
- summary: Autocomplete for Rails apps using Hotwire
127
+ summary: Accessible Autocomplete for Rails apps
126
128
  test_files: []