stimulus_plumbers 0.3.3 → 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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -0
  3. data/app/assets/javascripts/stimulus-plumbers/controllers.manifest.json +273 -0
  4. data/app/assets/javascripts/stimulus-plumbers/stimulus-plumbers-controllers.es.js +228 -145
  5. data/app/assets/javascripts/stimulus-plumbers/stimulus-plumbers-controllers.umd.js +1 -1
  6. data/app/assets/stylesheets/stimulus_plumbers/tokens.css +43 -7
  7. data/config/locales/en.yml +10 -0
  8. data/lib/stimulus_plumbers/components/avatar.rb +14 -13
  9. data/lib/stimulus_plumbers/components/button/group.rb +9 -4
  10. data/lib/stimulus_plumbers/components/button/slots.rb +11 -0
  11. data/lib/stimulus_plumbers/components/button.rb +30 -34
  12. data/lib/stimulus_plumbers/components/calendar/turbo/days_of_month.rb +151 -0
  13. data/lib/stimulus_plumbers/components/calendar/turbo/days_of_week.rb +62 -0
  14. data/lib/stimulus_plumbers/components/calendar/turbo/months_of_year.rb +99 -0
  15. data/lib/stimulus_plumbers/components/calendar/turbo/years_of_decade.rb +86 -0
  16. data/lib/stimulus_plumbers/components/calendar/turbo.rb +65 -0
  17. data/lib/stimulus_plumbers/components/calendar.rb +70 -29
  18. data/lib/stimulus_plumbers/components/card/slots.rb +26 -0
  19. data/lib/stimulus_plumbers/components/card.rb +54 -14
  20. data/lib/stimulus_plumbers/components/combobox/builder.rb +45 -0
  21. data/lib/stimulus_plumbers/components/combobox/date/navigation.rb +72 -0
  22. data/lib/stimulus_plumbers/components/combobox/date/navigator.rb +25 -0
  23. data/lib/stimulus_plumbers/components/combobox/date.rb +34 -24
  24. data/lib/stimulus_plumbers/components/combobox/dropdown.rb +27 -24
  25. data/lib/stimulus_plumbers/components/combobox/options/option.rb +1 -1
  26. data/lib/stimulus_plumbers/components/combobox/options/option_group.rb +1 -1
  27. data/lib/stimulus_plumbers/components/combobox/time/drum.rb +1 -1
  28. data/lib/stimulus_plumbers/components/combobox/time.rb +48 -49
  29. data/lib/stimulus_plumbers/components/combobox/trigger.rb +17 -12
  30. data/lib/stimulus_plumbers/components/combobox/typeahead.rb +63 -16
  31. data/lib/stimulus_plumbers/components/combobox.rb +58 -38
  32. data/lib/stimulus_plumbers/components/divider.rb +9 -8
  33. data/lib/stimulus_plumbers/components/icon.rb +5 -1
  34. data/lib/stimulus_plumbers/components/link/slots.rb +11 -0
  35. data/lib/stimulus_plumbers/components/link.rb +63 -0
  36. data/lib/stimulus_plumbers/components/list/item/slots.rb +13 -0
  37. data/lib/stimulus_plumbers/components/list/item.rb +83 -0
  38. data/lib/stimulus_plumbers/components/list/section.rb +73 -0
  39. data/lib/stimulus_plumbers/components/list.rb +31 -0
  40. data/lib/stimulus_plumbers/components/popover/panel.rb +32 -0
  41. data/lib/stimulus_plumbers/components/popover/trigger.rb +27 -0
  42. data/lib/stimulus_plumbers/components/popover.rb +44 -18
  43. data/lib/stimulus_plumbers/engine.rb +1 -0
  44. data/lib/stimulus_plumbers/form/base.rb +103 -0
  45. data/lib/stimulus_plumbers/form/builder.rb +71 -24
  46. data/lib/stimulus_plumbers/form/field.rb +56 -88
  47. data/lib/stimulus_plumbers/form/fields/error.rb +1 -1
  48. data/lib/stimulus_plumbers/form/fields/fieldset.rb +11 -8
  49. data/lib/stimulus_plumbers/form/fields/hint.rb +1 -1
  50. data/lib/stimulus_plumbers/form/fields/inputs/checkbox.rb +115 -0
  51. data/lib/stimulus_plumbers/form/fields/inputs/combobox.rb +24 -0
  52. data/lib/stimulus_plumbers/form/fields/inputs/datetime.rb +40 -58
  53. data/lib/stimulus_plumbers/form/fields/inputs/file.rb +9 -8
  54. data/lib/stimulus_plumbers/form/fields/inputs/password.rb +30 -23
  55. data/lib/stimulus_plumbers/form/fields/inputs/radio.rb +60 -0
  56. data/lib/stimulus_plumbers/form/fields/inputs/search.rb +31 -54
  57. data/lib/stimulus_plumbers/form/fields/inputs/select/grouped.rb +22 -33
  58. data/lib/stimulus_plumbers/form/fields/inputs/select/timezone.rb +3 -46
  59. data/lib/stimulus_plumbers/form/fields/inputs/select/weekday.rb +3 -26
  60. data/lib/stimulus_plumbers/form/fields/inputs/select.rb +62 -61
  61. data/lib/stimulus_plumbers/form/fields/inputs/submit.rb +10 -7
  62. data/lib/stimulus_plumbers/form/fields/inputs/text.rb +29 -22
  63. data/lib/stimulus_plumbers/form/fields/inputs/text_area.rb +9 -8
  64. data/lib/stimulus_plumbers/form/fields/label/floating.rb +41 -0
  65. data/lib/stimulus_plumbers/form/fields/label.rb +9 -3
  66. data/lib/stimulus_plumbers/form/fields/renderer.rb +39 -0
  67. data/lib/stimulus_plumbers/helpers/button_helper.rb +1 -1
  68. data/lib/stimulus_plumbers/helpers/calendar_helper.rb +2 -2
  69. data/lib/stimulus_plumbers/helpers/calendar_turbo_helper.rb +56 -4
  70. data/lib/stimulus_plumbers/helpers/card_helper.rb +1 -11
  71. data/lib/stimulus_plumbers/helpers/combobox_helper.rb +27 -60
  72. data/lib/stimulus_plumbers/helpers/icon_helper.rb +11 -0
  73. data/lib/stimulus_plumbers/helpers/link_helper.rb +11 -0
  74. data/lib/stimulus_plumbers/helpers/list_helper.rb +11 -0
  75. data/lib/stimulus_plumbers/helpers/plumber_helper.rb +3 -6
  76. data/lib/stimulus_plumbers/helpers.rb +6 -2
  77. data/lib/stimulus_plumbers/logger.rb +4 -3
  78. data/lib/stimulus_plumbers/plumber/base.rb +6 -1
  79. data/lib/stimulus_plumbers/plumber/dispatcher/klass_proxy.rb +4 -3
  80. data/lib/stimulus_plumbers/plumber/dispatcher/method_call.rb +4 -3
  81. data/lib/stimulus_plumbers/plumber/dispatcher.rb +4 -4
  82. data/lib/stimulus_plumbers/plumber/options/aria.rb +17 -0
  83. data/lib/stimulus_plumbers/plumber/options/html.rb +29 -0
  84. data/lib/stimulus_plumbers/plumber/options/stimulus.rb +29 -0
  85. data/lib/stimulus_plumbers/plumber/options/theme.rb +19 -0
  86. data/lib/stimulus_plumbers/plumber/options/token_list.rb +29 -0
  87. data/lib/stimulus_plumbers/plumber/renderer.rb +136 -41
  88. data/lib/stimulus_plumbers/plumber/slots.rb +74 -0
  89. data/lib/stimulus_plumbers/themes/base.rb +5 -7
  90. data/lib/stimulus_plumbers/themes/schema/avatar/ranges.rb +13 -0
  91. data/lib/stimulus_plumbers/themes/schema/button/ranges.rb +16 -0
  92. data/lib/stimulus_plumbers/themes/schema/card/ranges.rb +13 -0
  93. data/lib/stimulus_plumbers/themes/schema/form/checkbox/ranges.rb +16 -0
  94. data/lib/stimulus_plumbers/themes/schema/form/radio/ranges.rb +16 -0
  95. data/lib/stimulus_plumbers/themes/schema/form/ranges.rb +1 -2
  96. data/lib/stimulus_plumbers/themes/schema/link/ranges.rb +14 -0
  97. data/lib/stimulus_plumbers/themes/schema/ranges.rb +1 -5
  98. data/lib/stimulus_plumbers/themes/schema.rb +119 -48
  99. data/lib/stimulus_plumbers/version.rb +1 -1
  100. data/lib/stimulus_plumbers.rb +20 -15
  101. metadata +42 -15
  102. data/lib/stimulus_plumbers/components/action_list/item.rb +0 -30
  103. data/lib/stimulus_plumbers/components/action_list/section.rb +0 -28
  104. data/lib/stimulus_plumbers/components/action_list.rb +0 -29
  105. data/lib/stimulus_plumbers/components/calendar/month/turbo/days_of_month.rb +0 -149
  106. data/lib/stimulus_plumbers/components/calendar/month/turbo/days_of_week.rb +0 -43
  107. data/lib/stimulus_plumbers/components/calendar/month/turbo.rb +0 -59
  108. data/lib/stimulus_plumbers/components/card/section.rb +0 -31
  109. data/lib/stimulus_plumbers/components/combobox/popover.rb +0 -47
  110. data/lib/stimulus_plumbers/components/date_picker/navigation.rb +0 -41
  111. data/lib/stimulus_plumbers/components/date_picker/navigator.rb +0 -23
  112. data/lib/stimulus_plumbers/components/popover/builder.rb +0 -25
  113. data/lib/stimulus_plumbers/form/fields/inputs/choice.rb +0 -69
  114. data/lib/stimulus_plumbers/helpers/action_list_helper.rb +0 -25
  115. data/lib/stimulus_plumbers/plumber/html_options.rb +0 -52
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Components
5
+ class Combobox
6
+ class Date
7
+ class Navigator < Plumber::Base
8
+ def render(...)
9
+ render_navigator(...)
10
+ end
11
+
12
+ private
13
+
14
+ def render_navigator(icon: nil, **kwargs, &block)
15
+ html_options = merge_html_options(
16
+ theme.resolve(:combobox_date_navigation_navigator),
17
+ kwargs
18
+ )
19
+ Components::Button.new(template).render(variant: :ghost, size: nil, icon_leading: icon, **html_options, &block)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -5,52 +5,62 @@ module StimulusPlumbers
5
5
  class Combobox
6
6
  class Date < Plumber::Base
7
7
  STIMULUS_CONTROLLER = "combobox-date"
8
- CALENDAR_OUTLET = "#{STIMULUS_CONTROLLER}-calendar-month-outlet".freeze
8
+ CALENDAR_OUTLET = "#{STIMULUS_CONTROLLER}-calendar-month-outlet".freeze
9
+ STIMULUS_ACTION = [
10
+ "calendar-observer:selected->#{STIMULUS_CONTROLLER}#onSelect",
11
+ "#{STIMULUS_CONTROLLER}:selected->#{Combobox::STIMULUS_CONTROLLER}#onSelect",
12
+ "#{STIMULUS_CONTROLLER}:selected->#{Components::Popover::STIMULUS_CONTROLLER}#closeOnSelect"
13
+ ].join(" ").freeze
9
14
 
10
- def self.default_opts
11
- {
12
- input: { data: { combobox_date_date_value: nil } },
13
- popover: { label: "Picker", role: "dialog", tag: :div }
14
- }
15
+ def self.calendar_id_for(panel_id)
16
+ [panel_id, "calendar"].compact.join("_")
15
17
  end
16
18
 
17
- def self.calendar_id_for(popover_id)
18
- [popover_id, "calendar"].compact.join("_")
19
- end
19
+ module Metadata
20
+ module_function
20
21
 
21
- def render(...)
22
- render_date(...)
22
+ def haspopup = "dialog"
23
+ def popup_id_for(panel_id) = panel_id
24
+ def trigger_icon = "calendar"
25
+ def trigger_options = {}
26
+ def stimulus_data(_panel_id, _options) = { input_formatter_format_value: "date" }
23
27
  end
24
28
 
29
+ def render(...) = render_date(...)
30
+
25
31
  private
26
32
 
27
- def render_date(value: nil, popover_id: nil)
28
- calendar_id = self.class.calendar_id_for(popover_id)
33
+ def render_date(panel_attrs: {}, value: nil, label: "Picker", labelledby: nil)
34
+ calendar_id = self.class.calendar_id_for(panel_attrs[:id])
35
+
36
+ template.content_tag(
37
+ :div,
38
+ **merge_html_options(panel_attrs, dialog_attrs(value, calendar_id, label, labelledby))
39
+ ) do
40
+ template.safe_join([navigation, calendar(id: calendar_id)])
41
+ end
42
+ end
29
43
 
44
+ def dialog_attrs(value, calendar_id, label, labelledby)
30
45
  data = {
31
- controller: STIMULUS_CONTROLLER,
46
+ controller: STIMULUS_CONTROLLER,
32
47
  CALENDAR_OUTLET => "##{calendar_id}",
33
- action: [
34
- "calendar-month-observer:selected->#{STIMULUS_CONTROLLER}#onSelect",
35
- "#{STIMULUS_CONTROLLER}:selected->#{Combobox::STIMULUS_CONTROLLER}#onSelect"
36
- ].join(" "),
48
+ action: STIMULUS_ACTION,
37
49
  "#{STIMULUS_CONTROLLER}-date-value" => value
38
50
  }.compact
39
51
 
40
- template.content_tag(:div, data: data) do
41
- template.safe_join([navigation, calendar_month(id: calendar_id)])
42
- end
52
+ { role: "dialog", aria: labelled_aria(label, labelledby: labelledby), data: data }
43
53
  end
44
54
 
45
55
  def navigation
46
- DatePicker::Navigation.new(template).render(
56
+ Navigation.new(template).render(
47
57
  step: "month",
48
58
  stimulus_controller: STIMULUS_CONTROLLER
49
59
  )
50
60
  end
51
61
 
52
- def calendar_month(**kwargs)
53
- Calendar.new(template).month(**kwargs)
62
+ def calendar(**kwargs)
63
+ Calendar.new(template).render(**kwargs)
54
64
  end
55
65
  end
56
66
  end
@@ -8,39 +8,42 @@ module StimulusPlumbers
8
8
  STIMULUS_ACTION = [
9
9
  "click->#{STIMULUS_CONTROLLER}#select",
10
10
  "keydown->#{STIMULUS_CONTROLLER}#onNavigate",
11
- "#{STIMULUS_CONTROLLER}:selected->#{Combobox::STIMULUS_CONTROLLER}#onSelect"
11
+ "#{STIMULUS_CONTROLLER}:selected->#{Combobox::STIMULUS_CONTROLLER}#onSelect",
12
+ "#{STIMULUS_CONTROLLER}:selected->#{Components::Popover::STIMULUS_CONTROLLER}#closeOnSelect"
12
13
  ].join(" ").freeze
13
14
 
14
- def self.default_opts
15
- {
16
- popover: {
17
- tag: :div,
18
- haspopup: "listbox",
19
- data: { controller: STIMULUS_CONTROLLER, action: STIMULUS_ACTION }
20
- }
21
- }
22
- end
15
+ module Metadata
16
+ module_function
23
17
 
24
- def render(...)
25
- render_dropdown(...)
18
+ def haspopup = "listbox"
19
+ def popup_id_for(panel_id) = panel_id
20
+ def trigger_icon = "chevron-down"
21
+ def trigger_options = {}
22
+ def stimulus_data(_panel_id, _options) = {}
26
23
  end
27
24
 
25
+ def render(...) = render_dropdown(...)
26
+
28
27
  private
29
28
 
30
- def render_dropdown(options: [], value: nil, label: nil, labelledby: nil)
31
- listbox_attrs = merge_html_options(
32
- { classes: theme.resolve(:combobox_listbox).fetch(:classes, "") },
33
- { role: "listbox", data: { "#{STIMULUS_CONTROLLER}_target": "listbox" } }
29
+ def render_dropdown(panel_attrs: {}, options: [], value: nil, label: nil, labelledby: nil)
30
+ template.content_tag(
31
+ :ul,
32
+ Options.new(template).render(options, value: value),
33
+ **listbox_attrs(panel_attrs: panel_attrs, label: label, labelledby: labelledby)
34
34
  )
35
- if labelledby
36
- listbox_attrs[:aria] = { labelledby: labelledby }
37
- elsif label
38
- listbox_attrs[:aria] = { label: label }
39
- end
35
+ end
40
36
 
41
- template.content_tag(:ul, **listbox_attrs) do
42
- Options.new(template).render(options, value: value)
43
- end
37
+ def listbox_attrs(panel_attrs: {}, label: nil, labelledby: nil)
38
+ merge_html_options(
39
+ panel_attrs,
40
+ theme.resolve(:combobox_listbox),
41
+ {
42
+ role: "listbox",
43
+ aria: labelled_aria(label, labelledby: labelledby),
44
+ data: { controller: STIMULUS_CONTROLLER, action: STIMULUS_ACTION, combobox_dropdown_target: "listbox" }
45
+ }
46
+ )
44
47
  end
45
48
  end
46
49
  end
@@ -16,7 +16,7 @@ module StimulusPlumbers
16
16
  aria[:disabled] = "true" if disabled
17
17
 
18
18
  attrs = merge_html_options(
19
- { classes: theme.resolve(:combobox_option, selected: selected, disabled: disabled).fetch(:classes, "") },
19
+ theme.resolve(:combobox_option, selected: selected, disabled: disabled),
20
20
  { role: "option", aria: aria, data: { value: value } }
21
21
  )
22
22
 
@@ -13,7 +13,7 @@ module StimulusPlumbers
13
13
 
14
14
  def render_option_group(label:, options:, value: nil)
15
15
  attrs = merge_html_options(
16
- { classes: theme.resolve(:combobox_option_group).fetch(:classes, "") },
16
+ theme.resolve(:combobox_option_group),
17
17
  { role: "group", aria: { label: label } }
18
18
  )
19
19
 
@@ -15,7 +15,7 @@ module StimulusPlumbers
15
15
  template.content_tag(
16
16
  :ul,
17
17
  **merge_html_options(
18
- { classes: theme.resolve(:combobox_listbox).fetch(:classes, "") },
18
+ theme.resolve(:combobox_listbox),
19
19
  {
20
20
  role: "listbox",
21
21
  tabindex: "0",
@@ -5,36 +5,50 @@ module StimulusPlumbers
5
5
  class Combobox
6
6
  class Time < Plumber::Base
7
7
  STIMULUS_CONTROLLER = "combobox-time"
8
-
9
- def self.default_opts
10
- {
11
- popover: { label: "Picker", role: "dialog", tag: :div }
12
- }
8
+ STIMULUS_ACTION = [
9
+ "#{STIMULUS_CONTROLLER}:selected->#{Combobox::STIMULUS_CONTROLLER}#onSelect",
10
+ "#{STIMULUS_CONTROLLER}:selected->#{Components::Popover::STIMULUS_CONTROLLER}#closeOnSelect"
11
+ ].join(" ").freeze
12
+
13
+ module Metadata
14
+ module_function
15
+
16
+ def haspopup = "dialog"
17
+ def popup_id_for(panel_id) = panel_id
18
+ def trigger_icon = "clock"
19
+ def trigger_options = {}
20
+
21
+ def stimulus_data(_panel_id, options)
22
+ {
23
+ input_formatter_format_value: "time",
24
+ input_formatter_options_value: { format: options.fetch(:format, :h12) }.to_json
25
+ }
26
+ end
13
27
  end
14
28
 
15
- def render(...)
16
- render_time(...)
17
- end
29
+ def render(...) = render_time(...)
18
30
 
19
31
  private
20
32
 
21
- def render_time(format: :h12, step: 1, value: nil)
33
+ def render_time(panel_attrs: {}, format: :h12, step: 1, value: nil, label: "Picker", labelledby: nil)
22
34
  @format = format
23
35
  @step = [1, step.to_i].max
24
36
  @time = parse_time(value)
25
37
 
26
- template.content_tag(
27
- :div,
28
- **merge_html_options(
29
- { classes: theme.resolve(:combobox_time).fetch(:classes, "") },
30
- { data: { controller: STIMULUS_CONTROLLER,
31
- action: "#{STIMULUS_CONTROLLER}:selected->#{Combobox::STIMULUS_CONTROLLER}#onSelect"
32
- }
33
- }
34
- )
35
- ) do
36
- template.safe_join(drums)
37
- end
38
+ attrs = merge_html_options(
39
+ panel_attrs,
40
+ theme.resolve(:combobox_time),
41
+ dialog_attrs(label, labelledby)
42
+ )
43
+ template.content_tag(:div, **attrs) { template.safe_join(drums) }
44
+ end
45
+
46
+ def dialog_attrs(label, labelledby)
47
+ {
48
+ role: "dialog",
49
+ aria: labelled_aria(label, labelledby: labelledby),
50
+ data: { controller: STIMULUS_CONTROLLER, action: STIMULUS_ACTION }
51
+ }
38
52
  end
39
53
 
40
54
  def drums
@@ -43,50 +57,35 @@ module StimulusPlumbers
43
57
  cols
44
58
  end
45
59
 
46
- def hour_drum
60
+ def render_drum(target, label, items, selected)
47
61
  drum.render(
48
62
  stimulus_controller: STIMULUS_CONTROLLER,
49
- target: "hour",
50
- label: "Hour",
51
- items: hour_items,
52
- selected: current_hour
63
+ target: target,
64
+ label: label,
65
+ items: items,
66
+ selected: selected
53
67
  )
54
68
  end
55
69
 
70
+ def hour_drum
71
+ render_drum("hour", "Hour", hour_items, current_hour)
72
+ end
73
+
56
74
  def minute_drum
57
- items = (0...60).step(@step).map do |m|
58
- s = m.to_s.rjust(2, "0")
59
- [s, s]
60
- end
75
+ items = (0...60).step(@step).map { |m| [m.to_s.rjust(2, "0")] * 2 }
61
76
  selected = @time ? snap_minute(@time.min).to_s.rjust(2, "0") : nil
62
- drum.render(
63
- stimulus_controller: STIMULUS_CONTROLLER,
64
- target: "minute",
65
- label: "Minute",
66
- items: items,
67
- selected: selected
68
- )
77
+ render_drum("minute", "Minute", items, selected)
69
78
  end
70
79
 
71
80
  def period_drum
72
- selected = @time && (@time.hour < 12 ? "AM" : "PM")
73
- drum.render(
74
- stimulus_controller: STIMULUS_CONTROLLER,
75
- target: "period",
76
- label: "Period",
77
- items: [%w[AM AM], %w[PM PM]],
78
- selected: selected
79
- )
81
+ render_drum("period", "Period", [%w[AM AM], %w[PM PM]], @time && (@time.hour < 12 ? "AM" : "PM"))
80
82
  end
81
83
 
82
84
  def hour_items
83
85
  if @format == :h12
84
86
  (1..12).map { |h| [h.to_s, h.to_s] }
85
87
  else
86
- (0..23).map do |h|
87
- s = h.to_s.rjust(2, "0")
88
- [s, s]
89
- end
88
+ (0..23).map { |h| [h.to_s.rjust(2, "0")] * 2 }
90
89
  end
91
90
  end
92
91
 
@@ -4,6 +4,11 @@ module StimulusPlumbers
4
4
  module Components
5
5
  class Combobox
6
6
  class Trigger < Plumber::Base
7
+ STIMULUS_ACTION = [
8
+ "focus->#{Components::Popover::STIMULUS_CONTROLLER}#open",
9
+ "keydown.esc->#{Components::Popover::STIMULUS_CONTROLLER}#close"
10
+ ].join(" ").freeze
11
+
7
12
  def render(...)
8
13
  render_trigger(...)
9
14
  end
@@ -12,8 +17,7 @@ module StimulusPlumbers
12
17
 
13
18
  def render_trigger(
14
19
  stimulus_controller:,
15
- popover_id:,
16
- haspopup:,
20
+ popover:,
17
21
  readonly: true,
18
22
  aria: {},
19
23
  id: nil,
@@ -24,8 +28,7 @@ module StimulusPlumbers
24
28
  )
25
29
  input_html = render_input(
26
30
  stimulus_controller: stimulus_controller,
27
- popover_id: popover_id,
28
- haspopup: haspopup,
31
+ popover: popover,
29
32
  readonly: readonly,
30
33
  aria: aria,
31
34
  id: id,
@@ -40,8 +43,7 @@ module StimulusPlumbers
40
43
 
41
44
  def render_input(
42
45
  stimulus_controller:,
43
- popover_id:,
44
- haspopup:,
46
+ popover:,
45
47
  readonly:,
46
48
  aria:,
47
49
  id:,
@@ -49,22 +51,21 @@ module StimulusPlumbers
49
51
  **kwargs
50
52
  )
51
53
  stimulus_data = {
54
+ popover_target: popover.dig(:data, :popover_target),
52
55
  "#{stimulus_controller}_target": "trigger",
53
56
  input_formatter_target: "input",
54
- action: "focus->#{stimulus_controller}#open keydown.esc->#{stimulus_controller}#close"
57
+ action: STIMULUS_ACTION
55
58
  }
56
59
 
57
- trigger_aria = { haspopup: haspopup, expanded: "false", controls: popover_id }
58
-
59
60
  template.tag.input(
60
61
  **merge_html_options(
61
- { classes: theme.resolve(:combobox_trigger).fetch(:classes, "") },
62
+ theme.resolve(:combobox_trigger),
62
63
  {
63
64
  id: id,
64
65
  type: "text",
65
66
  readonly: (readonly ? true : nil),
66
67
  role: "combobox",
67
- aria: trigger_aria.deep_merge(aria),
68
+ aria: popover.fetch(:aria, {}).deep_merge(aria),
68
69
  data: stimulus_data
69
70
  },
70
71
  { data: data },
@@ -82,7 +83,11 @@ module StimulusPlumbers
82
83
  end
83
84
 
84
85
  def render_trigger_icon(name)
85
- Icon.new(template).render(name: name, aria: { hidden: "true" })
86
+ Components::Icon.new(template).render(
87
+ name: name,
88
+ classes: theme.resolve(:combobox_trigger_icon).fetch(:classes, ""),
89
+ aria: { hidden: "true" }
90
+ )
86
91
  end
87
92
  end
88
93
  end
@@ -4,33 +4,80 @@ module StimulusPlumbers
4
4
  module Components
5
5
  class Combobox
6
6
  class Typeahead < Plumber::Base
7
- def self.default_opts
8
- Dropdown.default_opts.deep_merge(
9
- trigger: { aria: { autocomplete: "list" }, readonly: false }
7
+ def self.listbox_id_for(panel_id)
8
+ [panel_id, "listbox"].compact.join("_")
9
+ end
10
+
11
+ module Metadata
12
+ module_function
13
+
14
+ def haspopup = "listbox"
15
+ def popup_id_for(panel_id) = Typeahead.listbox_id_for(panel_id)
16
+ def trigger_icon = nil
17
+ def trigger_options = { readonly: false, aria: { autocomplete: "list" } }
18
+
19
+ def stimulus_data(panel_id, _options)
20
+ {
21
+ input_combobox_combobox_dropdown_outlet: "##{panel_id}",
22
+ action: "input->#{Combobox::STIMULUS_CONTROLLER}#onInput"
23
+ }
24
+ end
25
+ end
26
+
27
+ def render(...) = render_typeahead(...)
28
+
29
+ private
30
+
31
+ def render_typeahead(panel_attrs: {}, options: [], value: nil, labelledby: nil, label: nil, url: nil)
32
+ template.content_tag(
33
+ :div,
34
+ template.safe_join([render_listbox(panel_attrs[:id], options, value, labelledby, label), loading, empty]),
35
+ **wrapper_attrs(panel_attrs: panel_attrs, url: url)
10
36
  )
11
37
  end
12
38
 
13
- def render(options: [], value: nil, label: nil, labelledby: nil)
14
- template.safe_join(
15
- [
16
- Dropdown.new(template).render(options: options, value: value, label: label, labelledby: labelledby),
17
- loading,
18
- empty
19
- ]
39
+ def wrapper_attrs(panel_attrs: {}, url: nil)
40
+ merge_html_options(
41
+ panel_attrs,
42
+ {
43
+ data: {
44
+ controller: Dropdown::STIMULUS_CONTROLLER,
45
+ action: Dropdown::STIMULUS_ACTION,
46
+ combobox_dropdown_url_value: url
47
+ }.compact
48
+ }
20
49
  )
21
50
  end
22
51
 
23
- private
52
+ def render_listbox(panel_id, options, value, labelledby, label)
53
+ template.content_tag(
54
+ :ul,
55
+ Options.new(template).render(options, value: value),
56
+ **merge_html_options(
57
+ theme.resolve(:combobox_listbox),
58
+ {
59
+ id: self.class.listbox_id_for(panel_id),
60
+ role: "listbox",
61
+ aria: labelled_aria(label, labelledby: labelledby),
62
+ data: { "#{Dropdown::STIMULUS_CONTROLLER}_target": "listbox" }
63
+ }
64
+ )
65
+ )
66
+ end
24
67
 
25
68
  def loading
26
69
  template.content_tag(
27
70
  :div,
28
71
  **merge_html_options(
29
- { classes: theme.resolve(:combobox_typeahead_loading).fetch(:classes, "") },
30
- { hidden: "", aria: { live: "polite" }, data: { "#{Dropdown::STIMULUS_CONTROLLER}_target": "loading" } }
72
+ theme.resolve(:combobox_typeahead_loading),
73
+ { hidden: "", role: "status", data: { "#{Dropdown::STIMULUS_CONTROLLER}_target": "loading" } }
31
74
  )
32
75
  ) do
33
- Icon.new(template).render(name: "spinner", classes: "size-(--sp-icon-size) animate-spin")
76
+ Components::Icon.new(template).render(
77
+ name: "spinner",
78
+ classes: theme.resolve(:combobox_typeahead_loading_icon).fetch(:classes, ""),
79
+ aria: { hidden: "true" }
80
+ )
34
81
  end
35
82
  end
36
83
 
@@ -38,10 +85,10 @@ module StimulusPlumbers
38
85
  template.content_tag(
39
86
  :div,
40
87
  **merge_html_options(
41
- { classes: theme.resolve(:combobox_typeahead_empty).fetch(:classes, "") },
88
+ theme.resolve(:combobox_typeahead_empty),
42
89
  { hidden: "", role: "status", data: { "#{Dropdown::STIMULUS_CONTROLLER}_target": "empty" } }
43
90
  )
44
- ) { "No results" }
91
+ ) { I18n.t("stimulus_plumbers.combobox.typeahead.empty", default: "No results") }
45
92
  end
46
93
  end
47
94
  end
@@ -7,60 +7,80 @@ module StimulusPlumbers
7
7
  FORMAT_CONTROLLER = "input-formatter"
8
8
  FORMAT_ACTION = "input-combobox:changed->input-formatter#format"
9
9
 
10
- def self.popover_id_for(trigger_id)
11
- [trigger_id, "popover"].compact.join("_")
12
- end
10
+ def render(trigger: {}, input: {}, id: nil, label: nil, close_on_select: nil, **kwargs, &block)
11
+ trigger_opts = trigger.dup
12
+ builder = resolve_builder(&block)
13
+ trigger_id = id || trigger_opts.delete(:id) || template.sp_dom_id
14
+ panel_id = Popover.panel_id_for(trigger_id)
13
15
 
14
- def render(...)
15
- render_combobox(...)
16
+ template.content_tag(:div, **combobox_attrs(input, close_on_select, builder, panel_id, kwargs)) do
17
+ build_popover(trigger_opts, input, builder, trigger_id, panel_id, label)
18
+ end
16
19
  end
17
20
 
18
21
  private
19
22
 
20
- def render_combobox(trigger: {}, input: {}, popover: {}, **kwargs, &block)
21
- popover_id = self.class.popover_id_for(trigger[:id])
22
- initial_value = input[:value]
23
- haspopup = popover.delete(:haspopup) { popover[:role] || "dialog" }
24
- html_options = merge_html_options({ data: build_stimulus_data(initial_value) }, kwargs)
25
-
26
- template.content_tag(:div, **html_options) do
27
- template.safe_join(
28
- [
29
- combobox_trigger(popover_id, trigger, haspopup),
30
- hidden_input(input),
31
- combobox_popover(popover_id, popover, &block)
32
- ]
33
- )
34
- end
23
+ def resolve_builder
24
+ builder = Combobox::Builder.new
25
+ yield builder if block_given?
26
+ builder
35
27
  end
36
28
 
37
- def build_stimulus_data(initial_value)
38
- {
39
- controller: "#{STIMULUS_CONTROLLER} #{FORMAT_CONTROLLER}",
40
- action: FORMAT_ACTION
41
- }.tap do |data|
42
- data[:input_combobox_value_value] = initial_value if initial_value.present?
29
+ def build_popover(trigger, input, builder, trigger_id, panel_id, label)
30
+ metadata = builder.metadata
31
+
32
+ Components::Popover.new(template).build(panel_id: panel_id) do |p|
33
+ p.trigger(haspopup: metadata.haspopup, controls: metadata.popup_id_for(panel_id)) do |attrs|
34
+ build_combobox_trigger(attrs, trigger, input, metadata, trigger_id, label)
35
+ end
36
+ p.build_panel(classes: theme.resolve(:combobox_popover).fetch(:classes, "")) do |panel_attrs|
37
+ builder.render_panel(template, panel_attrs: panel_attrs)
38
+ end
43
39
  end
44
40
  end
45
41
 
46
- def combobox_trigger(popover_id, trigger, haspopup)
47
- Combobox::Trigger.new(template).render(
48
- stimulus_controller: STIMULUS_CONTROLLER,
49
- popover_id: popover_id,
50
- haspopup: haspopup,
51
- **trigger
42
+ def combobox_attrs(input, close_on_select, builder, panel_id, kwargs)
43
+ merge_html_options(
44
+ theme.resolve(:combobox),
45
+ kwargs,
46
+ { data: stimulus_data(input[:value], close_on_select) },
47
+ { data: builder.metadata.stimulus_data(panel_id, builder.options) }
52
48
  )
53
49
  end
54
50
 
55
- def combobox_popover(popover_id, popover, &block)
56
- Combobox::Popover.new(template).render(
57
- stimulus_controller: STIMULUS_CONTROLLER,
58
- id: popover_id,
59
- **popover,
60
- &block
51
+ def build_combobox_trigger(attrs, trigger, input, metadata, trigger_id, label)
52
+ opts = trigger_options(metadata, trigger)
53
+ opts[:aria] = (opts[:aria] || {}).merge(label: label) if label
54
+
55
+ template.safe_join(
56
+ [
57
+ Combobox::Trigger.new(template).render(
58
+ stimulus_controller: STIMULUS_CONTROLLER,
59
+ popover: attrs,
60
+ id: trigger_id,
61
+ **opts
62
+ ),
63
+ hidden_input(input)
64
+ ]
61
65
  )
62
66
  end
63
67
 
68
+ def trigger_options(metadata, trigger)
69
+ defaults = metadata.trigger_options.dup
70
+ defaults[:icon_trailing] = metadata.trigger_icon if metadata.trigger_icon
71
+ defaults.deep_merge(trigger)
72
+ end
73
+
74
+ def stimulus_data(initial_value, close_on_select)
75
+ data = {
76
+ controller: "#{Popover::STIMULUS_CONTROLLER} #{STIMULUS_CONTROLLER} #{FORMAT_CONTROLLER}",
77
+ action: FORMAT_ACTION
78
+ }
79
+ data[:input_combobox_value_value] = initial_value if initial_value.present?
80
+ data[:popover_close_on_select_value] = close_on_select unless close_on_select.nil?
81
+ data
82
+ end
83
+
64
84
  def hidden_input(input)
65
85
  stimulus_data = merge_html_options(
66
86
  { "#{STIMULUS_CONTROLLER}_target": "input" },