Skip to content

Commit 56be961

Browse files
committed
feat(): add aria live announcer
1 parent 540099e commit 56be961

File tree

11 files changed

+200
-1
lines changed

11 files changed

+200
-1
lines changed
-24.4 KB
Loading
-24.4 KB
Loading

src/components/aria-live/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# MdAriaLive
2+
`MdAriaLive` is component, which announces messages to several screenreaders.
3+
4+
## `<md-aria-live>`
5+
### Methods
6+
7+
| Name | Description |
8+
| --- | --- |
9+
| `announce(message: string)` | This announces a text message to the screenreader |
10+
11+
### Examples
12+
A basic local variable can be assigned to the `md-aria-live` component.
13+
```html
14+
<md-aria-live #live>
15+
<button (click)="live.announce('Hey Google')">Announce Text</button>
16+
</md-toolbar>
17+
```
18+
19+
The component is also useable through the Dependency Injection.
20+
21+
```ts
22+
export class ChildComponent {
23+
24+
constructor(private live: MdAriaLive) { }
25+
26+
announceText() {
27+
this.live.announce("Hey Google");
28+
}
29+
30+
}
31+
```
32+
33+
### Supported Screenreaders
34+
- JAWS (Windows)
35+
- NVDA (Windows)
36+
- VoiceOver (OSX and iOS)
37+
- TalkBack (Android)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<ng-content></ng-content>
2+
<div class="_md-aria-announcer" aria-live="polite" aria-atomic="true"></div>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
:host {
2+
3+
// This hides the announcer element visually.
4+
// That means it's still accessible for screen-readers but not visible.
5+
._md-aria-announcer {
6+
border: 0;
7+
clip: rect(0 0 0 0);
8+
height: 1px;
9+
margin: -1px;
10+
overflow: hidden;
11+
padding: 0;
12+
position: absolute;
13+
text-transform: none;
14+
width: 1px;
15+
}
16+
17+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {
2+
inject,
3+
TestComponentBuilder, ComponentFixture
4+
} from 'angular2/testing';
5+
import {
6+
it,
7+
describe,
8+
expect,
9+
beforeEach,
10+
} from '../../core/facade/testing';
11+
import {Component} from 'angular2/core';
12+
import {By} from 'angular2/platform/browser';
13+
import {MdAriaLive} from './aria-live';
14+
15+
export function main() {
16+
describe('MdAriaLive', () => {
17+
let builder: TestComponentBuilder;
18+
let timeoutMock: any;
19+
20+
beforeEach(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
21+
builder = tcb;
22+
}));
23+
24+
beforeAll(() => {
25+
timeoutMock = mockTimeout();
26+
});
27+
28+
afterAll(() => {
29+
timeoutMock.restoreTimeout();
30+
});
31+
32+
it('should correctly update the announce text', (done: () => void) => {
33+
return builder
34+
.createAsync(TestApp)
35+
.then((view: ComponentFixture) => {
36+
let announcerElement = view.debugElement.query(By.css('._md-aria-announcer'));
37+
let buttonElement = view.debugElement.query(By.css('button'));
38+
39+
view.detectChanges();
40+
41+
buttonElement.nativeElement.click();
42+
43+
timeoutMock.flushTimeout();
44+
45+
expect(announcerElement.nativeElement.textContent).toBe('Test');
46+
47+
done();
48+
});
49+
});
50+
51+
function mockTimeout() {
52+
let browserTimeout = window.setTimeout;
53+
let stack: any = {};
54+
55+
window.setTimeout = (fn: Function, timeout: number) => {
56+
var id = browserTimeout(() => {
57+
stack[id] = null;
58+
fn();
59+
}, timeout);
60+
61+
stack[id] = fn;
62+
return id;
63+
};
64+
65+
return {
66+
flushTimeout: () => {
67+
for (let id in stack) {
68+
if (stack.hasOwnProperty(id)) {
69+
stack[id]();
70+
stack[id] = null;
71+
}
72+
}
73+
},
74+
restoreTimeout: () => {
75+
window.setTimeout = browserTimeout;
76+
}
77+
};
78+
}
79+
80+
});
81+
}
82+
83+
84+
@Component({
85+
selector: 'test-app',
86+
template: `
87+
<md-aria-live #announcer>
88+
<button (click)="announcer.announce('Test')">Announce</button>
89+
</md-aria-live>
90+
`,
91+
directives: [MdAriaLive],
92+
})
93+
class TestApp {
94+
}
95+
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {
2+
Component,
3+
ElementRef,
4+
AfterContentInit
5+
} from 'angular2/core';
6+
7+
import {DOM} from '../../core/platform/dom/dom_adapter';
8+
9+
@Component({
10+
selector: 'md-aria-live',
11+
templateUrl: './components/aria-live/aria-live.html',
12+
styleUrls: ['./components/aria-live/aria-live.css'],
13+
})
14+
export class MdAriaLive implements AfterContentInit {
15+
16+
private announcerEl: HTMLElement;
17+
18+
constructor(private elementRef: ElementRef) { }
19+
20+
ngAfterContentInit() {
21+
this.announcerEl = DOM.querySelector(this.elementRef.nativeElement, '._md-aria-announcer');
22+
}
23+
24+
announce(message: string): void {
25+
this.announcerEl.textContent = '';
26+
27+
// The most screen-readers need a delay after the clear action, to probably recognise the
28+
// announcement. 100ms is a average delay, which works for the most supported readers.
29+
setTimeout(() => this.announcerEl.textContent = message, 100);
30+
}
31+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<md-aria-live #ariaLive>
2+
3+
<button md-button (click)="ariaLive.announce('Hey Google')">Announce Text</button>
4+
5+
</md-aria-live>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import {Component} from 'angular2/core';
2+
import {MdAriaLive} from '../../components/aria-live/aria-live';
3+
4+
@Component({
5+
selector: 'toolbar-demo',
6+
templateUrl: 'demo-app/aria-live/aria-live-demo.html',
7+
directives: [MdAriaLive]
8+
})
9+
export class AriaLiveDemo {}

src/demo-app/demo-app.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ <h1>Angular Material2 Demos</h1>
1111
<li><a [routerLink]="['ToolbarDemo']">Toolbar demo</a></li>
1212
<li><a [routerLink]="['RadioDemo']">Radio demo</a></li>
1313
<li><a [routerLink]="['ListDemo']">List demo</a></li>
14+
<li><a [routerLink]="['AriaLiveDemo']">Aria Live demo</a></li>
1415
</ul>
1516
<button md-raised-button (click)="root.dir = (root.dir == 'rtl' ? 'ltr' : 'rtl')">
1617
{{root.dir.toUpperCase()}}

0 commit comments

Comments
 (0)