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

Commit

Permalink
feat(interaction): added service to detect last interaction
Browse files Browse the repository at this point in the history
Fixes #5563 Fixes #5434 Closes #5583
  • Loading branch information
devversion committed Apr 9, 2016
1 parent 317c1c8 commit e9faf66
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 3 deletions.
8 changes: 5 additions & 3 deletions src/components/sidenav/sidenav.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,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 @@ -223,6 +223,7 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate,
function postLink(scope, element, attr, sidenavCtrl) {
var lastParentOverFlow;
var backdrop;
var triggeringInteractionType;
var triggeringElement = null;
var promise = $q.when(true);
var isLockedOpenParsed = $parse(attr.mdIsLockedOpen);
Expand Down Expand Up @@ -295,6 +296,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 @@ -351,9 +353,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 === 'keyboard') {
// reset focus to originating element (if available) upon close
triggeringElement && triggeringElement.focus();
triggeringElement.focus();
triggeringElement = null;
}

Expand Down
78 changes: 78 additions & 0 deletions src/components/sidenav/sidenav.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,84 @@ describe('mdSidenav', function() {

});

describe("focus", function() {

var $material, $mdInteraction, $mdConstant;

beforeEach( inject(function(_$material_, _$mdInteraction_, _$mdConstant_) {
$material = _$material_;
$mdInteraction = _$mdInteraction_;
$mdConstant = _$mdConstant_
}));

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 keyboardEvent = document.createEvent("KeyboardEvent");
keyboardEvent.initEvent("keydown", true, true, window, 0, 0, 0, 0, $mdConstant.KEY_CODE.ENTER, $mdConstant.KEY_CODE.ENTER);
triggerElement[0].dispatchEvent(keyboardEvent);

controller.$toggleOpen(true);
flush();

triggerElement.blur();

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 mouseEvent = document.createEvent("MouseEvent");
mouseEvent.initMouseEvent("mousedown", true, true, window, null, 0, 0, 0, 0, false, false, false, false, 0, null);
triggerElement[0].dispatchEvent(mouseEvent);

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 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
95 changes: 95 additions & 0 deletions src/core/services/interaction/interaction.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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 last interaction type can be retrieved by using the `getLastInteractionType` method, which returns
* the following possible values:
* - `touch`
* - `mouse`
* - `keyboard`
*
* Here is an example markup for using the interaction service.
* ```
* var lastType = $mdInteraction.getLastInteractionType();
* if (lastType === 'keyboard') {
* restoreFocus();
* }}
* ```
*
*/
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;

// 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
var inputMap = {
'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
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;

// The timeout of 650ms is needed to delay the touchstart, because otherwise the touch will call
// the `onInput` function multiple times.
timer = $timeout(function() {
buffer = false;
}, 650);
}

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 in body.
* Possible return values are `mouse`, `keyboard` and `touch`
* @returns {string}
*/
this.getLastInteractionType = function() {
return lastInteractionType;
}
}
32 changes: 32 additions & 0 deletions src/core/services/interaction/interaction.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
describe("$mdInteraction service", function() {
var $mdInteraction;

beforeEach(module('material.core'));

beforeEach(inject(function(_$mdInteraction_) {
$mdInteraction = _$mdInteraction_;
}));

describe("last interaction type", function() {

it("imitates a basic keyboard interaction and checks it", function() {

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", function() {

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 e9faf66

Please sign in to comment.