Skip to content

Commit

Permalink
feat(avatar): ensure different backgrounds are generated for similar …
Browse files Browse the repository at this point in the history
…usernames, full names and user IDs (#9277)

**Related Issue:** #6497

## Summary

This updates `avatar` to create unique background colors for similar
name strings. This is done by mixing the input string in a deterministic
way before building the hash and mapping the color.
  • Loading branch information
jcfranco authored May 9, 2024
1 parent bc11b0d commit bab77b5
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 9 deletions.
27 changes: 26 additions & 1 deletion packages/calcite-components/src/components/avatar/avatar.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { newE2EPage } from "@stencil/core/testing";
import { accessible, defaults, hidden, renders } from "../../tests/commonTests";
import { placeholderImage } from "../../../.storybook/placeholderImage";
import { html } from "../../../support/formatting";
import { CSS } from "./resources";

const placeholderUrl = placeholderImage({
width: 120,
Expand Down Expand Up @@ -63,7 +65,7 @@ describe("calcite-avatar", () => {
const background = document.querySelector("calcite-avatar").shadowRoot.querySelector(".background");
return background.getAttribute("style");
});
expect(style).toEqual("background-color: rgb(245, 219, 214);");
expect(style).toEqual("background-color: rgb(245, 214, 236);");
});

it("renders default icon when no information is passed", async () => {
Expand All @@ -73,4 +75,27 @@ describe("calcite-avatar", () => {
const visible = await icon.isVisible();
expect(visible).toBe(true);
});

it("generates unique background if names are similar", async () => {
const page = await newE2EPage();
await page.setContent(html`
<calcite-avatar full-name="John Doe" username="john_doe"></calcite-avatar>
<calcite-avatar full-name="John Doe 1" username="john_doe1"></calcite-avatar>
<calcite-avatar full-name="John Doe 2" username="john_doe2"></calcite-avatar>
`);

const avatars = [
await page.find(`calcite-avatar:nth-child(1) >>> .${CSS.background}`),
await page.find(`calcite-avatar:nth-child(2) >>> .${CSS.background}`),
await page.find(`calcite-avatar:nth-child(3) >>> .${CSS.background}`),
];

const [firstBgColor, secondBgColor, thirdBgColor] = await Promise.all(
avatars.map((avatar) => avatar.getComputedStyle().then(({ backgroundColor }) => backgroundColor)),
);

expect(firstBgColor).not.toEqual(secondBgColor);
expect(secondBgColor).not.toEqual(thirdBgColor);
expect(firstBgColor).not.toEqual(thirdBgColor);
});
});
9 changes: 5 additions & 4 deletions packages/calcite-components/src/components/avatar/avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Component, Element, h, Prop, State } from "@stencil/core";
import { getModeName } from "../../utils/dom";
import { isValidHex } from "../color-picker/utils";
import { Scale } from "../interfaces";
import { CSS } from "./resources";
import { hexToHue, stringToHex } from "./utils";

@Component({
Expand Down Expand Up @@ -65,7 +66,7 @@ export class Avatar {
return (
<img
alt={this.label || ""}
class="thumbnail"
class={CSS.thumbnail}
onError={() => (this.thumbnailFailedToLoad = true)}
src={this.thumbnail}
/>
Expand All @@ -76,16 +77,16 @@ export class Avatar {
return (
<span
aria-label={this.label || this.fullName}
class="background"
class={CSS.background}
role="figure"
style={{ backgroundColor }}
>
{initials ? (
<span aria-hidden="true" class="initials">
<span aria-hidden="true" class={CSS.initials}>
{initials}
</span>
) : (
<calcite-icon class="icon" icon="user" scale={this.scale} />
<calcite-icon class={CSS.icon} icon="user" scale={this.scale} />
)}
</span>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const CSS = {
thumbnail: "thumbnail",
background: "background",
initials: "initials",
icon: "icon",
};
23 changes: 19 additions & 4 deletions packages/calcite-components/src/components/avatar/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import { hexToRGB } from "../color-picker/utils";
* Convert a string to a valid hex by hashing its contents
* and using the hash as a seed for three distinct color values
*
* @param str
* @param string
*/
export function stringToHex(str: string): string {
export function stringToHex(string: string): string {
// improve random color generation for similar strings.
string = mixStringDeterministically(string);

let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
for (let i = 0; i < string.length; i++) {
hash = string.charCodeAt(i) + ((hash << 5) - hash);
}

let hex = "#";
Expand All @@ -21,6 +24,18 @@ export function stringToHex(str: string): string {
return hex;
}

/**
* The function splits the string into two halves, reverses each half, and then concatenates them.
*
* @param {string} string - The input string to be mixed.
* @returns {string} - The mixed string.
*/
function mixStringDeterministically(string: string): string {
const midPoint = Math.floor(string.length / 2);
const reversed = string.split("").reverse().join("");
return reversed.substring(midPoint) + reversed.slice(0, midPoint);
}

/**
* Find the hue of a color given the separate RGB color channels
*
Expand Down

0 comments on commit bab77b5

Please sign in to comment.