diff --git a/.changeset/odd-dots-laugh.md b/.changeset/odd-dots-laugh.md
new file mode 100644
index 0000000000..8667f57324
--- /dev/null
+++ b/.changeset/odd-dots-laugh.md
@@ -0,0 +1,5 @@
+---
+'@primer/view-components': minor
+---
+
+Support leading and trailing icons for Links
diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/beta/breadcrumbs/with_beta_truncate/focused.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/beta/breadcrumbs/with_beta_truncate/focused.png
index 3c6d150001..a9f4eda881 100644
Binary files a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/beta/breadcrumbs/with_beta_truncate/focused.png and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/beta/breadcrumbs/with_beta_truncate/focused.png differ
diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/beta/breadcrumbs/with_deprecated_truncate/default.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/beta/breadcrumbs/with_deprecated_truncate/default.png
index 7c59ca6894..7e9b84e9cc 100644
Binary files a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/beta/breadcrumbs/with_deprecated_truncate/default.png and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/beta/breadcrumbs/with_deprecated_truncate/default.png differ
diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/beta/breadcrumbs/with_deprecated_truncate/focused.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/beta/breadcrumbs/with_deprecated_truncate/focused.png
index d849d47a8a..a9f4eda881 100644
Binary files a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/beta/breadcrumbs/with_deprecated_truncate/focused.png and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/beta/breadcrumbs/with_deprecated_truncate/focused.png differ
diff --git a/app/components/primer/base_component.rb b/app/components/primer/base_component.rb
index ffb713e654..2e90df8e8c 100644
--- a/app/components/primer/base_component.rb
+++ b/app/components/primer/base_component.rb
@@ -151,6 +151,7 @@ class BaseComponent < Primer::Component
# | :- | :- | :- |
# | classes | String | CSS class name value to be concatenated with generated Primer CSS classes. |
# | test_selector | String | Adds `data-test-selector='given value'` in non-Production environments for testing purposes. |
+ # | trim | Boolean | Calls `strip` on the content to remove trailing and leading white spaces. |
def initialize(tag:, classes: nil, **system_arguments)
@tag = tag
@@ -158,6 +159,8 @@ def initialize(tag:, classes: nil, **system_arguments)
@result = Primer::Classify.call(**@system_arguments.merge(classes: classes))
+ @trim = !!@system_arguments.delete(:trim)
+
@system_arguments[:"data-view-component"] = true
# Filter out Primer keys so they don't get assigned as HTML attributes
@content_tag_args = add_test_selector(@system_arguments).except(*Primer::Classify::Utilities::UTILITIES.keys)
@@ -167,7 +170,7 @@ def call
if SELF_CLOSING_TAGS.include?(@tag)
tag(@tag, @content_tag_args.merge(@result))
else
- content_tag(@tag, content, @content_tag_args.merge(@result))
+ content_tag(@tag, @trim ? trimmed_content : content, @content_tag_args.merge(@result))
end
end
end
diff --git a/app/components/primer/beta/button.rb b/app/components/primer/beta/button.rb
index f595c2245a..7a362aa4fd 100644
--- a/app/components/primer/beta/button.rb
+++ b/app/components/primer/beta/button.rb
@@ -159,17 +159,6 @@ def before_render
"Button--invisible-noVisuals"
)
end
-
- def trimmed_content
- return if content.blank?
-
- trimmed_content = content.strip
-
- return trimmed_content unless content.html_safe?
-
- # strip unsets `html_safe`, so we have to set it back again to guarantee that HTML blocks won't break
- trimmed_content.html_safe # rubocop:disable Rails/OutputSafety
- end
end
end
end
diff --git a/app/components/primer/beta/link.html.erb b/app/components/primer/beta/link.html.erb
new file mode 100644
index 0000000000..9d95eb0da2
--- /dev/null
+++ b/app/components/primer/beta/link.html.erb
@@ -0,0 +1,14 @@
+<%= render Primer::ConditionalWrapper.new(condition: tooltip?, trim: true, tag: :span, position: :relative) do %>
+ <%= render(Primer::BaseComponent.new(trim: true, **@system_arguments)) do %>
+ <%= render(Primer::BaseComponent.new(tag: :span, classes: "Link-content", trim: true)) do %>
+ <% if leading_visual %>
+ <%= leading_visual %>
+ <% end %>
+ <%= content %>
+ <% if trailing_visual %>
+ <%= trailing_visual %>
+ <% end %>
+ <% end %>
+ <% end %>
+ <%= tooltip if tooltip? %>
+<% end %>
diff --git a/app/components/primer/beta/link.pcss b/app/components/primer/beta/link.pcss
index 647736b767..9a832d715f 100644
--- a/app/components/primer/beta/link.pcss
+++ b/app/components/primer/beta/link.pcss
@@ -67,3 +67,12 @@
color: inherit !important;
}
}
+
+.Link-content {
+ display: inline-flex;
+ align-items: center;
+ /* stylelint-disable-next-line primer/typography */
+ line-height: normal;
+ gap: var(--base-size-4);
+ text-decoration: inherit;
+}
diff --git a/app/components/primer/beta/link.rb b/app/components/primer/beta/link.rb
index 91462e6ae7..1fdfd0442a 100644
--- a/app/components/primer/beta/link.rb
+++ b/app/components/primer/beta/link.rb
@@ -30,6 +30,32 @@ class Link < Primer::Component
Primer::Alpha::Tooltip.new(**system_arguments)
}
+ # Leading visuals appear to the left of the link text.
+ #
+ # Use:
+ #
+ # - `leading_visual_icon` which accepts the arguments accepted by <%= link_to_component(Primer::Beta::Octicon) %>.
+ #
+ # @param system_arguments [Hash] Same arguments as <%= link_to_component(Primer::Beta::Octicon) %>.
+ renders_one :leading_visual, types: {
+ icon: lambda { |**system_arguments|
+ Primer::Beta::Octicon.new(**system_arguments)
+ }
+ }
+
+ # Trailing visuals appear to the right of the link text.
+ #
+ # Use:
+ #
+ # - `trailing_visual_icon` which accepts the arguments accepted by <%= link_to_component(Primer::Beta::Octicon) %>.
+ #
+ # @param system_arguments [Hash] Same arguments as <%= link_to_component(Primer::Beta::Octicon) %>.
+ renders_one :trailing_visual, types: {
+ icon: lambda { |**system_arguments|
+ Primer::Beta::Octicon.new(**system_arguments)
+ }
+ }
+
# @param href [String] URL to be used for the Link. Required. If the requirements are not met an error will be raised in non production environments. In production, an empty link element will be rendered.
# @param scheme [Symbol] <%= one_of(Primer::Beta::Link::SCHEME_MAPPINGS.keys) %>
# @param muted [Boolean] Uses light gray for Link color, and blue on hover.
@@ -54,20 +80,6 @@ def initialize(href: nil, scheme: DEFAULT_SCHEME, muted: false, underline: false
def before_render
raise ArgumentError, "href is required" if @system_arguments[:href].nil? && !Rails.env.production?
end
-
- def call
- if tooltip.present?
- render Primer::BaseComponent.new(tag: :span, position: :relative) do
- render(Primer::BaseComponent.new(**@system_arguments)) do
- content
- end.to_s + tooltip.to_s
- end
- else
- render(Primer::BaseComponent.new(**@system_arguments)) do
- content
- end
- end
- end
end
end
end
diff --git a/app/components/primer/button_component.rb b/app/components/primer/button_component.rb
index 74b112773f..2160386d7c 100644
--- a/app/components/primer/button_component.rb
+++ b/app/components/primer/button_component.rb
@@ -111,16 +111,5 @@ def initialize(
def link?
@scheme == LINK_SCHEME
end
-
- def trimmed_content
- return if content.blank?
-
- trimmed_content = content.strip
-
- return trimmed_content unless content.html_safe?
-
- # strip unsets `html_safe`, so we have to set it back again to guarantee that HTML blocks won't break
- trimmed_content.html_safe # rubocop:disable Rails/OutputSafety
- end
end
end
diff --git a/app/components/primer/component.rb b/app/components/primer/component.rb
index 690c6a6b1b..84b5e8c620 100644
--- a/app/components/primer/component.rb
+++ b/app/components/primer/component.rb
@@ -148,5 +148,12 @@ def shouldnt_raise_error?
def should_raise_aria_error?
!Rails.env.production? && raise_on_invalid_aria? && !ENV["PRIMER_WARNINGS_DISABLED"]
end
+
+ def trimmed_content
+ return content unless content.present?
+
+ # strip unsets `html_safe`, so we have to set it back again to guarantee that HTML blocks won't break
+ content.html_safe? ? content.strip.html_safe : content.strip # rubocop:disable Rails/OutputSafety
+ end
end
end
diff --git a/app/components/primer/conditional_wrapper.rb b/app/components/primer/conditional_wrapper.rb
index 57c61e93b7..27fcc46368 100644
--- a/app/components/primer/conditional_wrapper.rb
+++ b/app/components/primer/conditional_wrapper.rb
@@ -10,12 +10,15 @@ class ConditionalWrapper < Primer::Component
def initialize(condition:, **base_component_arguments)
@condition = condition
@base_component_arguments = base_component_arguments
+ @trim = !!@base_component_arguments.delete(:trim)
end
def call
- return content unless @condition
+ unless @condition
+ return @trim ? trimmed_content : content
+ end
- BaseComponent.new(**@base_component_arguments).render_in(self) { content }
+ BaseComponent.new(trim: @trim, **@base_component_arguments).render_in(self) { content }
end
end
end
diff --git a/previews/primer/beta/link_preview.rb b/previews/primer/beta/link_preview.rb
index b8e0734342..0a76c35cd4 100644
--- a/previews/primer/beta/link_preview.rb
+++ b/previews/primer/beta/link_preview.rb
@@ -9,8 +9,14 @@ class LinkPreview < ViewComponent::Preview
# @param underline [Boolean]
# @param muted [Boolean]
# @param scheme [Symbol] select [default, primary, secondary]
- def playground(scheme: :default, muted: false, underline: true)
- render(Primer::Beta::Link.new(href: "#", scheme: scheme, muted: muted, underline: underline)) { "This is a link!" }
+ # @param leading_visual_icon [Symbol] octicon
+ # @param trailing_visual_icon [Symbol] octicon
+ def playground(scheme: :default, muted: false, underline: true, leading_visual_icon: nil, trailing_visual_icon: nil)
+ render(Primer::Beta::Link.new(href: "#", scheme: scheme, muted: muted, underline: underline)) do |link|
+ link.with_leading_visual_icon(icon: leading_visual_icon) if leading_visual_icon && leading_visual_icon != :none
+ link.with_trailing_visual_icon(icon: trailing_visual_icon) if trailing_visual_icon && trailing_visual_icon != :none
+ "This is a link!"
+ end
end
# @label Default Options
@@ -66,6 +72,22 @@ def color_scheme_secondary_muted
render(Primer::Beta::Link.new(href: "#", scheme: :secondary, muted: true)) { "This is a muted secondary link color." }
end
# @!endgroup
+
+ # @label With leading icon
+ def with_leading_icon
+ render(Primer::Beta::Link.new(href: "#")) do |component|
+ component.with_leading_visual_icon(icon: :"mark-github")
+ "Link with leading icon"
+ end
+ end
+
+ # @label With trailing icon
+ def with_trailing_icon
+ render(Primer::Beta::Link.new(href: "#")) do |component|
+ component.with_trailing_visual_icon(icon: :"link-external")
+ "Link with trailing icon"
+ end
+ end
end
end
end
diff --git a/test/components/beta/link_test.rb b/test/components/beta/link_test.rb
index dbfbd7e94c..66d230347c 100644
--- a/test/components/beta/link_test.rb
+++ b/test/components/beta/link_test.rb
@@ -15,7 +15,7 @@ def test_renders_content_and_not_muted_link
def test_renders_no_additional_whitespace
result = render_inline(Primer::Beta::Link.new(href: "http://joe-jonas-shirtless.com")) { "content" }
- assert_match(%r{^]+>content$}, result.to_s)
+ assert_match(%r{^]+>]+>content$}, result.to_s)
end
def test_renders_without_trailing_newline
@@ -92,4 +92,26 @@ def test_renders_with_tooltip_sibling
assert_selector("a[href='http://google.com'] + tool-tip", text: "Tooltip text", visible: false)
end
+
+ def test_renders_leading_visual_icon
+ render_inline(Primer::Beta::Link.new(href: "http://google.com")) do |component|
+ component.with_leading_visual_icon(icon: "plus")
+ "content"
+ end
+
+ assert_selector("a[href='http://google.com']")
+ assert_selector(".octicon-plus")
+ end
+
+ def test_renders_trailing_visual_icon
+ render_inline(Primer::Beta::Link.new(href: "http://google.com")) do |component|
+ component.with_leading_visual_icon(icon: "plus")
+ component.with_trailing_visual_icon(icon: "alert")
+ "content"
+ end
+
+ assert_selector("a[href='http://google.com']")
+ assert_selector("a svg:first-child.octicon-plus")
+ assert_selector("a svg:nth-child(2).octicon-alert")
+ end
end