ariadne_view_components 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|