Skip to content

Commit 7ace81c

Browse files
committed
fix(aria-describer): messages not being read out in IE and Edge
Fixes the messages from the `AriaDescriber` not being read out in Edge or IE, because the message container is `aria-hidden` and has `display: none`. Fixes #12298.
1 parent 8a71288 commit 7ace81c

File tree

3 files changed

+70
-12
lines changed

3 files changed

+70
-12
lines changed

src/cdk/a11y/aria-describer/aria-describer.spec.ts

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,74 @@
11
import {A11yModule, CDK_DESCRIBEDBY_HOST_ATTRIBUTE} from '../index';
22
import {AriaDescriber, MESSAGES_CONTAINER_ID} from './aria-describer';
3-
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
4-
import {Component, ElementRef, ViewChild} from '@angular/core';
3+
import {ComponentFixture, TestBed} from '@angular/core/testing';
4+
import {Component, ElementRef, ViewChild, Provider} from '@angular/core';
5+
import {Platform} from '@angular/cdk/platform';
56

67
describe('AriaDescriber', () => {
78
let ariaDescriber: AriaDescriber;
89
let component: TestApp;
910
let fixture: ComponentFixture<TestApp>;
1011

11-
beforeEach(async(() => {
12+
function createFixture(providers: Provider[] = []) {
1213
TestBed.configureTestingModule({
1314
imports: [A11yModule],
1415
declarations: [TestApp],
15-
providers: [AriaDescriber],
16+
providers: [AriaDescriber, ...providers],
1617
}).compileComponents();
17-
}));
1818

19-
beforeEach(() => {
2019
fixture = TestBed.createComponent(TestApp);
2120
component = fixture.componentInstance;
2221
ariaDescriber = component.ariaDescriber;
2322
fixture.detectChanges();
24-
});
23+
}
2524

2625
afterEach(() => {
2726
ariaDescriber.ngOnDestroy();
2827
});
2928

3029
it('should initialize without the message container', () => {
30+
createFixture();
3131
expect(getMessagesContainer()).toBeNull();
3232
});
3333

3434
it('should be able to create a message element', () => {
35+
createFixture();
3536
ariaDescriber.describe(component.element1, 'My Message');
3637
expectMessages(['My Message']);
3738
});
3839

3940
it('should be able to describe using an element', () => {
41+
createFixture();
4042
const descriptionNode = fixture.nativeElement.querySelector('#description-with-existing-id');
4143
ariaDescriber.describe(component.element1, descriptionNode);
4244
expectMessage(component.element1, 'Hello');
4345
});
4446

47+
it('should hide the message container', () => {
48+
createFixture();
49+
ariaDescriber.describe(component.element1, 'My Message');
50+
expect(getMessagesContainer().classList).toContain('cdk-visually-hidden');
51+
});
52+
4553
it('should not register empty strings', () => {
54+
createFixture();
4655
ariaDescriber.describe(component.element1, '');
4756
expect(getMessageElements()).toBe(null);
4857
});
4958

5059
it('should not register non-string values', () => {
60+
createFixture();
5161
expect(() => ariaDescriber.describe(component.element1, null!)).not.toThrow();
5262
expect(getMessageElements()).toBe(null);
5363
});
5464

5565
it('should not throw when trying to remove non-string value', () => {
66+
createFixture();
5667
expect(() => ariaDescriber.removeDescription(component.element1, null!)).not.toThrow();
5768
});
5869

5970
it('should de-dupe a message registered multiple times', () => {
71+
createFixture();
6072
ariaDescriber.describe(component.element1, 'My Message');
6173
ariaDescriber.describe(component.element2, 'My Message');
6274
ariaDescriber.describe(component.element3, 'My Message');
@@ -67,6 +79,7 @@ describe('AriaDescriber', () => {
6779
});
6880

6981
it('should de-dupe a message registered multiple via an element node', () => {
82+
createFixture();
7083
const descriptionNode = fixture.nativeElement.querySelector('#description-with-existing-id');
7184
ariaDescriber.describe(component.element1, descriptionNode);
7285
ariaDescriber.describe(component.element2, descriptionNode);
@@ -77,6 +90,7 @@ describe('AriaDescriber', () => {
7790
});
7891

7992
it('should be able to register multiple messages', () => {
93+
createFixture();
8094
ariaDescriber.describe(component.element1, 'First Message');
8195
ariaDescriber.describe(component.element2, 'Second Message');
8296
expectMessages(['First Message', 'Second Message']);
@@ -85,6 +99,7 @@ describe('AriaDescriber', () => {
8599
});
86100

87101
it('should be able to unregister messages', () => {
102+
createFixture();
88103
ariaDescriber.describe(component.element1, 'My Message');
89104
expectMessages(['My Message']);
90105

@@ -104,6 +119,7 @@ describe('AriaDescriber', () => {
104119
});
105120

106121
it('should not remove nodes that were set as messages when unregistering', () => {
122+
createFixture();
107123
const descriptionNode = fixture.nativeElement.querySelector('#description-with-existing-id');
108124

109125
expect(document.body.contains(descriptionNode))
@@ -123,6 +139,7 @@ describe('AriaDescriber', () => {
123139
});
124140

125141
it('should keep nodes set as descriptions inside their original position in the DOM', () => {
142+
createFixture();
126143
const descriptionNode = fixture.nativeElement.querySelector('#description-with-existing-id');
127144
const initialParent = descriptionNode.parentNode;
128145

@@ -141,6 +158,7 @@ describe('AriaDescriber', () => {
141158
});
142159

143160
it('should be able to unregister messages while having others registered', () => {
161+
createFixture();
144162
ariaDescriber.describe(component.element1, 'Persistent Message');
145163
ariaDescriber.describe(component.element2, 'My Message');
146164
expectMessages(['Persistent Message', 'My Message']);
@@ -159,24 +177,28 @@ describe('AriaDescriber', () => {
159177
});
160178

161179
it('should be able to append to an existing list of aria describedby', () => {
180+
createFixture();
162181
ariaDescriber.describe(component.element4, 'My Message');
163182
expectMessages(['My Message']);
164183
expectMessage(component.element4, 'My Message');
165184
});
166185

167186
it('should be able to handle multiple regisitrations of the same message to an element', () => {
187+
createFixture();
168188
ariaDescriber.describe(component.element1, 'My Message');
169189
ariaDescriber.describe(component.element1, 'My Message');
170190
expectMessages(['My Message']);
171191
expectMessage(component.element1, 'My Message');
172192
});
173193

174194
it('should not throw when attempting to describe a non-element node', () => {
195+
createFixture();
175196
const node: any = document.createComment('Not an element node');
176197
expect(() => ariaDescriber.describe(node, 'This looks like an element')).not.toThrow();
177198
});
178199

179200
it('should clear any pre-existing containers', () => {
201+
createFixture();
180202
const extraContainer = document.createElement('div');
181203
extraContainer.id = MESSAGES_CONTAINER_ID;
182204
document.body.appendChild(extraContainer);
@@ -192,27 +214,31 @@ describe('AriaDescriber', () => {
192214
});
193215

194216
it('should not describe messages that match up with the aria-label of the element', () => {
217+
createFixture();
195218
component.element1.setAttribute('aria-label', 'Hello');
196219
ariaDescriber.describe(component.element1, 'Hello');
197220
ariaDescriber.describe(component.element1, 'Hi');
198221
expectMessages(['Hi']);
199222
});
200223

201224
it('should assign an id to the description element, if it does not have one', () => {
225+
createFixture();
202226
const descriptionNode = fixture.nativeElement.querySelector('[description-without-id]');
203227
expect(descriptionNode.getAttribute('id')).toBeFalsy();
204228
ariaDescriber.describe(component.element1, descriptionNode);
205229
expect(descriptionNode.getAttribute('id')).toBeTruthy();
206230
});
207231

208232
it('should not overwrite the existing id of the description element', () => {
233+
createFixture();
209234
const descriptionNode = fixture.nativeElement.querySelector('#description-with-existing-id');
210235
expect(descriptionNode.id).toBe('description-with-existing-id');
211236
ariaDescriber.describe(component.element1, descriptionNode);
212237
expect(descriptionNode.id).toBe('description-with-existing-id');
213238
});
214239

215240
it('should not remove pre-existing description nodes on destroy', () => {
241+
createFixture();
216242
const descriptionNode = fixture.nativeElement.querySelector('#description-with-existing-id');
217243

218244
expect(document.body.contains(descriptionNode))
@@ -231,6 +257,7 @@ describe('AriaDescriber', () => {
231257
});
232258

233259
it('should remove the aria-describedby attribute if there are no more messages', () => {
260+
createFixture();
234261
const element = component.element1;
235262

236263
expect(element.hasAttribute('aria-describedby')).toBe(false);
@@ -242,10 +269,27 @@ describe('AriaDescriber', () => {
242269
expect(element.hasAttribute('aria-describedby')).toBe(false);
243270
});
244271

272+
it('should set `aria-hidden` on the container by default', () => {
273+
createFixture([{provide: Platform, useValue: {BLINK: true}}]);
274+
ariaDescriber.describe(component.element1, 'My Message');
275+
expect(getMessagesContainer().getAttribute('aria-hidden')).toBe('true');
276+
});
277+
278+
it('should disable `aria-hidden` on the container in IE', () => {
279+
createFixture([{provide: Platform, useValue: {TRIDENT: true}}]);
280+
ariaDescriber.describe(component.element1, 'My Message');
281+
expect(getMessagesContainer().getAttribute('aria-hidden')).toBe('false');
282+
});
283+
284+
it('should disable `aria-hidden` on the container in Edge', () => {
285+
createFixture([{provide: Platform, useValue: {EDGE: true}}]);
286+
ariaDescriber.describe(component.element1, 'My Message');
287+
expect(getMessagesContainer().getAttribute('aria-hidden')).toBe('false');
288+
});
245289
});
246290

247291
function getMessagesContainer() {
248-
return document.querySelector(`#${MESSAGES_CONTAINER_ID}`);
292+
return document.querySelector(`#${MESSAGES_CONTAINER_ID}`)!;
249293
}
250294

251295
function getMessageElements(): Node[] | null {

src/cdk/a11y/aria-describer/aria-describer.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {DOCUMENT} from '@angular/common';
1010
import {Inject, Injectable, OnDestroy} from '@angular/core';
1111
import {addAriaReferencedId, getAriaReferenceIds, removeAriaReferencedId} from './aria-reference';
12+
import {Platform} from '@angular/cdk/platform';
1213

1314

1415
/**
@@ -50,7 +51,12 @@ let messagesContainer: HTMLElement | null = null;
5051
export class AriaDescriber implements OnDestroy {
5152
private _document: Document;
5253

53-
constructor(@Inject(DOCUMENT) _document: any) {
54+
constructor(
55+
@Inject(DOCUMENT) _document: any,
56+
/**
57+
* @breaking-change 8.0.0 `_platform` parameter to be made required.
58+
*/
59+
private _platform?: Platform) {
5460
this._document = _document;
5561
}
5662

@@ -153,6 +159,8 @@ export class AriaDescriber implements OnDestroy {
153159
/** Creates the global container for all aria-describedby messages. */
154160
private _createMessagesContainer() {
155161
if (!messagesContainer) {
162+
// @breaking-change 8.0.0 `_platform` null check can be removed once the parameter is required
163+
const canBeAriaHidden = !this._platform || (!this._platform.EDGE && !this._platform.TRIDENT);
156164
const preExistingContainer = this._document.getElementById(MESSAGES_CONTAINER_ID);
157165

158166
// When going from the server to the client, we may end up in a situation where there's
@@ -165,8 +173,13 @@ export class AriaDescriber implements OnDestroy {
165173

166174
messagesContainer = this._document.createElement('div');
167175
messagesContainer.id = MESSAGES_CONTAINER_ID;
168-
messagesContainer.setAttribute('aria-hidden', 'true');
169-
messagesContainer.style.display = 'none';
176+
messagesContainer.classList.add('cdk-visually-hidden');
177+
178+
// IE and Edge won't read out the messages if they're in an `aria-hidden` container.
179+
// We only disable `aria-hidden` for these platforms, because it comes with the
180+
// disadvantage that people might hit the messages when they've navigated past
181+
// the end of the document using the arrow keys.
182+
messagesContainer.setAttribute('aria-hidden', canBeAriaHidden + '');
170183
this._document.body.appendChild(messagesContainer);
171184
}
172185
}

tools/public_api_guard/cdk/a11y.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ export declare class ActiveDescendantKeyManager<T> extends ListKeyManager<Highli
1010
}
1111

1212
export declare class AriaDescriber implements OnDestroy {
13-
constructor(_document: any);
13+
constructor(_document: any,
14+
_platform?: Platform | undefined);
1415
describe(hostElement: Element, message: string | HTMLElement): void;
1516
ngOnDestroy(): void;
1617
removeDescription(hostElement: Element, message: string | HTMLElement): void;

0 commit comments

Comments
 (0)