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

Commit 34823ac

Browse files
devversionjelbourn
authored andcommitted
fix(button): only apply focus effect for keyboard interaction. (#9826)
* Only apply the focus effect for programmatic focus or by keyboard interaction (as same as it does currently, but not with a random timeout). References #7963 Fixes #8749
1 parent 72b4f10 commit 34823ac

File tree

4 files changed

+119
-28
lines changed

4 files changed

+119
-28
lines changed

src/components/button/button.js

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ function MdAnchorDirective($mdTheming) {
114114
* </md-button>
115115
* </hljs>
116116
*/
117-
function MdButtonDirective($mdButtonInkRipple, $mdTheming, $mdAria, $timeout) {
117+
function MdButtonDirective($mdButtonInkRipple, $mdTheming, $mdAria, $mdInteraction) {
118118

119119
return {
120120
restrict: 'EA',
@@ -162,20 +162,17 @@ function MdButtonDirective($mdButtonInkRipple, $mdTheming, $mdAria, $timeout) {
162162
});
163163

164164
if (!element.hasClass('md-no-focus')) {
165-
// restrict focus styles to the keyboard
166-
scope.mouseActive = false;
167-
element.on('mousedown', function() {
168-
scope.mouseActive = true;
169-
$timeout(function(){
170-
scope.mouseActive = false;
171-
}, 100);
172-
})
173-
.on('focus', function() {
174-
if (scope.mouseActive === false) {
165+
166+
element.on('focus', function() {
167+
168+
// Only show the focus effect when being focused through keyboard interaction or programmatically
169+
if (!$mdInteraction.isUserInvoked() || $mdInteraction.getLastInteractionType() === 'keyboard') {
175170
element.addClass('md-focused');
176171
}
177-
})
178-
.on('blur', function(ev) {
172+
173+
});
174+
175+
element.on('blur', function() {
179176
element.removeClass('md-focused');
180177
});
181178
}

src/components/button/button.spec.js

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,20 +59,52 @@ describe('md-button', function() {
5959
expect(button.hasClass('md-button')).toBe(true);
6060
}));
6161

62-
it('should not set focus state on mousedown', inject(function ($compile, $rootScope){
62+
it('should apply focus effect with keyboard interaction', inject(function ($compile, $rootScope){
6363
var button = $compile('<md-button>')($rootScope.$new());
64+
var body = angular.element(document.body);
65+
6466
$rootScope.$apply();
65-
button.triggerHandler('mousedown');
66-
expect(button[0]).not.toHaveClass('md-focused');
67+
68+
// Fake a keyboard interaction for the $mdInteraction service.
69+
body.triggerHandler('keydown');
70+
button.triggerHandler('focus');
71+
72+
expect(button).toHaveClass('md-focused');
73+
74+
button.triggerHandler('blur');
75+
76+
expect(button).not.toHaveClass('md-focused');
6777
}));
6878

69-
it('should set focus state on focus and remove on blur', inject(function ($compile, $rootScope){
79+
it('should apply focus effect when programmatically focusing', inject(function ($compile, $rootScope){
7080
var button = $compile('<md-button>')($rootScope.$new());
81+
7182
$rootScope.$apply();
83+
7284
button.triggerHandler('focus');
73-
expect(button[0]).toHaveClass('md-focused');
85+
86+
expect(button).toHaveClass('md-focused');
87+
7488
button.triggerHandler('blur');
75-
expect(button[0]).not.toHaveClass('md-focused');
89+
90+
expect(button).not.toHaveClass('md-focused');
91+
}));
92+
93+
it('should not apply focus effect with mouse interaction', inject(function ($compile, $rootScope){
94+
var button = $compile('<md-button>')($rootScope.$new());
95+
var body = angular.element(document.body);
96+
97+
$rootScope.$apply();
98+
99+
// Fake a mouse interaction for the $mdInteraction service.
100+
body.triggerHandler('mousedown');
101+
button.triggerHandler('focus');
102+
103+
expect(button).not.toHaveClass('md-focused');
104+
105+
button.triggerHandler('blur');
106+
107+
expect(button).not.toHaveClass('md-focused');
76108
}));
77109

78110
it('should not set the focus state if focus is disabled', inject(function($compile, $rootScope) {

src/core/services/interaction/interaction.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,15 @@ angular
3333
* </hljs>
3434
*
3535
*/
36-
function MdInteractionService($timeout) {
36+
function MdInteractionService($timeout, $mdUtil) {
3737
this.$timeout = $timeout;
38+
this.$mdUtil = $mdUtil;
3839

3940
this.bodyElement = angular.element(document.body);
4041
this.isBuffering = false;
4142
this.bufferTimeout = null;
4243
this.lastInteractionType = null;
44+
this.lastInteractionTime = null;
4345

4446
// Type Mappings for the different events
4547
// There will be three three interaction types
@@ -87,7 +89,7 @@ MdInteractionService.prototype.initializeEvents = function() {
8789

8890
/**
8991
* Event listener for normal interaction events, which should be tracked.
90-
* @param event {MouseEvent|KeyboardEvent|PointerEvent}
92+
* @param event {MouseEvent|KeyboardEvent|PointerEvent|TouchEvent}
9193
*/
9294
MdInteractionService.prototype.onInputEvent = function(event) {
9395
if (this.isBuffering) {
@@ -101,6 +103,7 @@ MdInteractionService.prototype.onInputEvent = function(event) {
101103
}
102104

103105
this.lastInteractionType = type;
106+
this.lastInteractionTime = this.$mdUtil.now();
104107
};
105108

106109
/**
@@ -129,4 +132,18 @@ MdInteractionService.prototype.onBufferInputEvent = function(event) {
129132
*/
130133
MdInteractionService.prototype.getLastInteractionType = function() {
131134
return this.lastInteractionType;
132-
};
135+
};
136+
137+
/**
138+
* @ngdoc method
139+
* @name $mdInteraction#isUserInvoked
140+
* @description Method to detect whether any interaction happened recently or not.
141+
* @param {number=} checkDelay Time to check for any interaction to have been triggered.
142+
* @returns {boolean} Whether there was any interaction or not.
143+
*/
144+
MdInteractionService.prototype.isUserInvoked = function(checkDelay) {
145+
var delay = angular.isNumber(checkDelay) ? checkDelay : 15;
146+
147+
// Check for any interaction to be within the specified check time.
148+
return this.lastInteractionTime >= this.$mdUtil.now() - delay;
149+
};

src/core/services/interaction/interaction.spec.js

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
describe("$mdInteraction service", function() {
2-
var $mdInteraction;
2+
3+
var $mdInteraction = null;
4+
var bodyElement = null;
35

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

68
beforeEach(inject(function($injector) {
79
$mdInteraction = $injector.get('$mdInteraction');
10+
11+
bodyElement = angular.element(document.body);
812
}));
913

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

12-
var bodyElement = null;
13-
14-
beforeEach(function() {
15-
bodyElement = angular.element(document.body);
16-
});
1716

1817
it("should detect a keyboard interaction", function() {
1918

@@ -30,4 +29,50 @@ describe("$mdInteraction service", function() {
3029
});
3130

3231
});
32+
33+
describe('isUserInvoked', function() {
34+
35+
var element = null;
36+
37+
beforeEach(function() {
38+
element = angular.element('<button>Click</button>');
39+
40+
bodyElement.append(element);
41+
});
42+
43+
afterEach(function() {
44+
element.remove();
45+
});
46+
47+
it('should be true when programmatically focusing an element', function() {
48+
element.focus();
49+
50+
expect($mdInteraction.isUserInvoked()).toBe(false);
51+
});
52+
53+
it('should be false when focusing an element through keyboard', function() {
54+
55+
// Fake a focus event triggered by a keyboard interaction.
56+
bodyElement.triggerHandler('keydown');
57+
element.focus();
58+
59+
expect($mdInteraction.isUserInvoked()).toBe(true);
60+
});
61+
62+
it('should allow passing a custom check delay', function(done) {
63+
bodyElement.triggerHandler('keydown');
64+
65+
// The keyboard interaction is still in the same tick, so the interaction happened earlier than 15ms (as default)
66+
expect($mdInteraction.isUserInvoked()).toBe(true);
67+
68+
setTimeout(function() {
69+
// Expect the keyboard interaction to be older than 5ms (safer than exactly 10ms) as check time.
70+
expect($mdInteraction.isUserInvoked(5)).toBe(false);
71+
72+
done();
73+
}, 10);
74+
});
75+
76+
});
77+
3378
});

0 commit comments

Comments
 (0)