Skip to content

Commit fb83a63

Browse files
committed
Make icons aria-hidden by default
1 parent 48f9cb2 commit fb83a63

File tree

3 files changed

+53
-29
lines changed

3 files changed

+53
-29
lines changed

src/lib/icon/icon.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,9 @@ match the current theme's colors using the `color` attribute. This can be change
8282

8383
Similar to an `<img>` element, an icon alone does not convey any useful information for a
8484
screen-reader user. The user of `<md-icon>` must provide additional information pertaining to how
85-
the icon is used.
85+
the icon is used. Based on the use-cases described below, `md-icon` is marked as
86+
`aria-hidden="true"` by default, but this can be overriden by adding `aria-hidden="false"` to the
87+
element.
8688

8789
In thinking about accessibility, it is useful to place icon use into one of three categories:
8890
1. **Decorative**: the icon conveys no real semantic meaning and is purely cosmetic.

src/lib/icon/icon.spec.ts

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@ describe('MdIcon', () => {
4242
TestBed.configureTestingModule({
4343
imports: [HttpModule, MdIconModule],
4444
declarations: [
45-
MdIconColorTestApp,
46-
MdIconLigatureTestApp,
47-
MdIconCustomFontCssTestApp,
48-
MdIconFromSvgNameTestApp,
45+
IconWithColor,
46+
IconWithLigature,
47+
IconWithCustomFontCss,
48+
IconFromSvgName,
49+
IconWithAriaHiddenFalse,
4950
],
5051
providers: [
5152
MockBackend,
@@ -75,7 +76,7 @@ describe('MdIcon', () => {
7576
}));
7677

7778
it('should apply class based on color attribute', () => {
78-
let fixture = TestBed.createComponent(MdIconColorTestApp);
79+
let fixture = TestBed.createComponent(IconWithColor);
7980

8081
const testComponent = fixture.componentInstance;
8182
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
@@ -85,9 +86,23 @@ describe('MdIcon', () => {
8586
expect(sortedClassNames(mdIconElement)).toEqual(['mat-icon', 'mat-primary', 'material-icons']);
8687
});
8788

89+
it('should mark md-icon as aria-hidden by default', () => {
90+
const fixture = TestBed.createComponent(IconWithLigature);
91+
const iconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
92+
expect(iconElement.getAttribute('aria-hidden'))
93+
.toBe('true', 'Expected the md-icon element has aria-hidden="true" by default');
94+
});
95+
96+
it('should not override a user-provided aria-hidden attribute', () => {
97+
const fixture = TestBed.createComponent(IconWithAriaHiddenFalse);
98+
const iconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
99+
expect(iconElement.getAttribute('aria-hidden'))
100+
.toBe('false', 'Expected the md-icon element has the user-provided aria-hidden value');
101+
});
102+
88103
describe('Ligature icons', () => {
89104
it('should add material-icons class by default', () => {
90-
let fixture = TestBed.createComponent(MdIconLigatureTestApp);
105+
let fixture = TestBed.createComponent(IconWithLigature);
91106

92107
const testComponent = fixture.componentInstance;
93108
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
@@ -99,7 +114,7 @@ describe('MdIcon', () => {
99114
it('should use alternate icon font if set', () => {
100115
mdIconRegistry.setDefaultFontSetClass('myfont');
101116

102-
let fixture = TestBed.createComponent(MdIconLigatureTestApp);
117+
let fixture = TestBed.createComponent(IconWithLigature);
103118

104119
const testComponent = fixture.componentInstance;
105120
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
@@ -114,7 +129,7 @@ describe('MdIcon', () => {
114129
mdIconRegistry.addSvgIcon('fluffy', trust('cat.svg'));
115130
mdIconRegistry.addSvgIcon('fido', trust('dog.svg'));
116131

117-
let fixture = TestBed.createComponent(MdIconFromSvgNameTestApp);
132+
let fixture = TestBed.createComponent(IconFromSvgName);
118133
const testComponent = fixture.componentInstance;
119134
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
120135
let svgElement: SVGElement;
@@ -143,7 +158,7 @@ describe('MdIcon', () => {
143158
mdIconRegistry.addSvgIcon('fluffy', 'farm-set-1.svg');
144159

145160
expect(() => {
146-
let fixture = TestBed.createComponent(MdIconFromSvgNameTestApp);
161+
let fixture = TestBed.createComponent(IconFromSvgName);
147162
fixture.componentInstance.iconName = 'fluffy';
148163
fixture.detectChanges();
149164
}).toThrowError(/unsafe value used in a resource URL context/);
@@ -153,7 +168,7 @@ describe('MdIcon', () => {
153168
mdIconRegistry.addSvgIconSetInNamespace('farm', 'farm-set-1.svg');
154169

155170
expect(() => {
156-
let fixture = TestBed.createComponent(MdIconFromSvgNameTestApp);
171+
let fixture = TestBed.createComponent(IconFromSvgName);
157172
fixture.componentInstance.iconName = 'farm:pig';
158173
fixture.detectChanges();
159174
}).toThrowError(/unsafe value used in a resource URL context/);
@@ -162,7 +177,7 @@ describe('MdIcon', () => {
162177
it('should extract icon from SVG icon set', () => {
163178
mdIconRegistry.addSvgIconSetInNamespace('farm', trust('farm-set-1.svg'));
164179

165-
let fixture = TestBed.createComponent(MdIconFromSvgNameTestApp);
180+
let fixture = TestBed.createComponent(IconFromSvgName);
166181

167182
const testComponent = fixture.componentInstance;
168183
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
@@ -197,7 +212,7 @@ describe('MdIcon', () => {
197212
mdIconRegistry.addSvgIconSetInNamespace('farm', trust('farm-set-2.svg'));
198213
mdIconRegistry.addSvgIconSetInNamespace('arrows', trust('arrow-set.svg'));
199214

200-
let fixture = TestBed.createComponent(MdIconFromSvgNameTestApp);
215+
let fixture = TestBed.createComponent(IconFromSvgName);
201216

202217
const testComponent = fixture.componentInstance;
203218
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
@@ -236,7 +251,7 @@ describe('MdIcon', () => {
236251
it('should unwrap <symbol> nodes', () => {
237252
mdIconRegistry.addSvgIconSetInNamespace('farm', trust('farm-set-3.svg'));
238253

239-
const fixture = TestBed.createComponent(MdIconFromSvgNameTestApp);
254+
const fixture = TestBed.createComponent(IconFromSvgName);
240255
const testComponent = fixture.componentInstance;
241256
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
242257

@@ -255,7 +270,7 @@ describe('MdIcon', () => {
255270
it('should not wrap <svg> elements in icon sets in another svg tag', () => {
256271
mdIconRegistry.addSvgIconSet(trust('arrow-set.svg'));
257272

258-
let fixture = TestBed.createComponent(MdIconFromSvgNameTestApp);
273+
let fixture = TestBed.createComponent(IconFromSvgName);
259274

260275
const testComponent = fixture.componentInstance;
261276
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
@@ -272,7 +287,7 @@ describe('MdIcon', () => {
272287
it('should return unmodified copies of icons from icon sets', () => {
273288
mdIconRegistry.addSvgIconSet(trust('arrow-set.svg'));
274289

275-
let fixture = TestBed.createComponent(MdIconFromSvgNameTestApp);
290+
let fixture = TestBed.createComponent(IconFromSvgName);
276291

277292
const testComponent = fixture.componentInstance;
278293
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
@@ -305,7 +320,7 @@ describe('MdIcon', () => {
305320
mdIconRegistry.registerFontClassAlias('f1', 'font1');
306321
mdIconRegistry.registerFontClassAlias('f2');
307322

308-
let fixture = TestBed.createComponent(MdIconCustomFontCssTestApp);
323+
let fixture = TestBed.createComponent(IconWithCustomFontCss);
309324

310325
const testComponent = fixture.componentInstance;
311326
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
@@ -340,7 +355,7 @@ describe('MdIcon without HttpModule', () => {
340355
beforeEach(async(() => {
341356
TestBed.configureTestingModule({
342357
imports: [MdIconModule],
343-
declarations: [MdIconFromSvgNameTestApp],
358+
declarations: [IconFromSvgName],
344359
});
345360

346361
TestBed.compileComponents();
@@ -357,7 +372,7 @@ describe('MdIcon without HttpModule', () => {
357372
expect(() => {
358373
mdIconRegistry.addSvgIcon('fido', sanitizer.bypassSecurityTrustResourceUrl('dog.svg'));
359374

360-
let fixture = TestBed.createComponent(MdIconFromSvgNameTestApp);
375+
let fixture = TestBed.createComponent(IconFromSvgName);
361376

362377
fixture.componentInstance.iconName = 'fido';
363378
fixture.detectChanges();
@@ -366,27 +381,27 @@ describe('MdIcon without HttpModule', () => {
366381
});
367382

368383

369-
/** Test components that contain an MdIcon. */
370384
@Component({template: `<md-icon>{{iconName}}</md-icon>`})
371-
class MdIconLigatureTestApp {
385+
class IconWithLigature {
372386
iconName = '';
373387
}
374388

375389
@Component({template: `<md-icon [color]="iconColor">{{iconName}}</md-icon>`})
376-
class MdIconColorTestApp {
390+
class IconWithColor {
377391
iconName = '';
378392
iconColor = 'primary';
379393
}
380394

381-
@Component({
382-
template: `<md-icon [fontSet]="fontSet" [fontIcon]="fontIcon"></md-icon>`
383-
})
384-
class MdIconCustomFontCssTestApp {
395+
@Component({template: `<md-icon [fontSet]="fontSet" [fontIcon]="fontIcon"></md-icon>`})
396+
class IconWithCustomFontCss {
385397
fontSet = '';
386398
fontIcon = '';
387399
}
388400

389401
@Component({template: `<md-icon [svgIcon]="iconName"></md-icon>`})
390-
class MdIconFromSvgNameTestApp {
402+
class IconFromSvgName {
391403
iconName = '';
392404
}
405+
406+
@Component({template: '<md-icon aria-hidden="false">face</md-icon>'})
407+
class IconWithAriaHiddenFalse { }

src/lib/icon/icon.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
Renderer2,
99
SimpleChange,
1010
ViewEncapsulation,
11-
AfterViewChecked,
11+
Attribute,
1212
} from '@angular/core';
1313
import {MdIconRegistry} from './icon-registry';
1414

@@ -81,7 +81,14 @@ export class MdIcon implements OnChanges, OnInit {
8181
constructor(
8282
private _elementRef: ElementRef,
8383
private _renderer: Renderer2,
84-
private _mdIconRegistry: MdIconRegistry) { }
84+
private _mdIconRegistry: MdIconRegistry,
85+
@Attribute('aria-hidden') ariaHidden: string) {
86+
// If the user has not explicitly set aria-hidden, mark the icon as hidden, as this is
87+
// the right thing to do for the majority of icon use-cases.
88+
if (!ariaHidden) {
89+
_renderer.setAttribute(_elementRef.nativeElement, 'aria-hidden', 'true');
90+
}
91+
}
8592

8693
_updateColor(newColor: string) {
8794
this._setElementColor(this._color, false);

0 commit comments

Comments
 (0)