diff --git a/src/components/sidenav/sidenav.js b/src/components/sidenav/sidenav.js index 593a90ff066..da417398242 100644 --- a/src/components/sidenav/sidenav.js +++ b/src/components/sidenav/sidenav.js @@ -208,7 +208,7 @@ function SidenavFocusDirective() { * - `` * - `` (locks open on small screens) */ -function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate, $compile, $parse, $log, $q, $document) { +function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $mdInteraction, $animate, $compile, $parse, $log, $q, $document) { return { restrict: 'E', scope: { @@ -227,6 +227,7 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate, */ function postLink(scope, element, attr, sidenavCtrl) { var lastParentOverFlow; + var triggeringInteractionType; var triggeringElement = null; var promise = $q.when(true); @@ -289,6 +290,7 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate, if ( isOpen ) { // Capture upon opening.. triggeringElement = $document[0].activeElement; + triggeringInteractionType = $mdInteraction.getLastInteractionType(); } disableParentScroll(isOpen); @@ -344,9 +346,9 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate, // When the current `updateIsOpen()` animation finishes promise.then(function(result) { - if ( !scope.isOpen ) { + if ( !scope.isOpen && triggeringElement && triggeringInteractionType && 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 beee8374388..cea09b4f32e 100644 --- a/src/components/sidenav/sidenav.spec.js +++ b/src/components/sidenav/sidenav.spec.js @@ -173,6 +173,85 @@ describe('mdSidenav', function() { }); + describe("focus", function() { + + var $material, $mdInteraction; + + beforeEach( inject(function(_$material_, _$mdInteraction_) { + $material = _$material_; + $mdInteraction = _$mdInteraction_; + })); + + function flush() { + $material.flushInterimElement(); + } + + function setupTrigger() { + var el; + inject(function($compile, $rootScope) { + var parent = angular.element(document.body); + el = angular.element(''); + parent.append(el); + $compile(parent)($rootScope); + $rootScope.$apply(); + }); + return el; + } + + it("should restore after sidenav triggered by keyboard", function(done) { + var sidenavElement = setup(''); + var triggerElement = setupTrigger(); + var controller = sidenavElement.controller('mdSidenav'); + + triggerElement.focus(); + + var evt = document.createEvent("KeyboardEvent"); + evt.initEvent("keydown", true, true, window, 0, 0, 0, 0, 13, 13); + triggerElement[0].dispatchEvent(evt); + + controller.$toggleOpen(true); + flush(); + + triggerElement.blur(); + + expect(document.activeElement).not.toBe(triggerElement[0]); + + controller.$toggleOpen(false); + flush(); + + expect($mdInteraction.getLastInteractionType()).toBe("keyboard"); + expect(document.activeElement).toBe(triggerElement[0]); + done(); + }); + + it("should not restore after sidenav triggered by mouse", function(done) { + var sidenavElement = setup(''); + var triggerElement = setupTrigger(); + var controller = sidenavElement.controller('mdSidenav'); + + triggerElement.focus(); + + var event = document.createEvent("MouseEvent"); + event.initMouseEvent("mousedown", true, true, window, null, 0, 0, 0, 0, false, false, false, false, 0, null); + triggerElement[0].dispatchEvent(event); + + controller.$toggleOpen(true); + flush(); + + expect(document.activeElement).toBe(triggerElement[0]); + + triggerElement.blur(); + + controller.$toggleOpen(false); + flush(); + + expect($mdInteraction.getLastInteractionType()).toBe("mouse"); + expect(document.activeElement).not.toBe(triggerElement[0]); + done(); + }); + + }); + describe("controller Promise API", function() { var $material, $rootScope; @@ -186,7 +265,6 @@ describe('mdSidenav', function() { $timeout = _$timeout_; })); - it('should open(), close(), and toggle() with promises', function () { var el = setup(''); var scope = el.isolateScope(); diff --git a/src/core/core.js b/src/core/core.js index b75effcf632..d7db626010b 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..2248fd450f4 --- /dev/null +++ b/src/core/services/interaction/interaction.js @@ -0,0 +1,58 @@ +angular + .module('material.core.interaction', []) + .service('$mdInteraction', MdInteractionService); + +function MdInteractionService($timeout) { + var body = angular.element(document.body); + var _mouseEvent = window.MSPointerEvent ? 'MSPointerDown' : window.PointerEvent ? 'pointerdown' : 'mousedown'; + var buffer = false; + var timer; + var lastInteractionType; + var inputMap = { + 'keydown': 'keyboard', + 'mousedown': 'mouse', + 'mouseenter': 'mouse', + 'touchstart': 'touch', + 'pointerdown': 'pointer', + 'MSPointerDown': 'pointer' + }; + var pointerMap = { + 2: 'touch', + 3: 'touch', + 4: 'mouse' + }; + + function onInput(event) { + if (buffer) return; + var type = inputMap[event.type]; + if (type === 'pointer') { + type = (typeof event.pointerType === 'number') ? pointerMap[event.pointerType] : event.pointerType; + } + lastInteractionType = type; + } + + function onBufferInput(event) { + $timeout.cancel(timer); + + onInput(event); + buffer = true; + + timer = $timeout(function() { + buffer = false; + }, 1000); + } + + body.on('keydown', onInput); + body.on(_mouseEvent, onInput); + body.on('mouseenter', onInput); + if ('ontouchstart' in document.documentElement) body.on('touchstart', onBufferInput); + + /** + * Gets the last interaction type triggered by body. + * Possible values for return are `mouse`, `keyboard` and `touch` + * @returns {string} + */ + this.getLastInteractionType = function() { + return 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..f99f61d3626 --- /dev/null +++ b/src/core/services/interaction/interaction.spec.js @@ -0,0 +1,26 @@ +describe("$mdInteraction", function() { + beforeEach(module('material.core')); + + describe("last interaction type", function() { + + it("imitates a basic keyboard interaction and checks it", inject(function($mdInteraction) { + + var event = document.createEvent('Event'); + event.keyCode = 37; + event.initEvent('keydown', false, true); + document.body.dispatchEvent(event); + + expect($mdInteraction.getLastInteractionType()).toBe('keyboard'); + })); + + it("dispatches a mousedown event on the document body and checks it", inject(function($mdInteraction) { + + var event = document.createEvent("MouseEvent"); + event.initMouseEvent("mousedown", true, true, window, null, 0, 0, 0, 0, false, false, false, false, 0, null); + document.body.dispatchEvent(event); + + expect($mdInteraction.getLastInteractionType()).toBe("mouse"); + })); + + }); +}); \ No newline at end of file