Skip to content

Commit 17545fa

Browse files
committed
feat(interaction): added service to detect last interaction
Fixes angular#5563 Fixes angular#5434 Closes angular#5583 Closes angular#5589
1 parent 34e0278 commit 17545fa

File tree

5 files changed

+186
-4
lines changed

5 files changed

+186
-4
lines changed

src/components/sidenav/sidenav.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ function SidenavFocusDirective() {
208208
* - `<md-sidenav md-is-locked-open="$mdMedia('min-width: 1000px')"></md-sidenav>`
209209
* - `<md-sidenav md-is-locked-open="$mdMedia('sm')"></md-sidenav>` (locks open on small screens)
210210
*/
211-
function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate, $compile, $parse, $log, $q, $document) {
211+
function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $mdInteraction, $animate, $compile, $parse, $log, $q, $document) {
212212
return {
213213
restrict: 'E',
214214
scope: {
@@ -227,6 +227,7 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate,
227227
*/
228228
function postLink(scope, element, attr, sidenavCtrl) {
229229
var lastParentOverFlow;
230+
var triggeringInteractionType;
230231
var triggeringElement = null;
231232
var promise = $q.when(true);
232233

@@ -289,6 +290,7 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate,
289290
if ( isOpen ) {
290291
// Capture upon opening..
291292
triggeringElement = $document[0].activeElement;
293+
triggeringInteractionType = $mdInteraction.getLastInteractionType();
292294
}
293295

294296
disableParentScroll(isOpen);
@@ -344,9 +346,9 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate,
344346
// When the current `updateIsOpen()` animation finishes
345347
promise.then(function(result) {
346348

347-
if ( !scope.isOpen ) {
349+
if ( !scope.isOpen && triggeringElement && triggeringInteractionType && triggeringInteractionType === 'keyboard') {
348350
// reset focus to originating element (if available) upon close
349-
triggeringElement && triggeringElement.focus();
351+
triggeringElement.focus();
350352
triggeringElement = null;
351353
}
352354

src/components/sidenav/sidenav.spec.js

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,86 @@ describe('mdSidenav', function() {
173173

174174
});
175175

176+
describe("focus", function() {
177+
178+
var $material, $mdInteraction, $mdConstant;
179+
180+
beforeEach( inject(function(_$material_, _$mdInteraction_, _$mdConstant_) {
181+
$material = _$material_;
182+
$mdInteraction = _$mdInteraction_;
183+
$mdConstant = _$mdConstant_
184+
}));
185+
186+
function flush() {
187+
$material.flushInterimElement();
188+
}
189+
190+
function setupTrigger() {
191+
var el;
192+
inject(function($compile, $rootScope) {
193+
var parent = angular.element(document.body);
194+
el = angular.element('<button>Toggle</button>');
195+
parent.append(el);
196+
$compile(parent)($rootScope);
197+
$rootScope.$apply();
198+
});
199+
return el;
200+
}
201+
202+
it("should restore after sidenav triggered by keyboard", function(done) {
203+
var sidenavElement = setup('');
204+
var triggerElement = setupTrigger();
205+
var controller = sidenavElement.controller('mdSidenav');
206+
207+
triggerElement.focus();
208+
209+
var keyboardEvent = document.createEvent("KeyboardEvent");
210+
keyboardEvent.initEvent("keydown", true, true, window, 0, 0, 0, 0, $mdConstant.KEY_CODE.ENTER, $mdConstant.KEY_CODE.ENTER);
211+
triggerElement[0].dispatchEvent(keyboardEvent);
212+
213+
controller.$toggleOpen(true);
214+
flush();
215+
216+
triggerElement.blur();
217+
218+
expect(document.activeElement).not.toBe(triggerElement[0]);
219+
220+
controller.$toggleOpen(false);
221+
flush();
222+
223+
expect($mdInteraction.getLastInteractionType()).toBe("keyboard");
224+
expect(document.activeElement).toBe(triggerElement[0]);
225+
done();
226+
});
227+
228+
it("should not restore after sidenav triggered by mouse", function(done) {
229+
var sidenavElement = setup('');
230+
var triggerElement = setupTrigger();
231+
var controller = sidenavElement.controller('mdSidenav');
232+
233+
triggerElement.focus();
234+
235+
var mouseEvent = document.createEvent("MouseEvent");
236+
mouseEvent.initMouseEvent("mousedown", true, true, window, null, 0, 0, 0, 0, false, false, false, false, 0, null);
237+
triggerElement[0].dispatchEvent(mouseEvent);
238+
239+
controller.$toggleOpen(true);
240+
flush();
241+
242+
expect(document.activeElement).toBe(triggerElement[0]);
243+
244+
triggerElement.blur();
245+
246+
controller.$toggleOpen(false);
247+
flush();
248+
249+
expect($mdInteraction.getLastInteractionType()).toBe("mouse");
250+
expect(document.activeElement).not.toBe(triggerElement[0]);
251+
done();
252+
});
253+
254+
});
255+
176256
describe("controller Promise API", function() {
177257
var $material, $rootScope;
178258

@@ -186,7 +266,6 @@ describe('mdSidenav', function() {
186266
$timeout = _$timeout_;
187267
}));
188268

189-
190269
it('should open(), close(), and toggle() with promises', function () {
191270
var el = setup('');
192271
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: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
angular
2+
.module('material.core.interaction', [])
3+
.service('$mdInteraction', MdInteractionService);
4+
5+
function MdInteractionService($timeout) {
6+
var body = angular.element(document.body);
7+
var mouseEvent = window.MSPointerEvent ? 'MSPointerDown' : window.PointerEvent ? 'pointerdown' : 'mousedown';
8+
var buffer = false;
9+
var timer;
10+
var lastInteractionType;
11+
12+
// Type Mappings for the different events
13+
// There will be three three interaction types
14+
// `keyboard`, `mouse` and `touch`
15+
// type `pointer` will be evaluated in `pointerMap` for IE Browser events
16+
var inputMap = {
17+
'keydown': 'keyboard',
18+
'mousedown': 'mouse',
19+
'mouseenter': 'mouse',
20+
'touchstart': 'touch',
21+
'pointerdown': 'pointer',
22+
'MSPointerDown': 'pointer'
23+
};
24+
25+
// IE PointerDown events will be validated in `touch` or `mouse`
26+
// Index numbers referenced here: https://msdn.microsoft.com/library/windows/apps/hh466130.aspx
27+
var pointerMap = {
28+
2: 'touch',
29+
3: 'touch',
30+
4: 'mouse'
31+
};
32+
33+
function onInput(event) {
34+
if (buffer) return;
35+
var type = inputMap[event.type];
36+
if (type === 'pointer') {
37+
type = (typeof event.pointerType === 'number') ? pointerMap[event.pointerType] : event.pointerType;
38+
}
39+
lastInteractionType = type;
40+
}
41+
42+
function onBufferInput(event) {
43+
$timeout.cancel(timer);
44+
45+
onInput(event);
46+
buffer = true;
47+
48+
timer = $timeout(function() {
49+
buffer = false;
50+
}, 1000);
51+
}
52+
53+
body.on('keydown', onInput);
54+
body.on(mouseEvent, onInput);
55+
body.on('mouseenter', onInput);
56+
if ('ontouchstart' in document.documentElement) {
57+
body.on('touchstart', onBufferInput);
58+
}
59+
60+
/**
61+
* Gets the last interaction type triggered in body.
62+
* Possible return values are `mouse`, `keyboard` and `touch`
63+
* @returns {string}
64+
*/
65+
this.getLastInteractionType = function() {
66+
return lastInteractionType;
67+
}
68+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
describe("$mdInteraction", 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)