Skip to content

Commit

Permalink
feat(): add aria live announcer
Browse files Browse the repository at this point in the history
Fixes #106
  • Loading branch information
devversion committed Mar 30, 2016
1 parent 0983de3 commit a2d1ccd
Show file tree
Hide file tree
Showing 10 changed files with 237 additions and 1 deletion.
37 changes: 37 additions & 0 deletions src/components/live-announcer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# MdAriaLive
`MdAriaLive` is a component, which announces messages to several screenreaders.

## `<md-aria-live>`
### Methods

| Name | Description |
| --- | --- |
| `announce(message: string)` | This announces a text message to the screenreader |

### Examples
A basic local variable can be assigned to the `md-aria-live` component.
```html
<md-aria-live #live>
<button (click)="live.announce('Hey Google')">Announce Text</button>
</md-toolbar>
```

The component is also useable through the Dependency Injection.

```ts
export class ChildComponent {

constructor(private live: MdAriaLive) { }

announceText() {
this.live.announce("Hey Google");
}

}
```

### Supported Screenreaders
- JAWS (Windows)
- NVDA (Windows)
- VoiceOver (OSX and iOS)
- TalkBack (Android)
2 changes: 2 additions & 0 deletions src/components/live-announcer/live-announcer.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<ng-content></ng-content>
<div class="md-live-announcer" aria-live="polite" aria-atomic="true"></div>
9 changes: 9 additions & 0 deletions src/components/live-announcer/live-announcer.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@import 'mixins';

:host {

.md-live-announcer {
@include md-visually-hidden();
}

}
119 changes: 119 additions & 0 deletions src/components/live-announcer/live-announcer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import {
inject,
TestComponentBuilder,
ComponentFixture,
fakeAsync,
flushMicrotasks,
tick
} from 'angular2/testing';
import {
it,
describe,
expect,
beforeEach,
} from '../../core/facade/testing';
import {Component} from 'angular2/core';
import {By} from 'angular2/platform/browser';
import {MdLiveAnnouncer} from './live-announcer';

export function main() {
describe('MdLiveAnnouncer', () => {
let builder: TestComponentBuilder;

beforeEach(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
builder = tcb;
}));

it('should correctly update the announce text', fakeAsyncTest(() => {
let appFixture: ComponentFixture = null;

builder.createAsync(TestApp).then(fixture => {
appFixture = fixture;
});

flushMicrotasks();

let announcerElement = appFixture.debugElement
.query(By.css('.md-live-announcer')).nativeElement;
let buttonElement = appFixture.debugElement
.query(By.css('button')).nativeElement;

appFixture.detectChanges();

buttonElement.click();

// This flushes our 100ms timeout for the screenreaders.
tick(100);

expect(announcerElement.textContent).toBe('Test');
}));

it('should correctly update the politeness attribute', fakeAsyncTest(() => {
let appFixture: ComponentFixture = null;

builder.createAsync(TestApp).then(fixture => {
appFixture = fixture;
});

flushMicrotasks();

let live: MdLiveAnnouncer = appFixture.debugElement
.query(By.css('md-live-announcer')).componentInstance;
let announcerElement = appFixture.debugElement
.query(By.css('.md-live-announcer')).nativeElement;

appFixture.detectChanges();

live.announce('Hey Google', 'assertive');

// This flushes our 100ms timeout for the screenreaders.
tick(100);

expect(announcerElement.textContent).toBe('Hey Google');
expect(announcerElement.getAttribute('aria-live')).toBe('assertive');
}));

it('should apply the aria-live value polite by default', fakeAsyncTest(() => {
let appFixture: ComponentFixture = null;

builder.createAsync(TestApp).then(fixture => {
appFixture = fixture;
});

flushMicrotasks();

let live: MdLiveAnnouncer = appFixture.debugElement
.query(By.css('md-live-announcer')).componentInstance;
let announcerElement = appFixture.debugElement
.query(By.css('.md-live-announcer')).nativeElement;

appFixture.detectChanges();

live.announce('Hey Google');

// This flushes our 100ms timeout for the screenreaders.
tick(100);

expect(announcerElement.textContent).toBe('Hey Google');
expect(announcerElement.getAttribute('aria-live')).toBe('polite');
}));

});
}

function fakeAsyncTest(fn: () => void) {
return inject([], fakeAsync(fn));
}

@Component({
selector: 'test-app',
template: `
<md-live-announcer #announcer>
<button (click)="announcer.announce('Test')">Announce</button>
</md-live-announcer>
`,
directives: [MdLiveAnnouncer],
})
class TestApp {
}

36 changes: 36 additions & 0 deletions src/components/live-announcer/live-announcer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
Component,
ElementRef,
AfterContentInit
} from 'angular2/core';

import {DOM} from '../../core/platform/dom/dom_adapter';

@Component({
selector: 'md-live-announcer',
templateUrl: './components/live-announcer/live-announcer.html',
styleUrls: ['./components/live-announcer/live-announcer.css'],
})
export class MdLiveAnnouncer implements AfterContentInit {

private announcerEl: HTMLElement;

constructor(private elementRef: ElementRef) { }

ngAfterContentInit() {
this.announcerEl = DOM.querySelector(this.elementRef.nativeElement, '.md-live-announcer');
}

announce(message: string, politeness: string = 'polite'): void {
this.announcerEl.textContent = '';

this.announcerEl.setAttribute('aria-live', politeness);

// This 100ms timeout is necessary for some browser + screen-reader combinations:
// - Both JAWS and NVDA over IE11 will not announce anything without a non-zero timeout.
// - With Chrome and IE11 with NVDA or JAWS, a repeated (identical) message won't be read a
// second time without clearing and then using a non-zero delay.
// (using JAWS 17 at time of this writing).
setTimeout(() => this.announcerEl.textContent = message, 100);
}
}
16 changes: 16 additions & 0 deletions src/core/style/_mixins.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,19 @@
// Use a transform to create a new stacking context.
transform: translate3D(0, 0, 0);
}

/**
* This mixin hides an element visually.
* That means it's still accessible for screen-readers but not visible in view.
*/
@mixin md-visually-hidden {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
text-transform: none;
width: 1px;
}
1 change: 1 addition & 0 deletions src/demo-app/demo-app.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ <h1>Angular Material2 Demos</h1>
<li><a [routerLink]="['ToolbarDemo']">Toolbar demo</a></li>
<li><a [routerLink]="['RadioDemo']">Radio demo</a></li>
<li><a [routerLink]="['ListDemo']">List demo</a></li>
<li><a [routerLink]="['LiveAnnouncerDemo']">Live Announcer demo</a></li>
</ul>
<button md-raised-button (click)="root.dir = (root.dir == 'rtl' ? 'ltr' : 'rtl')">
{{root.dir.toUpperCase()}}
Expand Down
4 changes: 3 additions & 1 deletion src/demo-app/demo-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {PortalDemo} from './portal/portal-demo';
import {ToolbarDemo} from './toolbar/toolbar-demo';
import {OverlayDemo} from './overlay/overlay-demo';
import {ListDemo} from './list/list-demo';
import {LiveAnnouncerDemo} from './live-announcer/live-announcer-demo';

@Component({
selector: 'home',
Expand All @@ -38,6 +39,7 @@ export class Home {}
new Route({path: '/overlay', name: 'OverlayDemo', component: OverlayDemo}),
new Route({path: '/checkbox', name: 'CheckboxDemo', component: CheckboxDemo}),
new Route({path: '/toolbar', name: 'ToolbarDemo', component: ToolbarDemo}),
new Route({path: '/list', name: 'ListDemo', component: ListDemo})
new Route({path: '/list', name: 'ListDemo', component: ListDemo}),
new Route({path: '/live-announcer', name: 'LiveAnnouncerDemo', component: LiveAnnouncerDemo})
])
export class DemoApp { }
5 changes: 5 additions & 0 deletions src/demo-app/live-announcer/live-announcer-demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<md-live-announcer #live>

<button md-button (click)="live.announce('Hey Google')">Announce Text</button>

</md-live-announcer>
9 changes: 9 additions & 0 deletions src/demo-app/live-announcer/live-announcer-demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {Component} from 'angular2/core';
import {MdLiveAnnouncer} from '../../components/live-announcer/live-announcer';

@Component({
selector: 'toolbar-demo',
templateUrl: 'demo-app/live-announcer/live-announcer-demo.html',
directives: [MdLiveAnnouncer]
})
export class LiveAnnouncerDemo {}

0 comments on commit a2d1ccd

Please sign in to comment.