1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-25603] Use angular template for avatar (#15978)

We should be able to simplify the angular component by using svg element directly.
This commit is contained in:
Oscar Hinton
2025-10-03 11:51:25 +02:00
committed by GitHub
parent cb20889a94
commit a1e226b598
4 changed files with 63 additions and 86 deletions

View File

@@ -12,7 +12,6 @@
[color]="currentAccount.avatarColor"
size="small"
aria-hidden="true"
class="[&>img]:tw-block"
></bit-avatar>
</button>
<ng-template #defaultButton>

View File

@@ -137,3 +137,8 @@ body.body-full {
margin-bottom: 0;
}
}
/** Temporary fix for avatar, will not be required once we migrate to tailwind preflight **/
bit-avatar svg {
display: block;
}

View File

@@ -453,7 +453,7 @@ export class Utils {
};
}
static isNullOrWhitespace(str: string): boolean {
static isNullOrWhitespace(str: string | null | undefined): boolean {
return str == null || typeof str !== "string" || str.trim() === "";
}

View File

@@ -1,6 +1,5 @@
import { NgClass } from "@angular/common";
import { Component, OnChanges, input } from "@angular/core";
import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser";
import { Component, computed, input } from "@angular/core";
import { Utils } from "@bitwarden/common/platform/misc/utils";
@@ -22,12 +21,34 @@ const SizeClasses: Record<SizeTypes, string[]> = {
*/
@Component({
selector: "bit-avatar",
template: `@if (src) {
<img [src]="src" title="{{ title() || text() }}" [ngClass]="classList" />
}`,
template: `
<span [title]="title() || text()">
<svg
xmlns="http://www.w3.org/2000/svg"
pointer-events="none"
[style.backgroundColor]="backgroundColor()"
[ngClass]="classList()"
attr.viewBox="0 0 {{ svgSize }} {{ svgSize }}"
>
<text
text-anchor="middle"
y="50%"
x="50%"
dy="0.35em"
pointer-events="auto"
[attr.fill]="textColor()"
[style.fontWeight]="svgFontWeight"
[style.fontSize.px]="svgFontSize"
font-family='Roboto,"Helvetica Neue",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"'
>
{{ displayChars() }}
</text>
</svg>
</span>
`,
imports: [NgClass],
})
export class AvatarComponent implements OnChanges {
export class AvatarComponent {
readonly border = input(false);
readonly color = input<string>();
readonly id = input<string>();
@@ -35,36 +56,40 @@ export class AvatarComponent implements OnChanges {
readonly title = input<string>();
readonly size = input<SizeTypes>("default");
private svgCharCount = 2;
private svgFontSize = 20;
private svgFontWeight = 300;
private svgSize = 48;
src?: SafeResourceUrl;
protected readonly svgCharCount = 2;
protected readonly svgFontSize = 20;
protected readonly svgFontWeight = 300;
protected readonly svgSize = 48;
constructor(public sanitizer: DomSanitizer) {}
ngOnChanges() {
this.generate();
}
get classList() {
return ["tw-rounded-full", "tw-inline"]
protected readonly classList = computed(() => {
return ["tw-rounded-full"]
.concat(SizeClasses[this.size()] ?? [])
.concat(this.border() ? ["tw-border", "tw-border-solid", "tw-border-secondary-600"] : []);
}
});
private generate() {
const color = this.color();
const text = this.text();
protected readonly backgroundColor = computed(() => {
const id = this.id();
if (!text && !color && !id) {
throw new Error("Must supply `text`, `color`, or `id` input.");
const upperCaseText = this.text()?.toUpperCase() ?? "";
if (!Utils.isNullOrWhitespace(this.color())) {
return this.color()!;
}
let chars: string | null = null;
const upperCaseText = text?.toUpperCase() ?? "";
chars = this.getFirstLetters(upperCaseText, this.svgCharCount);
if (!Utils.isNullOrWhitespace(id)) {
return Utils.stringToColor(id!.toString());
}
return Utils.stringToColor(upperCaseText);
});
protected readonly textColor = computed(() => {
return Utils.pickTextColorBasedOnBgColor(this.backgroundColor(), 135, true);
});
protected readonly displayChars = computed(() => {
const upperCaseText = this.text()?.toUpperCase() ?? "";
let chars = this.getFirstLetters(upperCaseText, this.svgCharCount);
if (chars == null) {
chars = this.unicodeSafeSubstring(upperCaseText, this.svgCharCount);
}
@@ -75,30 +100,10 @@ export class AvatarComponent implements OnChanges {
chars = emojiMatch[0];
}
let svg: HTMLElement;
let hexColor = color ?? "";
if (!Utils.isNullOrWhitespace(hexColor)) {
svg = this.createSvgElement(this.svgSize, hexColor);
} else if (!Utils.isNullOrWhitespace(id ?? "")) {
hexColor = Utils.stringToColor(id!.toString());
svg = this.createSvgElement(this.svgSize, hexColor);
} else {
hexColor = Utils.stringToColor(upperCaseText);
svg = this.createSvgElement(this.svgSize, hexColor);
}
return chars;
});
const charObj = this.createTextElement(chars, hexColor);
svg.appendChild(charObj);
const html = window.document.createElement("div").appendChild(svg).outerHTML;
const svgHtml = window.btoa(unescape(encodeURIComponent(html)));
// This is safe because the only user provided value, chars is set using `textContent`
this.src = this.sanitizer.bypassSecurityTrustResourceUrl(
"data:image/svg+xml;base64," + svgHtml,
);
}
private getFirstLetters(data: string, count: number): string | null {
private getFirstLetters(data: string, count: number): string | undefined {
const parts = data.split(" ");
if (parts.length > 1) {
let text = "";
@@ -107,39 +112,7 @@ export class AvatarComponent implements OnChanges {
}
return text;
}
return null;
}
private createSvgElement(size: number, color: string): HTMLElement {
const svgTag = window.document.createElement("svg");
svgTag.setAttribute("xmlns", "http://www.w3.org/2000/svg");
svgTag.setAttribute("pointer-events", "none");
svgTag.setAttribute("width", size.toString());
svgTag.setAttribute("height", size.toString());
svgTag.style.backgroundColor = color;
svgTag.style.width = size + "px";
svgTag.style.height = size + "px";
return svgTag;
}
private createTextElement(character: string, color: string): HTMLElement {
const textTag = window.document.createElement("text");
textTag.setAttribute("text-anchor", "middle");
textTag.setAttribute("y", "50%");
textTag.setAttribute("x", "50%");
textTag.setAttribute("dy", "0.35em");
textTag.setAttribute("pointer-events", "auto");
textTag.setAttribute("fill", Utils.pickTextColorBasedOnBgColor(color, 135, true));
textTag.setAttribute(
"font-family",
'Roboto,"Helvetica Neue",Helvetica,Arial,' +
'sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"',
);
// Warning do not use innerHTML here, characters are user provided
textTag.textContent = character;
textTag.style.fontWeight = this.svgFontWeight.toString();
textTag.style.fontSize = this.svgFontSize + "px";
return textTag;
return undefined;
}
private unicodeSafeSubstring(str: string, count: number) {