From 1bb5f6f475d04cb812ff69f042283c7ee456f33a Mon Sep 17 00:00:00 2001 From: Damien Robson Date: Fri, 24 Jan 2025 11:01:22 +0000 Subject: [PATCH] feat(portrait): allows customers to provide a custom background colour for the Portrait component --- .../portrait/__internal__/utils.test.ts | 118 ++++++++++++++ src/components/portrait/__internal__/utils.ts | 121 ++++++++++++++ .../portrait/portrait-test.stories.tsx | 36 ++++- .../portrait/portrait.component.tsx | 10 ++ src/components/portrait/portrait.mdx | 36 ++++- src/components/portrait/portrait.pw.tsx | 59 +++++++ src/components/portrait/portrait.stories.tsx | 147 ++++++++++++++++- src/components/portrait/portrait.style.tsx | 27 ++-- src/components/portrait/portrait.test.tsx | 148 +++++++++++++++++- src/components/profile/profile.component.tsx | 9 ++ src/components/profile/profile.mdx | 17 ++ src/components/profile/profile.stories.tsx | 48 ++++++ src/components/profile/profile.test.tsx | 33 ++++ src/components/switch/switch.component.tsx | 6 +- 14 files changed, 790 insertions(+), 25 deletions(-) create mode 100644 src/components/portrait/__internal__/utils.test.ts create mode 100644 src/components/portrait/__internal__/utils.ts diff --git a/src/components/portrait/__internal__/utils.test.ts b/src/components/portrait/__internal__/utils.test.ts new file mode 100644 index 0000000000..f6215289ca --- /dev/null +++ b/src/components/portrait/__internal__/utils.test.ts @@ -0,0 +1,118 @@ +import getColoursForPortrait from "./utils"; + +test("returns the default string if no arguments are passed", () => { + const result = getColoursForPortrait(undefined); + expect(result).toBe( + `background-color: var(--colorsUtilityReadOnly400); color: var(--colorsUtilityYin090);`, + ); +}); + +test("returns a fixed string if only the `backgroundColor` argument is set to true", () => { + const result = getColoursForPortrait("#FF0000"); + expect(result).toBe( + "background-color: #FF0000; color: var(--colorsUtilityYin090);", + ); +}); + +test("returns a fixed string if the `darkBackground` argument is set to true", () => { + const result = getColoursForPortrait(undefined, true); + expect(result).toBe( + "background-color: var(--colorsUtilityYin090); color: var(--colorsUtilityReadOnly600);", + ); +}); + +test("returns a fixed string if neither `darkBackground` nor `backgroundColor` argument are defined", () => { + const result = getColoursForPortrait(undefined, false); + expect(result).toBe( + `background-color: var(--colorsUtilityReadOnly400); color: var(--colorsUtilityYin090);`, + ); +}); + +test("returns a string with the custom background color if the `backgroundColor` argument is defined", () => { + const result = getColoursForPortrait("#FF0000", false); + expect(result).toBe( + "background-color: #FF0000; color: var(--colorsUtilityYin090);", + ); +}); + +test("returns a string with the custom background color if only the `backgroundColor` argument is defined and all others are false", () => { + const result = getColoursForPortrait("#FF0000", false, false, false); + expect(result).toBe( + "background-color: #FF0000; color: var(--colorsUtilityYin090);", + ); +}); + +test("returns a string with the custom background color if the `backgroundColor` argument is defined and `largeText` argument is true", () => { + const result = getColoursForPortrait("#FF0000", false, true); + expect(result).toBe( + "background-color: #FF0000; color: var(--colorsUtilityYin090);", + ); +}); + +test("returns a string with the custom background color if the `backgroundColor` and `largeText` arguments are defined and `strict` argument is false", () => { + const result = getColoursForPortrait("#FF0000", false, true, false); + expect(result).toBe( + "background-color: #FF0000; color: var(--colorsUtilityYin090);", + ); +}); + +test("returns a string with the custom background color if the `backgroundColor` and `largeText` arguments are defined and `strict` argument is true", () => { + const result = getColoursForPortrait("#FF0000", false, true, true); + expect(result).toBe( + "background-color: #FF0000; color: var(--colorsUtilityYin090);", + ); +}); + +describe("Contrast ratio tests", () => { + it("uses a white foreground colour if the white contrast ratio meets the minimum contrast threshold and is higher than the black contrast ratio", () => { + const result = getColoursForPortrait("#0000FF"); + expect(result).toBe("background-color: #0000FF; color: #FFFFFF;"); + }); + + it("uses a black foreground colour if the black contrast ratio meets the minimum contrast threshold", () => { + const result = getColoursForPortrait("#FFFF00"); + expect(result).toBe( + "background-color: #FFFF00; color: var(--colorsUtilityYin090);", + ); + }); +}); + +test("returns a string with the custom background color and light text if the `backgroundColor` argument is set to a colour with poor contrast ratios (higher white contrast)", () => { + const result = getColoursForPortrait("#0000FF"); + expect(result).toBe("background-color: #0000FF; color: #FFFFFF;"); +}); + +test("returns a string with the custom colors if the `backgroundColor` and `foregroundColor` arguments are provided and all others are false", () => { + const result = getColoursForPortrait( + "#FF0000", + false, + false, + false, + "#00FF00", + ); + expect(result).toBe("background-color: #FF0000; color: #00FF00;"); +}); + +test("returns a string with the custom foreground color if `foregroundColor` argument is present but `backgroundColor` is omitted", () => { + const result = getColoursForPortrait( + undefined, + false, + false, + false, + "#00FF00", + ); + expect(result).toBe( + `background-color: var(--colorsUtilityReadOnly400); color: #00FF00;`, + ); +}); + +test("returns a string with the custom colors if the `darkBackground`, `foregroundColor` and `backgroundColor` props are set", () => { + const result = getColoursForPortrait( + "#FF0000", + true, + false, + false, + "#00FF00", + ); + expect(result).toBe("background-color: #FF0000; color: #00FF00;"); +}); diff --git a/src/components/portrait/__internal__/utils.ts b/src/components/portrait/__internal__/utils.ts new file mode 100644 index 0000000000..32a41580b7 --- /dev/null +++ b/src/components/portrait/__internal__/utils.ts @@ -0,0 +1,121 @@ +// Function provided by ChatGPT +// Calculate the contrast ratio of a pair of luminance values +const getContrastRatio = (luminance1: number, luminance2: number): number => { + // Ensure L1 is the lighter color and L2 is the darker color + const [L1, L2] = + luminance1 > luminance2 + ? [luminance1, luminance2] + : [luminance2, luminance1]; + return (L1 + 0.05) / (L2 + 0.05); +}; + +// Function provided by ChatGPT +// Calculates the foreground color based on the background color +const calculateLuminance = (hexColor: string): number => { + // Remove the hash + const hex = hexColor.replace("#", ""); + // Split the hex into RGB components + const r = parseInt(hex.slice(0, 2), 16); + const g = parseInt(hex.slice(2, 4), 16); + const b = parseInt(hex.slice(4, 6), 16); + + // Function to normalize RGB values (0-255) to floating integer values (0-1) + const normalize = (value: number): number => { + // Divide by 255 and apply gamma correction + const v = value / 255; + return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; + }; + + // Normalize RGB values + const normalizedR = normalize(r); + const normalizedG = normalize(g); + const normalizedB = normalize(b); + + // Calculate luminance + const luminance = + 0.2126 * normalizedR + 0.7152 * normalizedG + 0.0722 * normalizedB; + + // Return black or white based on luminance + return luminance; +}; + +// Function provided by ChatGPT +// Get the accessible foreground color based on the background color +function getAccessibleForegroundColor( + backgroundColor: string, + largeText: boolean, + strict: boolean, +): string { + const bgLuminance = calculateLuminance(backgroundColor); + const whiteLuminance = calculateLuminance("#FFFFFF"); + const blackLuminance = calculateLuminance("#000000"); + + const whiteContrast = getContrastRatio(bgLuminance, whiteLuminance); + const blackContrast = getContrastRatio(bgLuminance, blackLuminance); + + const strictThreshold = largeText ? 4.5 : 7.0; + const nonStrictThreshold = largeText ? 3.0 : 4.5; + const minContrast = strict ? strictThreshold : nonStrictThreshold; + + /* istanbul ignore else */ + if (whiteContrast >= minContrast && whiteContrast > blackContrast) { + return "#FFFFFF"; + } + + /* istanbul ignore else */ + if (blackContrast >= minContrast) { + return "var(--colorsUtilityYin090)"; + } + + // If no color meets the contrast ratio, return the color with the highest contrast + // In theory this is possible only if the background color is a shade of grey, but + // this is a fallback mechanism as finding a colour which fails both contrast ratios + // is highly unlikely. + /* istanbul ignore next */ + return whiteContrast > blackContrast + ? "#FFFFFF" + : "var(--colorsUtilityYin090)"; +} + +// Helper function to return the desired colours for the portrait +const getColoursForPortrait = ( + // The custom background colour, if any + backgroundColour: string | undefined, + // Whether the portrait is on a dark background + darkBackground = false, + // Whether the text is large + largeText = false, + /** + * Whether to use strict contrast (i.e., WCAG AAA). If this is false, it uses WCAG AA contrast + * ratios (4.5:1 for normal text, 3:1 for large text). If true, it uses 7:1 for normal text and + * 4.5:1 for large text. + */ + strict = false, + // The custom foreground colour, if any + foregroundColor: string | undefined = undefined, +): string => { + let fgColor = "var(--colorsUtilityYin090)"; + let bgColor = "var(--colorsUtilityReadOnly400)"; + + // If only dark background is set, set dark background colours + if (darkBackground && !backgroundColour && !foregroundColor) { + bgColor = "var(--colorsUtilityYin090)"; + fgColor = "var(--colorsUtilityReadOnly600)"; + } + + // If custom background colour is set, use it. Calculate foreground colour based on it. + if (backgroundColour) { + bgColor = backgroundColour; + fgColor = getAccessibleForegroundColor(backgroundColour, largeText, strict); + } + + // If custom foreground colour is set, use it + if (foregroundColor) { + fgColor = foregroundColor; + } + + // Return relevant colours + return `background-color: ${bgColor}; color: ${fgColor};`; +}; + +export default getColoursForPortrait; diff --git a/src/components/portrait/portrait-test.stories.tsx b/src/components/portrait/portrait-test.stories.tsx index 823a52569f..e53945edc0 100644 --- a/src/components/portrait/portrait-test.stories.tsx +++ b/src/components/portrait/portrait-test.stories.tsx @@ -6,7 +6,7 @@ import Portrait, { PortraitProps } from "./portrait.component"; export default { title: "Portrait/Test", - includeStories: ["Default"], + includeStories: ["Default", "CustomColors"], parameters: { info: { disable: true }, chromatic: { @@ -32,6 +32,16 @@ export default { type: "select", }, }, + backgroundColor: { + control: { + type: "color", + }, + }, + foregroundColor: { + control: { + type: "color", + }, + }, }, }; @@ -51,6 +61,30 @@ Default.args = { shape: "circle", }; +export const CustomColors = ({ + backgroundColor, + foregroundColor, + ...args +}: PortraitProps) => ( + +); + +CustomColors.storyName = "Custom Colors"; +CustomColors.args = { + src: "", + initials: "", + iconType: "accessibility_web", + size: "M", + shape: "circle", + backgroundColor: "#000000", + foregroundColor: "#FFFFFF", +}; + export const PortraitDefaultComponent = ({ ...props }) => { return ; }; diff --git a/src/components/portrait/portrait.component.tsx b/src/components/portrait/portrait.component.tsx index 104ac92cc6..e4c66e19a8 100644 --- a/src/components/portrait/portrait.component.tsx +++ b/src/components/portrait/portrait.component.tsx @@ -60,10 +60,16 @@ export interface PortraitProps extends MarginProps { tooltipBgColor?: string; /** [Legacy] Override font color of the Tooltip, provide any color from palette or any valid css color value. */ tooltipFontColor?: string; + /** The hex code of the background colour */ + backgroundColor?: string; + /** The hex code of the foreground colour. This will only take effect if use in conjunction with `backgroundColor` */ + foregroundColor?: string; } const Portrait = ({ alt, + backgroundColor, + foregroundColor = undefined, name, darkBackground = false, gravatar = "", @@ -170,6 +176,8 @@ const Portrait = ({ darkBackground={darkBackground} size={size} shape={shape} + backgroundColor={backgroundColor} + foregroundColor={foregroundColor} > {portrait} @@ -186,6 +194,8 @@ const Portrait = ({ darkBackground={darkBackground} size={size} shape={shape} + backgroundColor={backgroundColor} + foregroundColor={foregroundColor} > {portrait} diff --git a/src/components/portrait/portrait.mdx b/src/components/portrait/portrait.mdx index a69d98505b..18e0c45c26 100644 --- a/src/components/portrait/portrait.mdx +++ b/src/components/portrait/portrait.mdx @@ -34,53 +34,77 @@ import Portrait from "carbon-react/lib/components/portrait"; ### Default +By default, the `Portrait` will render a circle with a user icon. + ### Initials -Basic way of using the `Portrait` component is to simply pass your initials as `initials` prop. +The `Portrait` component can also render initials in place of the icon if the `initials` prop is provided. ### Src -To use an image, simply pass any valid image URL as a `src` prop. +To use an image instead of the default icon or initials, pass any valid image URL via the `src` prop. ### IconType -Portrait also supports the declaration of a icon to fallback on when `src` or `initials` are falsy. +`Portrait` allows you to specify an icon, which will be shown if the `src` or `initials` props are omitted. ### With tooltip -By providing a node element to the `tooltipMessage` prop, a tooltip can be displayed when hovering over the Portrait. +A tooltip can be displayed when hovering over the `Portrait` by providing the `tooltipMessage`, `tooltipPosition` and `tooltipColor` props. ### Sizes +The `Portrait` component can be rendered in a variety of sizes by passing the desired size as the `size` prop. + ### Shapes +The `Portrait` component can be rendered in a variety of shapes by passing the desired shape as the `shape` prop. + ### Dark background +The `Portrait` component can be rendered with a dark background by passing the `darkBackground` prop. + ### With margin -Margins can be applied to the `portrait` component using styled-system. -To see a full list of available margin props, please visit the props table at the bottom of this page. +Margins can be applied to the `Portrait` component using styled-system. To see a full list of available margin props, please visit the props +table at the bottom of this page. [Vist Props Table](#props) +### Custom colors + +The `Portrait` component provides a set of props that allow for custom foreground and background colors +to be applied. These props are `backgroundColor` and `foregroundColor`, and both accept a HEX color code or +[design system token](https://zeroheight.com/2ccf2b601/p/217e24-design-tokens/b/870b8a). + +Using these props will override the default colors of the `Portrait` component. They will also bypass the +`darkBackground` prop; setting it alongside `backgroundColor` or `forgroundColor` will have no effect. + +When a `backgroundColor` is provided, the `foregroundColor` will be automatically set to a contrasting color. +This is calculated internally to ensure accessibility standards are met. If a custom `foregroundColor` is provided, +this value will be used instead of the calculated one; please ensure that the color contrast is sufficient and that +the component remains accessible. + + + ## Props ### Portrait diff --git a/src/components/portrait/portrait.pw.tsx b/src/components/portrait/portrait.pw.tsx index 02fcc87f0e..f2d100882c 100644 --- a/src/components/portrait/portrait.pw.tsx +++ b/src/components/portrait/portrait.pw.tsx @@ -609,4 +609,63 @@ test.describe("Accessibility tests for Portrait component", () => { await checkAccessibility(page); }); + + [ + { value: "#A3CAF0", label: "paleblue" }, + { value: "#FD9BA3", label: "palepink" }, + { value: "#B4AEEA", label: "palepurple" }, + { value: "#ECE6AF", label: "palegoldenrod" }, + { value: "#EBAEDE", label: "paleorchid" }, + { value: "#EBC7AE", label: "paledesert" }, + { value: "#AEECEB", label: "paleturquoise" }, + { value: "#AEECD6", label: "palemint" }, + { value: "#000000", label: "black" }, + { value: "#FFFFFF", label: "white" }, + { value: "#2F4F4F", label: "darkslategray" }, + { value: "#696969", label: "dimgray" }, + { value: "#808080", label: "gray" }, + { value: "#A9A9A9", label: "darkgray" }, + { value: "#C0C0C0", label: "silver" }, + { value: "#D3D3D3", label: "lightgray" }, + { value: "#DCDCDC", label: "gainsboro" }, + { value: "#F5F5F5", label: "whitesmoke" }, + { value: "#FFFFE0", label: "lightyellow" }, + { value: "#FFFACD", label: "lemonchiffon" }, + { value: "#FAFAD2", label: "lightgoldenrodyellow" }, + { value: "#FFE4B5", label: "moccasin" }, + { value: "#FFDAB9", label: "peachpuff" }, + { value: "#FFDEAD", label: "navajowhite" }, + { value: "#F5DEB3", label: "wheat" }, + { value: "#FFF8DC", label: "cornsilk" }, + { value: "#FFFFF0", label: "ivory" }, + { value: "#0000FF", label: "blue" }, + { value: "#0000CD", label: "mediumblue" }, + { value: "#00008B", label: "darkblue" }, + { value: "#000080", label: "navy" }, + { value: "#191970", label: "midnightblue" }, + { value: "#4169E1", label: "royalblue" }, + { value: "#4682B4", label: "steelblue" }, + { value: "#5F9EA0", label: "cadetblue" }, + { value: "#6495ED", label: "cornflowerblue" }, + { value: "#87CEFA", label: "lightskyblue" }, + { value: "#87CEEB", label: "skyblue" }, + { value: "#00BFFF", label: "deepskyblue" }, + { value: "#1E90FF", label: "dodgerblue" }, + { value: "#ADD8E6", label: "lightblue" }, + { value: "#B0C4DE", label: "lightsteelblue" }, + { value: "#708090", label: "slateblue" }, + { value: "#6A5ACD", label: "slateblue2" }, + { value: "#7B68EE", label: "mediumslateblue" }, + { value: "#8A2BE2", label: "blueviolet" }, + { value: "#9370DB", label: "mediumpurple" }, + ].forEach(({ label, value }) => { + test(`should pass accessibility checks when backgroundColor is ${label} (${value})`, async ({ + mount, + page, + }) => { + await mount(); + + await checkAccessibility(page); + }); + }); }); diff --git a/src/components/portrait/portrait.stories.tsx b/src/components/portrait/portrait.stories.tsx index de67ca7683..e92bd8d5e4 100644 --- a/src/components/portrait/portrait.stories.tsx +++ b/src/components/portrait/portrait.stories.tsx @@ -1,9 +1,11 @@ -import React from "react"; +import React, { useState } from "react"; import { Meta, StoryObj } from "@storybook/react"; +import Typography from "../typography"; import generateStyledSystemProps from "../../../.storybook/utils/styled-system-props"; import Box from "../box"; +import { Select, Option } from "../select"; import Portrait from "."; const styledSystemProps = generateStyledSystemProps({ @@ -116,3 +118,146 @@ export const WithMargin: Story = () => { ); }; WithMargin.storyName = "With Margin"; + +export const CustomColors: Story = () => { + const fgColors = [ + { value: "#000000", label: "black" }, + { value: "#FFFFFF", label: "white" }, + { value: "#007e45", label: "sagegreen" }, + ]; + const bgColors = [ + { value: "#A3CAF0", label: "paleblue" }, + { value: "#FD9BA3", label: "palepink" }, + { value: "#B4AEEA", label: "palepurple" }, + { value: "#ECE6AF", label: "palegoldenrod" }, + { value: "#EBAEDE", label: "paleorchid" }, + { value: "#EBC7AE", label: "paledesert" }, + { value: "#AEECEB", label: "paleturquoise" }, + { value: "#AEECD6", label: "palemint" }, + { value: "#000000", label: "black" }, + { value: "#FFFFFF", label: "white" }, + { value: "#2F4F4F", label: "darkslategray" }, + { value: "#696969", label: "dimgray" }, + { value: "#808080", label: "gray" }, + { value: "#A9A9A9", label: "darkgray" }, + { value: "#C0C0C0", label: "silver" }, + { value: "#D3D3D3", label: "lightgray" }, + { value: "#DCDCDC", label: "gainsboro" }, + { value: "#F5F5F5", label: "whitesmoke" }, + { value: "#FFFFE0", label: "lightyellow" }, + { value: "#FFFACD", label: "lemonchiffon" }, + { value: "#FAFAD2", label: "lightgoldenrodyellow" }, + { value: "#FFE4B5", label: "moccasin" }, + { value: "#FFDAB9", label: "peachpuff" }, + { value: "#FFDEAD", label: "navajowhite" }, + { value: "#F5DEB3", label: "wheat" }, + { value: "#FFF8DC", label: "cornsilk" }, + { value: "#FFFFF0", label: "ivory" }, + { value: "#0000FF", label: "blue" }, + { value: "#0000CD", label: "mediumblue" }, + { value: "#00008B", label: "darkblue" }, + { value: "#000080", label: "navy" }, + { value: "#191970", label: "midnightblue" }, + { value: "#4169E1", label: "royalblue" }, + { value: "#4682B4", label: "steelblue" }, + { value: "#5F9EA0", label: "cadetblue" }, + { value: "#6495ED", label: "cornflowerblue" }, + { value: "#87CEFA", label: "lightskyblue" }, + { value: "#87CEEB", label: "skyblue" }, + { value: "#00BFFF", label: "deepskyblue" }, + { value: "#1E90FF", label: "dodgerblue" }, + { value: "#ADD8E6", label: "lightblue" }, + { value: "#B0C4DE", label: "lightsteelblue" }, + { value: "#708090", label: "slateblue" }, + { value: "#6A5ACD", label: "slateblue2" }, + { value: "#7B68EE", label: "mediumslateblue" }, + { value: "#8A2BE2", label: "blueviolet" }, + { value: "#9370DB", label: "mediumpurple" }, + ]; + const [colour, setColour] = useState(fgColors[0].value); + const [bgColour, setBgColour] = useState(bgColors[0].value); + return ( + <> + + + + + + + + + + + + + + + + + + + The following examples demonstrate using the design token approach + + + + + + + + + ); +}; +CustomColors.storyName = "Custom Color"; diff --git a/src/components/portrait/portrait.style.tsx b/src/components/portrait/portrait.style.tsx index 6221bba00e..fcb296bb64 100644 --- a/src/components/portrait/portrait.style.tsx +++ b/src/components/portrait/portrait.style.tsx @@ -1,14 +1,21 @@ import React from "react"; + import styled from "styled-components"; + import { margin, MarginProps } from "styled-system"; -import BaseTheme from "../../style/themes/base"; import Icon from "../icon"; +import profileConfigSizes from "../profile/profile.config"; +import BaseTheme from "../../style/themes/base"; + import { PortraitSizes, PortraitShapes } from "./portrait.component"; import { PORTRAIT_SIZE_PARAMS } from "./portrait.config"; -import profileConfigSizes from "../profile/profile.config"; + +import getColoursForPortrait from "./__internal__/utils"; type StyledPortraitProps = { + backgroundColor?: string; + foregroundColor?: string; darkBackground?: boolean; size: PortraitSizes; shape?: PortraitShapes; @@ -56,14 +63,14 @@ export const StyledIcon = styled(Icon)>` export const StyledPortraitContainer = styled.div< StyledPortraitProps & MarginProps >` - color: ${({ darkBackground }) => - darkBackground - ? "var(--colorsUtilityReadOnly600)" - : "var(--colorsUtilityYin090)"}; - background-color: ${({ darkBackground }) => - darkBackground - ? "var(--colorsUtilityYin090)" - : "var(--colorsUtilityReadOnly400)"}; + ${({ darkBackground, backgroundColor, size, foregroundColor }) => + getColoursForPortrait( + backgroundColor, + darkBackground, + !["XS", "S"].includes(size), + true, + foregroundColor, + )}; ${({ hasValidImg, size }) => hasValidImg && `max-width: ${PORTRAIT_SIZE_PARAMS[size].dimensions}px;`} min-width: ${({ size }) => PORTRAIT_SIZE_PARAMS[size].dimensions}px; diff --git a/src/components/portrait/portrait.test.tsx b/src/components/portrait/portrait.test.tsx index 398c73292c..d7bd49e4c4 100644 --- a/src/components/portrait/portrait.test.tsx +++ b/src/components/portrait/portrait.test.tsx @@ -1,11 +1,15 @@ -import React from "react"; import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; + +import React from "react"; + import MD5 from "crypto-js/md5"; + import Logger from "../../__internal__/utils/logger"; -import Portrait from "."; import { testStyledSystemMargin } from "../../__spec_helper__/__internal__/test-utils"; +import Portrait from "."; + testStyledSystemMargin( (props) => , () => screen.getByTestId("portrait-wrapper"), @@ -19,6 +23,18 @@ test("renders with a default individual icon", () => { expect(icon).toHaveAttribute("type", "individual"); }); +test("renders with a dark background", () => { + render(); + + const icon = screen.getByTestId("icon"); + expect(icon).toBeVisible(); + expect(icon).toHaveAttribute("type", "individual"); + + const container = screen.getByTestId("portrait"); + expect(container).toHaveStyle("background-color: var(--colorsUtilityYin090)"); + expect(container).toHaveStyleRule("color", "var(--colorsUtilityReadOnly600)"); +}); + test("renders with a custom icon, if a valid icon is provided via the `iconType` prop", () => { render(); @@ -199,3 +215,131 @@ test("allows a custom onClick function to be passed via the `onClick` prop", asy expect(mockFunction).toHaveBeenCalledTimes(1); }); + +describe("custom background colours", () => { + it("renders with the correct colours when a dark colour is provided", () => { + render(); + + const icon = screen.getByTestId("icon"); + expect(icon).toBeVisible(); + expect(icon).toHaveAttribute("type", "individual"); + + const container = screen.getByTestId("portrait"); + expect(container).toHaveStyle("background-color: #000000"); + expect(container).toHaveStyleRule("color", "#FFFFFF"); + }); + + it("renders with the correct colours when a light colour is provided", () => { + render(); + + const icon = screen.getByTestId("icon"); + expect(icon).toBeVisible(); + expect(icon).toHaveAttribute("type", "individual"); + + const container = screen.getByTestId("portrait"); + expect(container).toHaveStyle("background-color: #FFFFFF"); + expect(container).toHaveStyleRule("color", "var(--colorsUtilityYin090)"); + }); + + [ + { value: "#A3CAF0", label: "blue" }, + { value: "#FD9BA3", label: "pink" }, + { value: "#B4AEEA", label: "purple" }, + { value: "#ECE6AF", label: "goldenrod" }, + { value: "#EBAEDE", label: "orchid" }, + { value: "#EBC7AE", label: "desert" }, + { value: "#AEECEB", label: "turquoise" }, + { value: "#AEECD6", label: "mint" }, + ].forEach(({ value, label }) => { + it(`renders with the correct colours when the background colour is set to ${label}`, () => { + render(); + + const icon = screen.getByTestId("icon"); + expect(icon).toBeVisible(); + expect(icon).toHaveAttribute("type", "individual"); + + const container = screen.getByTestId("portrait"); + expect(container).toHaveStyle(`background-color: ${value}`); + }); + }); + + [ + { value: "#A3CAF0", label: "blue" }, + { value: "#FD9BA3", label: "pink" }, + { value: "#B4AEEA", label: "purple" }, + { value: "#ECE6AF", label: "goldenrod" }, + { value: "#EBAEDE", label: "orchid" }, + { value: "#EBC7AE", label: "desert" }, + { value: "#AEECEB", label: "turquoise" }, + { value: "#AEECD6", label: "mint" }, + ].forEach(({ value, label }) => { + it(`renders with the correct colours when the foreground colour is set to ${label}`, () => { + render(); + + const icon = screen.getByTestId("icon"); + expect(icon).toBeVisible(); + expect(icon).toHaveAttribute("type", "individual"); + + const container = screen.getByTestId("portrait"); + expect(container).toHaveStyle(`color: ${value}`); + }); + }); + + it("renders with the correct foreground colour when provided in conjunction with a background colour", () => { + render( + , + ); + + const icon = screen.getByTestId("icon"); + expect(icon).toBeVisible(); + expect(icon).toHaveAttribute("type", "individual"); + + const container = screen.getByTestId("portrait"); + expect(container).toHaveStyle("background-color: #000000"); + expect(container).toHaveStyleRule("color", "#A0FFAF"); + }); + + it("overrides the colours set by the `darkBackground` prop if the custom colour props are set", () => { + render( + , + ); + + const icon = screen.getByTestId("icon"); + expect(icon).toBeVisible(); + expect(icon).toHaveAttribute("type", "individual"); + + const container = screen.getByTestId("portrait"); + expect(container).toHaveStyle("background-color: #000000"); + expect(container).toHaveStyleRule("color", "#A0FFAF"); + }); + + it("supports design tokens being used for the `backgroundColor` and `forefroundColor` props", () => { + render( + , + ); + + const icon = screen.getByTestId("icon"); + expect(icon).toBeVisible(); + expect(icon).toHaveAttribute("type", "individual"); + + const container = screen.getByTestId("portrait"); + expect(container).toHaveStyle( + "background-color: var(--colorsUtilityYin090)", + ); + expect(container).toHaveStyleRule("color", "var(--colorsLogo)"); + }); +}); diff --git a/src/components/profile/profile.component.tsx b/src/components/profile/profile.component.tsx index 75fd8e952c..a28edd3337 100644 --- a/src/components/profile/profile.component.tsx +++ b/src/components/profile/profile.component.tsx @@ -44,6 +44,10 @@ export interface ProfileProps extends MarginProps { size?: ProfileSize; /** Use a dark background. */ darkBackground?: boolean; + /** The hex code of the background colour to be passed to the avatar */ + backgroundColor?: string; + /** The hex code of the foreground colour to be passed to the avatar. Must be used in conjunction with `backgroundColor` */ + foregroundColor?: string; } export const Profile = ({ @@ -56,6 +60,8 @@ export const Profile = ({ email, text, darkBackground, + backgroundColor, + foregroundColor, ...props }: ProfileProps) => { if (!deprecatedClassNameWarningShown && className) { @@ -76,6 +82,9 @@ export const Profile = ({ name, initials: getInitials(), size, + backgroundColor, + foregroundColor, + "data-role": "profile-portrait", }; const avatar = () => { diff --git a/src/components/profile/profile.mdx b/src/components/profile/profile.mdx index 58da0f4d15..ed6361091a 100644 --- a/src/components/profile/profile.mdx +++ b/src/components/profile/profile.mdx @@ -70,6 +70,23 @@ To see a full list of available margin props, please visit the props table at th +### With custom Portrait background colour + +To change the background colour of the avatar, pass the `backgroundColor` prop to the component. This prop accepts any valid hex code color. + +See the [Portrait](/components/portrait) component for more information. + + + +### With custom Portrait foreground colour + +To change the foreground colour of the avatar, pass the `foregroundColor` prop to the component. This prop accepts any valid hex code color, +but must be used in conjunction with the `backgroundColor` prop; using the `foregroundColor` prop alone with not alter the avatar. + +See the [Portrait](/components/portrait) component for more information. + + + ## Props ### Profile diff --git a/src/components/profile/profile.stories.tsx b/src/components/profile/profile.stories.tsx index dd2082149b..717f283732 100644 --- a/src/components/profile/profile.stories.tsx +++ b/src/components/profile/profile.stories.tsx @@ -142,3 +142,51 @@ Responsive.parameters = { viewports: [1300, 900], }, }; + +export const WithCustomPortraitBackgroundColor: Story = () => { + return ( + + + + + ); +}; +WithCustomPortraitBackgroundColor.storyName = + "With Custom Portrait Background Color"; + +export const WithCustomPortraitForegroundColor: Story = () => { + return ( + + + + + ); +}; +WithCustomPortraitForegroundColor.storyName = + "With Custom Portrait Foreground Color"; diff --git a/src/components/profile/profile.test.tsx b/src/components/profile/profile.test.tsx index 755bed332b..ac523085fd 100644 --- a/src/components/profile/profile.test.tsx +++ b/src/components/profile/profile.test.tsx @@ -214,3 +214,36 @@ test("applies the `className` prop to the component wrapper", () => { ); expect(loggerSpy).toHaveBeenCalledTimes(1); }); + +test("renders with custom avatar colouring when `backgroundColor` is set", () => { + render(); + + const profile = screen.getByTestId("profile"); + const portrait = screen.getByTestId("profile-portrait"); + const avatar = screen.getByTestId("icon"); + + expect(profile).toContainElement(avatar); + expect(avatar).toBeVisible(); + expect(avatar).toHaveAttribute("type", "individual"); + expect(portrait).toHaveStyleRule("background-color", "#00FF00"); +}); + +test("renders with custom avatar foreground and background colouring when both `foregroundColor` and `backgroundColor` are set", () => { + render( + , + ); + + const profile = screen.getByTestId("profile"); + const portrait = screen.getByTestId("profile-portrait"); + const avatar = screen.getByTestId("icon"); + + expect(profile).toContainElement(avatar); + expect(avatar).toBeVisible(); + expect(avatar).toHaveAttribute("type", "individual"); + expect(portrait).toHaveStyleRule("background-color", "#00FF00"); + expect(portrait).toHaveStyleRule("color", "#FF00FF"); +}); diff --git a/src/components/switch/switch.component.tsx b/src/components/switch/switch.component.tsx index 7729321e73..de513ef6a2 100644 --- a/src/components/switch/switch.component.tsx +++ b/src/components/switch/switch.component.tsx @@ -269,11 +269,7 @@ export const Switch = React.forwardRef( flexDirection={!reverse ? reverseDirection : direction} width={labelInline ? "100%" : "auto"} > - +