ariadne_view_components 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +68 -0
- data/app/assets/javascripts/ariadne_view_components.js +2 -0
- data/app/assets/javascripts/ariadne_view_components.js.map +1 -0
- data/app/assets/stylesheets/application.tailwind.css +3 -0
- data/app/components/ariadne/ariadne.ts +14 -0
- data/app/components/ariadne/base_button.rb +60 -0
- data/app/components/ariadne/base_component.rb +155 -0
- data/app/components/ariadne/button_component.html.erb +4 -0
- data/app/components/ariadne/button_component.rb +158 -0
- data/app/components/ariadne/clipboard_copy_component.html.erb +8 -0
- data/app/components/ariadne/clipboard_copy_component.rb +50 -0
- data/app/components/ariadne/clipboard_copy_component.ts +19 -0
- data/app/components/ariadne/component.rb +123 -0
- data/app/components/ariadne/content.rb +12 -0
- data/app/components/ariadne/counter_component.rb +100 -0
- data/app/components/ariadne/flash_component.html.erb +31 -0
- data/app/components/ariadne/flash_component.rb +125 -0
- data/app/components/ariadne/heading_component.rb +49 -0
- data/app/components/ariadne/heroicon_component.html.erb +7 -0
- data/app/components/ariadne/heroicon_component.rb +116 -0
- data/app/components/ariadne/image_component.rb +51 -0
- data/app/components/ariadne/text.rb +25 -0
- data/app/components/ariadne/tooltip_component.rb +105 -0
- data/app/lib/ariadne/audited/dsl.rb +32 -0
- data/app/lib/ariadne/class_name_helper.rb +22 -0
- data/app/lib/ariadne/fetch_or_fallback_helper.rb +100 -0
- data/app/lib/ariadne/icon_helper.rb +47 -0
- data/app/lib/ariadne/join_style_arguments_helper.rb +14 -0
- data/app/lib/ariadne/logger_helper.rb +23 -0
- data/app/lib/ariadne/status/dsl.rb +41 -0
- data/app/lib/ariadne/tab_nav_helper.rb +35 -0
- data/app/lib/ariadne/tabbed_component_helper.rb +39 -0
- data/app/lib/ariadne/test_selector_helper.rb +20 -0
- data/app/lib/ariadne/underline_nav_helper.rb +44 -0
- data/app/lib/ariadne/view_helper.rb +22 -0
- data/lib/ariadne/classify/utilities.rb +199 -0
- data/lib/ariadne/classify/utilities.yml +1817 -0
- data/lib/ariadne/classify/validation.rb +18 -0
- data/lib/ariadne/classify.rb +210 -0
- data/lib/ariadne/view_components/constants.rb +53 -0
- data/lib/ariadne/view_components/engine.rb +30 -0
- data/lib/ariadne/view_components/linters.rb +3 -0
- data/lib/ariadne/view_components/statuses.rb +14 -0
- data/lib/ariadne/view_components/version.rb +7 -0
- data/lib/ariadne/view_components.rb +59 -0
- data/lib/rubocop/config/default.yml +14 -0
- data/lib/rubocop/cop/ariadne/ariadne_heroicon.rb +252 -0
- data/lib/rubocop/cop/ariadne/base_cop.rb +26 -0
- data/lib/rubocop/cop/ariadne/component_name_migration.rb +35 -0
- data/lib/rubocop/cop/ariadne/no_tag_memoize.rb +43 -0
- data/lib/rubocop/cop/ariadne/system_argument_instead_of_class.rb +57 -0
- data/lib/rubocop/cop/ariadne.rb +3 -0
- data/lib/tasks/ariadne_view_components.rake +47 -0
- data/lib/tasks/coverage.rake +19 -0
- data/lib/tasks/custom_utilities.yml +310 -0
- data/lib/tasks/docs.rake +525 -0
- data/lib/tasks/helpers/ast_processor.rb +44 -0
- data/lib/tasks/helpers/ast_traverser.rb +77 -0
- data/lib/tasks/static.rake +15 -0
- data/lib/tasks/tailwind.rake +31 -0
- data/lib/tasks/utilities.rake +121 -0
- data/lib/yard/docs_helper.rb +83 -0
- data/lib/yard/renders_many_handler.rb +19 -0
- data/lib/yard/renders_one_handler.rb +19 -0
- data/static/arguments.yml +251 -0
- data/static/assets/view-components.svg +18 -0
- data/static/audited_at.json +14 -0
- data/static/classes.yml +89 -0
- data/static/constants.json +243 -0
- data/static/statuses.json +14 -0
- data/static/tailwindcss.yml +727 -0
- metadata +193 -0
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ariadne
|
4
|
+
# Use `FlashComponent` to inform users of successful messages, pending actions, or urgent notices.
|
5
|
+
class FlashComponent < Ariadne::Component
|
6
|
+
include IconHelper
|
7
|
+
|
8
|
+
DEFAULT_SCHEME = :default
|
9
|
+
|
10
|
+
DISMISSIBLE_SCHEME_CLASS_MAPPINGS = {
|
11
|
+
default: "text-blue-500 bg-blue-50 hover:bg-blue-100 focus:ring-offset-blue-50 focus:ring-blue-600",
|
12
|
+
info: "text-blue-500 bg-blue-50 hover:bg-blue-100 focus:ring-offset-blue-50 focus:ring-blue-600",
|
13
|
+
success: "text-green-500 bg-green-50 hover:bg-green-100 focus:ring-offset-green-50 focus:ring-green-600",
|
14
|
+
warning: "text-yellow-500 bg-yellow-50 hover:bg-yellow-100 focus:ring-offset-yellow-50 focus:ring-yellow-600",
|
15
|
+
danger: "text-red-500 bg-red-50 hover:bg-red-100 focus:ring-offset-red-50 focus:ring-red-600",
|
16
|
+
}.freeze
|
17
|
+
VALID_DISMISSIBLE_SCHEMES = DISMISSIBLE_SCHEME_CLASS_MAPPINGS.keys.freeze
|
18
|
+
|
19
|
+
BG_SCHEME_CLASS_MAPPINGS = {
|
20
|
+
default: "bg-blue-50",
|
21
|
+
info: "bg-blue-50",
|
22
|
+
success: "bg-green-50",
|
23
|
+
warning: "bg-yellow-50",
|
24
|
+
danger: "bg-red-50",
|
25
|
+
}.freeze
|
26
|
+
VALID_BG_SCHEMES = BG_SCHEME_CLASS_MAPPINGS.keys.freeze
|
27
|
+
|
28
|
+
CONTENT_SCHEME_CLASS_MAPPINGS = {
|
29
|
+
default: "text-blue-700",
|
30
|
+
info: "text-blue-700",
|
31
|
+
success: "text-green-700",
|
32
|
+
warning: "text-yellow-700",
|
33
|
+
danger: "text-red-700",
|
34
|
+
}.freeze
|
35
|
+
VALID_CONTENT_SCHEMES = CONTENT_SCHEME_CLASS_MAPPINGS.keys.freeze
|
36
|
+
|
37
|
+
# Optional visuals appearing to the left of the flash banner.
|
38
|
+
#
|
39
|
+
# Use:
|
40
|
+
#
|
41
|
+
# - `icon` for a <%= link_to_component(Ariadne::HeroiconComponent) %>.
|
42
|
+
#
|
43
|
+
# @option params [Symbol] :icon The rendered tag name
|
44
|
+
# @option params [Symbol] :icon Name of <%= link_to_heroicons %> to use.
|
45
|
+
# @option params [Symbol] :variant <%= one_of(HeroiconsHelper::Icon::VARIANTS, sort: false) %>
|
46
|
+
# @option params [String] :classes <%= link_to_classes_docs %>
|
47
|
+
# @option params [Hash] :attributes Same arguments as <%= link_to_component(Ariadne::HeroiconComponent) %>.
|
48
|
+
renders_one :icon, lambda { |tag: :svg, icon:, variant:, classes: "", attributes: {}|
|
49
|
+
@icon = icon
|
50
|
+
@variant = variant
|
51
|
+
|
52
|
+
tag = check_incoming_tag(:svg, tag)
|
53
|
+
Ariadne::HeroiconComponent.new(tag: tag, icon: icon, variant: variant, classes: classes, attributes: attributes)
|
54
|
+
}
|
55
|
+
|
56
|
+
# Optional action content showed at the bottom of the component.
|
57
|
+
#
|
58
|
+
# @param tag [Symbol, String] The rendered tag name
|
59
|
+
# @param classes [String] <%= link_to_classes_docs %>
|
60
|
+
# @param attributes [Hash] <%= link_to_attributes_docs %>
|
61
|
+
renders_one :action, lambda { |tag: :div, classes: "", attributes: {}|
|
62
|
+
tag = check_incoming_tag(:div, tag)
|
63
|
+
|
64
|
+
actual_classes = class_names(classes)
|
65
|
+
|
66
|
+
Ariadne::BaseComponent.new(tag: tag, classes: actual_classes, attributes: attributes)
|
67
|
+
}
|
68
|
+
|
69
|
+
# @example Schemes
|
70
|
+
# <%= render(Ariadne::FlashComponent.new) { "This is a flash message!" } %>
|
71
|
+
# <%= render(Ariadne::FlashComponent.new(scheme: :warning)) { "This is a warning flash message!" } %>
|
72
|
+
# <%= render(Ariadne::FlashComponent.new(scheme: :danger)) { "This is a danger flash message!" } %>
|
73
|
+
# <%= render(Ariadne::FlashComponent.new(scheme: :success)) { "This is a success flash message!" } %>
|
74
|
+
#
|
75
|
+
# @example Dismissible
|
76
|
+
# <%= render(Ariadne::FlashComponent.new(dismissible: true)) { "This is a dismissible flash message!" } %>
|
77
|
+
#
|
78
|
+
# @example Icon
|
79
|
+
# <%= render(Ariadne::FlashComponent.new) do |component| %>
|
80
|
+
# <% component.icon(icon: :"user-group", variant: HeroiconsHelper::Icon::VARIANT_OUTLINE) %>
|
81
|
+
# Look at this icon.
|
82
|
+
# <% end %>
|
83
|
+
#
|
84
|
+
# @example With actions
|
85
|
+
# <%= render(Ariadne::FlashComponent.new) do |component| %>
|
86
|
+
# <% component.action do %>
|
87
|
+
# <%= render(Ariadne::ButtonComponent.new(size: :s)) { "Take action" } %>
|
88
|
+
# <% end %>
|
89
|
+
# This is a flash message with actions!
|
90
|
+
# <% end %>
|
91
|
+
#
|
92
|
+
# @param tag [Symbol, String] The rendered tag name.
|
93
|
+
# @param dismissible [Boolean] Whether the component can be dismissed with an X button.
|
94
|
+
# @param icon [Symbol, String] Name of <%= link_to_heroicons %> to use.
|
95
|
+
# @param scheme [Symbol] <%= one_of(Ariadne::FlashComponent::VALID_CONTENT_SCHEMES) %>
|
96
|
+
# @param classes [String] <%= link_to_classes_docs %>
|
97
|
+
# @param attributes [Hash] <%= link_to_attributes_docs %>
|
98
|
+
def initialize(tag: :div, dismissible: false, scheme: DEFAULT_SCHEME, classes: "", attributes: {})
|
99
|
+
@dismissible = dismissible
|
100
|
+
|
101
|
+
@tag = check_incoming_tag(:div, tag)
|
102
|
+
|
103
|
+
@scheme = fetch_or_raise(VALID_CONTENT_SCHEMES, scheme)
|
104
|
+
|
105
|
+
@classes = class_names(
|
106
|
+
CONTENT_SCHEME_CLASS_MAPPINGS[@scheme],
|
107
|
+
classes
|
108
|
+
)
|
109
|
+
|
110
|
+
@attributes = attributes
|
111
|
+
end
|
112
|
+
|
113
|
+
private def has_action?
|
114
|
+
action.present?
|
115
|
+
end
|
116
|
+
|
117
|
+
private def dismissible?
|
118
|
+
@dismissible.present?
|
119
|
+
end
|
120
|
+
|
121
|
+
private def dismissible_classes
|
122
|
+
DISMISSIBLE_SCHEME_CLASS_MAPPINGS[@scheme]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ariadne
|
4
|
+
# `Heading` should be used to communicate page organization and hierarchy.
|
5
|
+
#
|
6
|
+
# - Set tag to one of `:h1`, `:h2`, `:h3`, `:h4`, `:h5`, `:h6` based on what is appropriate for the page context.
|
7
|
+
# - Use `Heading` as the title of a section or sub section.
|
8
|
+
# - Do not use `Heading` for styling alone. For simply styling text, consider using <%= link_to_component(Ariadne::Text) %> with relevant <%= link_to_typography_docs %>
|
9
|
+
# such as `font_size` and `font_weight`.
|
10
|
+
# - Do not jump heading levels. For instance, do not follow a `<h1>` with an `<h3>`. Heading levels should increase by one in ascending order.
|
11
|
+
#
|
12
|
+
# @accessibility
|
13
|
+
# While sighted users rely on visual cues such as font size changes to determine what the heading is, assistive technology users rely on programatic cues that can be read out.
|
14
|
+
# When text on a page is visually implied to be a heading, ensure that it is coded as a heading. Additionally, visually implied heading level and coded heading level should be
|
15
|
+
# consistent. [See WCAG success criteria: 1.3.1: Info and Relationships](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships.html)
|
16
|
+
#
|
17
|
+
# Headings allow assistive technology users to quickly navigate around a page. Navigation to text that is not meant to be a heading can be a confusing experience.
|
18
|
+
# <%= link_to_heading_practices %>
|
19
|
+
class HeadingComponent < Ariadne::Component
|
20
|
+
TAG_OPTIONS = [:h1, :h2, :h3, :h4, :h5, :h6].freeze
|
21
|
+
|
22
|
+
TAG_TO_CLASSES = {
|
23
|
+
h1: "font-bold leading-7 sm:text-3xl sm:truncate",
|
24
|
+
h2: "text-3xl font-extrabold text-gray-900",
|
25
|
+
h3: "text-2xl font-extrabold text-gray-900",
|
26
|
+
}
|
27
|
+
# @example Default
|
28
|
+
# <%= render(Ariadne::HeadingComponent.new(tag: :h1)) { "H1 Text" } %>
|
29
|
+
# <%= render(Ariadne::HeadingComponent.new(tag: :h2)) { "H2 Text" } %>
|
30
|
+
# <%= render(Ariadne::HeadingComponent.new(tag: :h3)) { "H3 Text" } %>
|
31
|
+
# <%= render(Ariadne::HeadingComponent.new(tag: :h4)) { "H4 Text" } %>
|
32
|
+
# <%= render(Ariadne::HeadingComponent.new(tag: :h5)) { "H5 Text" } %>
|
33
|
+
# <%= render(Ariadne::HeadingComponent.new(tag: :h6)) { "H6 Text" } %>
|
34
|
+
#
|
35
|
+
# @param tag [String] <%= one_of(Ariadne::HeadingComponent::TAG_OPTIONS) %>
|
36
|
+
# @param classes [String] <%= link_to_classes_docs %>
|
37
|
+
# @param attributes [Hash] <%= link_to_attributes_docs %>
|
38
|
+
def initialize(tag: nil, classes: "", attributes: {})
|
39
|
+
@tag = fetch_or_raise(TAG_OPTIONS, tag)
|
40
|
+
@attributes = attributes
|
41
|
+
|
42
|
+
@classes = class_names(*TAG_TO_CLASSES[tag], classes)
|
43
|
+
end
|
44
|
+
|
45
|
+
def call
|
46
|
+
render(Ariadne::BaseComponent.new(tag: @tag, classes: @classes, attributes: @attributes)) { content }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "heroicons_helper"
|
4
|
+
|
5
|
+
module Ariadne
|
6
|
+
# `Heroicon` renders an <%= link_to_heroicons %> with <%= link_to_attributes_docs %>.
|
7
|
+
# `Heroicon` can also be rendered with the `heroicon` helper.
|
8
|
+
class HeroiconComponent < Ariadne::Component
|
9
|
+
include IconHelper
|
10
|
+
include HeroiconsHelper
|
11
|
+
|
12
|
+
SIZE_DEFAULT = :small
|
13
|
+
SIZE_MEDIUM = :medium
|
14
|
+
|
15
|
+
SIZE_MAPPINGS = {
|
16
|
+
SIZE_DEFAULT => 16,
|
17
|
+
SIZE_MEDIUM => 24,
|
18
|
+
}.freeze
|
19
|
+
SIZE_OPTIONS = SIZE_MAPPINGS.keys
|
20
|
+
|
21
|
+
PRELOADED_ICONS = [
|
22
|
+
{ name: "bell",
|
23
|
+
variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, },
|
24
|
+
{ name: "check",
|
25
|
+
variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, },
|
26
|
+
{ name: "chevron-down",
|
27
|
+
variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, },
|
28
|
+
{ name: "clipboard",
|
29
|
+
variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, },
|
30
|
+
{ name: "clock",
|
31
|
+
variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, },
|
32
|
+
{ name: "information-circle",
|
33
|
+
variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, },
|
34
|
+
{ name: "dots-horizontal",
|
35
|
+
variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, },
|
36
|
+
{ name: "link",
|
37
|
+
variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, },
|
38
|
+
{ name: "lock-closed",
|
39
|
+
variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, },
|
40
|
+
{ name: "mail",
|
41
|
+
variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, },
|
42
|
+
{ name: "pencil",
|
43
|
+
variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, },
|
44
|
+
{ name: "plus-sm",
|
45
|
+
variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, },
|
46
|
+
{ name: "question-mark-circle",
|
47
|
+
variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, },
|
48
|
+
{ name: "search",
|
49
|
+
variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, },
|
50
|
+
{ name: "search",
|
51
|
+
variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, },
|
52
|
+
{ name: "trash",
|
53
|
+
variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, },
|
54
|
+
{ name: "x",
|
55
|
+
variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, },
|
56
|
+
].freeze
|
57
|
+
|
58
|
+
# @example Default
|
59
|
+
# <%= render(Ariadne::HeroiconComponent.new(icon: :check, variant: HeroiconsHelper::Icon::VARIANT_OUTLINE)) %>
|
60
|
+
# <%= render(Ariadne::HeroiconComponent.new(icon: :check, variant: HeroiconsHelper::Icon::VARIANT_SOLID)) %>
|
61
|
+
#
|
62
|
+
# @example Medium
|
63
|
+
# <%= render(Ariadne::HeroiconComponent.new(icon: :"user-group", variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, size: :medium)) %>
|
64
|
+
#
|
65
|
+
# @example Helper
|
66
|
+
# <%= ariadne_heroicon(icon: :check, variant: HeroiconsHelper::Icon::VARIANT_OUTLINE) %>
|
67
|
+
#
|
68
|
+
# @param tag [Symbol, String] The rendered tag name
|
69
|
+
# @param classes [String] <%= link_to_classes_docs %>
|
70
|
+
# @param icon [Symbol, String] Name of <%= link_to_heroicons %> to use.
|
71
|
+
# @param variant [String] <%= one_of(HeroiconsHelper::Icon::VARIANTS, sort: false) %>
|
72
|
+
# @param size [Symbol] <%= one_of(Ariadne::HeroiconComponent::SIZE_MAPPINGS, sort: false) %>
|
73
|
+
# @param attributes [Hash] <%= link_to_attributes_docs %>
|
74
|
+
def initialize(tag: :svg, icon:, variant:, size: SIZE_DEFAULT, classes: "", attributes: {})
|
75
|
+
@tag = check_incoming_tag(:svg, tag)
|
76
|
+
|
77
|
+
check_icon_presence!(icon, variant)
|
78
|
+
|
79
|
+
@attributes = attributes
|
80
|
+
@attributes[:aria] ||= {}
|
81
|
+
|
82
|
+
if @attributes[:aria][:label] || @attributes[:"aria-label"]
|
83
|
+
@attributes[:role] = "img"
|
84
|
+
else
|
85
|
+
@attributes[:aria][:hidden] = true
|
86
|
+
end
|
87
|
+
|
88
|
+
# Don't allow sizes under 16px
|
89
|
+
if attributes[:height].present? && attributes[:height].to_i < 16 || attributes[:width].present? && attributes[:width].to_i < 16
|
90
|
+
attributes.delete(:height)
|
91
|
+
attributes.delete(:width)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Filter out classify options to prevent them from becoming invalid html attributes.
|
95
|
+
# Note height and width are both classify options and valid html attributes.
|
96
|
+
attributes = {
|
97
|
+
height: @attributes[:height] || SIZE_MAPPINGS[fetch_or_raise(SIZE_OPTIONS, size)],
|
98
|
+
width: @attributes[:width],
|
99
|
+
}
|
100
|
+
|
101
|
+
@icon = heroicon(icon, variant: variant, **attributes) # rubocop:disable Ariadne/AriadneHeroicon
|
102
|
+
|
103
|
+
@classes = class_names(
|
104
|
+
@icon.attributes[:class],
|
105
|
+
classes
|
106
|
+
)
|
107
|
+
@attributes.merge!(@icon.attributes.except(:class, :"aria-hidden"))
|
108
|
+
end
|
109
|
+
|
110
|
+
def self._after_compile
|
111
|
+
HeroiconsHelper::Cache.preload!(PRELOADED_ICONS) do |found, icon|
|
112
|
+
HeroiconComponent.new(icon: icon[:name], variant: icon[:variant]) unless found
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ariadne
|
4
|
+
# Use `Image` to render images.
|
5
|
+
#
|
6
|
+
# @accessibility
|
7
|
+
# Always provide a meaningful `alt`.
|
8
|
+
class ImageComponent < Ariadne::Component
|
9
|
+
# @example Default
|
10
|
+
#
|
11
|
+
# <%= render(Ariadne::ImageComponent.new(src: "https://github.com/github.png", alt: "GitHub")) %>
|
12
|
+
#
|
13
|
+
# @example Helper
|
14
|
+
#
|
15
|
+
# <%= ariadne_image(src: "https://github.com/github.png", alt: "GitHub") %>
|
16
|
+
#
|
17
|
+
# @example Lazy loading
|
18
|
+
#
|
19
|
+
# <%= render(Ariadne::ImageComponent.new(src: "https://github.com/github.png", alt: "GitHub", lazy: true)) %>
|
20
|
+
#
|
21
|
+
# @example Custom size
|
22
|
+
#
|
23
|
+
# <%= render(Ariadne::ImageComponent.new(src: "https://github.com/github.png", alt: "GitHub", attributes: { height: 100, width: 100 })) %>
|
24
|
+
#
|
25
|
+
# @param tag [Symbol, String] The rendered tag name
|
26
|
+
# @param classes [String] <%= link_to_classes_docs %>
|
27
|
+
# @param src [String] The source url of the image.
|
28
|
+
# @param alt [String] Specifies an alternate text for the image.
|
29
|
+
# @param lazy [Boolean] Whether or not to lazily load the image.
|
30
|
+
# @param attributes [Hash] <%= link_to_attributes_docs %>
|
31
|
+
def initialize(tag: :img, src:, alt:, lazy: false, classes: "", attributes: {})
|
32
|
+
@attributes = attributes
|
33
|
+
|
34
|
+
@src = src
|
35
|
+
@tag = check_incoming_tag(:img, tag)
|
36
|
+
@classes = classes
|
37
|
+
|
38
|
+
@attributes[:alt] = alt
|
39
|
+
@attributes[:src] = image_path(@src)
|
40
|
+
|
41
|
+
return unless lazy
|
42
|
+
|
43
|
+
@attributes[:loading] = :lazy
|
44
|
+
@attributes[:decoding] = :async
|
45
|
+
end
|
46
|
+
|
47
|
+
def call
|
48
|
+
render(Ariadne::BaseComponent.new(tag: @tag, classes: @classes, attributes: @attributes))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ariadne
|
4
|
+
# `Text` is a wrapper component that will apply typography styles to the text inside.
|
5
|
+
class Text < Ariadne::Component
|
6
|
+
DEFAULT_TAG = :span
|
7
|
+
|
8
|
+
# @example Default
|
9
|
+
# <%= render(Ariadne::Text.new(tag: :p, attributes: { font_weight: :bold })) { "Bold Text" } %>
|
10
|
+
# <%= render(Ariadne::Text.new(tag: :p, attributes: { color: :danger })) { "Danger Text" } %>
|
11
|
+
#
|
12
|
+
# @param tag [Symbol, String] The rendered tag name
|
13
|
+
# @param classes [String] <%= link_to_classes_docs %>
|
14
|
+
# @param attributes [Hash] <%= link_to_attributes_docs %>
|
15
|
+
def initialize(tag: DEFAULT_TAG, classes: "", attributes: {})
|
16
|
+
@tag = tag
|
17
|
+
@classes = classes
|
18
|
+
@attributes = attributes
|
19
|
+
end
|
20
|
+
|
21
|
+
def call
|
22
|
+
render(Ariadne::BaseComponent.new(tag: @tag, classes: @classes, attributes: @attributes)) { content }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ariadne
|
4
|
+
# `Tooltip` only appears on mouse hover or keyboard focus and contain a label or description text.
|
5
|
+
# Use tooltips sparingly and as a last resort.
|
6
|
+
#
|
7
|
+
# When using a tooltip, follow the provided guidelines to avoid accessibility issues.
|
8
|
+
#
|
9
|
+
# - Tooltip text should be brief and to the point. The tooltip content must be a string.
|
10
|
+
# - Tooltips should contain only **non-essential text**. Tooltips can easily be missed and are not accessible on touch devices so never
|
11
|
+
# use tooltips to convey critical information.
|
12
|
+
#
|
13
|
+
# @accessibility
|
14
|
+
# - **Never set tooltips on static elements.** Tooltips should only be used on interactive elements like buttons or links to avoid excluding keyboard-only users
|
15
|
+
# and screen reader users.
|
16
|
+
# - Place `Tooltip` adjacent after its trigger element in the DOM. This allows screen reader users to navigate to and copy the tooltip
|
17
|
+
# content.
|
18
|
+
# ### Which `type` should I set?
|
19
|
+
# Setting `:description` establishes an `aria-describedby` relationship, while `:label` establishes an `aria-labelledby` relationship between the trigger element and the tooltip,
|
20
|
+
#
|
21
|
+
# The `type` drastically changes semantics and screen reader behavior so follow these guidelines carefully:
|
22
|
+
# - When there is already a visible label text on the trigger element, the tooltip is likely intended to supplement the existing text, so set `type: :description`.
|
23
|
+
# The majority of tooltips will fall under this category.
|
24
|
+
# - When there is no visible text on the trigger element and the tooltip content is appropriate as a label for the element, set `type: :label`.
|
25
|
+
# This type is usually only appropriate for an icon-only control.
|
26
|
+
class TooltipComponent < Ariadne::Component
|
27
|
+
DIRECTION_DEFAULT = :s
|
28
|
+
DIRECTION_OPTIONS = [DIRECTION_DEFAULT, :n, :e, :w, :ne, :nw, :se, :sw].freeze
|
29
|
+
|
30
|
+
TYPE_DEFAULT = :description
|
31
|
+
TYPE_OPTIONS = [:label, TYPE_DEFAULT].freeze
|
32
|
+
# @example As a description for an icon-only button
|
33
|
+
# @description
|
34
|
+
# If the tooltip content provides supplementary description, set `type: :description` to establish an `aria-describedby` relationship.
|
35
|
+
# The trigger element should also have a _concise_ accessible label via `aria-label`.
|
36
|
+
# @code
|
37
|
+
# <%= render(Ariadne::HeroiconComponent.new(icon: :moon, variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, attributes: { id: "bold-button-0" })) %>
|
38
|
+
# <%= render(Ariadne::TooltipComponent.new(attributes: { for: "bold-button-0" }, type: :description, text: "Add bold text", direction: :ne)) %>
|
39
|
+
# @example As a label for an icon-only button
|
40
|
+
# @description
|
41
|
+
# If the tooltip labels the icon-only button, set `type: :label`. This tooltip content becomes the accessible name for the button.
|
42
|
+
# @code
|
43
|
+
# <%= render(Ariadne::ButtonComponent.new(attributes: {id: "like-button"})) { "👍" } %>
|
44
|
+
# <%= render(Ariadne::TooltipComponent.new(attributes: { for: "like-button" }, type: :label, text: "Like", direction: :n)) %>
|
45
|
+
#
|
46
|
+
# @example As a description for a button with visible label
|
47
|
+
# @description
|
48
|
+
# If the button already has visible label text, the tooltip content is likely supplementary so set `type: :description`.
|
49
|
+
# @code
|
50
|
+
# <%= render(Ariadne::ButtonComponent.new(attributes: {id: "save-button"}, scheme: :success)) { "Save" } %>
|
51
|
+
# <%= render(Ariadne::TooltipComponent.new(attributes: { for: "save-button"}, type: :description, text: "This will immediately impact all organization members", direction: :ne)) %>
|
52
|
+
# @example With direction
|
53
|
+
# @description
|
54
|
+
# Set direction of tooltip with `direction`. The tooltip is responsive and will automatically adjust direction to avoid cutting off.
|
55
|
+
# @code
|
56
|
+
# <%= render(Ariadne::ButtonComponent.new(attributes: {id: "North", m: 2})) { "North" } %>
|
57
|
+
# <%= render(Ariadne::TooltipComponent.new(attributes: { for: "North"}, type: :description, text: "This is a North-facing tooltip, and is responsive.", direction: :n)) %>
|
58
|
+
# <%= render(Ariadne::ButtonComponent.new(attributes: {id: "South", m: 2})) { "South" } %>
|
59
|
+
# <%= render(Ariadne::TooltipComponent.new(attributes: { for: "South"}, type: :description, text: "This is a South-facing tooltip and is responsive.", direction: :s)) %>
|
60
|
+
# <%= render(Ariadne::ButtonComponent.new(attributes: {id: "East", m: 2})) { "East" } %>
|
61
|
+
# <%= render(Ariadne::TooltipComponent.new(attributes: { for: "East"}, type: :description, text: "This is a East-facing tooltip and is responsive.", direction: :e)) %>
|
62
|
+
# <%= render(Ariadne::ButtonComponent.new(attributes: {id: "West", m: 2})) { "West" } %>
|
63
|
+
# <%= render(Ariadne::TooltipComponent.new(attributes: { for: "West"}, type: :description, text: "This is a West-facing tooltip and is responsive.", direction: :w)) %>
|
64
|
+
# <%= render(Ariadne::ButtonComponent.new(attributes: {id: "Northeast", m: 2})) { "Northeast" } %>
|
65
|
+
# <%= render(Ariadne::TooltipComponent.new(attributes: { for: "Northeast"}, type: :description, text: "This is a Northeast-facing tooltip and is responsive.", direction: :ne)) %>
|
66
|
+
# <%= render(Ariadne::ButtonComponent.new(attributes: {id: "Southeast", m: 2})) { "Southeast" } %>
|
67
|
+
# <%= render(Ariadne::TooltipComponent.new(attributes: { for: "Southeast"}, type: :description, text: "This is a Southeast-facing tooltip and is responsive.", direction: :se)) %>
|
68
|
+
# <%= render(Ariadne::ButtonComponent.new(attributes: {id: "Northwest", m: 2})) { "Northwest" } %>
|
69
|
+
# <%= render(Ariadne::TooltipComponent.new(attributes: { for: "Northwest"}, type: :description, text: "This is a Northwest-facing tooltip and is responsive.", direction: :nw)) %>
|
70
|
+
# <%= render(Ariadne::ButtonComponent.new(attributes: {id: "Southwest", m: 2})) { "Southwest" } %>
|
71
|
+
# <%= render(Ariadne::TooltipComponent.new(attributes: { for: "Southwest"}, type: :description, text: "This is a Southwest-facing tooltip and is responsive.", direction: :sw)) %>
|
72
|
+
# @example With relative parent
|
73
|
+
# @description
|
74
|
+
# When the tooltip and trigger element have a parent container with `relative: position`, it should not affect width of the tooltip.
|
75
|
+
# @code
|
76
|
+
# <span style="position: relative;">
|
77
|
+
# <%= render(Ariadne::ButtonComponent.new(attributes: {id: "test-button"}, scheme: :info)) { "Test" } %>
|
78
|
+
# <%= render(Ariadne::TooltipComponent.new(attributes: { for: "test-button" }, type: :description, text: "This tooltip should take up the full width", direction: :ne)) %>
|
79
|
+
# </span>
|
80
|
+
# @param tag [Symbol, String] The rendered tag name
|
81
|
+
# @param type [Symbol] <%= one_of(Ariadne::TooltipComponent::TYPE_OPTIONS) %>
|
82
|
+
# @param text [String] The text content of the tooltip. This should be brief and no longer than a sentence.
|
83
|
+
# @param direction [Symbol] <%= one_of(Ariadne::TooltipComponent::DIRECTION_OPTIONS) %>
|
84
|
+
# @param classes [String] <%= link_to_classes_docs %>
|
85
|
+
# @param attributes [Hash] <%= link_to_attributes_docs %>
|
86
|
+
def initialize(tag: :"tool-tip", type: TYPE_DEFAULT, text:, direction: DIRECTION_DEFAULT, classes: "", attributes: {})
|
87
|
+
raise TypeError, "tooltip text must be a string" unless text.is_a?(String)
|
88
|
+
|
89
|
+
@tag = check_incoming_tag(:"tool-tip", tag)
|
90
|
+
|
91
|
+
@text = text
|
92
|
+
@classes = classes
|
93
|
+
|
94
|
+
@attributes = attributes
|
95
|
+
@attributes[:hidden] = true
|
96
|
+
@attributes[:visible] ||= false
|
97
|
+
@attributes[:"data-direction"] = fetch_or_raise(DIRECTION_OPTIONS, direction)
|
98
|
+
@attributes[:"data-type"] = fetch_or_raise(TYPE_OPTIONS, type)
|
99
|
+
end
|
100
|
+
|
101
|
+
def call
|
102
|
+
render(Ariadne::BaseComponent.new(tag: @tag, classes: @classes, attributes: @attributes)) { @text }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
|
5
|
+
module Ariadne
|
6
|
+
# :nodoc:
|
7
|
+
module Audited
|
8
|
+
# DSL to register when a component has passed an accessibility audit.
|
9
|
+
#
|
10
|
+
# Example:
|
11
|
+
#
|
12
|
+
# class MyComponent < ViewComponent::Base
|
13
|
+
# include Ariadne::Audited::Dsl
|
14
|
+
# audited_at 'YYYY-MM-DD'
|
15
|
+
# end
|
16
|
+
module Dsl
|
17
|
+
extend ActiveSupport::Concern
|
18
|
+
|
19
|
+
included do
|
20
|
+
class_attribute :audit_date, instance_writer: false
|
21
|
+
end
|
22
|
+
|
23
|
+
class_methods do
|
24
|
+
def audited_at(date = nil)
|
25
|
+
return audit_date if date.nil?
|
26
|
+
|
27
|
+
self.audit_date = date
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Helps build a list of conditional class names
|
4
|
+
module Ariadne
|
5
|
+
# :nodoc:
|
6
|
+
module ClassNameHelper
|
7
|
+
def class_names(*args)
|
8
|
+
[].tap do |classes|
|
9
|
+
args.each do |class_name|
|
10
|
+
next if class_name.blank?
|
11
|
+
|
12
|
+
case class_name
|
13
|
+
when String
|
14
|
+
classes << class_name
|
15
|
+
else
|
16
|
+
raise ArgumentError, "Expected String class name, got #{class_name.class}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end.join(" ")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Ariadne::FetchOrFallbackHelper
|
4
|
+
# A little helper to enable graceful fallbacks
|
5
|
+
#
|
6
|
+
# Use this helper to quietly ensure a value is
|
7
|
+
# one that you expect:
|
8
|
+
#
|
9
|
+
# allowed_values - allowed options for *value*
|
10
|
+
# given_value - input being coerced
|
11
|
+
# fallback - returned if *given_value* is not included in *allowed_values*
|
12
|
+
#
|
13
|
+
# fetch_or_raise([1,2,3], 5) => 2
|
14
|
+
# fetch_or_raise([1,2,3], 1) => 1
|
15
|
+
# fetch_or_raise([1,2,3], nil) => 2
|
16
|
+
module Ariadne
|
17
|
+
# :nodoc:
|
18
|
+
module FetchOrFallbackHelper
|
19
|
+
include LoggerHelper
|
20
|
+
|
21
|
+
mattr_accessor :fallback_raises, default: true
|
22
|
+
|
23
|
+
InvalidValueError = Class.new(StandardError)
|
24
|
+
|
25
|
+
TRUE_OR_FALSE = [true, false].freeze
|
26
|
+
|
27
|
+
def fetch_or_raise(allowed_values, given_value)
|
28
|
+
raise ArgumentError, "allowed_values must be an array; it was #{allowed_values.class}" unless allowed_values.is_a?(Array)
|
29
|
+
|
30
|
+
if allowed_values.include?(given_value)
|
31
|
+
given_value
|
32
|
+
else
|
33
|
+
raise InvalidValueError, <<~MSG
|
34
|
+
fetch_or_raise was called with an invalid value.
|
35
|
+
|
36
|
+
Expected one of: #{allowed_values.inspect}
|
37
|
+
Got: #{given_value.inspect}
|
38
|
+
MSG
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def fetch_or_raise_boolean(given_value)
|
43
|
+
fetch_or_raise(TRUE_OR_FALSE, given_value)
|
44
|
+
end
|
45
|
+
|
46
|
+
# TODO: test this
|
47
|
+
def check_incoming_tag(preferred_tag, given_tag)
|
48
|
+
return preferred_tag if given_tag.blank? || preferred_tag == given_tag
|
49
|
+
|
50
|
+
unless silence_warnings?
|
51
|
+
message = <<~MSG
|
52
|
+
Ariadne: note that `#{preferred_tag}` is the preferred tag here;
|
53
|
+
you passed `#{given_tag}` (which will still be used)
|
54
|
+
MSG
|
55
|
+
|
56
|
+
logger.warn(message)
|
57
|
+
end
|
58
|
+
|
59
|
+
given_tag
|
60
|
+
end
|
61
|
+
|
62
|
+
# TODO: test this
|
63
|
+
def check_incoming_attribute(preferred_attribute, given_attribute)
|
64
|
+
return preferred_attribute if given_attribute.blank? || preferred_attribute != given_attribute
|
65
|
+
|
66
|
+
unless silence_warnings?
|
67
|
+
message = <<~MSG
|
68
|
+
Ariadne: note that `#{preferred_attribute}` is the preferred attribute here;
|
69
|
+
you passed `#{given_attribute}` (which will still be used)
|
70
|
+
MSG
|
71
|
+
|
72
|
+
logger.warn(message)
|
73
|
+
end
|
74
|
+
|
75
|
+
given_attribute
|
76
|
+
end
|
77
|
+
|
78
|
+
# TODO: test this
|
79
|
+
def check_incoming_value(preferred_value, given_pair)
|
80
|
+
return preferred_value if given_pair.blank? || !given_pair.is_a?(Hash)
|
81
|
+
|
82
|
+
given_key = given_pair.keys.first
|
83
|
+
given_value = given_pair.values.first
|
84
|
+
|
85
|
+
return preferred_value if given_value.blank?
|
86
|
+
|
87
|
+
unless silence_warnings?
|
88
|
+
|
89
|
+
message = <<~MSG
|
90
|
+
Ariadne: note that `#{preferred_value}` is the preferred value for `#{given_key}` here;
|
91
|
+
you passed `#{given_value}` (which will still be used)
|
92
|
+
MSG
|
93
|
+
|
94
|
+
logger.warn(message)
|
95
|
+
end
|
96
|
+
|
97
|
+
given_value
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|