Skip to content
This repository was archived by the owner on Sep 5, 2024. It is now read-only.

Commit 28aa59a

Browse files
committed
feat(interaction): added service to detect last interaction
Fixes #5563 Fixes #5434 Closes #5583
1 parent d86efaf commit 28aa59a

File tree

5 files changed

+211
-4
lines changed

5 files changed

+211
-4
lines changed

src/components/sidenav/sidenav.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ function SidenavFocusDirective() {
209209
* - `<md-sidenav md-is-locked-open="$mdMedia('min-width: 1000px')"></md-sidenav>`
210210
* - `<md-sidenav md-is-locked-open="$mdMedia('sm')"></md-sidenav>` (locks open on small screens)
211211
*/
212-
function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate, $compile, $parse, $log, $q, $document) {
212+
function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $mdInteraction, $animate, $compile, $parse, $log, $q, $document) {
213213
return {
214214
restrict: 'E',
215215
scope: {
@@ -229,6 +229,7 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate,
229229
function postLink(scope, element, attr, sidenavCtrl) {
230230
var lastParentOverFlow;
231231
var backdrop;
232+
var triggeringInteractionType;
232233
var triggeringElement = null;
233234
var promise = $q.when(true);
234235
var isLockedOpenParsed = $parse(attr.mdIsLockedOpen);
@@ -300,6 +301,7 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate,
300301
if ( isOpen ) {
301302
// Capture upon opening..
302303
triggeringElement = $document[0].activeElement;
304+
triggeringInteractionType = $mdInteraction.getLastInteractionType();
303305
}
304306

305307
disableParentScroll(isOpen);
@@ -356,9 +358,9 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate,
356358
// When the current `updateIsOpen()` animation finishes
357359
promise.then(function(result) {
358360

359-
if ( !scope.isOpen ) {
361+
if ( !scope.isOpen && triggeringElement && triggeringInteractionType === 'keyboard') {
360362
// reset focus to originating element (if available) upon close
361-
triggeringElement && triggeringElement.focus();
363+
triggeringElement.focus();
362364
triggeringElement = null;
363365
}
364366

src/components/sidenav/sidenav.spec.js

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,84 @@ describe('mdSidenav', function() {
194194

195195
});
196196

197+
describe("focus", function() {
198+
199+
var $material, $mdInteraction, $mdConstant;
200+
201+
beforeEach( inject(function(_$material_, _$mdInteraction_, _$mdConstant_) {
202+
$material = _$material_;
203+
$mdInteraction = _$mdInteraction_;
204+
$mdConstant = _$mdConstant_
205+
}));
206+
207+
function flush() {
208+
$material.flushInterimElement();
209+
}
210+
211+
function setupTrigger() {
212+
var el;
213+
inject(function($compile, $rootScope) {
214+
var parent = angular.element(document.body);
215+
el = angular.element('<button>Toggle</button>');
216+
parent.append(el);
217+
$compile(parent)($rootScope);
218+
$rootScope.$apply();
219+
});
220+
return el;
221+
}
222+
223+
it("should restore after sidenav triggered by keyboard", function(done) {
224+
var sidenavElement = setup('');
225+
var triggerElement = setupTrigger();
226+
var controller = sidenavElement.controller('mdSidenav');
227+
228+
triggerElement.focus();
229+
230+
var keyboardEvent = document.createEvent("KeyboardEvent");
231+
keyboardEvent.initEvent("keydown", true, true, window, 0, 0, 0, 0, $mdConstant.KEY_CODE.ENTER, $mdConstant.KEY_CODE.ENTER);
232+
triggerElement[0].dispatchEvent(keyboardEvent);
233+
234+
controller.$toggleOpen(true);
235+
flush();
236+
237+
triggerElement.blur();
238+
239+
controller.$toggleOpen(false);
240+
flush();
241+
242+
expect($mdInteraction.getLastInteractionType()).toBe("keyboard");
243+
expect(document.activeElement).toBe(triggerElement[0]);
244+
done();
245+
});
246+
247+
it("should not restore after sidenav triggered by mouse", function(done) {
248+
var sidenavElement = setup('');
249+
var triggerElement = setupTrigger();
250+
var controller = sidenavElement.controller('mdSidenav');
251+
252+
triggerElement.focus();
253+
254+
var mouseEvent = document.createEvent("MouseEvent");
255+
mouseEvent.initMouseEvent("mousedown", true, true, window, null, 0, 0, 0, 0, false, false, false, false, 0, null);
256+
triggerElement[0].dispatchEvent(mouseEvent);
257+
258+
controller.$toggleOpen(true);
259+
flush();
260+
261+
expect(document.activeElement).toBe(triggerElement[0]);
262+
263+
triggerElement.blur();
264+
265+
controller.$toggleOpen(false);
266+
flush();
267+
268+
expect($mdInteraction.getLastInteractionType()).toBe("mouse");
269+
expect(document.activeElement).not.toBe(triggerElement[0]);
270+
done();
271+
});
272+
273+
});
274+
197275
describe("controller Promise API", function() {
198276
var $material, $rootScope;
199277

@@ -207,7 +285,6 @@ describe('mdSidenav', function() {
207285
$timeout = _$timeout_;
208286
}));
209287

210-
211288
it('should open(), close(), and toggle() with promises', function () {
212289
var el = setup('');
213290
var scope = el.isolateScope();

src/core/core.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ angular
77
'ngAnimate',
88
'material.core.animate',
99
'material.core.layout',
10+
'material.core.interaction',
1011
'material.core.gestures',
1112
'material.core.theming'
1213
])
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
angular
2+
.module('material.core.interaction', [])
3+
.service('$mdInteraction', MdInteractionService);
4+
5+
/*
6+
* @ngdoc service
7+
* @name $mdInteraction
8+
* @module material.core.interaction
9+
*
10+
* @description
11+
*
12+
* Service which keeps track of the last interaction type and validates them for several browsers.
13+
* The service hooks into the document's body and listens for touch, mouse and keyboard events.
14+
*
15+
* The last interaction type can be retrieved by using the `getLastInteractionType` method, which returns
16+
* the following possible values:
17+
* - `touch`
18+
* - `mouse`
19+
* - `keyboard`
20+
*
21+
* Here is an example markup for using the interaction service.
22+
* ```
23+
* var lastType = $mdInteraction.getLastInteractionType();
24+
* if (lastType === 'keyboard') {
25+
* restoreFocus();
26+
* }}
27+
* ```
28+
*
29+
*/
30+
function MdInteractionService($timeout) {
31+
var body = angular.element(document.body);
32+
var mouseEvent = window.MSPointerEvent ? 'MSPointerDown' : window.PointerEvent ? 'pointerdown' : 'mousedown';
33+
var buffer = false;
34+
var timer;
35+
var lastInteractionType;
36+
37+
// Type Mappings for the different events
38+
// There will be three three interaction types
39+
// `keyboard`, `mouse` and `touch`
40+
// type `pointer` will be evaluated in `pointerMap` for IE Browser events
41+
var inputMap = {
42+
'keydown': 'keyboard',
43+
'mousedown': 'mouse',
44+
'mouseenter': 'mouse',
45+
'touchstart': 'touch',
46+
'pointerdown': 'pointer',
47+
'MSPointerDown': 'pointer'
48+
};
49+
50+
// IE PointerDown events will be validated in `touch` or `mouse`
51+
// Index numbers referenced here: https://msdn.microsoft.com/library/windows/apps/hh466130.aspx
52+
var pointerMap = {
53+
2: 'touch',
54+
3: 'touch',
55+
4: 'mouse'
56+
};
57+
58+
function onInput(event) {
59+
if (buffer) return;
60+
var type = inputMap[event.type];
61+
if (type === 'pointer') {
62+
type = (typeof event.pointerType === 'number') ? pointerMap[event.pointerType] : event.pointerType;
63+
}
64+
lastInteractionType = type;
65+
}
66+
67+
function onBufferInput(event) {
68+
$timeout.cancel(timer);
69+
70+
onInput(event);
71+
buffer = true;
72+
73+
// The timeout of 650ms is needed to delay the touchstart, because otherwise the touch will call
74+
// the `onInput` function multiple times.
75+
timer = $timeout(function() {
76+
buffer = false;
77+
}, 650);
78+
}
79+
80+
body.on('keydown', onInput);
81+
body.on(mouseEvent, onInput);
82+
body.on('mouseenter', onInput);
83+
if ('ontouchstart' in document.documentElement) {
84+
body.on('touchstart', onBufferInput);
85+
}
86+
87+
/**
88+
* Gets the last interaction type triggered in body.
89+
* Possible return values are `mouse`, `keyboard` and `touch`
90+
* @returns {string}
91+
*/
92+
this.getLastInteractionType = function() {
93+
return lastInteractionType;
94+
}
95+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
describe("$mdInteraction service", function() {
2+
var $mdInteraction;
3+
4+
beforeEach(module('material.core'));
5+
6+
beforeEach(inject(function(_$mdInteraction_) {
7+
$mdInteraction = _$mdInteraction_;
8+
}));
9+
10+
describe("last interaction type", function() {
11+
12+
it("imitates a basic keyboard interaction and checks it", function() {
13+
14+
var event = document.createEvent('Event');
15+
event.keyCode = 37;
16+
event.initEvent('keydown', false, true);
17+
document.body.dispatchEvent(event);
18+
19+
expect($mdInteraction.getLastInteractionType()).toBe('keyboard');
20+
});
21+
22+
it("dispatches a mousedown event on the document body and checks it", function() {
23+
24+
var event = document.createEvent("MouseEvent");
25+
event.initMouseEvent("mousedown", true, true, window, null, 0, 0, 0, 0, false, false, false, false, 0, null);
26+
document.body.dispatchEvent(event);
27+
28+
expect($mdInteraction.getLastInteractionType()).toBe("mouse");
29+
});
30+
31+
});
32+
});

0 commit comments

Comments
 (0)