diff --git a/src/components/sidenav/sidenav.js b/src/components/sidenav/sidenav.js
index dd4bc9b82f4..6c4ee13813c 100644
--- a/src/components/sidenav/sidenav.js
+++ b/src/components/sidenav/sidenav.js
@@ -243,8 +243,8 @@ function SidenavFocusDirective() {
* - ``
* - `` (locks open on small screens)
*/
-function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming,
- $animate, $compile, $parse, $log, $q, $document, $window) {
+function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $mdInteraction, $animate, $compile,
+ $parse, $log, $q, $document, $window) {
return {
restrict: 'E',
scope: {
@@ -265,6 +265,7 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming,
var lastParentOverFlow;
var backdrop;
var disableScrollTarget = null;
+ var triggeringInteractionType;
var triggeringElement = null;
var previousContainerStyles;
var promise = $q.when(true);
@@ -356,6 +357,7 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming,
if ( isOpen ) {
// Capture upon opening..
triggeringElement = $document[0].activeElement;
+ triggeringInteractionType = $mdInteraction.getLastInteractionType();
}
disableParentScroll(isOpen);
@@ -455,9 +457,9 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming,
// When the current `updateIsOpen()` animation finishes
promise.then(function(result) {
- if ( !scope.isOpen ) {
+ if ( !scope.isOpen && triggeringElement && triggeringInteractionType === 'keyboard') {
// reset focus to originating element (if available) upon close
- triggeringElement && triggeringElement.focus();
+ triggeringElement.focus();
triggeringElement = null;
}
diff --git a/src/components/sidenav/sidenav.spec.js b/src/components/sidenav/sidenav.spec.js
index 21ad211c3ce..815414720ec 100644
--- a/src/components/sidenav/sidenav.spec.js
+++ b/src/components/sidenav/sidenav.spec.js
@@ -256,6 +256,82 @@ describe('mdSidenav', function() {
});
+ describe("focus", function() {
+
+ var $material, $mdInteraction, $mdConstant;
+ var triggerElement;
+
+ beforeEach(inject(function($injector) {
+ $material = $injector.get('$material');
+ $mdInteraction = $injector.get('$mdInteraction');
+ $mdConstant = $injector.get('$mdInteraction');
+
+ triggerElement = angular.element('');
+ document.body.appendChild(triggerElement[0]);
+ }));
+
+ afterEach(function() {
+ triggerElement.remove();
+ });
+
+ function dispatchEvent(eventName) {
+ angular.element(document.body).triggerHandler(eventName);
+ }
+
+ function flush() {
+ $material.flushInterimElement();
+ }
+
+ function blur() {
+ if ('documentMode' in document) {
+ document.body.focus();
+ } else {
+ triggerElement.blur();
+ }
+ }
+
+ it("should restore after sidenav triggered by keyboard", function() {
+ var sidenavEl = setup('');
+ var controller = sidenavEl.controller('mdSidenav');
+
+ triggerElement.focus();
+
+ dispatchEvent('keydown');
+
+ controller.$toggleOpen(true);
+ flush();
+
+ blur();
+
+ controller.$toggleOpen(false);
+ flush();
+
+ expect($mdInteraction.getLastInteractionType()).toBe("keyboard");
+ expect(document.activeElement).toBe(triggerElement[0]);
+ });
+
+ it("should not restore after sidenav triggered by mouse", function() {
+ var sidenavEl = setup('');
+ var controller = sidenavEl.controller('mdSidenav');
+
+ triggerElement.focus();
+
+ dispatchEvent('mousedown');
+
+ controller.$toggleOpen(true);
+ flush();
+
+ blur();
+
+ controller.$toggleOpen(false);
+ flush();
+
+ expect($mdInteraction.getLastInteractionType()).toBe("mouse");
+ expect(document.activeElement).not.toBe(triggerElement[0]);
+ });
+
+ });
+
describe("controller Promise API", function() {
var $material, $rootScope, $timeout;
diff --git a/src/core/core.js b/src/core/core.js
index 7383a891544..6fea6dd2bdc 100644
--- a/src/core/core.js
+++ b/src/core/core.js
@@ -7,6 +7,7 @@ angular
'ngAnimate',
'material.core.animate',
'material.core.layout',
+ 'material.core.interaction',
'material.core.gestures',
'material.core.theming'
])
diff --git a/src/core/services/interaction/interaction.js b/src/core/services/interaction/interaction.js
new file mode 100644
index 00000000000..e39211b7b80
--- /dev/null
+++ b/src/core/services/interaction/interaction.js
@@ -0,0 +1,132 @@
+/**
+ * @ngdoc module
+ * @name material.core.interaction
+ * @description
+ * User interaction detection to provide proper accessibility.
+ */
+angular
+ .module('material.core.interaction', [])
+ .service('$mdInteraction', MdInteractionService);
+
+
+/**
+ * @ngdoc service
+ * @name $mdInteraction
+ * @module material.core.interaction
+ *
+ * @description
+ *
+ * Service which keeps track of the last interaction type and validates them for several browsers.
+ * The service hooks into the document's body and listens for touch, mouse and keyboard events.
+ *
+ * The most recent interaction type can be retrieved by calling the `getLastInteractionType` method.
+ *
+ * Here is an example markup for using the interaction service.
+ *
+ *
+ * var lastType = $mdInteraction.getLastInteractionType();
+ *
+ * if (lastType === 'keyboard') {
+ * // We only restore the focus for keyboard users.
+ * restoreFocus();
+ * }
+ *
+ *
+ */
+function MdInteractionService($timeout) {
+ this.$timeout = $timeout;
+
+ this.bodyElement = angular.element(document.body);
+ this.isBuffering = false;
+ this.bufferTimeout = null;
+ this.lastInteractionType = null;
+
+ // Type Mappings for the different events
+ // There will be three three interaction types
+ // `keyboard`, `mouse` and `touch`
+ // type `pointer` will be evaluated in `pointerMap` for IE Browser events
+ this.inputEventMap = {
+ 'keydown': 'keyboard',
+ 'mousedown': 'mouse',
+ 'mouseenter': 'mouse',
+ 'touchstart': 'touch',
+ 'pointerdown': 'pointer',
+ 'MSPointerDown': 'pointer'
+ };
+
+ // IE PointerDown events will be validated in `touch` or `mouse`
+ // Index numbers referenced here: https://msdn.microsoft.com/library/windows/apps/hh466130.aspx
+ this.iePointerMap = {
+ 2: 'touch',
+ 3: 'touch',
+ 4: 'mouse'
+ };
+
+ this.initializeEvents();
+}
+
+/**
+ * Initializes the interaction service, by registering all interaction events to the
+ * body element.
+ */
+MdInteractionService.prototype.initializeEvents = function() {
+ // IE browsers can also trigger pointer events, which also leads to an interaction.
+ var pointerEvent = 'MSPointerEvent' in window ? 'MSPointerDown' : 'PointerEvent' in window ? 'pointerdown' : null;
+
+ this.bodyElement.on('keydown mousedown', this.onInputEvent.bind(this));
+
+ if ('ontouchstart' in document.documentElement) {
+ this.bodyElement.on('touchstart', this.onBufferInputEvent.bind(this));
+ }
+
+ if (pointerEvent) {
+ this.bodyElement.on(pointerEvent, this.onInputEvent.bind(this));
+ }
+
+};
+
+/**
+ * Event listener for normal interaction events, which should be tracked.
+ * @param event {MouseEvent|KeyboardEvent|PointerEvent}
+ */
+MdInteractionService.prototype.onInputEvent = function(event) {
+ if (this.isBuffering) {
+ return;
+ }
+
+ var type = this.inputEventMap[event.type];
+
+ if (type === 'pointer') {
+ type = this.iePointerMap[event.pointerType] || event.pointerType;
+ }
+
+ this.lastInteractionType = type;
+};
+
+/**
+ * Event listener for interaction events which should be buffered (touch events).
+ * @param event {TouchEvent}
+ */
+MdInteractionService.prototype.onBufferInputEvent = function(event) {
+ this.$timeout.cancel(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 = this.$timeout(function() {
+ this.isBuffering = false;
+ }.bind(this), 650, false);
+
+};
+
+/**
+ * @ngdoc method
+ * @name $mdInteraction#getLastInteractionType
+ * @description Retrieves the last interaction type triggered in body.
+ * @returns {string|null} Last interaction type.
+ */
+MdInteractionService.prototype.getLastInteractionType = function() {
+ return this.lastInteractionType;
+};
\ No newline at end of file
diff --git a/src/core/services/interaction/interaction.spec.js b/src/core/services/interaction/interaction.spec.js
new file mode 100644
index 00000000000..0344efed340
--- /dev/null
+++ b/src/core/services/interaction/interaction.spec.js
@@ -0,0 +1,33 @@
+describe("$mdInteraction service", function() {
+ var $mdInteraction;
+
+ beforeEach(module('material.core'));
+
+ beforeEach(inject(function($injector) {
+ $mdInteraction = $injector.get('$mdInteraction');
+ }));
+
+ describe("last interaction type", function() {
+
+ var bodyElement = null;
+
+ beforeEach(function() {
+ bodyElement = angular.element(document.body);
+ });
+
+ it("should detect a keyboard interaction", function() {
+
+ bodyElement.triggerHandler('keydown');
+
+ expect($mdInteraction.getLastInteractionType()).toBe('keyboard');
+ });
+
+ it("should detect a mouse interaction", function() {
+
+ bodyElement.triggerHandler('mousedown');
+
+ expect($mdInteraction.getLastInteractionType()).toBe("mouse");
+ });
+
+ });
+});
\ No newline at end of file