ariadne_view_components 0.0.12-x86_64-linux → 0.0.14-x86_64-linux

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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/ariadne.d.ts +1 -0
  3. data/app/assets/javascripts/ariadne_view_components.js +2 -2
  4. data/app/assets/javascripts/ariadne_view_components.js.map +1 -1
  5. data/app/assets/javascripts/comment-component.d.ts +1 -2
  6. data/app/assets/javascripts/tab-container-component.d.ts +1 -0
  7. data/app/assets/javascripts/tab-nav-component.d.ts +9 -0
  8. data/app/assets/stylesheets/ariadne_view_components.css +1 -0
  9. data/app/assets/stylesheets/dropdown.css +46 -0
  10. data/app/assets/stylesheets/tooltip-component.css +8 -8
  11. data/app/components/ariadne/ariadne.ts +3 -2
  12. data/app/components/ariadne/avatar_component.rb +81 -0
  13. data/app/components/ariadne/avatar_stack_component.html.erb +12 -0
  14. data/app/components/ariadne/avatar_stack_component.rb +75 -0
  15. data/app/components/ariadne/base_button.rb +13 -4
  16. data/app/components/ariadne/blankslate_component.rb +4 -4
  17. data/app/components/ariadne/body_component.rb +1 -1
  18. data/app/components/ariadne/button_component.html.erb +1 -1
  19. data/app/components/ariadne/button_component.rb +11 -5
  20. data/app/components/ariadne/comment-component.ts +32 -50
  21. data/app/components/ariadne/comment_component.html.erb +31 -13
  22. data/app/components/ariadne/comment_component.rb +18 -6
  23. data/app/components/ariadne/component.rb +3 -5
  24. data/app/components/ariadne/container_component.rb +1 -1
  25. data/app/components/ariadne/counter_component.rb +1 -1
  26. data/app/components/ariadne/details_component.html.erb +4 -0
  27. data/app/components/ariadne/details_component.rb +80 -0
  28. data/app/components/ariadne/dropdown/menu_component.html.erb +20 -0
  29. data/app/components/ariadne/dropdown/menu_component.rb +101 -0
  30. data/app/components/ariadne/dropdown/menu_component.ts +1 -0
  31. data/app/components/ariadne/dropdown_component.html.erb +8 -0
  32. data/app/components/ariadne/dropdown_component.rb +172 -0
  33. data/app/components/ariadne/flash_component.rb +1 -1
  34. data/app/components/ariadne/flex_component.rb +2 -2
  35. data/app/components/ariadne/footer_component.html.erb +1 -1
  36. data/app/components/ariadne/footer_component.rb +1 -1
  37. data/app/components/ariadne/grid_component.html.erb +2 -2
  38. data/app/components/ariadne/grid_component.rb +10 -8
  39. data/app/components/ariadne/header_component.rb +3 -3
  40. data/app/components/ariadne/heroicon_component.html.erb +1 -1
  41. data/app/components/ariadne/heroicon_component.rb +7 -6
  42. data/app/components/ariadne/inline_flex_component.html.erb +1 -0
  43. data/app/components/ariadne/inline_flex_component.rb +8 -1
  44. data/app/components/ariadne/link_component.rb +2 -2
  45. data/app/components/ariadne/list_component.html.erb +2 -11
  46. data/app/components/ariadne/list_component.rb +11 -15
  47. data/app/components/ariadne/main_component.rb +2 -2
  48. data/app/components/ariadne/narrow_container_component.rb +1 -1
  49. data/app/components/ariadne/panel_bar_component.rb +2 -2
  50. data/app/components/ariadne/pill_component.rb +20 -6
  51. data/app/components/ariadne/rich-text-area-component.ts +4 -4
  52. data/app/components/ariadne/rich_text_area_component.html.erb +2 -2
  53. data/app/components/ariadne/rich_text_area_component.rb +2 -2
  54. data/app/components/ariadne/slideover_component.html.erb +1 -1
  55. data/app/components/ariadne/slideover_component.rb +2 -2
  56. data/app/components/ariadne/tab-container-component.ts +24 -0
  57. data/app/components/ariadne/tab-nav-component.ts +34 -0
  58. data/app/components/ariadne/tab_component.html.erb +2 -6
  59. data/app/components/ariadne/tab_component.rb +75 -19
  60. data/app/components/ariadne/tab_container_component.erb +12 -0
  61. data/app/components/ariadne/tab_container_component.rb +67 -0
  62. data/app/components/ariadne/tab_nav_component.html.erb +7 -0
  63. data/app/components/ariadne/tab_nav_component.rb +72 -0
  64. data/app/components/ariadne/table_nav_component.html.erb +52 -0
  65. data/app/components/ariadne/table_nav_component.rb +338 -0
  66. data/app/components/ariadne/time_ago_component.rb +1 -1
  67. data/app/components/ariadne/timeline_component.rb +1 -1
  68. data/app/components/ariadne/tooltip_component.html.erb +1 -1
  69. data/app/components/ariadne/tooltip_component.rb +4 -4
  70. data/app/lib/ariadne/action_view_extensions/form_helper.rb +21 -7
  71. data/app/lib/ariadne/fetch_or_fallback_helper.rb +11 -3
  72. data/app/lib/ariadne/form_builder.rb +2 -2
  73. data/app/lib/ariadne/icon_helper.rb +17 -13
  74. data/lib/ariadne/view_components/constants.rb +2 -2
  75. data/lib/ariadne/view_components/statuses.rb +2 -2
  76. data/lib/ariadne/view_components/version.rb +1 -1
  77. data/lib/rubocop/config/default.yml +1 -1
  78. data/lib/tasks/docs.rake +22 -97
  79. data/static/arguments.yml +151 -23
  80. data/static/audited_at.json +17 -1
  81. data/static/classes.yml +160 -268
  82. data/static/constants.json +208 -40
  83. data/static/statuses.json +17 -1
  84. data/tailwind.config.js +28 -19
  85. metadata +24 -10
  86. data/app/components/ariadne/tab_bar_component.html.erb +0 -3
  87. data/app/components/ariadne/tab_bar_component.rb +0 -45
  88. data/app/lib/ariadne/join_style_arguments_helper.rb +0 -14
  89. data/app/lib/ariadne/tab_nav_helper.rb +0 -35
  90. data/app/lib/ariadne/tabbed_component_helper.rb +0 -39
  91. data/app/lib/ariadne/test_selector_helper.rb +0 -20
  92. data/app/lib/ariadne/underline_nav_helper.rb +0 -44
@@ -11,13 +11,15 @@ module Ariadne
11
11
  include IconHelper
12
12
  include HeroiconsHelper
13
13
 
14
- SIZE_DEFAULT = :sm
15
- SIZE_MEDIUM = :md
14
+ SIZE_MICRO = :mu
15
+ SIZE_SMALL = :sm
16
+ SIZE_DEFAULT = :md
16
17
  SIZE_LARGE = :lg
17
18
 
18
19
  SIZE_MAPPINGS = {
19
- SIZE_DEFAULT => 16,
20
- SIZE_MEDIUM => 24,
20
+ SIZE_MICRO => 16,
21
+ SIZE_SMALL => 20,
22
+ SIZE_DEFAULT => 24,
21
23
  SIZE_LARGE => 128,
22
24
  }.freeze
23
25
  SIZE_OPTIONS = SIZE_MAPPINGS.keys
@@ -107,10 +109,9 @@ module Ariadne
107
109
  }
108
110
 
109
111
  @icon = heroicon(icon, variant: variant, **attributes)
110
-
111
112
  @classes = class_names(
112
113
  @icon.attributes[:class],
113
- classes
114
+ classes,
114
115
  )
115
116
  @attributes.merge!(@icon.attributes.except(:class, :"aria-hidden"))
116
117
 
@@ -2,4 +2,5 @@
2
2
  <%= icon %>
3
3
  <%= item %>
4
4
  <%= text %>
5
+ <%= dropdown %>
5
6
  <% end %>
@@ -19,7 +19,7 @@ module Ariadne
19
19
  </svg>
20
20
  MSG
21
21
  STATE_CLOSED_SVG = <<~MSG
22
- <svg viewBox="0 0 24 24" width="12" height="12" class="ariadne-stroke-state-closed fill-state-closed " stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
22
+ <svg viewBox="0 0 24 24" width="12" height="12" class="ariadne-stroke-state-closed ariadne-fill-state-closed " stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
23
23
  <circle cx="12" cy="12" r="10"></circle>
24
24
  </svg>
25
25
  MSG
@@ -42,12 +42,19 @@ module Ariadne
42
42
  Ariadne::BaseComponent.new(tag: :span, classes: actual_classes, attributes: attributes) { content }
43
43
  }
44
44
 
45
+ renders_one :dropdown, "Ariadne::DropdownComponent"
46
+
45
47
  # @example Default
46
48
  #
47
49
  # <%= render(Ariadne::InlineFlexComponent.new) do |c| %>
48
50
  # <% c.item { Ariadne::InlineFlexComponent::STATE_OPEN_SVG.html_safe } %>
49
51
  # <% end %>
50
52
  #
53
+ # # TODO: STATE_CLOSED_SVG colors didn't show until it was listed in an example
54
+ # <%= render(Ariadne::InlineFlexComponent.new) do |c| %>
55
+ # <% c.item { Ariadne::InlineFlexComponent::STATE_CLOSED_SVG.html_safe } %>
56
+ # <% end %>
57
+ #
51
58
  # <%= render(Ariadne::InlineFlexComponent.new) do |c| %>
52
59
  # <% c.icon(icon: :check, size: :sm, variant: HeroiconsHelper::Icon::VARIANT_SOLID) %>
53
60
  # <% c.text { "Closed" } %>
@@ -6,8 +6,8 @@ module Ariadne
6
6
  DEFAULT_TAG = :a
7
7
  TAG_OPTIONS = [DEFAULT_TAG, :span].freeze
8
8
 
9
- DEFAULT_CLASSES = "ariadne-cursor-pointer"
10
- DEFAULT_ACTIONABLE_CLASSES = " ariadne-cursor-pointer ariadne-underline ariadne-decoration-double ariadne-font-semibold hover:ariadne-text-button-text-color focus:ariadne-outline-none focus:ariadne-ring-2 focus:ariadne-ring-offset-2 focus:ariadne-ring-purple-500"
9
+ DEFAULT_CLASSES = "ariadne-cursor-pointer ariadne-font-semibold hover:ariadne-text-button-text-color focus:ariadne-outline-none focus:ariadne-ring-2 focus:ariadne-ring-offset-2 focus:ariadne-ring-purple-500"
10
+ DEFAULT_ACTIONABLE_CLASSES = "ariadne-underline ariadne-decoration-double"
11
11
 
12
12
  # `Tooltip` that appears on mouse hover or keyboard focus over the button. Use tooltips sparingly and as a last resort.
13
13
  # **Important:** This tooltip defaults to `type: :description`. In a few scenarios, `type: :label` may be more appropriate.
@@ -1,15 +1,6 @@
1
1
  <%= render Ariadne::BaseComponent.new(tag: @tag, classes: @classes, attributes: @attributes) do |list| %>
2
+ <%= content %>
2
3
  <% items.each do |item| %>
3
- <% if item.linked? %>
4
- <%= render Ariadne::BaseComponent.new(tag: :li, classes: item.classes, attributes: item.attributes) do %>
5
- <%= render Ariadne::LinkComponent.new(href: item.link[:href], classes: item.link[:classes], attributes: item.link[:attributes]) do %>
6
- <%= item.entry %>
7
- <% end %>
8
- <% end %>
9
- <% else %>
10
- <%= render Ariadne::BaseComponent.new(tag: :li, classes: item.classes, attributes: item.attributes) do %>
11
- <%= item.entry %>
12
- <% end %>
13
- <% end %>
4
+ <%= item %>
14
5
  <% end %>
15
6
  <% end %>
@@ -13,7 +13,7 @@ module Ariadne
13
13
  # <%= render(Ariadne::ListComponent.new) do |list| %>
14
14
  # <% numbers.each do |number| %>
15
15
  # <%= list.item do |item| %>
16
- # <%= item.entry { number } %>
16
+ # <%= number %>
17
17
  # <% end %>
18
18
  # <% end %>
19
19
  # <% end %>
@@ -27,23 +27,11 @@ module Ariadne
27
27
  @attributes = attributes
28
28
  end
29
29
 
30
- def render?
31
- items.any?
32
- end
33
-
34
30
  # This component is part of `ListComponent` and should not be
35
31
  # used as a standalone component.
36
32
  class ListItem < Ariadne::Component
37
33
  DEFAULT_ITEM_CLASSES = "ariadne-relative ariadne-p-1.5 focus:ariadne-ring-2 focus:ariadne-ring-offset-2 focus:ariadne-ring-purple-500 hover:ariadne-bg-button-hover-color"
38
34
 
39
- renders_one :entry, lambda { |static_content = nil, &block|
40
- next static_content if static_content.present?
41
-
42
- render(Ariadne::BaseComponent.new(tag: :div)) do
43
- view_context.capture { block&.call }
44
- end
45
- }
46
-
47
35
  attr_reader :link, :classes, :attributes
48
36
 
49
37
  def initialize(link: {}, classes: "", attributes: {})
@@ -56,12 +44,20 @@ module Ariadne
56
44
  @selected
57
45
  end
58
46
 
59
- def linked?
47
+ private def linked?
60
48
  @link.present?
61
49
  end
62
50
 
63
51
  def call
64
- Ariadne::BaseComponent.new(tag: :div, classes: @classes, attributes: @attributes)
52
+ render(Ariadne::BaseComponent.new(tag: :li, classes: @classes, attributes: @attributes)) do
53
+ if linked?
54
+ render(Ariadne::LinkComponent.new(href: @link[:href], classes: @link[:classes], attributes: @link[:attributes])) do
55
+ content
56
+ end
57
+ else
58
+ content
59
+ end
60
+ end
65
61
  end
66
62
  end
67
63
  end
@@ -5,7 +5,7 @@ module Ariadne
5
5
  # Add additional usage considerations or best practices that may aid the user to use the component correctly.
6
6
  # @accessibility Add any accessibility considerations
7
7
  class MainComponent < Ariadne::Component
8
- DEFAULT_CLASSES = "flex-auto"
8
+ DEFAULT_CLASSES = "ariadne-flex-auto"
9
9
 
10
10
  # @example Default
11
11
  #
@@ -17,7 +17,7 @@ module Ariadne
17
17
  @tag = :main
18
18
  @classes = class_names(
19
19
  DEFAULT_CLASSES,
20
- classes
20
+ classes,
21
21
  )
22
22
 
23
23
  @attributes = attributes
@@ -21,7 +21,7 @@ module Ariadne
21
21
  @tag = :div
22
22
  @classes = class_names(
23
23
  DEFAULT_CLASSES,
24
- classes
24
+ classes,
25
25
  )
26
26
 
27
27
  @attributes = attributes
@@ -23,7 +23,7 @@ module Ariadne
23
23
  @tag = DEFAULT_TAG
24
24
  @classes = class_names(
25
25
  DEFAULT_CLASSES,
26
- classes
26
+ classes,
27
27
  )
28
28
 
29
29
  @attributes = attributes
@@ -72,7 +72,7 @@ module Ariadne
72
72
  end
73
73
 
74
74
  def call
75
- Ariadne::BaseComponent.new(tag: :div, classes: @classes, attributes: @attributes)
75
+ render(Ariadne::BaseComponent.new(tag: :div, classes: @classes, attributes: @attributes))
76
76
  end
77
77
  end
78
78
  end
@@ -6,25 +6,39 @@ module Ariadne
6
6
  DEFAULT_TAG = :span
7
7
  TAG_OPTIONS = [DEFAULT_TAG].freeze
8
8
 
9
- DEFAULT_CLASSES = "ariadne-flex-shrink-0 ariadne-inline-block ariadne-px-2 ariadne-py-0.5 ariadne-text-xs ariadne-font-medium ariadne-rounded-full"
9
+ DEFAULT_CLASSES = "ariadne-flex-shrink-0 ariadne-inline-block ariadne-px-2 ariadne-py-0.5 ariadne-text-xs ariadne-font-medium ariadne-rounded-full ariadne-whitespace-nowrap"
10
10
 
11
11
  # @example Default
12
12
  #
13
- # <%= render(Ariadne::PillComponent.new(color: "FF0000")) { "Admin" } %>
13
+ # <%= render(Ariadne::PillComponent.new(color: [49, 186, 115, 1.0])) { "Admin" } %>
14
14
  #
15
15
  # @param tag [Symbol, String] The rendered tag name.
16
- # @param color [String] The hex color of the pill.
16
+ # @param color [String] The rgba color of the pill.
17
17
  # @param classes [String] <%= link_to_classes_docs %>
18
18
  # @param attributes [Hash] <%= link_to_attributes_docs %>
19
19
  def initialize(tag: DEFAULT_TAG, color:, classes: "", attributes: {})
20
20
  @tag = check_incoming_tag(DEFAULT_TAG, tag)
21
+
22
+ @red = color[0]
23
+ @green = color[1]
24
+ @blue = color[2]
25
+ @alpha = color[3]
26
+
27
+ @attributes = attributes
28
+ @attributes["style"] = "background-color: rgba(#{@red}, #{@green}, #{@blue}, #{@alpha});"
29
+ @text_color = contrast_of(@red, @green, @blue)
30
+
21
31
  @classes = class_names(
22
32
  DEFAULT_CLASSES,
23
- classes
33
+ classes,
34
+ @text_color,
24
35
  )
36
+ end
25
37
 
26
- @attributes = attributes
27
- @attributes["style"] = "background-color: ##{color};"
38
+ private def contrast_of(red, green, blue)
39
+ luminance = (0.299 * red + 0.587 * green + 0.114 * blue) / 255
40
+
41
+ luminance > 0.5 ? "ariadne-text-black" : "ariadne-text-white"
28
42
  end
29
43
  end
30
44
  end
@@ -5,16 +5,16 @@ import {StarterKit} from '@tiptap/starter-kit'
5
5
 
6
6
  export default class RichTextArea extends Controller {
7
7
  connect() {
8
- const editorElement = document.querySelector('.tiptap-editor')
9
- if (editorElement) {
8
+ const editorElements = document.getElementsByClassName('tiptap-editor')
9
+ for (const editorElement of editorElements) {
10
10
  new Editor({
11
11
  element: editorElement,
12
12
  extensions: [StarterKit],
13
- content: 'Hello World!',
13
+ content: '',
14
14
  editorProps: {
15
15
  attributes: {
16
16
  class:
17
- 'ariadne-prose ariadne-prose-sm sm:ariadne-prose lg:ariadne-prose-lg xl:ariadne-prose-2xl ariadne-m-5 focus:ariadne-outline-none'
17
+ 'ariadne-m-5 focus:ariadne-outline-none ariadne-block ariadne-w-full ariadne-resize-none ariadne-p-0 ariadne-pb-2 ariadne-border-none focus:ariadne-ring-0 sm:ariadne-text-sm'
18
18
  }
19
19
  }
20
20
  })
@@ -1,6 +1,6 @@
1
1
  <%= render(Ariadne::BaseComponent.new(tag: @tag, classes: @classes, attributes: @attributes)) do %>
2
2
  <% if @has_form %>
3
- <input type="hidden" data-tiptap-value-container=true name="<%= @name %>" id="<%= @name %>" />
3
+ <input type="hidden" data-tiptap-value-container=true name="<%= @name %>" id="<%= @name %>">
4
4
  <% end %>
5
- <textarea class="tiptap-editor"></textarea>
5
+ <div class="tiptap-editor"></div>
6
6
  <% end %>
@@ -6,7 +6,7 @@ module Ariadne
6
6
  # @accessibility Add any accessibility considerations
7
7
  class RichTextAreaComponent < Ariadne::Component
8
8
  DEFAULT_TAG = :div
9
- DEFAULT_CLASSES = "ariadne-block ariadne-w-full ariadne-border-0 ariadne-py-3 focus:ariadne-ring-0 sm:ariadne-text-sm"
9
+ DEFAULT_CLASSES = "ariadne-block"
10
10
 
11
11
  # @example Default
12
12
  #
@@ -25,7 +25,7 @@ module Ariadne
25
25
 
26
26
  @classes = class_names(
27
27
  DEFAULT_CLASSES,
28
- classes
28
+ classes,
29
29
  )
30
30
  @attributes = attributes
31
31
  @attributes[:"data-controller"] = "rich-text-area-component"
@@ -1,6 +1,6 @@
1
1
  <%= render Ariadne::BaseComponent.new(tag: @tag, classes: @classes, attributes: @attributes) do |slideover| %>
2
2
  <div data-slideover-component-target="expandWrapper" class="ariadne-flex ariadne-flex-col ariadne-z-10">
3
- <div data-slideover-component-target="expandable" class="ariadne-hidden bg-filter-panel ariadne-px-3 ariadne-pb-4">
3
+ <div data-slideover-component-target="expandable" class="ariadne-hidden ariadne-px-3 ariadne-pb-4">
4
4
  <%= content %>
5
5
  </div>
6
6
  <div data-slideover-component-target="buttonWrapper" data-slideover-component-form-id="<%= @form_id %>" class="ariadne-relative ariadne-flex ariadne-justify-center">
@@ -57,7 +57,7 @@ module Ariadne
57
57
  #
58
58
  # @param tag [Symbol, String] The rendered tag name.
59
59
  # @param direction [Symbol] <%= one_of(Ariadne::SlideoverComponent::VALID_DIRECTIONS) %>
60
- # @param form_id [String] The ID of any <form> tag to submit when the button is clicked.
60
+ # @param form_id [String] The ID of any `form` tag to submit when the button is clicked.
61
61
  # @param open_text [String] The text to use to open the slideover.
62
62
  # @param close_text [String] The text to use to close the slideover.
63
63
  # @param classes [String] <%= link_to_classes_docs %>
@@ -66,7 +66,7 @@ module Ariadne
66
66
  @tag = check_incoming_tag(DEFAULT_TAG, tag)
67
67
  @classes = class_names(
68
68
  DEFAULT_CLASSES,
69
- classes
69
+ classes,
70
70
  )
71
71
 
72
72
  @direction = fetch_or_raise(VALID_DIRECTIONS, direction)
@@ -0,0 +1,24 @@
1
+ import '@github/tab-container-element'
2
+
3
+ // keep in sync with tab_container_component.rb
4
+ const DEFAULT_SELECTED_CLASSES: string[] = ['ariadne-border-indigo-500', 'ariadne-text-indigo-600']
5
+ const DEFAULT_UNSELECTED_CLASSES: string[] = [
6
+ 'ariadne-text-gray-500',
7
+ 'hover:ariadne-text-gray-700',
8
+ 'hover:ariadne-border-gray-300'
9
+ ]
10
+
11
+ for (const tabContainer of document.getElementsByTagName('tab-container')) {
12
+ tabContainer.addEventListener('tab-container-change', function (event: Event) {
13
+ const newPanel = (event as CustomEvent).detail.relatedTarget as HTMLElement
14
+ const tabContainer = newPanel.closest('tab-container') as HTMLElement
15
+ const tabList = tabContainer.firstElementChild as HTMLElement
16
+ const currentTab = tabList.querySelector('[aria-selected="true"]') as HTMLElement
17
+ const tabId = newPanel.getAttribute('id')?.split('-').slice(1).join('-')
18
+ const newTab = tabList.querySelector(`#${tabId}`) as HTMLElement
19
+ currentTab.classList.remove(...DEFAULT_SELECTED_CLASSES)
20
+ currentTab.classList.add(...DEFAULT_UNSELECTED_CLASSES)
21
+ newTab.classList.add(...DEFAULT_SELECTED_CLASSES)
22
+ newTab.classList.remove(...DEFAULT_UNSELECTED_CLASSES)
23
+ })
24
+ }
@@ -0,0 +1,34 @@
1
+ import {Controller} from '@hotwired/stimulus'
2
+
3
+ export default class TabNavComponent extends Controller {
4
+ static targets = ['tab']
5
+
6
+ declare readonly tabTargets: [HTMLAnchorElement]
7
+
8
+ // keep in synch with tab_nav_component
9
+ SELECTED_CLASSES = ['ariadne-border-indigo-500', 'ariadne-text-indigo-600']
10
+ UNSELECTED_CLASSES = ['ariadne-text-gray-500', 'hover:ariadne-text-gray-700', 'hover:ariadne-border-gray-300']
11
+
12
+ connect() {
13
+ for (const tab of this.tabTargets) {
14
+ if (tab.hasAttribute('aria-current')) {
15
+ tab.classList.add(...this.SELECTED_CLASSES)
16
+ tab.classList.remove(...this.UNSELECTED_CLASSES)
17
+ }
18
+ }
19
+ }
20
+
21
+ toggle(event: Event) {
22
+ for (const tab of this.tabTargets) {
23
+ if (tab === event.target) {
24
+ tab.setAttribute('aria-current', 'page')
25
+ tab.classList.add(...this.SELECTED_CLASSES)
26
+ tab.classList.remove(...this.UNSELECTED_CLASSES)
27
+ } else if (tab.hasAttribute('aria-current')) {
28
+ tab.removeAttribute('aria-current')
29
+ tab.classList.add(...this.UNSELECTED_CLASSES)
30
+ tab.classList.remove(...this.SELECTED_CLASSES)
31
+ }
32
+ }
33
+ }
34
+ }
@@ -1,7 +1,3 @@
1
- <%= render(Ariadne::BaseComponent.new(tag: @tag, classes: @classes, attributes: @attributes)) do |component| %>
2
- <% if text.present? %>
3
- <%= text %>
4
- <% else %>
5
- <%= content %>
6
- <% end %>
1
+ <%= render(Ariadne::BaseComponent.new(tag: @tag, classes: @classes, attributes: @attributes)) do %>
2
+ <%= text || content %>
7
3
  <% end %>
@@ -1,43 +1,99 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ariadne
4
- # Represents a sequence of tabs which, when clicked, swap between panel contents.
4
+ # This component is part of navigation components such as `Ariadne::TabContainerComponent`.
5
+ # and `Ariadne::TabNavComponent` and should not be used by itself.
6
+ #
7
+ # @accessibility
8
+ # `TabComponent` renders the selected anchor tab with `aria-current="page"` by default.
9
+ # When the selected tab does not correspond to the current page, such as in a nested inner tab, make sure to use aria-current="true"
5
10
  class TabComponent < Ariadne::Component
6
- DEFAULT_TAG = :button
7
- TAG_OPTIONS = [DEFAULT_TAG].freeze
11
+ DEFAULT_ARIA_CURRENT_FOR_ANCHOR = :page
12
+ ARIA_CURRENT_OPTIONS_FOR_ANCHOR = [true, DEFAULT_ARIA_CURRENT_FOR_ANCHOR].freeze
8
13
 
9
- # The Tab's text.
14
+ # Panel controlled by the Tab. This will not render anything in the tab itself.
15
+ # It will provide a accessor for the Tab's parent to call and render the panel
16
+ # content in the appropriate place.
17
+ #
18
+ # @param classes [String] <%= link_to_classes_docs %>
19
+ # @param attributes [Hash] <%= link_to_attributes_docs %>
20
+ renders_one :panel, lambda { |classes: "", attributes: {}|
21
+ attributes[:id] = @panel_id
22
+ tag = :div
23
+ attributes[:role] ||= :tabpanel
24
+ attributes[:tabindex] = 0
25
+ attributes[:hidden] = true unless @selected
26
+
27
+ label_present = aria("label", attributes) || aria("labelledby", attributes)
28
+ unless label_present
29
+ if @id.present?
30
+ attributes[:"aria-labelledby"] = @id
31
+ else
32
+ raise ArgumentError, "Panels must be labelled. Either set a unique `id` on the tab, or set an `aria-label` directly on the panel"
33
+ end
34
+ end
35
+
36
+ Ariadne::BaseComponent.new(tag: tag, classes: classes, attributes: attributes)
37
+ }
38
+
39
+ # The tab's text.
10
40
  #
11
41
  # @param kwargs [Hash] The same arguments as <%= link_to_component(Ariadne::Text) %>.
12
42
  renders_one :text, Ariadne::Text
43
+ # TODO: test what hapenns with really long inbox names
13
44
 
14
- attr_reader :selected
15
-
16
- DEFAULT_CLASSES = "ariadne-border-gray-300 ariadne-border ariadne-shadow ariadne-py-5 ariadne-px-5 ariadne-rounded-md"
45
+ attr_reader :selected, :id, :classes, :attributes
17
46
 
18
- BASE_TAB_CLASSES = "ariadne-whitespace-nowrap ariadne-py-4 ariadne-px-1 ariadne-border-b-2 ariadne-font-medium ariadne-text-sm"
47
+ DEFAULT_CLASSES = "ariadne-border-transparent ariadne-text-gray-500 hover:ariadne-text-gray-700 hover:ariadne-border-gray-300 ariadne-whitespace-nowrap ariadne-py-4 ariadne-px-1 ariadne-border-b-2 ariadne-font-medium ariadne-text-sm"
19
48
 
20
49
  # @example Default
21
50
  #
22
- # <%= render(Ariadne::TabComponent.new) { "Example" } %>
23
- #
24
- # @param tag [Symbol, String] The rendered tag name.
51
+ # <%= render(Ariadne::TabComponent.new(id: "pub-comment")) do |tab| %>
52
+ # <% tab.text { "Public comment" } %>
53
+ # <% tab.panel { "Public comment panel" } %>
54
+ # <% end %>
55
+ # @param id [String] The unique ID of the tab.
25
56
  # @param selected [Boolean] Whether the tab is selected or not.
57
+ # @param with_panel [Boolean] Whether the Tab has an associated panel.
58
+ # @param href [String] The link to navigate to when the tab is clicked.
26
59
  # @param classes [String] <%= link_to_classes_docs %>
27
60
  # @param attributes [Hash] <%= link_to_attributes_docs %>
28
- def initialize(tag: DEFAULT_TAG, selected: false, classes: "", attributes: {})
29
- @tag = check_incoming_tag(DEFAULT_TAG, tag)
61
+ def initialize(id: nil, selected: false, with_panel: false, href: nil, classes: "", attributes: {})
62
+ @id = id
63
+
64
+ @href = href
65
+ @selected = selected
66
+
30
67
  @classes = class_names(
31
68
  DEFAULT_CLASSES,
32
- classes
69
+ classes,
33
70
  )
34
71
 
35
72
  @attributes = attributes
36
- @attributes[:id] ||= "tabs-tab-#{@tab_idx}"
37
- @attributes[:"aria-controls"] = "tabs-panel-#{@tab_idx}"
38
- @attributes[:"aria-current"] = "page" if selected
39
- @attributes[:selected] = selected
40
- @classes = class_names(BASE_TAB_CLASSES, classes)
73
+ @attributes[:id] ||= @id
74
+
75
+ if with_panel # TODO: test
76
+ if @id.blank?
77
+ raise ArgumentError, "Tabs with panels must have a unique `id`"
78
+ end
79
+
80
+ @tag = :button
81
+ @attributes[:type] = :button
82
+ @attributes[:role] = :tab
83
+ @panel_id = "panel-#{@id}"
84
+ @attributes[:"aria-controls"] = @panel_id
85
+ else
86
+ @tag = :a
87
+ end
88
+
89
+ return unless @selected
90
+
91
+ if @tag == :a
92
+ aria_current = aria("current", attributes) || DEFAULT_ARIA_CURRENT_FOR_ANCHOR
93
+ @attributes[:"aria-current"] = fetch_or_raise(ARIA_CURRENT_OPTIONS_FOR_ANCHOR, aria_current)
94
+ else
95
+ @attributes[:"aria-selected"] = true
96
+ end
41
97
  end
42
98
  end
43
99
  end
@@ -0,0 +1,12 @@
1
+ <%= render(Ariadne::BaseComponent.new(tag: @tag, classes: @classes, attributes: @attributes)) do |component| %>
2
+ <div role="tablist">
3
+ <% tabs.each do |tab| %>
4
+ <%= tab %>
5
+ <% end %>
6
+ </div>
7
+ <% tabs.each do |tab| %>
8
+ <div class="ariadne-pt-2">
9
+ <%= tab.panel %>
10
+ </div>
11
+ <% end %>
12
+ <% end %>
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ariadne
4
+ # A container for a row of tags. Keyboard support is provided.
5
+ # @accessibility This component requires you to pass in a `sr_label`
6
+ # attribute, which will be used to label the tabs for screen readers.
7
+ class TabContainerComponent < Ariadne::Component
8
+ DEFAULT_TAG = :"tab-container"
9
+
10
+ DEFAULT_CLASSES = ""
11
+
12
+ DEFAULT_SELECTED_CLASSES = "ariadne-border-indigo-500 ariadne-text-indigo-600"
13
+ DEFAULT_UNSELECTED_CLASSES = "ariadne-text-gray-500 hover:ariadne-text-gray-700 hover:ariadne-border-gray-300"
14
+
15
+ # Tabs and panels to be rendered.
16
+ #
17
+ # @param id [String] The unique ID of the tab.
18
+ # @param selected [Boolean] Whether the tab is selected.
19
+ # @param classes [String] <%= link_to_classes_docs %>
20
+ # @param attributes [Hash] <%= link_to_attributes_docs %>
21
+ renders_many :tabs, lambda { |id: "", selected: false, classes: "", attributes: {}|
22
+ actual_classes = class_names(selected ? DEFAULT_SELECTED_CLASSES : DEFAULT_UNSELECTED_CLASSES, classes)
23
+ Ariadne::TabComponent.new(
24
+ id: id,
25
+ selected: selected,
26
+ with_panel: true,
27
+
28
+ classes: actual_classes,
29
+ attributes: attributes,
30
+ )
31
+ }
32
+
33
+ # @example Default
34
+ #
35
+ # <%= render(Ariadne::TabContainerComponent.new(sr_label: "Comments")) do |tab_container| %>
36
+ # <%= render(Ariadne::TabComponent.new(id: "pub-comment")) do |tab| %>
37
+ # <% tab.text { "Tab 1" } %>
38
+ # <% tab.panel { "Panel 1" } %>
39
+ # <% end %>
40
+ # <%= render(Ariadne::TabComponent.new(id: "pub-comment")) do |tab| %>
41
+ # <% tab.text { "Tab 2" } %>
42
+ # <% tab.panel { "Panel 2" } %>
43
+ # <% end %>
44
+ # <% end %>
45
+ #
46
+ # %>
47
+ #
48
+ # @param sr_label [String] Sets an `aria-label` that helps assistive technology users understand the purpose of the tabs.
49
+ # @param classes [String] <%= link_to_classes_docs %>
50
+ # @param attributes [Hash] <%= link_to_attributes_docs %>
51
+ def initialize(sr_label:, classes: "", attributes: {})
52
+ @tag = DEFAULT_TAG
53
+ @classes = class_names(
54
+ DEFAULT_CLASSES,
55
+ classes,
56
+ )
57
+
58
+ @attributes = attributes
59
+ @attributes[:"aria-label"] = sr_label
60
+ @attributes[:role] = :presentation
61
+ end
62
+
63
+ def render?
64
+ tabs.present?
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,7 @@
1
+ <%= render Ariadne::BaseComponent.new(tag: @tag, classes: @classes, attributes: @attributes) do |component| %>
2
+ <ul class="ariadne-list-none">
3
+ <% tabs.each do |tab| %>
4
+ <li class="ariadne-inline-flex"><%= tab %></li>
5
+ <% end %>
6
+ </ul>
7
+ <% end %>