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 #5589

Closed
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
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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about documentation?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this service should be documented, it's not really generic and there aren't much use cases for a user to use that service. It's mostly a service which helps us with the accessibility for the components. But if you wan't me to document it, it won't be a problem.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regardless of whether we make the docs public, it is always nice to have them for ourselves 😄

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, will do that tomorrow 👍

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',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'd really like to know what are those, please add an explanation

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Input Map

MSPointerdown < IE11: https://msdn.microsoft.com/de-de/library/windows/apps/hh465891.aspx
Pointerdown >= IE11: https://msdn.microsoft.com/de-de/library/windows/apps/hh465891.aspx
Example -> touchstart: Is associated with a touch interaction (so return the api value: "touch")
The rest you definitely know 👍

Pointer Map:

https://msdn.microsoft.com/de-de/library/windows/apps/hh466130.aspx
MSPOINTER_TYPE_TOUCH: 0x00000002
MSPOINTER_TYPE_PEN: 0x00000003
MSPOINTER_TYPE_MOUSE: 0x00000004

Then I will validate the right pointer event type :)

I hope, know its clear :)
I'm up for more questions

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 please add this in a comment :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@devversion i pinged you on gitter

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");
});

});
});