Skip to content

feat(): add interaction service for a11y #305

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/core/interaction/interaction.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {
it,
describe,
expect,
beforeEach,
inject,
beforeEachProviders,
} from 'angular2/testing';
import {MdInteraction, MdInteractionType} from './interaction';

export function main() {
describe('MdInteraction', () => {
let interaction: MdInteraction;

beforeEachProviders(() => [MdInteraction]);

beforeEach(inject([MdInteraction], (_interaction: MdInteraction) => {
interaction = _interaction;
}));

it('should correctly detect the keyboard interaction', () => {

let event = <KeyboardEvent> document.createEvent('Event');

event.keyCode = 37;
event.initEvent('keydown', false, true);

document.body.dispatchEvent(event);

expect(interaction.getLastInteractionType()).toBe(MdInteractionType.KEYBOARD);
});

it('should correctly detect the mouse interaction', () => {
let eventType = 'PointerEvent' in window ? 'pointerdown' : 'mousedown';
let event = document.createEvent('MouseEvent');

event.initMouseEvent(eventType, true, true, window, null, 0, 0, 0, 0,
false, false, false, false, 0, null);

if (eventType === 'pointerdown') {
// https://msdn.microsoft.com/library/windows/apps/hh466130.aspx
(<PointerEvent> event).pointerType = 4;
}

document.body.dispatchEvent(event);

expect(interaction.getLastInteractionType()).toBe(MdInteractionType.MOUSE);
});

});
}
92 changes: 92 additions & 0 deletions src/core/interaction/interaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {Injectable} from 'angular2/core';

export enum MdInteractionType {
KEYBOARD,
MOUSE,
TOUCH
}

// Interface for the interaction mappings, which holds an index definition
// to avoid implicitly warnings.
interface MdInteractionMap {
[index: string]: any;
}

@Injectable()
export class MdInteraction {

private _isBuffering: boolean = false;
private _bufferTimeout: number;
private _lastInteractionType: MdInteractionType;

private _inputTypeMap: MdInteractionMap = {
'keydown': MdInteractionType.KEYBOARD,
'mousedown': MdInteractionType.MOUSE,
'mouseenter': MdInteractionType.MOUSE,
'touchstart': MdInteractionType.TOUCH,
'pointerdown': 'pointer',
'MSPointerDown': 'pointer',
};

// IE dispatches `pointerdown` events, which need to be validated separately.
// Index numbers referenced here: https://msdn.microsoft.com/library/windows/apps/hh466130.aspx
private _iePointerMap: MdInteractionMap = {
2: MdInteractionType.TOUCH,
3: MdInteractionType.TOUCH,
4: MdInteractionType.MOUSE
};

constructor() {
let mouseEvent = 'PointerEvent' in window ? 'pointerdown' : 'mousedown';

document.body.addEventListener('keydown', (e: Event) => this._onInputEvent(e));
document.body.addEventListener(mouseEvent, (e: Event) => this._onInputEvent(e));
document.body.addEventListener('mouseenter', (e: Event) => this._onInputEvent(e));

if ('ontouchstart' in document.documentElement) {
document.body.addEventListener('touchstart', (e: TouchEvent) => this._onTouchInputEvent(e));
}
}

private _onInputEvent(event: Event) {
if (this._isBuffering) {
return;
}

let type = this._inputTypeMap[event.type];

if (type === 'pointer') {
let pointerType = (<any> event)['pointerType'];
type = (typeof pointerType === 'number') ? this._iePointerMap[pointerType]
: this._parsePointerType(pointerType);
}

if (type !== undefined) {
this._lastInteractionType = type;
}
}

private _onTouchInputEvent(event: TouchEvent) {
clearTimeout(this._bufferTimeout);

this._onInputEvent(event);
this._isBuffering = true;

// The timeout of 650ms is needed to delay the touchstart, because otherwise the touch will call
// the `onInput` function multiple times.
this._bufferTimeout = setTimeout(() => this._isBuffering = false, 650);
}

private _parsePointerType(pointerType: string) {
switch (pointerType) {
case 'mouse':
return MdInteractionType.MOUSE;
case 'touch':
return MdInteractionType.TOUCH;
}
}

getLastInteractionType(): MdInteractionType {
return this._lastInteractionType;
}
}
1 change: 1 addition & 0 deletions src/demo-app/demo-app.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ <h1>Angular Material2 Demos</h1>
<li><a [routerLink]="['LiveAnnouncerDemo']">Live Announcer demo</a></li>
<li><a [routerLink]="['SidenavDemo']">Sidenav demo</a></li>
<li><a [routerLink]="['GesturesDemo']">Gestures demo</a></li>
<li><a [routerLink]="['InteractionDemo']">Interaction 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 @@ -16,6 +16,7 @@ import {ListDemo} from './list/list-demo';
import {InputDemo} from './input/input-demo';
import {LiveAnnouncerDemo} from './live-announcer/live-announcer-demo';
import {GesturesDemo} from './gestures/gestures-demo';
import {InteractionDemo} from './interaction/interaction-demo';

@Component({
selector: 'home',
Expand Down Expand Up @@ -46,6 +47,7 @@ export class Home {}
new Route({path: '/toolbar', name: 'ToolbarDemo', component: ToolbarDemo}),
new Route({path: '/list', name: 'ListDemo', component: ListDemo}),
new Route({path: '/live-announcer', name: 'LiveAnnouncerDemo', component: LiveAnnouncerDemo}),
new Route({path: '/gestures', name: 'GesturesDemo', component: GesturesDemo})
new Route({path: '/gestures', name: 'GesturesDemo', component: GesturesDemo}),
new Route({path: '/interaction', name: 'InteractionDemo', component: InteractionDemo}),
])
export class DemoApp { }
11 changes: 11 additions & 0 deletions src/demo-app/interaction/interaction-demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<div class="demo-interaction">
<p>
The MdInteraction service is able to recognize the last interaction type.<br/>
This is very helpful when providing special accessibility features for different interactions.
</p>

<button md-raised-button (click)="alertLastInteraction()">
Show me the last interaction
</button>

</div>
3 changes: 3 additions & 0 deletions src/demo-app/interaction/interaction-demo.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.demo-interaction {
padding: 8px;
}
31 changes: 31 additions & 0 deletions src/demo-app/interaction/interaction-demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {Component} from 'angular2/core';
import {MdInteraction, MdInteractionType} from '../../core/interaction/interaction';
import {MdButton} from '../../components/button/button';

@Component({
selector: 'interaction-demo',
templateUrl: 'demo-app/interaction/interaction-demo.html',
styleUrls: ['demo-app/interaction/interaction-demo.css'],
directives: [MdButton]
})
export class InteractionDemo {

constructor(private interaction: MdInteraction) {};

alertLastInteraction() {
let lastInteraction = this.interaction.getLastInteractionType();

switch (lastInteraction) {
case MdInteractionType.KEYBOARD:
alert('Keyboards are sometimes very loud - Try switching to a mouse.');
break;
case MdInteractionType.MOUSE:
alert('Using a mouse is cool! What sensitivity are you using?');
break;
case MdInteractionType.TOUCH:
alert('Material also runs well on touch devices.');
break;
}
}

}
2 changes: 2 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import {MdLiveAnnouncer} from './core/live-announcer/live-announcer';
import {provide} from 'angular2/core';
import {createOverlayContainer} from './core/overlay/overlay-container';
import {MdGestureConfig} from './core/gestures/MdGestureConfig';
import {MdInteraction} from './core/interaction/interaction';

bootstrap(DemoApp, [
ROUTER_PROVIDERS,
MdLiveAnnouncer,
MdInteraction,
provide(OVERLAY_CONTAINER_TOKEN, {useValue: createOverlayContainer()}),
provide(HAMMER_GESTURE_CONFIG, {useClass: MdGestureConfig})
]);