Skip to content

Commit

Permalink
feat(interaction): added service to detect last interaction
Browse files Browse the repository at this point in the history
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 angular#5563 Fixes angular#5434 Closes angular#5583 Closes angular#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.
  • Loading branch information
devversion committed Nov 12, 2015
1 parent 5ae3d4c commit 0cf58e4
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 4 deletions.
8 changes: 5 additions & 3 deletions src/components/sidenav/sidenav.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ function SidenavFocusDirective() {
* - `<md-sidenav md-is-locked-open="$mdMedia('min-width: 1000px')"></md-sidenav>`
* - `<md-sidenav md-is-locked-open="$mdMedia('sm')"></md-sidenav>` (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: {
Expand All @@ -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);

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}

Expand Down
80 changes: 79 additions & 1 deletion src/components/sidenav/sidenav.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('<button>Toggle</button>');
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;

Expand All @@ -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();
Expand Down
1 change: 1 addition & 0 deletions src/core/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ angular
'ngAnimate',
'material.core.animate',
'material.core.layout',
'material.core.interaction',
'material.core.gestures',
'material.core.theming'
])
Expand Down
58 changes: 58 additions & 0 deletions src/core/services/interaction/interaction.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
26 changes: 26 additions & 0 deletions src/core/services/interaction/interaction.spec.js
Original file line number Diff line number Diff line change
@@ -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");
}));

});
});

0 comments on commit 0cf58e4

Please sign in to comment.