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

Commit 7ea6489

Browse files
committed
feat(interaction): added service to detect last interaction
Fixes #5563 Fixes #5434 Closes #5583
1 parent 0cd2a59 commit 7ea6489

File tree

5 files changed

+211
-3
lines changed

5 files changed

+211
-3
lines changed

src/components/sidenav/sidenav.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ function SidenavFocusDirective() {
243243
* - `<md-sidenav md-is-locked-open="$mdMedia('min-width: 1000px')"></md-sidenav>`
244244
* - `<md-sidenav md-is-locked-open="$mdMedia('sm')"></md-sidenav>` (locks open on small screens)
245245
*/
246-
function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate, $compile, $parse, $log, $q, $document) {
246+
function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $mdInteraction, $animate, $compile, $parse, $log, $q, $document) {
247247
return {
248248
restrict: 'E',
249249
scope: {
@@ -264,6 +264,7 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate,
264264
var lastParentOverFlow;
265265
var backdrop;
266266
var disableScrollTarget = null;
267+
var triggeringInteractionType;
267268
var triggeringElement = null;
268269
var previousContainerStyles;
269270
var promise = $q.when(true);
@@ -354,6 +355,7 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate,
354355
if ( isOpen ) {
355356
// Capture upon opening..
356357
triggeringElement = $document[0].activeElement;
358+
triggeringInteractionType = $mdInteraction.getLastInteractionType();
357359
}
358360

359361
disableParentScroll(isOpen);
@@ -451,9 +453,9 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate,
451453
// When the current `updateIsOpen()` animation finishes
452454
promise.then(function(result) {
453455

454-
if ( !scope.isOpen ) {
456+
if ( !scope.isOpen && triggeringElement && triggeringInteractionType === 'keyboard') {
455457
// reset focus to originating element (if available) upon close
456-
triggeringElement && triggeringElement.focus();
458+
triggeringElement.focus();
457459
triggeringElement = null;
458460
}
459461

src/components/sidenav/sidenav.spec.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,84 @@ describe('mdSidenav', function() {
240240

241241
});
242242

243+
describe("focus", function() {
244+
245+
var $material, $mdInteraction, $mdConstant;
246+
247+
beforeEach( inject(function(_$material_, _$mdInteraction_, _$mdConstant_) {
248+
$material = _$material_;
249+
$mdInteraction = _$mdInteraction_;
250+
$mdConstant = _$mdConstant_
251+
}));
252+
253+
function flush() {
254+
$material.flushInterimElement();
255+
}
256+
257+
function setupTrigger() {
258+
var el;
259+
inject(function($compile, $rootScope) {
260+
var parent = angular.element(document.body);
261+
el = angular.element('<button>Toggle</button>');
262+
parent.append(el);
263+
$compile(parent)($rootScope);
264+
$rootScope.$apply();
265+
});
266+
return el;
267+
}
268+
269+
it("should restore after sidenav triggered by keyboard", function(done) {
270+
var sidenavElement = setup('');
271+
var triggerElement = setupTrigger();
272+
var controller = sidenavElement.controller('mdSidenav');
273+
274+
triggerElement.focus();
275+
276+
var keyboardEvent = document.createEvent("KeyboardEvent");
277+
keyboardEvent.initEvent("keydown", true, true, window, 0, 0, 0, 0, $mdConstant.KEY_CODE.ENTER, $mdConstant.KEY_CODE.ENTER);
278+
triggerElement[0].dispatchEvent(keyboardEvent);
279+
280+
controller.$toggleOpen(true);
281+
flush();
282+
283+
triggerElement.blur();
284+
285+
controller.$toggleOpen(false);
286+
flush();
287+
288+
expect($mdInteraction.getLastInteractionType()).toBe("keyboard");
289+
expect(document.activeElement).toBe(triggerElement[0]);
290+
done();
291+
});
292+
293+
it("should not restore after sidenav triggered by mouse", function(done) {
294+
var sidenavElement = setup('');
295+
var triggerElement = setupTrigger();
296+
var controller = sidenavElement.controller('mdSidenav');
297+
298+
triggerElement.focus();
299+
300+
var mouseEvent = document.createEvent("MouseEvent");
301+
mouseEvent.initMouseEvent("mousedown", true, true, window, null, 0, 0, 0, 0, false, false, false, false, 0, null);
302+
triggerElement[0].dispatchEvent(mouseEvent);
303+
304+
controller.$toggleOpen(true);
305+
flush();
306+
307+
expect(document.activeElement).toBe(triggerElement[0]);
308+
309+
triggerElement.blur();
310+
311+
controller.$toggleOpen(false);
312+
flush();
313+
314+
expect($mdInteraction.getLastInteractionType()).toBe("mouse");
315+
expect(document.activeElement).not.toBe(triggerElement[0]);
316+
done();
317+
});
318+
319+
});
320+
243321
describe("controller Promise API", function() {
244322
var $material, $rootScope, $timeout;
245323

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, false);
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)