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

Commit e9faf66

Browse files
committed
feat(interaction): added service to detect last interaction
Fixes #5563 Fixes #5434 Closes #5583
1 parent 317c1c8 commit e9faf66

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
@@ -203,7 +203,7 @@ function SidenavFocusDirective() {
203203
* - `<md-sidenav md-is-locked-open="$mdMedia('min-width: 1000px')"></md-sidenav>`
204204
* - `<md-sidenav md-is-locked-open="$mdMedia('sm')"></md-sidenav>` (locks open on small screens)
205205
*/
206-
function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate, $compile, $parse, $log, $q, $document) {
206+
function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $mdInteraction, $animate, $compile, $parse, $log, $q, $document) {
207207
return {
208208
restrict: 'E',
209209
scope: {
@@ -223,6 +223,7 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate,
223223
function postLink(scope, element, attr, sidenavCtrl) {
224224
var lastParentOverFlow;
225225
var backdrop;
226+
var triggeringInteractionType;
226227
var triggeringElement = null;
227228
var promise = $q.when(true);
228229
var isLockedOpenParsed = $parse(attr.mdIsLockedOpen);
@@ -295,6 +296,7 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate,
295296
if ( isOpen ) {
296297
// Capture upon opening..
297298
triggeringElement = $document[0].activeElement;
299+
triggeringInteractionType = $mdInteraction.getLastInteractionType();
298300
}
299301

300302
disableParentScroll(isOpen);
@@ -351,9 +353,9 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate,
351353
// When the current `updateIsOpen()` animation finishes
352354
promise.then(function(result) {
353355

354-
if ( !scope.isOpen ) {
356+
if ( !scope.isOpen && triggeringElement && triggeringInteractionType === 'keyboard') {
355357
// reset focus to originating element (if available) upon close
356-
triggeringElement && triggeringElement.focus();
358+
triggeringElement.focus();
357359
triggeringElement = null;
358360
}
359361

src/components/sidenav/sidenav.spec.js

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

200200
});
201201

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

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)