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

feat(interaction): added service to detect last interaction #7965

Merged
merged 1 commit into from
Oct 12, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions src/components/sidenav/sidenav.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,8 @@ 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, $window) {
function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $mdInteraction, $animate, $compile,
$parse, $log, $q, $document, $window) {
return {
restrict: 'E',
scope: {
Expand All @@ -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);
Expand Down Expand Up @@ -356,6 +357,7 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming,
if ( isOpen ) {
// Capture upon opening..
triggeringElement = $document[0].activeElement;
triggeringInteractionType = $mdInteraction.getLastInteractionType();
}

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

Expand Down
76 changes: 76 additions & 0 deletions src/components/sidenav/sidenav.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('<button>Trigger Element</button>');
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;

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
132 changes: 132 additions & 0 deletions src/core/services/interaction/interaction.js
Original file line number Diff line number Diff line change
@@ -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.
*
* <hljs lang="js">
* var lastType = $mdInteraction.getLastInteractionType();
*
* if (lastType === 'keyboard') {
* // We only restore the focus for keyboard users.
* restoreFocus();
* }
* </hljs>
*
*/
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;
};
33 changes: 33 additions & 0 deletions src/core/services/interaction/interaction.spec.js
Original file line number Diff line number Diff line change
@@ -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");
});

});
});