diff --git a/apps/browser/src/auth/popup/account-switching/current-account.component.html b/apps/browser/src/auth/popup/account-switching/current-account.component.html index c16abdadf2..2e2440f625 100644 --- a/apps/browser/src/auth/popup/account-switching/current-account.component.html +++ b/apps/browser/src/auth/popup/account-switching/current-account.component.html @@ -12,7 +12,6 @@ [color]="currentAccount.avatarColor" size="small" aria-hidden="true" - class="[&>img]:tw-block" > diff --git a/apps/browser/src/popup/scss/pages.scss b/apps/browser/src/popup/scss/pages.scss index 4c2daab215..56c5f80c86 100644 --- a/apps/browser/src/popup/scss/pages.scss +++ b/apps/browser/src/popup/scss/pages.scss @@ -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; +} diff --git a/libs/common/src/platform/misc/utils.ts b/libs/common/src/platform/misc/utils.ts index 43a9e43d92..c771bee546 100644 --- a/libs/common/src/platform/misc/utils.ts +++ b/libs/common/src/platform/misc/utils.ts @@ -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() === ""; } diff --git a/libs/components/src/avatar/avatar.component.ts b/libs/components/src/avatar/avatar.component.ts index 6f83c9ca10..8ece033c73 100644 --- a/libs/components/src/avatar/avatar.component.ts +++ b/libs/components/src/avatar/avatar.component.ts @@ -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 = { */ @Component({ selector: "bit-avatar", - template: `@if (src) { - - }`, + template: ` + + + + {{ displayChars() }} + + + + `, imports: [NgClass], }) -export class AvatarComponent implements OnChanges { +export class AvatarComponent { readonly border = input(false); readonly color = input(); readonly id = input(); @@ -35,36 +56,40 @@ export class AvatarComponent implements OnChanges { readonly title = input(); readonly size = input("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) {