Skip to content

Commit 498db46

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 ae41a0a commit 498db46

File tree

3 files changed

+62
-12
lines changed

3 files changed

+62
-12
lines changed

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

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,66 @@
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;
23-
});
22+
}
2423

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

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

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

39+
it('should hide the message container', () => {
40+
createFixture();
41+
ariaDescriber.describe(component.element1, 'My Message');
42+
expect(getMessagesContainer().classList).toContain('cdk-visually-hidden');
43+
});
44+
3845
it('should not register empty strings', () => {
46+
createFixture();
3947
ariaDescriber.describe(component.element1, '');
4048
expect(getMessageElements()).toBe(null);
4149
});
4250

4351
it('should not register non-string values', () => {
52+
createFixture();
4453
expect(() => ariaDescriber.describe(component.element1, null!)).not.toThrow();
4554
expect(getMessageElements()).toBe(null);
4655
});
4756

4857
it('should not throw when trying to remove non-string value', () => {
58+
createFixture();
4959
expect(() => ariaDescriber.removeDescription(component.element1, null!)).not.toThrow();
5060
});
5161

5262
it('should de-dupe a message registered multiple times', () => {
63+
createFixture();
5364
ariaDescriber.describe(component.element1, 'My Message');
5465
ariaDescriber.describe(component.element2, 'My Message');
5566
ariaDescriber.describe(component.element3, 'My Message');
@@ -60,6 +71,7 @@ describe('AriaDescriber', () => {
6071
});
6172

6273
it('should be able to register multiple messages', () => {
74+
createFixture();
6375
ariaDescriber.describe(component.element1, 'First Message');
6476
ariaDescriber.describe(component.element2, 'Second Message');
6577
expectMessages(['First Message', 'Second Message']);
@@ -68,6 +80,7 @@ describe('AriaDescriber', () => {
6880
});
6981

7082
it('should be able to unregister messages', () => {
83+
createFixture();
7184
ariaDescriber.describe(component.element1, 'My Message');
7285
expectMessages(['My Message']);
7386

@@ -87,6 +100,7 @@ describe('AriaDescriber', () => {
87100
});
88101

89102
it('should be able to unregister messages while having others registered', () => {
103+
createFixture();
90104
ariaDescriber.describe(component.element1, 'Persistent Message');
91105
ariaDescriber.describe(component.element2, 'My Message');
92106
expectMessages(['Persistent Message', 'My Message']);
@@ -105,24 +119,28 @@ describe('AriaDescriber', () => {
105119
});
106120

107121
it('should be able to append to an existing list of aria describedby', () => {
122+
createFixture();
108123
ariaDescriber.describe(component.element4, 'My Message');
109124
expectMessages(['My Message']);
110125
expectMessage(component.element4, 'My Message');
111126
});
112127

113128
it('should be able to handle multiple regisitrations of the same message to an element', () => {
129+
createFixture();
114130
ariaDescriber.describe(component.element1, 'My Message');
115131
ariaDescriber.describe(component.element1, 'My Message');
116132
expectMessages(['My Message']);
117133
expectMessage(component.element1, 'My Message');
118134
});
119135

120136
it('should not throw when attempting to describe a non-element node', () => {
137+
createFixture();
121138
const node: any = document.createComment('Not an element node');
122139
expect(() => ariaDescriber.describe(node, 'This looks like an element')).not.toThrow();
123140
});
124141

125142
it('should clear any pre-existing containers', () => {
143+
createFixture();
126144
const extraContainer = document.createElement('div');
127145
extraContainer.id = MESSAGES_CONTAINER_ID;
128146
document.body.appendChild(extraContainer);
@@ -139,10 +157,28 @@ describe('AriaDescriber', () => {
139157
ariaDescriber.describe(component.element1, 'Hi');
140158
expectMessages(['Hi']);
141159
});
160+
161+
it('should set `aria-hidden` on the container by default', () => {
162+
createFixture([{provide: Platform, useValue: {BLINK: true}}]);
163+
ariaDescriber.describe(component.element1, 'My Message');
164+
expect(getMessagesContainer().getAttribute('aria-hidden')).toBe('true');
165+
});
166+
167+
it('should disable `aria-hidden` on the container in IE', () => {
168+
createFixture([{provide: Platform, useValue: {TRIDENT: true}}]);
169+
ariaDescriber.describe(component.element1, 'My Message');
170+
expect(getMessagesContainer().getAttribute('aria-hidden')).toBe('false');
171+
});
172+
173+
it('should disable `aria-hidden` on the container in Edge', () => {
174+
createFixture([{provide: Platform, useValue: {EDGE: true}}]);
175+
ariaDescriber.describe(component.element1, 'My Message');
176+
expect(getMessagesContainer().getAttribute('aria-hidden')).toBe('false');
177+
});
142178
});
143179

144180
function getMessagesContainer() {
145-
return document.querySelector(`#${MESSAGES_CONTAINER_ID}`);
181+
return document.querySelector(`#${MESSAGES_CONTAINER_ID}`)!;
146182
}
147183

148184
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

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

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

@@ -146,6 +152,8 @@ export class AriaDescriber implements OnDestroy {
146152
/** Creates the global container for all aria-describedby messages. */
147153
private _createMessagesContainer() {
148154
if (!messagesContainer) {
155+
// @breaking-change 8.0.0 `_platform` null check can be removed once the parameter is required
156+
const canBeAriaHidden = !this._platform || (!this._platform.EDGE && !this._platform.TRIDENT);
149157
const preExistingContainer = this._document.getElementById(MESSAGES_CONTAINER_ID);
150158

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

159167
messagesContainer = this._document.createElement('div');
160168
messagesContainer.id = MESSAGES_CONTAINER_ID;
161-
messagesContainer.setAttribute('aria-hidden', 'true');
162-
messagesContainer.style.display = 'none';
169+
messagesContainer.classList.add('cdk-visually-hidden');
170+
171+
// IE and Edge won't read out the messages if they're in an `aria-hidden` container.
172+
// We only disable `aria-hidden` for these platforms, because it comes with the
173+
// disadvantage that people might hit the messages when they've navigated past
174+
// the end of the document using the arrow keys.
175+
messagesContainer.setAttribute('aria-hidden', canBeAriaHidden + '');
163176
this._document.body.appendChild(messagesContainer);
164177
}
165178
}

tools/public_api_guard/cdk/a11y.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ 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,
19+
_platform?: Platform | undefined);
1920
describe(hostElement: Element, message: string): void;
2021
ngOnDestroy(): void;
2122
removeDescription(hostElement: Element, message: string): void;

0 commit comments

Comments
 (0)