Skip to content

Commit ee24cc9

Browse files
authored
fix(web-components): remove empty text nodes from avatar (#33963)
1 parent 01e5afd commit ee24cc9

File tree

6 files changed

+105
-5
lines changed

6 files changed

+105
-5
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "fix: remove empty text nodes from avatar",
4+
"packageName": "@fluentui/web-components",
5+
"email": "863023+radium-v@users.noreply.github.com",
6+
"dependentChangeType": "patch"
7+
}

packages/web-components/docs/web-components.api.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,10 +465,18 @@ export class BaseAnchor extends FASTElement {
465465
// @public
466466
export class BaseAvatar extends FASTElement {
467467
constructor();
468+
// (undocumented)
469+
connectedCallback(): void;
470+
// @internal
471+
defaultSlot: HTMLSlotElement;
472+
// (undocumented)
473+
disconnectedCallback(): void;
468474
// @internal
469475
elementInternals: ElementInternals;
470476
initials?: string | undefined;
471477
name?: string | undefined;
478+
// @internal
479+
slotchangeHandler(): void;
472480
}
473481

474482
// @public

packages/web-components/src/avatar/avatar.base.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
1-
import { attr, FASTElement } from '@microsoft/fast-element';
1+
import { attr, FASTElement, Updates } from '@microsoft/fast-element';
22

33
/**
44
* The base class used for constructing a fluent-avatar custom element
55
* @public
66
*/
77
export class BaseAvatar extends FASTElement {
8+
/**
9+
* Signal to remove event listeners when the component is disconnected.
10+
*
11+
* @internal
12+
*/
13+
private abortSignal?: AbortController;
14+
15+
/**
16+
* Reference to the default slot element.
17+
* @internal
18+
*/
19+
public defaultSlot!: HTMLSlotElement;
20+
821
/**
922
* The internal {@link https://developer.mozilla.org/docs/Web/API/ElementInternals | `ElementInternals`} instance for the component.
1023
*
@@ -32,9 +45,54 @@ export class BaseAvatar extends FASTElement {
3245
@attr
3346
public initials?: string | undefined;
3447

48+
connectedCallback(): void {
49+
super.connectedCallback();
50+
this.slotchangeHandler();
51+
}
52+
3553
constructor() {
3654
super();
3755

3856
this.elementInternals.role = 'img';
3957
}
58+
59+
disconnectedCallback(): void {
60+
this.abortSignal?.abort();
61+
this.abortSignal = undefined;
62+
63+
super.disconnectedCallback();
64+
}
65+
66+
/**
67+
* Removes any empty text nodes from the default slot when the slotted content changes.
68+
*
69+
* @param e - The event object
70+
* @internal
71+
*/
72+
public slotchangeHandler(): void {
73+
this.normalize();
74+
75+
const elements = this.defaultSlot.assignedElements();
76+
77+
if (!elements.length && !this.innerText.trim()) {
78+
const nodes = this.defaultSlot.assignedNodes() as Element[];
79+
80+
nodes
81+
.filter(node => node.nodeType === Node.TEXT_NODE)
82+
.forEach(node => {
83+
this.removeChild(node);
84+
});
85+
}
86+
87+
Updates.enqueue(() => {
88+
if (!this.abortSignal || this.abortSignal.signal.aborted) {
89+
this.abortSignal = new AbortController();
90+
}
91+
92+
this.defaultSlot.addEventListener('slotchange', () => this.slotchangeHandler(), {
93+
once: true,
94+
signal: this.abortSignal.signal,
95+
});
96+
});
97+
}
4098
}

packages/web-components/src/avatar/avatar.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,27 @@ test.describe('Avatar', () => {
2828
await expect(element).toHaveJSProperty('elementInternals.role', 'img');
2929
});
3030

31+
test('should render an icon when no name or initials are provided', async ({ fastPage }) => {
32+
const { element } = fastPage;
33+
const icon = element.locator('svg');
34+
35+
await fastPage.setTemplate({ innerHTML: `\n\n\n` });
36+
37+
await expect(icon).toBeVisible();
38+
39+
await test.step('should NOT render the icon when empty elements are present in the default slot', async () => {
40+
await fastPage.setTemplate({ innerHTML: `<div></div><span></span>\n\n` });
41+
42+
await expect(icon).toBeHidden();
43+
});
44+
45+
await test.step('should retain comment nodes in the default slot when no name or initials are provided', async () => {
46+
await fastPage.setTemplate({ innerHTML: `\n<!--hello-->\n<!--world-->\n` });
47+
48+
await expect(icon).toBeVisible();
49+
});
50+
});
51+
3152
test('When no name value is set, should render with custom initials based on the provided initials value', async ({
3253
fastPage,
3354
}) => {
@@ -36,6 +57,12 @@ test.describe('Avatar', () => {
3657
await fastPage.setTemplate({ attributes: { initials: 'JD' } });
3758

3859
await expect(element).toContainText('JD');
60+
61+
await test.step('should NOT render the default icon', async () => {
62+
const icon = element.locator('svg');
63+
64+
await expect(icon).toBeHidden();
65+
});
3966
});
4067

4168
test('When name value is set, should generate initials based on the provided name value', async ({ fastPage }) => {

packages/web-components/src/avatar/avatar.template.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type ElementViewTemplate, html } from '@microsoft/fast-element';
1+
import { type ElementViewTemplate, html, ref } from '@microsoft/fast-element';
22
import type { Avatar } from './avatar.js';
33

44
const defaultIconTemplate = html`<svg
@@ -21,7 +21,7 @@ const defaultIconTemplate = html`<svg
2121
*/
2222
export function avatarTemplate<T extends Avatar>(): ElementViewTemplate<T> {
2323
return html<T>`
24-
<slot>${x => (x.name || x.initials ? x.generateInitials() : defaultIconTemplate)}</slot>
24+
<slot ${ref('defaultSlot')}>${x => (x.name || x.initials ? x.generateInitials() : defaultIconTemplate)}</slot>
2525
<slot name="badge"></slot>
2626
`;
2727
}

packages/web-components/src/avatar/avatar.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,8 @@ export class Avatar extends BaseAvatar {
125125
const size = this.size ?? 32;
126126

127127
return (
128-
this.initials ??
129-
getInitials(this.name, window.getComputedStyle(this as unknown as HTMLElement).direction === 'rtl', {
128+
this.initials ||
129+
getInitials(this.name, window.getComputedStyle(this).direction === 'rtl', {
130130
firstInitialOnly: size <= 16,
131131
})
132132
);

0 commit comments

Comments
 (0)