Skip to content

Commit c967286

Browse files
committed
feat(): add interaction service for a11y
Closes #289.
1 parent d4a3cde commit c967286

File tree

8 files changed

+188
-1
lines changed

8 files changed

+188
-1
lines changed
+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import {
2+
it,
3+
describe,
4+
expect,
5+
beforeEach,
6+
inject,
7+
beforeEachProviders,
8+
} from 'angular2/testing';
9+
import {MdInteraction, MdInteractionType} from './interaction';
10+
11+
export function main() {
12+
describe('MdInteraction', () => {
13+
let interaction: MdInteraction;
14+
15+
beforeEachProviders(() => [MdInteraction]);
16+
17+
beforeEach(inject([MdInteraction], (_interaction: MdInteraction) => {
18+
interaction = _interaction;
19+
}));
20+
21+
it('should correctly detect the keyboard interaction', () => {
22+
23+
let event = <KeyboardEvent> document.createEvent('Event');
24+
25+
event.keyCode = 37;
26+
event.initEvent('keydown', false, true);
27+
28+
document.body.dispatchEvent(event);
29+
30+
expect(interaction.getLastInteractionType()).toBe(MdInteractionType.KEYBOARD);
31+
});
32+
33+
it('should correctly detect the mouse interaction', () => {
34+
35+
let event = document.createEvent('MouseEvent');
36+
37+
event.initMouseEvent('mousedown', true, true, window, null, 0, 0, 0, 0,
38+
false, false, false, false, 0, null);
39+
document.body.dispatchEvent(event);
40+
41+
expect(interaction.getLastInteractionType()).toBe(MdInteractionType.MOUSE);
42+
});
43+
44+
});
45+
}

src/core/interaction/interaction.ts

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import {Injectable} from 'angular2/core';
2+
3+
export enum MdInteractionType {
4+
KEYBOARD,
5+
MOUSE,
6+
TOUCH,
7+
}
8+
9+
// Interface for the interaction mappings, which holds an index definition
10+
// to avoid implicitly warnings.
11+
interface MdInteractionMap {
12+
[index: string]: any;
13+
}
14+
15+
@Injectable()
16+
export class MdInteraction {
17+
18+
private _isBuffering: boolean = false;
19+
private _bufferTimeout: number;
20+
private _lastInteractionType: MdInteractionType;
21+
22+
private _inputTypeMap: MdInteractionMap = {
23+
'keydown': MdInteractionType.KEYBOARD,
24+
'mousedown': MdInteractionType.MOUSE,
25+
'mouseenter': MdInteractionType.MOUSE,
26+
'touchstart': MdInteractionType.TOUCH,
27+
'pointerdown': 'pointer',
28+
'MSPointerDown': 'pointer',
29+
};
30+
31+
// IE dispatches `pointerdown` events, which need to be validated separately.
32+
// Index numbers referenced here: https://msdn.microsoft.com/library/windows/apps/hh466130.aspx
33+
private _iePointerMap: MdInteractionMap = {
34+
2: MdInteractionType.TOUCH,
35+
3: MdInteractionType.TOUCH,
36+
4: MdInteractionType.MOUSE
37+
};
38+
39+
constructor() {
40+
let mouseEvent = 'PointerEvent' in window ? 'pointerdown' : 'mousedown';
41+
42+
document.body.addEventListener('keydown', (e: Event) => this._onInputEvent(e));
43+
document.body.addEventListener(mouseEvent, (e: Event) => this._onInputEvent(e));
44+
document.body.addEventListener('mouseenter', (e: Event) => this._onInputEvent(e));
45+
46+
if ('ontouchstart' in document.documentElement) {
47+
document.body.addEventListener('touchstart', (e: TouchEvent) => this._onTouchInputEvent(e));
48+
}
49+
}
50+
51+
private _onInputEvent(event: Event) {
52+
if (this._isBuffering) {
53+
return;
54+
}
55+
56+
let type = this._inputTypeMap[event.type];
57+
58+
if (type === 'pointer') {
59+
let pointerType = (<any> event)['pointerType'];
60+
type = (typeof pointerType === 'number') ? this._iePointerMap[pointerType]
61+
: this._parsePointerType(pointerType);
62+
}
63+
64+
if (type !== undefined) {
65+
this._lastInteractionType = type;
66+
}
67+
}
68+
69+
private _onTouchInputEvent(event: TouchEvent) {
70+
clearTimeout(this._bufferTimeout);
71+
72+
this._onInputEvent(event);
73+
this._isBuffering = true;
74+
75+
// The timeout of 650ms is needed to delay the touchstart, because otherwise the touch will call
76+
// the `onInput` function multiple times.
77+
this._bufferTimeout = setTimeout(() => this._isBuffering = false, 650);
78+
}
79+
80+
private _parsePointerType(pointerType: string) {
81+
switch (pointerType) {
82+
case 'mouse':
83+
return MdInteractionType.MOUSE;
84+
case 'touch':
85+
return MdInteractionType.TOUCH;
86+
}
87+
}
88+
89+
getLastInteractionType(): MdInteractionType {
90+
return this._lastInteractionType;
91+
}
92+
}

src/demo-app/demo-app.html

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ <h1>Angular Material2 Demos</h1>
1515
<li><a [routerLink]="['LiveAnnouncerDemo']">Live Announcer demo</a></li>
1616
<li><a [routerLink]="['SidenavDemo']">Sidenav demo</a></li>
1717
<li><a [routerLink]="['GesturesDemo']">Gestures demo</a></li>
18+
<li><a [routerLink]="['InteractionDemo']">Interaction demo</a></li>
1819
</ul>
1920
<button md-raised-button (click)="root.dir = (root.dir == 'rtl' ? 'ltr' : 'rtl')">
2021
{{root.dir.toUpperCase()}}

src/demo-app/demo-app.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {ListDemo} from './list/list-demo';
1616
import {InputDemo} from './input/input-demo';
1717
import {LiveAnnouncerDemo} from './live-announcer/live-announcer-demo';
1818
import {GesturesDemo} from './gestures/gestures-demo';
19+
import {InteractionDemo} from './interaction/interaction-demo';
1920

2021
@Component({
2122
selector: 'home',
@@ -46,6 +47,7 @@ export class Home {}
4647
new Route({path: '/toolbar', name: 'ToolbarDemo', component: ToolbarDemo}),
4748
new Route({path: '/list', name: 'ListDemo', component: ListDemo}),
4849
new Route({path: '/live-announcer', name: 'LiveAnnouncerDemo', component: LiveAnnouncerDemo}),
49-
new Route({path: '/gestures', name: 'GesturesDemo', component: GesturesDemo})
50+
new Route({path: '/gestures', name: 'GesturesDemo', component: GesturesDemo}),
51+
new Route({path: '/interaction', name: 'InteractionDemo', component: InteractionDemo}),
5052
])
5153
export class DemoApp { }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<div class="demo-interaction">
2+
<p>
3+
The MdInteraction service is able to recognize the last interaction type.<br/>
4+
This is very helpful when providing special accessibility features for different interactions.
5+
</p>
6+
7+
<button md-raised-button (click)="alertLastInteraction()">
8+
Show me the last interaction
9+
</button>
10+
11+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.demo-interaction {
2+
padding: 8px;
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {Component} from 'angular2/core';
2+
import {MdInteraction, MdInteractionType} from '../../core/interaction/interaction';
3+
import {MdButton} from '../../components/button/button';
4+
5+
@Component({
6+
selector: 'interaction-demo',
7+
templateUrl: 'demo-app/interaction/interaction-demo.html',
8+
styleUrls: ['demo-app/interaction/interaction-demo.css'],
9+
directives: [MdButton]
10+
})
11+
export class InteractionDemo {
12+
13+
constructor(private interaction: MdInteraction) {};
14+
15+
alertLastInteraction() {
16+
let lastInteraction = this.interaction.getLastInteractionType();
17+
18+
switch (lastInteraction) {
19+
case MdInteractionType.KEYBOARD:
20+
alert('Keyboards are sometimes very loud - Try switching to a mouse.');
21+
break;
22+
case MdInteractionType.MOUSE:
23+
alert('Using a mouse is cool! What sensitivity are you using?');
24+
break;
25+
case MdInteractionType.TOUCH:
26+
alert('Material also runs well on touch devices.');
27+
break;
28+
}
29+
}
30+
31+
}

src/main.ts

+2
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import {MdLiveAnnouncer} from './core/live-announcer/live-announcer';
77
import {provide} from 'angular2/core';
88
import {createOverlayContainer} from './core/overlay/overlay-container';
99
import {MdGestureConfig} from './core/gestures/MdGestureConfig';
10+
import {MdInteraction} from './core/interaction/interaction';
1011

1112
bootstrap(DemoApp, [
1213
ROUTER_PROVIDERS,
1314
MdLiveAnnouncer,
15+
MdInteraction,
1416
provide(OVERLAY_CONTAINER_TOKEN, {useValue: createOverlayContainer()}),
1517
provide(HAMMER_GESTURE_CONFIG, {useClass: MdGestureConfig})
1618
]);

0 commit comments

Comments
 (0)