Skip to content

Commit f5883db

Browse files
authored
fix(material/badge): correctly apply badge description (#23562)
Previously, `MatBadge` was applying the provided description as an `aria-label` to the badge content element, which is a child element of the badge host. Then it was _also_ applying an `aria-describedby` to that same content element. The end result is that the badge was being treated mainly as text content for a control, which isn't really the best way to convey its complmentary nature. This behavior was causing problems in NVDA, where the badge content was overriding the host button's label. This change makes the badge content itself `aria-hidden` and applies the badge description to the badge's _host_ element via `aria-describedby` and completely removes any setting of `aria-label`. It also makes a handful of minor cleanup refactorings.
1 parent d13b8ea commit f5883db

File tree

4 files changed

+160
-212
lines changed

4 files changed

+160
-212
lines changed

src/dev-app/badge/badge-demo.html

Lines changed: 45 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,24 @@
11
<div>
22

3-
<div class="demo-badge">
4-
<h3>Text</h3>
5-
<span [matBadge]="badgeContent" matBadgeOverlap="false" *ngIf="visible">
6-
Hello
7-
</span>
8-
9-
<span matBadge="11111" matBadgeOverlap="false">
10-
Hello
11-
</span>
12-
13-
<span matBadge="22" matBadgeOverlap="false" matBadgePosition="below after" matBadgeColor="accent">
14-
Hello
15-
</span>
16-
17-
<span matBadge="22" matBadgeOverlap="false" matBadgePosition="above before" matBadgeColor="warn">
18-
Hello
19-
</span>
20-
21-
<span matBadge="⚡️" matBadgeOverlap="false" matBadgePosition="below before">
22-
Hello
23-
</span>
24-
25-
<span [matBadge]="badgeContent" matBadgeDescription="I've got {{badgeContent}} problems">
26-
Aria
27-
</span>
28-
29-
<span [matBadge]="badgeContent" matBadgeHidden="true">
30-
Hidden
31-
</span>
32-
33-
<input type="text" [(ngModel)]="badgeContent" />
34-
<button (click)="visible = !visible">Toggle</button>
35-
</div>
36-
373
<div class="demo-badge">
384
<h3>Buttons</h3>
39-
<button mat-raised-button [matBadge]="badgeContent">
40-
<mat-icon color="primary">home</mat-icon>
41-
</button>
42-
43-
<button mat-raised-button matBadge="22" matBadgePosition="below after" color="primary" matBadgeColor="accent">
44-
<mat-icon color="accent">home</mat-icon>
45-
</button>
46-
47-
<button mat-raised-button matBadge="22" matBadgePosition="above before">
48-
<mat-icon color="primary">home</mat-icon>
5+
<button mat-stroked-button matBadge="7" matBadgeDescription="7 unread messages">
6+
Inbox
497
</button>
508

51-
<button mat-raised-button [matBadge]="badgeContent" matBadgeDisabled>
52-
<mat-icon color="primary">home</mat-icon>
9+
<button mat-stroked-button matBadge="7" matBadgePosition="below after"
10+
matBadgeDescription="7 unread messages">
11+
Inbox
5312
</button>
5413

55-
<button mat-stroked-button [matBadge]="badgeContent">
56-
<mat-icon color="primary">home</mat-icon>
14+
<button mat-stroked-button matBadge="7" disabled matBadgeDisabled
15+
matBadgeDescription="7 unread messages">
16+
Inbox
5717
</button>
5818

59-
<button disabled mat-raised-button [matBadge]="badgeContent" matBadgeDisabled>
60-
<mat-icon color="primary">home</mat-icon>
61-
</button>
62-
63-
<button mat-raised-button matBadge="22" matBadgePosition="below before">
64-
<mat-icon color="primary">home</mat-icon>
65-
</button>
66-
67-
<button mat-raised-button>
68-
<mat-icon matBadge="22" color="accent">home</mat-icon>
19+
<button mat-stroked-button matBadge="7" matBadgeColor="accent"
20+
matBadgeDescription="7 unread messages">
21+
Inbox
6922
</button>
7023
</div>
7124

@@ -99,4 +52,38 @@ <h3>Size</h3>
9952

10053
</div>
10154

55+
<div class="demo-badge">
56+
<h3>Text</h3>
57+
<span [matBadge]="badgeContent" matBadgeOverlap="false" *ngIf="visible">
58+
Hello
59+
</span>
60+
61+
<span matBadge="11111" matBadgeOverlap="false">
62+
Hello
63+
</span>
64+
65+
<span matBadge="22" matBadgeOverlap="false" matBadgePosition="below after" matBadgeColor="accent">
66+
Hello
67+
</span>
68+
69+
<span matBadge="22" matBadgeOverlap="false" matBadgePosition="above before" matBadgeColor="warn">
70+
Hello
71+
</span>
72+
73+
<span matBadge="⚡️" matBadgeOverlap="false" matBadgePosition="below before">
74+
Hello
75+
</span>
76+
77+
<span [matBadge]="badgeContent" matBadgeDescription="I've got {{badgeContent}} problems">
78+
Aria
79+
</span>
80+
81+
<span [matBadge]="badgeContent" matBadgeHidden="true">
82+
Hidden
83+
</span>
84+
85+
<input type="text" [(ngModel)]="badgeContent" />
86+
<button (click)="visible = !visible">Toggle</button>
87+
</div>
88+
10289
</div>

src/material/badge/badge.spec.ts

Lines changed: 32 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import {ThemePalette} from '@angular/material/core';
77
describe('MatBadge', () => {
88
let fixture: ComponentFixture<any>;
99
let testComponent: BadgeTestApp;
10-
let badgeNativeElement: HTMLElement;
11-
let badgeDebugElement: DebugElement;
10+
let badgeHostNativeElement: HTMLElement;
11+
let badgeHostDebugElement: DebugElement;
1212

1313
beforeEach(fakeAsync(() => {
1414
TestBed
@@ -22,12 +22,12 @@ describe('MatBadge', () => {
2222
testComponent = fixture.debugElement.componentInstance;
2323
fixture.detectChanges();
2424

25-
badgeDebugElement = fixture.debugElement.query(By.directive(MatBadge))!;
26-
badgeNativeElement = badgeDebugElement.nativeElement;
25+
badgeHostDebugElement = fixture.debugElement.query(By.directive(MatBadge))!;
26+
badgeHostNativeElement = badgeHostDebugElement.nativeElement;
2727
}));
2828

2929
it('should update the badge based on attribute', () => {
30-
const badgeElement = badgeNativeElement.querySelector('.mat-badge-content')!;
30+
const badgeElement = badgeHostNativeElement.querySelector('.mat-badge-content')!;
3131
expect(badgeElement.textContent).toContain('1');
3232

3333
testComponent.badgeContent = '22';
@@ -36,7 +36,7 @@ describe('MatBadge', () => {
3636
});
3737

3838
it('should be able to pass in falsy values to the badge content', () => {
39-
const badgeElement = badgeNativeElement.querySelector('.mat-badge-content')!;
39+
const badgeElement = badgeHostNativeElement.querySelector('.mat-badge-content')!;
4040
expect(badgeElement.textContent).toContain('1');
4141

4242
testComponent.badgeContent = 0;
@@ -45,7 +45,7 @@ describe('MatBadge', () => {
4545
});
4646

4747
it('should treat null and undefined as empty strings in the badge content', () => {
48-
const badgeElement = badgeNativeElement.querySelector('.mat-badge-content')!;
48+
const badgeElement = badgeHostNativeElement.querySelector('.mat-badge-content')!;
4949
expect(badgeElement.textContent).toContain('1');
5050

5151
testComponent.badgeContent = null;
@@ -60,83 +60,83 @@ describe('MatBadge', () => {
6060
it('should apply class based on color attribute', () => {
6161
testComponent.badgeColor = 'primary';
6262
fixture.detectChanges();
63-
expect(badgeNativeElement.classList.contains('mat-badge-primary')).toBe(true);
63+
expect(badgeHostNativeElement.classList.contains('mat-badge-primary')).toBe(true);
6464

6565
testComponent.badgeColor = 'accent';
6666
fixture.detectChanges();
67-
expect(badgeNativeElement.classList.contains('mat-badge-accent')).toBe(true);
67+
expect(badgeHostNativeElement.classList.contains('mat-badge-accent')).toBe(true);
6868

6969
testComponent.badgeColor = 'warn';
7070
fixture.detectChanges();
71-
expect(badgeNativeElement.classList.contains('mat-badge-warn')).toBe(true);
71+
expect(badgeHostNativeElement.classList.contains('mat-badge-warn')).toBe(true);
7272

7373
testComponent.badgeColor = undefined;
7474
fixture.detectChanges();
7575

76-
expect(badgeNativeElement.classList).not.toContain('mat-badge-accent');
76+
expect(badgeHostNativeElement.classList).not.toContain('mat-badge-accent');
7777
});
7878

7979
it('should update the badge position on direction change', () => {
80-
expect(badgeNativeElement.classList.contains('mat-badge-above')).toBe(true);
81-
expect(badgeNativeElement.classList.contains('mat-badge-after')).toBe(true);
80+
expect(badgeHostNativeElement.classList.contains('mat-badge-above')).toBe(true);
81+
expect(badgeHostNativeElement.classList.contains('mat-badge-after')).toBe(true);
8282

8383
testComponent.badgeDirection = 'below before';
8484
fixture.detectChanges();
8585

86-
expect(badgeNativeElement.classList.contains('mat-badge-below')).toBe(true);
87-
expect(badgeNativeElement.classList.contains('mat-badge-before')).toBe(true);
86+
expect(badgeHostNativeElement.classList.contains('mat-badge-below')).toBe(true);
87+
expect(badgeHostNativeElement.classList.contains('mat-badge-before')).toBe(true);
8888
});
8989

9090
it('should change visibility to hidden', () => {
91-
expect(badgeNativeElement.classList.contains('mat-badge-hidden')).toBe(false);
91+
expect(badgeHostNativeElement.classList.contains('mat-badge-hidden')).toBe(false);
9292

9393
testComponent.badgeHidden = true;
9494
fixture.detectChanges();
9595

96-
expect(badgeNativeElement.classList.contains('mat-badge-hidden')).toBe(true);
96+
expect(badgeHostNativeElement.classList.contains('mat-badge-hidden')).toBe(true);
9797
});
9898

9999
it('should change badge sizes', () => {
100-
expect(badgeNativeElement.classList.contains('mat-badge-medium')).toBe(true);
100+
expect(badgeHostNativeElement.classList.contains('mat-badge-medium')).toBe(true);
101101

102102
testComponent.badgeSize = 'small';
103103
fixture.detectChanges();
104104

105-
expect(badgeNativeElement.classList.contains('mat-badge-small')).toBe(true);
105+
expect(badgeHostNativeElement.classList.contains('mat-badge-small')).toBe(true);
106106

107107
testComponent.badgeSize = 'large';
108108
fixture.detectChanges();
109109

110-
expect(badgeNativeElement.classList.contains('mat-badge-large')).toBe(true);
110+
expect(badgeHostNativeElement.classList.contains('mat-badge-large')).toBe(true);
111111
});
112112

113113
it('should change badge overlap', () => {
114-
expect(badgeNativeElement.classList.contains('mat-badge-overlap')).toBe(false);
114+
expect(badgeHostNativeElement.classList.contains('mat-badge-overlap')).toBe(false);
115115

116116
testComponent.badgeOverlap = true;
117117
fixture.detectChanges();
118118

119-
expect(badgeNativeElement.classList.contains('mat-badge-overlap')).toBe(true);
119+
expect(badgeHostNativeElement.classList.contains('mat-badge-overlap')).toBe(true);
120120
});
121121

122122
it('should toggle `aria-describedby` depending on whether the badge has a description', () => {
123-
const badgeContent = badgeNativeElement.querySelector('.mat-badge-content')!;
124-
125-
expect(badgeContent.getAttribute('aria-describedby')).toBeFalsy();
123+
expect(badgeHostNativeElement.hasAttribute('aria-describedby')).toBeFalse();
126124

127125
testComponent.badgeDescription = 'Describing a badge';
128126
fixture.detectChanges();
129127

130-
expect(badgeContent.getAttribute('aria-describedby')).toBeTruthy();
128+
const describedById = badgeHostNativeElement.getAttribute('aria-describedby') || '';
129+
const description = document.getElementById(describedById)?.textContent;
130+
expect(description).toBe('Describing a badge');
131131

132132
testComponent.badgeDescription = '';
133133
fixture.detectChanges();
134134

135-
expect(badgeContent.getAttribute('aria-describedby')).toBeFalsy();
135+
expect(badgeHostNativeElement.hasAttribute('aria-describedby')).toBeFalse();
136136
});
137137

138138
it('should toggle visibility based on whether the badge has content', () => {
139-
const classList = badgeNativeElement.classList;
139+
const classList = badgeHostNativeElement.classList;
140140

141141
expect(classList.contains('mat-badge-hidden')).toBe(false);
142142

@@ -162,7 +162,7 @@ describe('MatBadge', () => {
162162
});
163163

164164
it('should apply view encapsulation on create badge content', () => {
165-
const badge = badgeNativeElement.querySelector('.mat-badge-content')!;
165+
const badge = badgeHostNativeElement.querySelector('.mat-badge-content')!;
166166
let encapsulationAttr: Attr | undefined;
167167

168168
for (let i = 0; i < badge.attributes.length; i++) {
@@ -176,7 +176,7 @@ describe('MatBadge', () => {
176176
});
177177

178178
it('should toggle a class depending on the badge disabled state', () => {
179-
const element: HTMLElement = badgeDebugElement.nativeElement;
179+
const element: HTMLElement = badgeHostDebugElement.nativeElement;
180180

181181
expect(element.classList).not.toContain('mat-badge-disabled');
182182

@@ -186,25 +186,6 @@ describe('MatBadge', () => {
186186
expect(element.classList).toContain('mat-badge-disabled');
187187
});
188188

189-
it('should update the aria-label if the description changes', () => {
190-
const badgeContent = badgeNativeElement.querySelector('.mat-badge-content')!;
191-
192-
fixture.componentInstance.badgeDescription = 'initial content';
193-
fixture.detectChanges();
194-
195-
expect(badgeContent.getAttribute('aria-label')).toBe('initial content');
196-
197-
fixture.componentInstance.badgeDescription = 'changed content';
198-
fixture.detectChanges();
199-
200-
expect(badgeContent.getAttribute('aria-label')).toBe('changed content');
201-
202-
fixture.componentInstance.badgeDescription = '';
203-
fixture.detectChanges();
204-
205-
expect(badgeContent.hasAttribute('aria-label')).toBe(false);
206-
});
207-
208189
it('should clear any pre-existing badges', () => {
209190
const preExistingFixture = TestBed.createComponent(PreExistingBadge);
210191
preExistingFixture.detectChanges();
@@ -220,7 +201,7 @@ describe('MatBadge', () => {
220201
});
221202

222203
it('should expose the badge element', () => {
223-
const badgeElement = badgeNativeElement.querySelector('.mat-badge-content')!;
204+
const badgeElement = badgeHostNativeElement.querySelector('.mat-badge-content')!;
224205
expect(fixture.componentInstance.badgeInstance.getBadgeElement()).toBe(badgeElement);
225206
});
226207

@@ -288,9 +269,7 @@ class NestedBadge {
288269

289270

290271
@Component({
291-
template: `
292-
<ng-template matBadge="1">Notifications</ng-template>
293-
`
272+
template: `<ng-template matBadge="1">Notifications</ng-template>`,
294273
})
295274
class BadgeOnTemplate {
296275
}

0 commit comments

Comments
 (0)