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

Commit 0cf58e4

Browse files
committed
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.
1 parent 5ae3d4c commit 0cf58e4

File tree

5 files changed

+169
-4
lines changed

5 files changed

+169
-4
lines changed

src/components/sidenav/sidenav.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ function SidenavFocusDirective() {
208208
* - `<md-sidenav md-is-locked-open="$mdMedia('min-width: 1000px')"></md-sidenav>`
209209
* - `<md-sidenav md-is-locked-open="$mdMedia('sm')"></md-sidenav>` (locks open on small screens)
210210
*/
211-
function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate, $compile, $parse, $log, $q, $document) {
211+
function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $mdInteraction, $animate, $compile, $parse, $log, $q, $document) {
212212
return {
213213
restrict: 'E',
214214
scope: {
@@ -227,6 +227,7 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate,
227227
*/
228228
function postLink(scope, element, attr, sidenavCtrl) {
229229
var lastParentOverFlow;
230+
var triggeringInteractionType;
230231
var triggeringElement = null;
231232
var promise = $q.when(true);
232233

@@ -289,6 +290,7 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate,
289290
if ( isOpen ) {
290291
// Capture upon opening..
291292
triggeringElement = $document[0].activeElement;
293+
triggeringInteractionType = $mdInteraction.getLastInteractionType();
292294
}
293295

294296
disableParentScroll(isOpen);
@@ -344,9 +346,9 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate,
344346
// When the current `updateIsOpen()` animation finishes
345347
promise.then(function(result) {
346348

347-
if ( !scope.isOpen ) {
349+
if ( !scope.isOpen && triggeringElement && triggeringInteractionType && triggeringInteractionType === 'keyboard') {
348350
// reset focus to originating element (if available) upon close
349-
triggeringElement && triggeringElement.focus();
351+
triggeringElement.focus();
350352
triggeringElement = null;
351353
}
352354

src/components/sidenav/sidenav.spec.js

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,85 @@ describe('mdSidenav', function() {
173173

174174
});
175175

176+
describe("focus", function() {
177+
178+
var $material, $mdInteraction;
179+
180+
beforeEach( inject(function(_$material_, _$mdInteraction_) {
181+
$material = _$material_;
182+
$mdInteraction = _$mdInteraction_;
183+
}));
184+
185+
function flush() {
186+
$material.flushInterimElement();
187+
}
188+
189+
function setupTrigger() {
190+
var el;
191+
inject(function($compile, $rootScope) {
192+
var parent = angular.element(document.body);
193+
el = angular.element('<button>Toggle</button>');
194+
parent.append(el);
195+
$compile(parent)($rootScope);
196+
$rootScope.$apply();
197+
});
198+
return el;
199+
}
200+
201+
it("should restore after sidenav triggered by keyboard", function(done) {
202+
var sidenavElement = setup('');
203+
var triggerElement = setupTrigger();
204+
var controller = sidenavElement.controller('mdSidenav');
205+
206+
triggerElement.focus();
207+
208+
var evt = document.createEvent("KeyboardEvent");
209+
evt.initEvent("keydown", true, true, window, 0, 0, 0, 0, 13, 13);
210+
triggerElement[0].dispatchEvent(evt);
211+
212+
controller.$toggleOpen(true);
213+
flush();
214+
215+
triggerElement.blur();
216+
217+
expect(document.activeElement).not.toBe(triggerElement[0]);
218+
219+
controller.$toggleOpen(false);
220+
flush();
221+
222+
expect($mdInteraction.getLastInteractionType()).toBe("keyboard");
223+
expect(document.activeElement).toBe(triggerElement[0]);
224+
done();
225+
});
226+
227+
it("should not restore after sidenav triggered by mouse", function(done) {
228+
var sidenavElement = setup('');
229+
var triggerElement = setupTrigger();
230+
var controller = sidenavElement.controller('mdSidenav');
231+
232+
triggerElement.focus();
233+
234+
var event = document.createEvent("MouseEvent");
235+
event.initMouseEvent("mousedown", true, true, window, null, 0, 0, 0, 0, false, false, false, false, 0, null);
236+
triggerElement[0].dispatchEvent(event);
237+
238+
controller.$toggleOpen(true);
239+
flush();
240+
241+
expect(document.activeElement).toBe(triggerElement[0]);
242+
243+
triggerElement.blur();
244+
245+
controller.$toggleOpen(false);
246+
flush();
247+
248+
expect($mdInteraction.getLastInteractionType()).toBe("mouse");
249+
expect(document.activeElement).not.toBe(triggerElement[0]);
250+
done();
251+
});
252+
253+
});
254+
176255
describe("controller Promise API", function() {
177256
var $material, $rootScope;
178257

@@ -186,7 +265,6 @@ describe('mdSidenav', function() {
186265
$timeout = _$timeout_;
187266
}));
188267

189-
190268
it('should open(), close(), and toggle() with promises', function () {
191269
var el = setup('');
192270
var scope = el.isolateScope();

src/core/core.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ angular
77
'ngAnimate',
88
'material.core.animate',
99
'material.core.layout',
10+
'material.core.interaction',
1011
'material.core.gestures',
1112
'material.core.theming'
1213
])
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
angular
2+
.module('material.core.interaction', [])
3+
.service('$mdInteraction', MdInteractionService);
4+
5+
function MdInteractionService($timeout) {
6+
var body = angular.element(document.body);
7+
var _mouseEvent = window.MSPointerEvent ? 'MSPointerDown' : window.PointerEvent ? 'pointerdown' : 'mousedown';
8+
var buffer = false;
9+
var timer;
10+
var lastInteractionType;
11+
var inputMap = {
12+
'keydown': 'keyboard',
13+
'mousedown': 'mouse',
14+
'mouseenter': 'mouse',
15+
'touchstart': 'touch',
16+
'pointerdown': 'pointer',
17+
'MSPointerDown': 'pointer'
18+
};
19+
var pointerMap = {
20+
2: 'touch',
21+
3: 'touch',
22+
4: 'mouse'
23+
};
24+
25+
function onInput(event) {
26+
if (buffer) return;
27+
var type = inputMap[event.type];
28+
if (type === 'pointer') {
29+
type = (typeof event.pointerType === 'number') ? pointerMap[event.pointerType] : event.pointerType;
30+
}
31+
lastInteractionType = type;
32+
}
33+
34+
function onBufferInput(event) {
35+
$timeout.cancel(timer);
36+
37+
onInput(event);
38+
buffer = true;
39+
40+
timer = $timeout(function() {
41+
buffer = false;
42+
}, 1000);
43+
}
44+
45+
body.on('keydown', onInput);
46+
body.on(_mouseEvent, onInput);
47+
body.on('mouseenter', onInput);
48+
if ('ontouchstart' in document.documentElement) body.on('touchstart', onBufferInput);
49+
50+
/**
51+
* Gets the last interaction type triggered by body.
52+
* Possible values for return are `mouse`, `keyboard` and `touch`
53+
* @returns {string}
54+
*/
55+
this.getLastInteractionType = function() {
56+
return lastInteractionType;
57+
}
58+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
describe("$mdInteraction", function() {
2+
beforeEach(module('material.core'));
3+
4+
describe("last interaction type", function() {
5+
6+
it("imitates a basic keyboard interaction and checks it", inject(function($mdInteraction) {
7+
8+
var event = document.createEvent('Event');
9+
event.keyCode = 37;
10+
event.initEvent('keydown', false, true);
11+
document.body.dispatchEvent(event);
12+
13+
expect($mdInteraction.getLastInteractionType()).toBe('keyboard');
14+
}));
15+
16+
it("dispatches a mousedown event on the document body and checks it", inject(function($mdInteraction) {
17+
18+
var event = document.createEvent("MouseEvent");
19+
event.initMouseEvent("mousedown", true, true, window, null, 0, 0, 0, 0, false, false, false, false, 0, null);
20+
document.body.dispatchEvent(event);
21+
22+
expect($mdInteraction.getLastInteractionType()).toBe("mouse");
23+
}));
24+
25+
});
26+
});

0 commit comments

Comments
 (0)