Skip to content

Reimplement icon component as SVG image mask#10065

Merged
aduth merged 12 commits intomainfrom
aduth-icon-svg-mask
Feb 15, 2024
Merged

Reimplement icon component as SVG image mask#10065
aduth merged 12 commits intomainfrom
aduth-icon-svg-mask

Conversation

@aduth
Copy link
Contributor

@aduth aduth commented Feb 9, 2024

🛠 Summary of changes

Updates IconComonent's rendering of icons to reference the original SVG icon, colorized using a combination of mask-image and background-color: currentColor.

This is related to the comment at #10056 (comment) .

USWDS's icon sprite has an advantage in being able to inherit the current text color of content and apply it to the SVG image, which cannot be done by default with an <img> referencing an SVG resource. The approach here uses the CSS mask-image property to emulate this effect.

The goal is to try to avoid wasted data sent from the sprite for icons which aren't used in a page. The sprite image is ~22kb after compression, so it's not catastrophically large, but it's relatively quite large when not already present in a page. By comparison, the introduction of the "language" icon is about 0.5kb compressed, which is actually a net reduction of about 2kb compared to the current globe-white.svg and globe-blue.svg currently used in the footer.

Notes on a few quirks / alternatives:

  • This uses an inline style tag to dynamically inject the URL of the icon to be used by the mask-image.
    • Alternatives considered:
      • Hardcode all variants into a stylesheet for the icon, e.g. a set of classes like .usa-icon--icon-language, where mask-image is assigned for this. Since there's a lot of icons, this would become quite large, and include data for icons not in use.
      • Inject the URL by another means, like a data- attribute
        • This would be possible via the CSS attr() function, but there is not good widespread browser support for this outside the content style
        • This could be possible via JavaScript, which is not a bad idea, but would not work for no-JavaScript (probably acceptable), and would introduce a new script to any page where the icon is loaded, perhaps with a brief delay in initializing the styles
      • Use <svg> <use> like the current icon markup, but referencing the icon directly. This will apparently be possible in SVG2 ("Allow 'use' to reference an external document's root element by omitting the fragment"), but this doesn't appear to be standardized yet or available in current browsers.
  • There's a few non-"dynamic" styles which are also inlined, for mask-size and background-color. Ideally these would be extracted to a stylesheet associated with the component, but there's an existing issue where the automatic component asset loading doesn't work for components rendered in the base.html.erb base layout, due to how render_stylesheet_once_tags is called before components in the base layout are rendered. Edit: This was fixed separately in Update base layout to be base-ier #10067
  • mask-image was only recently made available as standard (without vendor prefix) in Chrome 120 (12/2023), so the vendor prefixes need to be included here. For any of our standard stylesheets, this is handled automatically through our @18f/identity-build-sass build toolchain (Lightning CSS vendor prefixing)

📜 Testing Plan

Verify there are no regressions in any place we currently render an icon using IconComponent, e.g.

👀 Screenshots

The visual differences here come largely from the differences between globe-white.svg and the design system language icon.

Before After
image image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we used ActiveSupport's String#squish to let us author it with line breaks for readability but remove them for serialization?

Suggested change
<%= "#icon-#{unique_id} { mask-image: url(#{icon_path}); -webkit-mask-image: url(#{icon_path}); mask-size: 100%; -webkit-mask-size: 100%; background-color: currentColor; }" %>
<%= <<-STR.squish
#icon-#{unique_id} {
mask-image: url(#{icon_path});
-webkit-mask-image: url(#{icon_path});
mask-size: 100%;
-webkit-mask-size: 100%;
background-color: currentColor;
}
STR
%>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This motivated me toward #10067, which I'm hoping can fix the issue with component stylesheets so we can move the mask-size and background-color properties out and shorten it to something more like this, which hopefully reduces the readability concern a bit?

Suggested change
<%= "#icon-#{unique_id} { mask-image: url(#{icon_path}); -webkit-mask-image: url(#{icon_path}); mask-size: 100%; -webkit-mask-size: 100%; background-color: currentColor; }" %>
<%= "#icon-#{unique_id} { mask-image: url(#{icon_path}); -webkit-mask-image: url(#{icon_path}); }" %>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it still might be nice with some line breaks, but that is a little clearer

@aduth aduth force-pushed the aduth-icon-svg-mask branch from bd22a18 to 407968d Compare February 14, 2024 15:57
aduth and others added 5 commits February 14, 2024 17:01
Previous assertion used XPath, where XPath text matchers don't correctly ignore inline style tag contents
Since it's already rendered as simple string, pass as argument in content_tag. Also avoids excess whitespace
For clarity, avoid ambiguity with "and" keyword

Co-authored-by: Zach Margolis <zachary.margolis@gsa.gov>
Significant for use in ButtonComponent
@aduth aduth marked this pull request as ready for review February 14, 2024 22:16
@aduth
Copy link
Contributor Author

aduth commented Feb 15, 2024

One thing I noticed in some final testing is that this fixes a bug where the globe icon appears to disappear when the language toggle is expanded on mobile because we're not switching to the white version and the icon's default blue color is the same as the background. Now, since it inherits from the text color automatically, it is shown as expected.

Before After
before after

@aduth aduth merged commit 4926cdd into main Feb 15, 2024
@aduth aduth deleted the aduth-icon-svg-mask branch February 15, 2024 19:54
aduth added a commit that referenced this pull request Feb 20, 2024
aduth added a commit that referenced this pull request Feb 20, 2024
* Revert "Implement language picker expander as icon (#10098)"

This reverts commit f2d1559.

* Revert "Reimplement icon component as SVG image mask (#10065)"

This reverts commit 4926cdd.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants