From 0cf58e484adedc0948ae7c05381ed28c360143dc Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Thu, 12 Nov 2015 14:38:39 +0100 Subject: [PATCH] feat(interaction): added service to detect last interaction Sidenav want's to restore focus after the pane gets closed, this should only happen for keyboard interactions. The service got created to use that feature for other components too Fixes #5563 Fixes #5434 Closes #5583 Closes #5589 feat(interaction): add test for interaction which fakes a keyboard input and validates it test(interaction): add interaction spec for mousedown event + formats 4 spaces to 2 style(interaction): fix styling of the previous commits test(sidenav): add sidenav focus restore test triggered by keyboard test(sidenav): add focus restore test for mouse and patch other one fix(sidenav): test fix(sidenav): replace deprecated methods, and remove aria label test(sidenav): revert deprecated methods. --- src/components/sidenav/sidenav.js | 8 +- src/components/sidenav/sidenav.spec.js | 80 ++++++++++++++++++- src/core/core.js | 1 + src/core/services/interaction/interaction.js | 58 ++++++++++++++ .../services/interaction/interaction.spec.js | 26 ++++++ 5 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 src/core/services/interaction/interaction.js create mode 100644 src/core/services/interaction/interaction.spec.js 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