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"}
>
-
+