ariadne_view_components 0.0.43 → 0.0.44

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/app/components/ariadne/ariadne-form.d.ts +22 -0
  4. data/app/components/ariadne/ariadne-form.js +85 -0
  5. data/app/components/ariadne/ariadne-form.ts +96 -0
  6. data/app/components/ariadne/ariadne.d.ts +2 -0
  7. data/app/components/ariadne/ariadne.js +16 -0
  8. data/app/components/ariadne/ariadne.ts +21 -0
  9. data/app/components/ariadne/avatar_component.rb +81 -0
  10. data/app/components/ariadne/avatar_stack_component/avatar_stack_component.html.erb +12 -0
  11. data/app/components/ariadne/avatar_stack_component.rb +75 -0
  12. data/app/components/ariadne/base_button.rb +70 -0
  13. data/app/components/ariadne/base_component.rb +37 -0
  14. data/app/components/ariadne/blankslate_component/blankslate_component.html.erb +26 -0
  15. data/app/components/ariadne/blankslate_component.rb +148 -0
  16. data/app/components/ariadne/body_component.rb +30 -0
  17. data/app/components/ariadne/button_component/button_component.html.erb +4 -0
  18. data/app/components/ariadne/button_component.rb +165 -0
  19. data/app/components/ariadne/clipboard_copy_component/clipboard-copy-component.d.ts +4 -0
  20. data/app/components/ariadne/clipboard_copy_component/clipboard-copy-component.js +18 -0
  21. data/app/components/ariadne/clipboard_copy_component/clipboard-copy-component.ts +19 -0
  22. data/app/components/ariadne/clipboard_copy_component/clipboard_copy_component.html.erb +9 -0
  23. data/app/components/ariadne/clipboard_copy_component.rb +90 -0
  24. data/app/components/ariadne/comment_component/comment_component.html.erb +37 -0
  25. data/app/components/ariadne/comment_component.rb +71 -0
  26. data/app/components/ariadne/component.rb +127 -0
  27. data/app/components/ariadne/container_component/container_component.html.erb +3 -0
  28. data/app/components/ariadne/container_component.rb +25 -0
  29. data/app/components/ariadne/content.rb +12 -0
  30. data/app/components/ariadne/counter_component.rb +100 -0
  31. data/app/components/ariadne/details_component/details_component.html.erb +4 -0
  32. data/app/components/ariadne/details_component.rb +81 -0
  33. data/app/components/ariadne/dropdown/menu_component.html.erb +20 -0
  34. data/app/components/ariadne/dropdown/menu_component.rb +101 -0
  35. data/app/components/ariadne/dropdown/menu_component.ts +1 -0
  36. data/app/components/ariadne/dropdown_component/dropdown_component.html.erb +8 -0
  37. data/app/components/ariadne/dropdown_component.rb +172 -0
  38. data/app/components/ariadne/flash_component/flash_component.html.erb +31 -0
  39. data/app/components/ariadne/flash_component.rb +128 -0
  40. data/app/components/ariadne/flex_component/flex_component.html.erb +5 -0
  41. data/app/components/ariadne/flex_component.rb +56 -0
  42. data/app/components/ariadne/footer_component/footer_component.html.erb +7 -0
  43. data/app/components/ariadne/footer_component.rb +23 -0
  44. data/app/components/ariadne/grid_component/grid_component.html.erb +26 -0
  45. data/app/components/ariadne/grid_component.rb +67 -0
  46. data/app/components/ariadne/header_component/header_component.html.erb +29 -0
  47. data/app/components/ariadne/header_component.rb +111 -0
  48. data/app/components/ariadne/heading_component.rb +49 -0
  49. data/app/components/ariadne/heroicon_component/heroicon_component.html.erb +4 -0
  50. data/app/components/ariadne/heroicon_component.rb +166 -0
  51. data/app/components/ariadne/image_component.rb +53 -0
  52. data/app/components/ariadne/inline_flex_component/inline_flex_component.html.erb +6 -0
  53. data/app/components/ariadne/inline_flex_component.rb +72 -0
  54. data/app/components/ariadne/link_component.rb +65 -0
  55. data/app/components/ariadne/list_component/list_component.html.erb +6 -0
  56. data/app/components/ariadne/list_component.rb +70 -0
  57. data/app/components/ariadne/narrow_container_component/narrow_container_component.html.erb +3 -0
  58. data/app/components/ariadne/narrow_container_component.rb +30 -0
  59. data/app/components/ariadne/panel_bar_component/panel_bar_component.html.erb +20 -0
  60. data/app/components/ariadne/panel_bar_component.rb +80 -0
  61. data/app/components/ariadne/pill_component/pill_component.html.erb +3 -0
  62. data/app/components/ariadne/pill_component.rb +44 -0
  63. data/app/components/ariadne/rich_text_area_component/rich-text-area-component.d.ts +6 -0
  64. data/app/components/ariadne/rich_text_area_component/rich-text-area-component.js +38 -0
  65. data/app/components/ariadne/rich_text_area_component/rich-text-area-component.ts +47 -0
  66. data/app/components/ariadne/rich_text_area_component/rich_text_area_component.html.erb +6 -0
  67. data/app/components/ariadne/rich_text_area_component.rb +35 -0
  68. data/app/components/ariadne/slideover_component/slideover-component.d.ts +9 -0
  69. data/app/components/ariadne/slideover_component/slideover-component.js +11 -0
  70. data/app/components/ariadne/slideover_component/slideover-component.ts +17 -0
  71. data/app/components/ariadne/slideover_component/slideover_component.html.erb +9 -0
  72. data/app/components/ariadne/slideover_component.rb +66 -0
  73. data/app/components/ariadne/tab_component/tab_component.html.erb +3 -0
  74. data/app/components/ariadne/tab_component.rb +98 -0
  75. data/app/components/ariadne/tab_container_component/tab-container-component.d.ts +1 -0
  76. data/app/components/ariadne/tab_container_component/tab-container-component.js +23 -0
  77. data/app/components/ariadne/tab_container_component/tab-container-component.ts +24 -0
  78. data/app/components/ariadne/tab_container_component.erb +10 -0
  79. data/app/components/ariadne/tab_container_component.rb +68 -0
  80. data/app/components/ariadne/tab_nav_component/tab-nav-component.d.ts +9 -0
  81. data/app/components/ariadne/tab_nav_component/tab-nav-component.js +33 -0
  82. data/app/components/ariadne/tab_nav_component/tab-nav-component.ts +34 -0
  83. data/app/components/ariadne/tab_nav_component/tab_nav_component.html.erb +7 -0
  84. data/app/components/ariadne/tab_nav_component.rb +72 -0
  85. data/app/components/ariadne/table_nav_component/table_nav_component.html.erb +52 -0
  86. data/app/components/ariadne/table_nav_component.rb +338 -0
  87. data/app/components/ariadne/text.rb +25 -0
  88. data/app/components/ariadne/time_ago_component/time-ago-component.d.ts +1 -0
  89. data/app/components/ariadne/time_ago_component/time-ago-component.js +1 -0
  90. data/app/components/ariadne/time_ago_component/time-ago-component.ts +1 -0
  91. data/app/components/ariadne/time_ago_component.rb +56 -0
  92. data/app/components/ariadne/timeline_component/timeline_component.html.erb +19 -0
  93. data/app/components/ariadne/timeline_component.rb +34 -0
  94. data/app/components/ariadne/tooltip_component/tooltip-component.d.ts +24 -0
  95. data/app/components/ariadne/tooltip_component/tooltip-component.js +43 -0
  96. data/app/components/ariadne/tooltip_component/tooltip-component.ts +57 -0
  97. data/app/components/ariadne/tooltip_component/tooltip_component.html.erb +4 -0
  98. data/app/components/ariadne/tooltip_component.rb +108 -0
  99. data/lib/ariadne/view_components/engine.rb +0 -22
  100. data/lib/ariadne/view_components/version.rb +1 -1
  101. data/tailwind.config.js +10 -15
  102. metadata +98 -2
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "view_component/version"
4
+
5
+ require "heroicons_helper"
6
+
7
+ module Ariadne
8
+ # @private
9
+ class Component < ViewComponent::Base
10
+ include ClassNameHelper
11
+ include FetchOrFallbackHelper
12
+ include ViewHelper
13
+ include Status::Dsl
14
+ include Audited::Dsl
15
+ include LoggerHelper
16
+ include Ariadne::ActionViewExtensions::FormHelper
17
+
18
+ BASE_WRAPPER_CLASSES = "ariadne-flex ariadne-flex-col ariadne-h-screen ariadne-justify-between"
19
+ BASE_MAIN_CLASSES = "ariadne-flex-auto"
20
+
21
+ # this is defined here for the Tailwind parser to pick up; it can be useful
22
+ # in situations where hiddenness is defined by a kwarg, eg `selected: false`
23
+ BASE_HIDDEN_CLASS = "ariadne-hidden"
24
+
25
+ INVALID_ARIA_LABEL_TAGS = [:div, :span, :p].freeze
26
+
27
+ private def raise_on_invalid_options?
28
+ Rails.application.config.ariadne_view_components.raise_on_invalid_options
29
+ end
30
+
31
+ private def raise_on_invalid_aria?
32
+ Rails.application.config.ariadne_view_components.raise_on_invalid_aria
33
+ end
34
+
35
+ private def deprecated_component_warning(new_class: nil, version: nil)
36
+ return if silence_deprecations?
37
+
38
+ message = "#{self.class.name} is deprecated"
39
+ message += " and will be removed in v#{version}." if version
40
+ message += " Use #{new_class.name} instead." if new_class
41
+
42
+ ActiveSupport::Deprecation.warn(message)
43
+ end
44
+
45
+ private def aria(val, attributes)
46
+ attributes[:"aria-#{val}"] || attributes.dig(:aria, val.to_sym)
47
+ end
48
+
49
+ private def validate_aria_label!
50
+ aria_label = aria("label", @attributes)
51
+ raise ArgumentError, "`aria-label` is required." if aria_label.blank?
52
+ end
53
+
54
+ private def check_denylist(denylist = [], attributes = {})
55
+ if should_raise_error?
56
+
57
+ # Convert denylist from:
58
+ # { [:p, :pt] => "message" } to:
59
+ # { p: "message", pt: "message" }
60
+ unpacked_denylist =
61
+ denylist.each_with_object({}) do |(keys, value), memo|
62
+ keys.each { |key| memo[key] = value }
63
+ end
64
+
65
+ violations = unpacked_denylist.keys & attributes.keys
66
+
67
+ if violations.any?
68
+ message = "Found #{violations.count} #{"violation".pluralize(violations)}:"
69
+ violations.each do |violation|
70
+ message += "\n The #{violation} argument is not allowed here. #{unpacked_denylist[violation]}"
71
+ end
72
+
73
+ raise(ArgumentError, message)
74
+ end
75
+ end
76
+
77
+ attributes
78
+ end
79
+
80
+ private def validate_attributes(tag:, denylist_name: :attributes_denylist, attributes: {})
81
+ deny_single_argument(:class, "Use `classes` instead.", attributes)
82
+
83
+ if (denylist = attributes[denylist_name])
84
+ check_denylist(denylist, attributes)
85
+
86
+ # Remove :attributes_denylist key and any denied keys from attributes
87
+ attributes.except!(denylist_name)
88
+ attributes.except!(*denylist.keys.flatten)
89
+ end
90
+
91
+ deny_aria_label(tag: tag, attributes: attributes)
92
+
93
+ attributes
94
+ end
95
+
96
+ private def deny_single_argument(key, help_text, attributes)
97
+ raise ArgumentError, "`#{key}` is an invalid argument. #{help_text}" \
98
+ if should_raise_error? && attributes.key?(key)
99
+
100
+ attributes.except!(key)
101
+ end
102
+
103
+ private def deny_aria_label(tag:, attributes:)
104
+ return attributes.except!(:skip_aria_label_check) if attributes[:skip_aria_label_check]
105
+ return if attributes[:role]
106
+ return unless INVALID_ARIA_LABEL_TAGS.include?(tag)
107
+
108
+ deny_aria_key(
109
+ :label,
110
+ "Don't use `aria-label` on `#{tag}` elements. See https://www.tpgi.com/short-note-on-aria-label-aria-labelledby-and-aria-describedby/",
111
+ attributes,
112
+ )
113
+ end
114
+
115
+ private def deny_aria_key(key, help_text, attributes)
116
+ raise ArgumentError, help_text if should_raise_aria_error? && aria(key, attributes)
117
+ end
118
+
119
+ private def should_raise_error?
120
+ raise_on_invalid_options? && !ENV["ARIADNE_WARNINGS_DISABLED"]
121
+ end
122
+
123
+ private def should_raise_aria_error?
124
+ raise_on_invalid_aria? && !ENV["ARIADNE_WARNINGS_DISABLED"]
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,3 @@
1
+ <%= render Ariadne::BaseComponent.new(tag: @tag, classes: @classes, attributes: @attributes) do |component| %>
2
+ <%= content %>
3
+ <% end %>
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ariadne
4
+ # The container wraps the majority, if not all, of the content on a page.
5
+ class ContainerComponent < Ariadne::Component
6
+ DEFAULT_CLASSES = "ariadne-px-4 sm:ariadne-px-6 lg:ariadne-px-8"
7
+
8
+ # @example Default
9
+ # <%= render(Ariadne::ContainerComponent.new) do |container| %>
10
+ # <%= render(Ariadne::ButtonComponent.new) { "Click me!" } %>
11
+ # <% end %>
12
+ #
13
+ # @param classes [String] <%= link_to_classes_docs %>
14
+ # @param attributes [Hash] <%= link_to_attributes_docs %>
15
+ def initialize(classes: "", attributes: {})
16
+ @tag = :div
17
+ @classes = merge_class_names(
18
+ DEFAULT_CLASSES,
19
+ classes,
20
+ )
21
+
22
+ @attributes = attributes
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ariadne
4
+ #
5
+ # Use `Content` as a helper to render content passed to a slot without adding any tags.
6
+ #
7
+ class Content < Ariadne::Component
8
+ def call
9
+ content
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ariadne
4
+ # Use `CounterComponent` to add a count to navigational elements and buttons.
5
+ #
6
+ # @accessibility
7
+ # Always use `CounterComponent` with adjacent text that provides supplementary information regarding what the count is for. For instance, `Counter`
8
+ # should be accompanied with text such as `issues` or `pull requests`.
9
+ #
10
+ class CounterComponent < Ariadne::Component
11
+ DEFAULT_CLASSES = "ariadne-inline-flex ariadne-items-center ariadne-p-1 ariadne-border ariadne-border-transparent ariadne-rounded-full ariadne-shadow-sm focus:ariadne-outline-none focus:ariadne-ring-2 focus:ariadne-ring-offset-2"
12
+
13
+ # @example Default
14
+ # <%= render(Ariadne::CounterComponent.new(count: 25)) %>
15
+ #
16
+ # @example Schemes
17
+ # <%= render(Ariadne::CounterComponent.new(count: 25)) %>
18
+ # <%= render(Ariadne::CounterComponent.new(count: 25)) %>
19
+ #
20
+ # @param tag [Symbol, String] The rendered tag name
21
+ # @param classes [String] <%= link_to_classes_docs %>
22
+ # @param count [Integer, Float::INFINITY, nil] The number to be displayed (e.x. # of issues, pull requests)
23
+ # @param limit [Integer, nil] Maximum value to display. Pass `nil` for no limit. (e.x. if `count` == 6,000 and `limit` == 5000, counter will display "5,000+")
24
+ # @param hide_if_zero [Boolean] If true, a `hidden` attribute is added to the counter if `count` is zero.
25
+ # @param text [String] Text to display instead of count.
26
+ # @param round [Boolean] Whether to apply rounding logic to value.
27
+ # @param attributes [Hash] <%= link_to_attributes_docs %>
28
+ def initialize(
29
+ tag: :span,
30
+ count: 0,
31
+ limit: 9_000,
32
+ hide_if_zero: false,
33
+ text: "",
34
+ round: false,
35
+ classes: "",
36
+ attributes: {}
37
+ )
38
+ @count = count
39
+ @limit = limit
40
+ @hide_if_zero = hide_if_zero
41
+ @text = text
42
+ @round = round
43
+ @attributes = attributes
44
+
45
+ @has_limit = !@limit.nil?
46
+
47
+ @tag = check_incoming_tag(:span, tag)
48
+
49
+ @attributes[:title] = title
50
+
51
+ @classes = merge_class_names(
52
+ DEFAULT_CLASSES,
53
+ classes,
54
+ )
55
+ @attributes[:hidden] = true if count == 0 && hide_if_zero
56
+ end
57
+
58
+ def call
59
+ render(Ariadne::BaseComponent.new(tag: @tag, classes: @classes, attributes: @attributes)) { value }
60
+ end
61
+
62
+ private def title
63
+ if @text.present?
64
+ @text
65
+ elsif @count.nil?
66
+ "Not available"
67
+ elsif @count == Float::INFINITY
68
+ "Infinity"
69
+ else
70
+ count = @count.to_i
71
+ str = number_with_delimiter(@has_limit ? [count, @limit].min : count)
72
+ str += "+" if @has_limit && count > @limit
73
+ str
74
+ end
75
+ end
76
+
77
+ private def value
78
+ if @text.present?
79
+ @text
80
+ elsif @count.nil?
81
+ "" # CSS will hide it
82
+ elsif @count == Float::INFINITY
83
+ "∞"
84
+ else
85
+ if @round
86
+ count = @has_limit ? [@count.to_i, @limit].min : @count.to_i
87
+ precision = count.between?(100_000, 999_999) ? 0 : 1
88
+ units = { thousand: "k", million: "m", billion: "b" }
89
+ str = number_to_human(count, precision: precision, significant: false, units: units, format: "%n%u")
90
+ else
91
+ @count = @count.to_i
92
+ str = number_with_delimiter(@has_limit ? [@count, @limit].min : @count)
93
+ end
94
+
95
+ str += "+" if @has_limit && @count.to_i > @limit
96
+ str
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,4 @@
1
+ <%= render Ariadne::BaseComponent.new(tag: @tag, classes: @classes, attributes: @attributes) do |component| %>
2
+ <%= summary %>
3
+ <%= body %>
4
+ <% end %>
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ariadne
4
+ # Use `DetailsComponent` to reveal content after clicking a button.
5
+ class DetailsComponent < Ariadne::Component
6
+ DEFAULT_TAG = :details
7
+ DEFAULT_BODY_TAG = :div
8
+
9
+ VALID_BODY_TAGS = [:ul, :"details-menu", :"details-dialog", DEFAULT_BODY_TAG].freeze
10
+
11
+ NO_OVERLAY = :none
12
+ OVERLAY_MAPPINGS = {
13
+ NO_OVERLAY => "",
14
+ :default => "",
15
+ }.freeze
16
+
17
+ # Use the Summary as a trigger to reveal the content.
18
+ #
19
+ # @param button [Boolean] (true) Whether to render the Summary as a button or not.
20
+ # @param classes [String] <%= link_to_classes_docs %>
21
+ # @param attributes [Hash] <%= link_to_attributes_docs %>
22
+ renders_one :summary, lambda { |button: true, classes: "", attributes: {}|
23
+ tag = :summary
24
+ attributes[:role] = "button"
25
+ if button
26
+ Ariadne::ButtonComponent.new(tag: tag, classes: classes, attributes: attributes)
27
+ else
28
+ Ariadne::BaseComponent.new(tag: tag, classes: classes, attributes: attributes)
29
+ end
30
+ }
31
+
32
+ DEFAULT_BODY_CLASSES = "ariadne-absolute ariadne-mt-2 ariadne-w-56 ariadne-divide-y ariadne-divide-gray-100 ariadne-rounded-md ariadne-shadow-lg ariadne-ring-1 ariadne-ring-black ariadne-ring-opacity-5 focus:ariadne-outline-none"
33
+ # Use the Body slot as the main content to be shown when triggered by the Summary.
34
+ #
35
+ # @param tag [Symbol] The tag to use for the body/ <%= one_of(Ariadne::DetailsComponent::VALID_BODY_TAGS) %>
36
+ # @param classes [String] <%= link_to_classes_docs %>
37
+ # @param attributes [Hash] <%= link_to_attributes_docs %>
38
+ renders_one :body, lambda { |tag: DEFAULT_BODY_TAG, classes: "", attributes: {}|
39
+ tag = fetch_or_raise(VALID_BODY_TAGS, tag)
40
+ actual_classes = merge_class_names(DEFAULT_BODY_CLASSES, classes)
41
+
42
+ Ariadne::BaseComponent.new(tag: tag, classes: actual_classes, attributes: attributes)
43
+ }
44
+
45
+ DEFAULT_CLASSES = ""
46
+
47
+ # @example Default
48
+ #
49
+ # <%= render Ariadne::DetailsComponent.new do |c| %>
50
+ # <% c.with_summary do %>
51
+ # Summary
52
+ # <% end %>
53
+ # <% c.with_body do %>
54
+ # Body
55
+ # <% end %>
56
+ # <% end %>
57
+ #
58
+ # @param overlay [Symbol] Dictates the type of overlay to render with. <%= one_of(Ariadne::DetailsComponent::OVERLAY_MAPPINGS.keys) %>
59
+ # @param reset [Boolean] If set to true, it will remove the default caret and remove style from the summary element
60
+ # @param classes [String] <%= link_to_classes_docs %>
61
+ # @param attributes [Hash] <%= link_to_attributes_docs %>
62
+ def initialize(overlay: NO_OVERLAY, reset: true, classes: "", attributes: {})
63
+ @tag = DEFAULT_TAG
64
+ @reset = reset
65
+
66
+ @classes = merge_class_names(
67
+ DEFAULT_CLASSES,
68
+ classes,
69
+ @reset ? "ariadne__details-reset" : "",
70
+ )
71
+
72
+ @attributes = attributes
73
+
74
+ @overlay = fetch_or_raise(OVERLAY_MAPPINGS.keys, overlay)
75
+ end
76
+
77
+ def render?
78
+ summary? && body?
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,20 @@
1
+ <%= render Ariadne::BaseComponent.new(tag: @tag, classes: @classes, attributes: @attributes) do |component| %>
2
+ <%= header %>
3
+ <% if list? %>
4
+ <ul>
5
+ <% items.each do |item| %>
6
+ <% if item.divider? %>
7
+ <%= item %>
8
+ <% else %>
9
+ <li>
10
+ <%= item %>
11
+ </li>
12
+ <% end %>
13
+ <% end %>
14
+ </ul>
15
+ <% else %>
16
+ <% items.each do |item| %>
17
+ <%= item %>
18
+ <% end %>
19
+ <% end %>
20
+ <% end %>
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ariadne
4
+ module Dropdown
5
+ # This component is part of `DropdownComponent` and should not be
6
+ # used as a standalone component.
7
+ class MenuComponent < Ariadne::Component
8
+ DEFAULT_AS_OPTION = :default
9
+ VALID_AS_OPTIONS = [DEFAULT_AS_OPTION, :list].freeze
10
+
11
+ DEFAULT_DIRECTION = :se
12
+ VALID_DIRECTIONS = [DEFAULT_DIRECTION, :sw, :w, :e, :ne, :s].freeze
13
+
14
+ DEFAULT_HEADER_CLASSES = "ariadne-text-sm ariadne-font-medium ariadne-text-gray-900"
15
+ # @param classes [String] <%= link_to_classes_docs %>
16
+ # @param attributes [Hash] <%= link_to_attributes_docs %>
17
+ renders_one :header, lambda { |classes: "", attributes: {}|
18
+ actual_classes = merge_class_names(DEFAULT_HEADER_CLASSES, classes)
19
+ Ariadne::BaseComponent.new(tag: :span, classes: actual_classes, attributes: attributes)
20
+ }
21
+
22
+ # @param tag [Boolean] <%= one_of(Ariadne::Dropdown::MenuComponent::Item::VALID_TAGS) %>.
23
+ # @param divider [Boolean] Whether the item is a divider without any function.
24
+ # @param classes [String] <%= link_to_classes_docs %>
25
+ # @param attributes [Hash] <%= link_to_attributes_docs %>
26
+ renders_many :items, lambda { |tag: Ariadne::Dropdown::MenuComponent::Item::DEFAULT_TAG, divider: false, classes: "", attributes: {}|
27
+ Ariadne::Dropdown::MenuComponent::Item.new(tag: tag, as: @as, divider: divider, classes: classes, attributes: attributes)
28
+ }
29
+
30
+ DEFAULT_TAG = :"details-menu"
31
+ TAG_OPTIONS = [DEFAULT_TAG].freeze
32
+
33
+ DEFAULT_CLASSES = ""
34
+
35
+ # @param as [Symbol] When `as` is `:list`, wraps the menu in a `<ul>` with a `<li>` for each item.
36
+ # @param direction [Symbol] <%= one_of(Ariadne::Dropdown::MenuComponent::VALID_DIRECTIONS) %>.
37
+ # @param classes [String] <%= link_to_classes_docs %>
38
+ # @param attributes [Hash] <%= link_to_attributes_docs %>
39
+ def initialize(as: DEFAULT_AS_OPTION, direction: VALID_DIRECTIONS, classes: "", attributes: {})
40
+ @tag = DEFAULT_TAG
41
+ @classes = merge_class_names(
42
+ DEFAULT_CLASSES,
43
+ classes,
44
+ )
45
+
46
+ @attributes = attributes
47
+
48
+ @direction = direction
49
+ @as = fetch_or_raise(VALID_AS_OPTIONS, as)
50
+
51
+ @attributes[:role] = "menu"
52
+ end
53
+
54
+ private def list?
55
+ @as == :list
56
+ end
57
+
58
+ # Items to be rendered in the `Dropdown` menu.
59
+ class Item < Ariadne::Component
60
+ DEFAULT_TAG = :a
61
+ BUTTON_TAGS = [:button, :summary].freeze
62
+ VALID_TAGS = [DEFAULT_TAG, *BUTTON_TAGS].freeze
63
+
64
+ DEFAULT_ITEM_CLASSES = "ariadne-block ariadne-px-4 ariadne-py-2 ariadne-text-sm ariadne-text-gray-700"
65
+
66
+ def initialize(as:, tag: DEFAULT_TAG, divider: false, classes: "", attributes: {})
67
+ @divider = divider
68
+ @as = as
69
+
70
+ @classes = merge_class_names(DEFAULT_ITEM_CLASSES, classes)
71
+ @attributes = attributes
72
+ @tag = fetch_or_raise(VALID_TAGS, tag)
73
+ @tag = :li if list? && divider?
74
+ @attributes[:role] ||= :menuitem
75
+ @attributes[:role] = :separator if @divider
76
+ end
77
+
78
+ def call
79
+ component = if BUTTON_TAGS.include?(@tag)
80
+ Ariadne::ButtonComponent.new(scheme: Ariadne::ButtonComponent::LINK_SCHEME, classes: @classes, attributes: @attributes)
81
+ else
82
+ Ariadne::BaseComponent.new(tag: @tag, classes: @classes, attributes: @attributes)
83
+ end
84
+
85
+ # divider has no content
86
+ render(component) if divider?
87
+
88
+ render(component) { content }
89
+ end
90
+
91
+ def divider?
92
+ @divider
93
+ end
94
+
95
+ def list?
96
+ @as == :list
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1 @@
1
+ import '@github/details-menu-element'
@@ -0,0 +1,8 @@
1
+ <%= render(Ariadne::DetailsComponent.new(classes: @classes, attributes: @attributes)) do |c| %>
2
+ <% c.with_summary(classes: @button_classes, attributes: @button_attributes) do %>
3
+ <%= button %>
4
+ <% end %>
5
+ <% c.with_body do %>
6
+ <%= menu %>
7
+ <% end %>
8
+ <% end %>
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ariadne
4
+ # `DropdownComponent` is a lightweight context menu for holding navigation and actions.
5
+ class DropdownComponent < Ariadne::Component
6
+ DEFAULT_TAG = :div
7
+ TAG_OPTIONS = [DEFAULT_TAG].freeze
8
+
9
+ DEFAULT_CLASSES = ""
10
+
11
+ # Required trigger for the dropdown. Has the same arguments as <%= link_to_component(Ariadne::ButtonComponent) %>,
12
+ # but it is locked as a `summary` tag.
13
+ #
14
+ # @param size [Symbol] <%= one_of(Ariadne::BaseButton::VALID_SIZES) %>
15
+ # @param type [Symbol] <%= one_of(Ariadne::BaseButton::VALID_TYPES) %>
16
+ # @param scheme [Symbol] <%= one_of(Ariadne::ButtonComponent::VALID_SCHEMES) %>
17
+ # @param classes [String] <%= link_to_classes_docs %>
18
+ # @param attributes [Hash] <%= link_to_attributes_docs %>
19
+ renders_one :button, lambda { |size: Ariadne::BaseButton::DEFAULT_SIZE, scheme: :none, classes: "", attributes: {}|
20
+ @button_classes = classes
21
+ @button_attributes = attributes
22
+ @button_attributes[:button] = true
23
+
24
+ Ariadne::ButtonComponent.new(tag: :summary, type: :button, scheme: scheme, dropdown: @with_caret, size: size, classes: classes, attributes: attributes)
25
+ }
26
+
27
+ # Required context menu for the dropdown.
28
+ #
29
+ # @param as [Symbol] When `as` is `:list`, wraps the menu in a `<ul>` with a `<li>` for each item.
30
+ # @param direction [Symbol] <%= one_of(Ariadne::Dropdown::MenuComponent::VALID_DIRECTIONS) %>
31
+ # @param header [String] Optional string to display as the header
32
+ # @param classes [String] <%= link_to_classes_docs %>
33
+ # @param attributes [Hash] <%= link_to_attributes_docs %>
34
+ renders_one :menu, "Ariadne::Dropdown::MenuComponent"
35
+ # TODO: remove *Item suffix from nested classes
36
+
37
+ # @example Default
38
+ # <%= render(Ariadne::DropdownComponent.new) do |c| %>
39
+ # <% c.with_button do %>
40
+ # Dropdown
41
+ # <% end %>
42
+ #
43
+ # <% c.with_menu do |menu| %>
44
+ # <% menu.with_header { "Header" } %>
45
+ # <% menu.with_item { "Item 1" } %>
46
+ # <% menu.with_item { "Item 2" } %>
47
+ # <% menu.with_item { "Item 3" } %>
48
+ # <% end %>
49
+ # <% end %>
50
+ #
51
+ # @example With dividers
52
+ #
53
+ # @description
54
+ # Dividers can be used to separate a group of items. They don't have any content.
55
+ # @code
56
+ # <%= render(Ariadne::DropdownComponent.new) do |c| %>
57
+ # <% c.with_button do %>
58
+ # Dropdown
59
+ # <% end %>
60
+ #
61
+ # <% c.with_menu do |menu| %>
62
+ # <% menu.with_header { "Header" } %>
63
+ # <%= menu.with_item { "Item 1" } %>
64
+ # <%= menu.with_item { "Item 2" } %>
65
+ # <%= menu.with_item(divider: true) %>
66
+ # <%= menu.with_item { "Item 3" } %>
67
+ # <%= menu.with_item { "Item 4" } %>
68
+ # <%= menu.with_item(divider: true) %>
69
+ # <%= menu.with_item { "Item 5" } %>
70
+ # <%= menu.with_item { "Item 6" } %>
71
+ # <% end %>
72
+ # <% end %>
73
+ #
74
+ # @example With direction
75
+ # <%= render(Ariadne::DropdownComponent.new) do |c| %>
76
+ # <% c.with_button do %>
77
+ # Dropdown
78
+ # <% end %>
79
+ #
80
+ # <% c.with_menu(direction: :s) do |menu| %>
81
+ # <% menu.header { "Header" } %>
82
+ # <%= menu.with_item { "Item 1" } %>
83
+ # <%= menu.with_item { "Item 2" } %>
84
+ # <%= menu.with_item { "Item 3" } %>
85
+ # <%= menu.with_item { "Item 4" } %>
86
+ # <% end %>
87
+ # <% end %>
88
+ #
89
+ # @example With caret
90
+ # <%= render(Ariadne::DropdownComponent.new(with_caret: true)) do |c| %>
91
+ # <% c.with_button do %>
92
+ # Dropdown
93
+ # <% end %>
94
+ #
95
+ # <% c.with_menu do |menu| %>
96
+ # <% menu.with_header { "Header" } %>
97
+ # <%= menu.with_item { "Item 1" } %>
98
+ # <%= menu.with_item { "Item 2" } %>
99
+ # <%= menu.with_item { "Item 3" } %>
100
+ # <%= menu.with_item { "Item 4" } %>
101
+ # <% end %>
102
+ # <% end %>
103
+ #
104
+ # @example Customizing the button
105
+ # <%= render(Ariadne::DropdownComponent.new) do |c| %>
106
+ # <% c.with_button(scheme: :info, size: :sm) do %>
107
+ # Dropdown
108
+ # <% end %>
109
+ #
110
+ # <% c.with_menu do |menu| %>
111
+ # <% menu.with_header { "Header" } %>
112
+ # <%= menu.with_item { "Item 1" } %>
113
+ # <%= menu.with_item { "Item 2" } %>
114
+ # <%= menu.with_item { "Item 3" } %>
115
+ # <%= menu.with_item { "Item 4" } %>
116
+ # <% end %>
117
+ # <% end %>
118
+ #
119
+ # @example Menu as list
120
+ # <%= render(Ariadne::DropdownComponent.new) do |c| %>
121
+ # <% c.with_button do %>
122
+ # Dropdown
123
+ # <% end %>
124
+ #
125
+ # <% c.with_menu(as: :list) do |menu| %>
126
+ # <% menu.with_header { "Header" } %>
127
+ # <% menu.with_item { "Item 1" } %>
128
+ # <% menu.with_item { "Item 2" } %>
129
+ # <% menu.with_item(divider: true) %>
130
+ # <% menu.with_item { "Item 3" } %>
131
+ # <% menu.with_item { "Item 4" } %>
132
+ # <% end %>
133
+ # <% end %>
134
+ #
135
+ # @example Customizing menu items
136
+ # <%= render(Ariadne::DropdownComponent.new) do |c| %>
137
+ # <% c.with_button do %>
138
+ # Dropdown
139
+ # <% end %>
140
+ #
141
+ # <% c.with_menu do |menu| %>
142
+ # <% menu.with_header { "Header" } %>
143
+ # <% menu.with_item(tag: :button) { "Item 1" } %>
144
+ # <% menu.with_item(classes: "ariadne-text-red-500") { "Item 2" } %>
145
+ # <% menu.with_item { "Item 3" } %>
146
+ # <% end %>
147
+ # <% end %>
148
+ #
149
+ # @param overlay [Symbol] <%= one_of(Ariadne::DetailsComponent::OVERLAY_MAPPINGS.keys) %>
150
+ # @param with_caret [Boolean] Whether or not a caret should be rendered in the button.
151
+ # @param classes [String] <%= link_to_classes_docs %>
152
+ # @param attributes [Hash] <%= link_to_attributes_docs %>
153
+ def initialize(overlay: :default, with_caret: false, classes: "", attributes: {})
154
+ @tag = check_incoming_tag(DEFAULT_TAG, tag)
155
+ @classes = merge_class_names(
156
+ DEFAULT_CLASSES,
157
+ classes,
158
+ )
159
+
160
+ @attributes = attributes
161
+
162
+ @with_caret = with_caret
163
+
164
+ @overlay = overlay
165
+ @attributes[:reset] = true
166
+ end
167
+
168
+ def render?
169
+ button.present? && menu.present?
170
+ end
171
+ end
172
+ end