-
Notifications
You must be signed in to change notification settings - Fork 6.8k
Commit
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
# MdLiveAnnouncer | ||
`MdLiveAnnouncer` is a service, which announces messages to several screenreaders. | ||
|
||
### Methods | ||
|
||
| Name | Description | | ||
| --- | --- | | ||
| `announce(message, politeness)` | This announces a text message to the supported screenreaders. <br><br>The politeness parameter sets the `aria-live` attribute on the announcer element | | ||
|
||
### Examples | ||
The service can be injected in a component. | ||
```ts | ||
@Component({ | ||
selector: 'my-component' | ||
providers: [MdLiveAnnouncer] | ||
}) | ||
export class MyComponent { | ||
|
||
constructor(live: MdLiveAnnouncer) { | ||
live.announce("Hey Google"); | ||
} | ||
|
||
} | ||
``` | ||
|
||
### Supported Screenreaders | ||
- JAWS (Windows) | ||
- NVDA (Windows) | ||
- VoiceOver (OSX and iOS) | ||
- TalkBack (Android) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
@import 'mixins'; | ||
|
||
.md-live-announcer { | ||
@include md-visually-hidden(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import { | ||
inject, | ||
TestComponentBuilder, | ||
ComponentFixture, | ||
fakeAsync, | ||
flushMicrotasks, | ||
tick, | ||
beforeEachProviders | ||
} 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 live: MdLiveAnnouncer; | ||
let builder: TestComponentBuilder; | ||
let liveEl: Element; | ||
|
||
beforeEachProviders(() => [MdLiveAnnouncer]); | ||
|
||
beforeEach(inject([TestComponentBuilder, MdLiveAnnouncer], | ||
(tcb: TestComponentBuilder, _live: MdLiveAnnouncer) => { | ||
builder = tcb; | ||
live = _live; | ||
liveEl = getLiveElement(); | ||
})); | ||
|
||
afterEach(() => { | ||
// In our tests we always remove the current live element, because otherwise we would have | ||
// multiple live elements due multiple service instantiations. | ||
liveEl.parentNode.removeChild(liveEl); | ||
}); | ||
|
||
it('should correctly update the announce text', fakeAsyncTest(() => { | ||
let appFixture: ComponentFixture = null; | ||
|
||
builder.createAsync(TestApp).then(fixture => { | ||
appFixture = fixture; | ||
}); | ||
|
||
flushMicrotasks(); | ||
|
||
let buttonElement = appFixture.debugElement | ||
.query(By.css('button')).nativeElement; | ||
|
||
buttonElement.click(); | ||
|
||
// This flushes our 100ms timeout for the screenreaders. | ||
tick(100); | ||
|
||
expect(liveEl.textContent).toBe('Test'); | ||
})); | ||
|
||
it('should correctly update the politeness attribute', fakeAsyncTest(() => { | ||
let appFixture: ComponentFixture = null; | ||
|
||
builder.createAsync(TestApp).then(fixture => { | ||
appFixture = fixture; | ||
}); | ||
|
||
flushMicrotasks(); | ||
|
||
live.announce('Hey Google', 'assertive'); | ||
|
||
// This flushes our 100ms timeout for the screenreaders. | ||
tick(100); | ||
|
||
expect(liveEl.textContent).toBe('Hey Google'); | ||
expect(liveEl.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(); | ||
|
||
live.announce('Hey Google'); | ||
|
||
// This flushes our 100ms timeout for the screenreaders. | ||
tick(100); | ||
|
||
expect(liveEl.textContent).toBe('Hey Google'); | ||
expect(liveEl.getAttribute('aria-live')).toBe('polite'); | ||
})); | ||
|
||
}); | ||
} | ||
|
||
function fakeAsyncTest(fn: () => void) { | ||
return inject([], fakeAsync(fn)); | ||
} | ||
|
||
function getLiveElement(): Element { | ||
return document.body.querySelector('.md-live-announcer'); | ||
} | ||
|
||
@Component({ | ||
selector: 'test-app', | ||
template: `<button (click)="announceText('Test')">Announce</button>`, | ||
}) | ||
class TestApp { | ||
|
||
constructor(private live: MdLiveAnnouncer) {}; | ||
|
||
announceText(message: string) { | ||
this.live.announce(message); | ||
} | ||
|
||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import {Injectable} from 'angular2/core'; | ||
|
||
export type AriaLivePoliteness = 'off' | 'polite' | 'assertive'; | ||
|
||
@Injectable() | ||
export class MdLiveAnnouncer { | ||
|
||
private _liveElement: Element; | ||
|
||
constructor() { | ||
this._liveElement = this._createLiveElement(); | ||
} | ||
|
||
/** | ||
* @param message Message to be announced to the screenreader | ||
* @param politeness The politeness of the announcer element. | ||
*/ | ||
announce(message: string, politeness: AriaLivePoliteness = 'polite'): void { | ||
this._liveElement.textContent = ''; | ||
|
||
// TODO: ensure changing the politeness works on all environments we support. | ||
this._liveElement.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._liveElement.textContent = message, 100); | ||
} | ||
|
||
private _createLiveElement(): Element { | ||
let liveEl = document.createElement('div'); | ||
|
||
liveEl.classList.add('md-live-announcer'); | ||
liveEl.setAttribute('aria-atomic', 'true'); | ||
liveEl.setAttribute('aria-live', 'polite'); | ||
|
||
document.body.appendChild(liveEl); | ||
|
||
return liveEl; | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
devversion
Author
Member
|
||
width: 1px; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
<div class="demo-live-announcer"> | ||
|
||
<button md-button (click)="announceText('Hey Google')">Announce Text</button> | ||
|
||
</div> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import {Component} from 'angular2/core'; | ||
import {MdLiveAnnouncer} from '../../core/live-announcer/live-announcer'; | ||
|
||
@Component({ | ||
selector: 'toolbar-demo', | ||
templateUrl: 'demo-app/live-announcer/live-announcer-demo.html', | ||
}) | ||
export class LiveAnnouncerDemo { | ||
|
||
constructor(private live: MdLiveAnnouncer) {} | ||
|
||
announceText(message: string) { | ||
this.live.announce(message); | ||
} | ||
|
||
} |
What is the
text-transform
attribute for? I double checked the.sr-only
class of Bootstrap v3. It is identical except for this attribute. cc @devversion