Skip to content

Commit 9f98064

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 aa22368 commit 9f98064

File tree

3 files changed

+60
-12
lines changed

3 files changed

+60
-12
lines changed

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

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,38 @@
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
});
@@ -42,21 +43,31 @@ describe('AriaDescriber', () => {
4243
expectMessage(component.element1, 'Hello');
4344
});
4445

46+
it('should hide the message container', () => {
47+
createFixture();
48+
ariaDescriber.describe(component.element1, 'My Message');
49+
expect(getMessagesContainer().classList).toContain('cdk-visually-hidden');
50+
});
51+
4552
it('should not register empty strings', () => {
53+
createFixture();
4654
ariaDescriber.describe(component.element1, '');
4755
expect(getMessageElements()).toBe(null);
4856
});
4957

5058
it('should not register non-string values', () => {
59+
createFixture();
5160
expect(() => ariaDescriber.describe(component.element1, null!)).not.toThrow();
5261
expect(getMessageElements()).toBe(null);
5362
});
5463

5564
it('should not throw when trying to remove non-string value', () => {
65+
createFixture();
5666
expect(() => ariaDescriber.removeDescription(component.element1, null!)).not.toThrow();
5767
});
5868

5969
it('should de-dupe a message registered multiple times', () => {
70+
createFixture();
6071
ariaDescriber.describe(component.element1, 'My Message');
6172
ariaDescriber.describe(component.element2, 'My Message');
6273
ariaDescriber.describe(component.element3, 'My Message');
@@ -77,6 +88,7 @@ describe('AriaDescriber', () => {
7788
});
7889

7990
it('should be able to register multiple messages', () => {
91+
createFixture();
8092
ariaDescriber.describe(component.element1, 'First Message');
8193
ariaDescriber.describe(component.element2, 'Second Message');
8294
expectMessages(['First Message', 'Second Message']);
@@ -85,6 +97,7 @@ describe('AriaDescriber', () => {
8597
});
8698

8799
it('should be able to unregister messages', () => {
100+
createFixture();
88101
ariaDescriber.describe(component.element1, 'My Message');
89102
expectMessages(['My Message']);
90103

@@ -141,6 +154,7 @@ describe('AriaDescriber', () => {
141154
});
142155

143156
it('should be able to unregister messages while having others registered', () => {
157+
createFixture();
144158
ariaDescriber.describe(component.element1, 'Persistent Message');
145159
ariaDescriber.describe(component.element2, 'My Message');
146160
expectMessages(['Persistent Message', 'My Message']);
@@ -159,24 +173,28 @@ describe('AriaDescriber', () => {
159173
});
160174

161175
it('should be able to append to an existing list of aria describedby', () => {
176+
createFixture();
162177
ariaDescriber.describe(component.element4, 'My Message');
163178
expectMessages(['My Message']);
164179
expectMessage(component.element4, 'My Message');
165180
});
166181

167182
it('should be able to handle multiple regisitrations of the same message to an element', () => {
183+
createFixture();
168184
ariaDescriber.describe(component.element1, 'My Message');
169185
ariaDescriber.describe(component.element1, 'My Message');
170186
expectMessages(['My Message']);
171187
expectMessage(component.element1, 'My Message');
172188
});
173189

174190
it('should not throw when attempting to describe a non-element node', () => {
191+
createFixture();
175192
const node: any = document.createComment('Not an element node');
176193
expect(() => ariaDescriber.describe(node, 'This looks like an element')).not.toThrow();
177194
});
178195

179196
it('should clear any pre-existing containers', () => {
197+
createFixture();
180198
const extraContainer = document.createElement('div');
181199
extraContainer.id = MESSAGES_CONTAINER_ID;
182200
document.body.appendChild(extraContainer);
@@ -226,10 +244,27 @@ describe('AriaDescriber', () => {
226244
'Expected description node to still be in the DOM after it is no longer being used.');
227245
});
228246

247+
it('should set `aria-hidden` on the container by default', () => {
248+
createFixture([{provide: Platform, useValue: {BLINK: true}}]);
249+
ariaDescriber.describe(component.element1, 'My Message');
250+
expect(getMessagesContainer().getAttribute('aria-hidden')).toBe('true');
251+
});
252+
253+
it('should disable `aria-hidden` on the container in IE', () => {
254+
createFixture([{provide: Platform, useValue: {TRIDENT: true}}]);
255+
ariaDescriber.describe(component.element1, 'My Message');
256+
expect(getMessagesContainer().getAttribute('aria-hidden')).toBe('false');
257+
});
258+
259+
it('should disable `aria-hidden` on the container in Edge', () => {
260+
createFixture([{provide: Platform, useValue: {EDGE: true}}]);
261+
ariaDescriber.describe(component.element1, 'My Message');
262+
expect(getMessagesContainer().getAttribute('aria-hidden')).toBe('false');
263+
});
229264
});
230265

231266
function getMessagesContainer() {
232-
return document.querySelector(`#${MESSAGES_CONTAINER_ID}`);
267+
return document.querySelector(`#${MESSAGES_CONTAINER_ID}`)!;
233268
}
234269

235270
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
@@ -15,6 +15,7 @@ import {
1515
Optional,
1616
SkipSelf,
1717
} from '@angular/core';
18+
import {Platform} from '@angular/cdk/platform';
1819
import {addAriaReferencedId, getAriaReferenceIds, removeAriaReferencedId} from './aria-reference';
1920

2021

@@ -57,7 +58,12 @@ let messagesContainer: HTMLElement | null = null;
5758
export class AriaDescriber implements OnDestroy {
5859
private _document: Document;
5960

60-
constructor(@Inject(DOCUMENT) _document: any) {
61+
constructor(
62+
@Inject(DOCUMENT) _document: any,
63+
/**
64+
* @breaking-change 8.0.0 `_platform` parameter to be made required.
65+
*/
66+
private _platform?: Platform) {
6167
this._document = _document;
6268
}
6369

@@ -160,6 +166,8 @@ export class AriaDescriber implements OnDestroy {
160166
/** Creates the global container for all aria-describedby messages. */
161167
private _createMessagesContainer() {
162168
if (!messagesContainer) {
169+
// @breaking-change 8.0.0 `_platform` null check can be removed once the parameter is required
170+
const canBeAriaHidden = !this._platform || (!this._platform.EDGE && !this._platform.TRIDENT);
163171
const preExistingContainer = this._document.getElementById(MESSAGES_CONTAINER_ID);
164172

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

173181
messagesContainer = this._document.createElement('div');
174182
messagesContainer.id = MESSAGES_CONTAINER_ID;
175-
messagesContainer.setAttribute('aria-hidden', 'true');
176-
messagesContainer.style.display = 'none';
183+
messagesContainer.classList.add('cdk-visually-hidden');
184+
185+
// IE and Edge won't read out the messages if they're in an `aria-hidden` container.
186+
// We only disable `aria-hidden` for these platforms, because it comes with the
187+
// disadvantage that people might hit the messages when they've navigated past
188+
// the end of the document using the arrow keys.
189+
messagesContainer.setAttribute('aria-hidden', canBeAriaHidden + '');
177190
this._document.body.appendChild(messagesContainer);
178191
}
179192
}

tools/public_api_guard/cdk/a11y.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export declare const ARIA_DESCRIBER_PROVIDER: {
1515
export declare function ARIA_DESCRIBER_PROVIDER_FACTORY(parentDispatcher: AriaDescriber, _document: any): AriaDescriber;
1616

1717
export declare class AriaDescriber implements OnDestroy {
18-
constructor(_document: any);
18+
constructor(_document: any, _platform?: Platform | undefined);
1919
describe(hostElement: Element, message: string | HTMLElement): void;
2020
ngOnDestroy(): void;
2121
removeDescription(hostElement: Element, message: string | HTMLElement): void;

0 commit comments

Comments
 (0)