From e83c193ee8c6c0f5409f66c2821a8b9739304882 Mon Sep 17 00:00:00 2001 From: Elisa Shapiro <83474365+ElisaShapiro@users.noreply.github.com> Date: Thu, 25 Jul 2024 14:54:33 -0400 Subject: [PATCH] [PLAY-1138] Icon kit: add a color prop (#3527) **What does this PR do?** A clear and concise description with your runway ticket url. [PLAY-1138](https://runway.powerhrg.com/backlog_items/PLAY-1138) adds a color prop to the icon kit so that a dev can color an icon with one of the [Playbook color tokens](https://playbook.powerapp.cloud/visual_guidelines/colors). An Icon Color doc example section has been added to the Rails and React Icon kit pages showing how to add a color prop (and showing that it works with both playbook-icons and font awesome icons). The classname is updated to include "color_#{color_token}" when a color token is used. Tests have been added as well. In screenshot section see exploration into color cascade/css hierarchy within an isolated playbook section -> IRL for nitro-web the css in place and cascades/overrides seem to like they are set up with a bit more complexity (and ID specificity). **Screenshots:** Screenshots to visualize your addition/change Rails Icon Color Doc Example rails for PR React Icon Color Doc Example react for PR Below is an example of expected/idealized color cascade/hierarchy. Three screenshots from within playbook with example code borrowed from nitro-web's Powerlife Did You Know section. Icon default color (no css wrapper or color prop applied) copied nitro-web no imported styles icon is
default color Icon takes color from css wrapper containing it and nearby text copied nitro-web code no color prop but ability
does not supersede wrapper Icon takes color from color prop copied nitro-web code color prop supersedes
wrapper **How to test?** Steps to confirm the desired behavior: 1. To see the Icon Color doc examples, go to the icon page in the [playbook review environment](https://pr3527.playbook.beta.gm.powerapp.cloud/kits/icon) and scroll down to the Icon Color doc example (or [go there directly](https://pr3527.playbook.beta.gm.powerapp.cloud/kits/icon#icon-color)). 4. See each doc example icon which uses different Playbook color tokens. Several of the icons are from playbook-icons and some are font awesome icons still - this works for them both. 5. To see evidence these changes do not break Nitro, go to the nitro-web review environment example pages [Rails (or go to /projects choose any project)](https://pr41186.nitro-web.beta.gm.powerapp.cloud/projects/3206553) and [React](https://pr41186.nitro-web.beta.gm.powerapp.cloud/user_profile/user_profile). 6. See colored icons on these pages (door-open icon on Door Builder button on Projects page for Rails; envelopes, tshirt, snowflake, boot, syringe, person-dress, utensils icons on User Profile page for React). #### Checklist: - [x] **LABELS** Add a label: `enhancement`, `bug`, `improvement`, `new kit`, `deprecated`, or `breaking`. See [Changelog & Labels](https://github.com/powerhome/playbook/wiki/Changelog-&-Labels) for details. - [x] **DEPLOY** I have added the `milano` label to show I'm ready for a review. - [x] **TESTS** I have added test coverage to my code. --- .../app/pb_kits/playbook/pb_icon/_icon.scss | 17 ++++++++++ .../app/pb_kits/playbook/pb_icon/_icon.tsx | 3 ++ .../pb_icon/docs/_icon_color.html.erb | 5 +++ .../playbook/pb_icon/docs/_icon_color.jsx | 34 +++++++++++++++++++ .../playbook/pb_icon/docs/_icon_color.md | 1 + .../pb_kits/playbook/pb_icon/docs/example.yml | 2 ++ .../pb_kits/playbook/pb_icon/docs/index.js | 1 + playbook/app/pb_kits/playbook/pb_icon/icon.rb | 10 +++++- .../app/pb_kits/playbook/pb_icon/icon.test.js | 31 ++++++++++++----- .../spec/pb_kits/playbook/kits/icon_spec.rb | 16 +++++++++ 10 files changed, 110 insertions(+), 10 deletions(-) create mode 100644 playbook/app/pb_kits/playbook/pb_icon/docs/_icon_color.html.erb create mode 100644 playbook/app/pb_kits/playbook/pb_icon/docs/_icon_color.jsx create mode 100644 playbook/app/pb_kits/playbook/pb_icon/docs/_icon_color.md diff --git a/playbook/app/pb_kits/playbook/pb_icon/_icon.scss b/playbook/app/pb_kits/playbook/pb_icon/_icon.scss index 59aa945237..953e8b5ba2 100644 --- a/playbook/app/pb_kits/playbook/pb_icon/_icon.scss +++ b/playbook/app/pb_kits/playbook/pb_icon/_icon.scss @@ -1,3 +1,20 @@ +@import "../tokens/colors"; + +// All the merges below create $icon_colors, a map of all color tokens in colors.scss +$merge_kits1: map-merge($status_colors, $category_colors); +$merge_kits2: map-merge($merge_kits1, $product_colors); +$merge_kits3: map-merge($merge_kits2, $text_colors); +$icon_colors: map-merge($merge_kits3, $data_colors); + +.pb_custom_icon, .pb_icon_kit { + @each $color_name, $color_value in $icon_colors { + &[class*="#{$color_name}"] { + color: $color_value; + } + } +} + +// Rails custom icon styles svg.pb_custom_icon { width: 1em; fill: currentColor; diff --git a/playbook/app/pb_kits/playbook/pb_icon/_icon.tsx b/playbook/app/pb_kits/playbook/pb_icon/_icon.tsx index c7ccbb9d43..42085e59d6 100644 --- a/playbook/app/pb_kits/playbook/pb_icon/_icon.tsx +++ b/playbook/app/pb_kits/playbook/pb_icon/_icon.tsx @@ -23,6 +23,7 @@ type IconProps = { aria?: {[key: string]: string}, border?: string, className?: string, + color?: string, customIcon?: {[key: string] :SVGElement}, data?: {[key: string]: string}, fixedWidth?: boolean, @@ -121,6 +122,7 @@ const Icon = (props: IconProps) => { aria = {}, border = false, className, + color, customIcon, data = {}, fixedWidth = true, @@ -169,6 +171,7 @@ const Icon = (props: IconProps) => { (!iconElement && !customIcon) ? 'pb_icon_kit' : '', (iconElement || customIcon) ? 'pb_custom_icon' : fontStyle, iconElement ? 'svg-inline--fa' : '', + color ? `color_${color}` : '', globalProps(props), className ) diff --git a/playbook/app/pb_kits/playbook/pb_icon/docs/_icon_color.html.erb b/playbook/app/pb_kits/playbook/pb_icon/docs/_icon_color.html.erb new file mode 100644 index 0000000000..3e3b2e5520 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_icon/docs/_icon_color.html.erb @@ -0,0 +1,5 @@ +<%= pb_rails("flex", props: {orientation: "column"}) do %> + <%= pb_rails("icon", props: { icon: "user", fixed_width: true, color: "primary", padding_bottom: "sm", size: "2x" }) %> + <%= pb_rails("icon", props: { icon: "recycle", fixed_width: true, color: "data_4", padding_bottom: "sm", size: "2x" }) %> + <%= pb_rails("icon", props: { icon: "roofing", fixed_width: true, color: "product_5_background", size: "2x" }) %> +<% end %> \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_icon/docs/_icon_color.jsx b/playbook/app/pb_kits/playbook/pb_icon/docs/_icon_color.jsx new file mode 100644 index 0000000000..d3662eb89c --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_icon/docs/_icon_color.jsx @@ -0,0 +1,34 @@ +import React from "react" +import Icon from "../_icon" + +const IconDefault = (props) => { + return ( +
+ + + +
+ ) +} + +export default IconDefault diff --git a/playbook/app/pb_kits/playbook/pb_icon/docs/_icon_color.md b/playbook/app/pb_kits/playbook/pb_icon/docs/_icon_color.md new file mode 100644 index 0000000000..98aeb62f78 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_icon/docs/_icon_color.md @@ -0,0 +1 @@ +Pass any text, status, data, product, or category Playbook color token to the `color` prop to change any icon's color. \ No newline at end of file diff --git a/playbook/app/pb_kits/playbook/pb_icon/docs/example.yml b/playbook/app/pb_kits/playbook/pb_icon/docs/example.yml index a2cef377dd..9d0f648d79 100644 --- a/playbook/app/pb_kits/playbook/pb_icon/docs/example.yml +++ b/playbook/app/pb_kits/playbook/pb_icon/docs/example.yml @@ -9,6 +9,7 @@ examples: - icon_sizes: Icon Sizes - icon_custom: Icon Custom - icon_fa_kit: Icon with FontAwesome Kit + - icon_color: Icon Color react: - icon_default: Icon Default @@ -20,6 +21,7 @@ examples: - icon_sizes: Icon Sizes - icon_custom: Icon Custom - icon_fa_kit: Icon with FontAwesome Kit + - icon_color: Icon Color swift: - icon_default_swift: Icon Default diff --git a/playbook/app/pb_kits/playbook/pb_icon/docs/index.js b/playbook/app/pb_kits/playbook/pb_icon/docs/index.js index 72808f09ac..8aa0aefb6a 100644 --- a/playbook/app/pb_kits/playbook/pb_icon/docs/index.js +++ b/playbook/app/pb_kits/playbook/pb_icon/docs/index.js @@ -7,3 +7,4 @@ export { default as IconBorder } from './_icon_border.jsx' export { default as IconSizes } from './_icon_sizes.jsx' export { default as IconCustom } from './_icon_custom.jsx' export { default as IconFaKit} from './_icon_fa_kit.jsx' +export { default as IconColor } from './_icon_color.jsx' diff --git a/playbook/app/pb_kits/playbook/pb_icon/icon.rb b/playbook/app/pb_kits/playbook/pb_icon/icon.rb index eb9ba3c05a..ebd1322e9e 100644 --- a/playbook/app/pb_kits/playbook/pb_icon/icon.rb +++ b/playbook/app/pb_kits/playbook/pb_icon/icon.rb @@ -36,6 +36,7 @@ class Icon < Playbook::KitBase default: "far" prop :spin, type: Playbook::Props::Boolean, default: false + prop :color, type: Playbook::Props::String def valid_emoji? emoji_regex = /\p{Emoji}/ @@ -45,6 +46,7 @@ def valid_emoji? def classname generate_classname( "pb_icon_kit", + color_class, font_style_class, icon_class, border_class, @@ -65,6 +67,7 @@ def custom_icon_classname generate_classname( "pb_icon_kit", border_class, + color_class, fixed_width_class, flip_class, inverse_class, @@ -104,7 +107,8 @@ def render_svg svg["aria"] = object.aria svg["height"] = "auto" svg["width"] = "auto" - doc.at_css("path")["fill"] = "currentColor" + fill_color = object.color || "currentColor" + doc.at_css("path")["fill"] = fill_color raw doc end @@ -200,6 +204,10 @@ def spin_class class_name = is_svg? ? "spin" : "fa-spin" spin ? class_name : nil end + + def color_class + color ? "color_#{color}" : nil + end end end end diff --git a/playbook/app/pb_kits/playbook/pb_icon/icon.test.js b/playbook/app/pb_kits/playbook/pb_icon/icon.test.js index 2e61187286..9f872e74a8 100644 --- a/playbook/app/pb_kits/playbook/pb_icon/icon.test.js +++ b/playbook/app/pb_kits/playbook/pb_icon/icon.test.js @@ -12,7 +12,7 @@ describe("Icon Kit", () => { data={{ testid: testId }} fixedWidth icon="user" - /> + /> ) const kit = screen.getByTestId(testId) @@ -27,7 +27,7 @@ describe("Icon Kit", () => { fixedWidth icon="user" rotation={rotateProp} - /> + /> ) const kit = screen.getByTestId(testId) @@ -44,7 +44,7 @@ describe("Icon Kit", () => { fixedWidth flip="horizontal" icon="user" - /> + /> ) const kit = screen.getByTestId(testId) @@ -59,7 +59,7 @@ describe("Icon Kit", () => { fixedWidth icon="spinner" spin - /> + /> ) const kit = screen.getByTestId(testId) @@ -73,7 +73,7 @@ describe("Icon Kit", () => { fixedWidth icon="arrow-left" pull="left" - /> + /> ) const kit = screen.getByTestId(testId) @@ -87,7 +87,7 @@ describe("Icon Kit", () => { fixedWidth icon="arrow-left" pull="left" - /> + /> ) const kit = screen.getByTestId(testId) @@ -101,7 +101,7 @@ describe("Icon Kit", () => { data={{ testid: testId }} fixedWidth icon="user" - /> + /> ) const kit = screen.getByTestId(testId) @@ -128,7 +128,7 @@ describe("Icon Kit", () => { data={{ testid: testId }} icon="user" size={sizeProp} - /> + /> ) const kit = screen.getByTestId(testId) @@ -145,11 +145,24 @@ describe("Icon Kit", () => { fixedWidth fontStyle="fas" icon="user" - /> + /> ) const kit = screen.getByTestId(testId) expect(kit).toHaveClass("fa-user pb_icon_kit fa-fw fas") }) + test("renders with color prop", () => { + render( + + ) + + const kit = screen.getByTestId(testId) + expect(kit).toHaveClass("color_primary") + }) + }) \ No newline at end of file diff --git a/playbook/spec/pb_kits/playbook/kits/icon_spec.rb b/playbook/spec/pb_kits/playbook/kits/icon_spec.rb index ff9a2a1df8..a8ca6e5785 100644 --- a/playbook/spec/pb_kits/playbook/kits/icon_spec.rb +++ b/playbook/spec/pb_kits/playbook/kits/icon_spec.rb @@ -51,6 +51,11 @@ is_expected.to define_prop(:spin) .of_type(Playbook::Props::Boolean) } + it { + is_expected.to define_prop(:color) + .of_type(Playbook::Props::String) + .with_default(nil) + } describe "#custom_icon" do it "returns an icon with custom data-collapsible-main attribute", :aggregate_failures do @@ -83,5 +88,16 @@ expect(subject.new(icon: icon, spin: true).classname).to eq "pb_icon_kit far fa-user fa-spin" expect(subject.new(icon: icon, classname: "additional_class").classname).to eq "pb_icon_kit far fa-user additional_class" end + it "includes color class when color prop is provided", :aggregate_failures do + icon = "user" + color = "primary" + + expect(subject.new(icon: icon, color: color).classname).to include "color_primary" + end + it "does not include color class when color prop is not provided", :aggregate_failures do + icon = "user" + + expect(subject.new(icon: icon).classname).not_to include "color_" + end end end