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

Commit 7251684

Browse files
fix(gestures, fabSpeedDial): Gesture ClickHijack iOS fixes and FabSpeedDial fixes
Extend the click-hijacking functionality of `$mdGestures`: **Background:** iOS emulates click events on a `350ms` delay in order to avoid sending clicks when users pan, perform gestures, etc. This leads to pages often feeling laggy in mobile browsers. This appears to be less of an issue in Safari, but is a major issue in embedded webview applications such as Chrome on iOS and PhoneGap built apps. To get around this `$mdGesture` currently listens to `touchstart` and `touchend` events to manually dispatch `click` events. This allows it to trigger events much quicker than the 350ms. It then configures the page to ignore clicks that were not originated by `$mdGesture`. **Problem:** While `$mdGesture` did successfully hijack the `click`event, it did not hijack other emulated events from iOS, including `mouseup`, `mousedown` and the subsequent `focus` event that could originate from a delayed `click`. **Example of problem:** Issue #4850 was caused by the `md-select` opening quickly via our simulated `click` event, followed by receiving a `350ms` OS delayed `mousedown` event resulting in an option immediately being selected. Some other example problems that impact the framework: - Ink ripples are 350ms delayed (use `mousedown` and `mouseup`) - Some components / items randomly ripple or trigger after being focused / click. Usually this isn't an issue, but can be (eg. #4850) - `focus` events are 350ms delayed (Affects input) This PR makes `$mdGesture` perform hijacking of the `mousedown`, `mouseup` and subsequent `focus` events in order to more quickly dispatch them. **Notes:** A few of our internal components needed some refactoring as they were properly discarding the emulated `mousedown` (which is now ignored DOM wide) and relying on those events for state, etc. Ripples will need a fix as they now properly ripple on `mousedown` but can hit a situation where they receive no later `mouseup` event. Perhaps listening to the document-wide `mouseup` event should be registered on `mousedown`. An example is scrolling in the Docs' Sidenav via touch. closes #4850, closes #4757. closes #5214. closes #5234. Refs #5195.
1 parent 5034a04 commit 7251684

File tree

7 files changed

+674
-662
lines changed

7 files changed

+674
-662
lines changed

src/components/fabSpeedDial/demoMoreOptions/index.html

+7-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,13 @@ <h3>$mdDialog</h3>
110110

111111
<script type="text/ng-template" id="dialog.html">
112112
<md-dialog>
113-
<md-dialog-content>Hello User! You clicked {{dialog.item.name}}.</md-dialog-content>
113+
<md-toolbar>
114+
<div class="md-toolbar-tools">Cool Dialog!</div>
115+
</md-toolbar>
116+
117+
<md-dialog-content layout-padding>
118+
Hello user! you clicked {{dialog.item.name}}.
119+
</md-dialog-content>
114120

115121
<div class="md-actions">
116122
<md-button aria-label="Close dialog" ng-click="dialog.close()" class="md-primary">

src/components/fabSpeedDial/fabController.js

+41-96
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
angular.module('material.components.fabShared', ['material.core'])
55
.controller('FabController', FabController);
66

7-
function FabController($scope, $element, $animate, $mdUtil, $mdConstant) {
7+
function FabController($scope, $element, $animate, $mdUtil, $mdConstant, $timeout) {
88
var vm = this;
99

1010
// NOTE: We use async evals below to avoid conflicts with any existing digest loops
@@ -42,11 +42,13 @@
4242
resetActionIndex();
4343
}
4444

45-
var events = [];
46-
4745
function setupListeners() {
4846
var eventTypes = [
49-
'mousedown', 'mouseup', 'click', 'touchstart', 'touchend', 'focusin', 'focusout'
47+
'$md.pressdown',
48+
49+
'click', // Fired via keyboard ENTER
50+
51+
'focusin', 'focusout'
5052
];
5153

5254
// Add our listeners
@@ -59,107 +61,41 @@
5961
angular.forEach(eventTypes, function(eventType) {
6062
$element.off(eventType, parseEvents);
6163
});
64+
6265
// remove any attached keyboard handlers in case element is removed while
6366
// speed dial is open
6467
disableKeyboard();
6568
});
6669
}
6770

68-
function resetEvents() {
69-
events = [];
70-
}
71-
72-
function equalsEvents(toCheck) {
73-
var isEqual, strippedCheck, moreToCheck;
74-
75-
// Quick check to make sure we don't get stuck in an infinite loop
76-
var numTests = 0;
77-
78-
do {
79-
// Strip out the question mark
80-
strippedCheck = toCheck.map(function(event) {
81-
return event.replace('?', '')
82-
});
83-
84-
// Check if they are equal
85-
isEqual = angular.equals(events, strippedCheck);
86-
87-
// If not, check to see if removing an optional event makes them equal
88-
if (!isEqual) {
89-
toCheck = removeOptionalEvent(toCheck);
90-
moreToCheck = (toCheck.length >= events.length && toCheck.length !== strippedCheck.length);
91-
}
92-
}
93-
while (numTests < 10 && !isEqual && moreToCheck);
94-
95-
return isEqual;
96-
}
97-
98-
function removeOptionalEvent(events) {
99-
var foundOptional = false;
100-
101-
return events.filter(function(event) {
102-
// If we have not found an optional one, keep searching
103-
if (!foundOptional && event.indexOf('?') !== -1) {
104-
foundOptional = true;
105-
106-
// If we find an optional one, remove only that one and keep going
107-
return false;
108-
}
109-
110-
return true;
111-
});
112-
}
113-
114-
function parseEvents(latestEvent) {
115-
events.push(latestEvent.type);
116-
117-
// Handle desktop click
118-
if (equalsEvents(['mousedown', 'focusout?', 'focusin?', 'mouseup', 'click'])) {
119-
handleItemClick(latestEvent);
120-
resetEvents();
71+
var recentEvent;
72+
function parseEvents(event) {
73+
// If we've had a recent press/click event, or material is sending us an additional event,
74+
// ignore it
75+
if (recentEvent && (isClick(recentEvent) || recentEvent.$material)) {
12176
return;
12277
}
12378

124-
// Handle mobile click/tap (and keyboard enter)
125-
if (equalsEvents(['touchstart?', 'touchend?', 'click'])) {
126-
handleItemClick(latestEvent);
127-
resetEvents();
128-
return;
129-
}
79+
// Otherwise, handle our events
80+
if (isClick(event)) {
81+
handleItemClick(event);
13082

131-
// Handle tab keys (focusin)
132-
if (equalsEvents(['focusin'])) {
83+
// Store our recent click event
84+
recentEvent = event;
85+
} else if (event.type == 'focusin') {
13386
vm.open();
134-
resetEvents();
135-
return;
136-
}
137-
138-
// Handle tab keys (focusout)
139-
if (equalsEvents(['focusout'])) {
87+
} else if (event.type == 'focusout') {
14088
vm.close();
141-
resetEvents();
142-
return;
14389
}
14490

145-
eventUnhandled();
91+
// Clear the recent event after all others have fired so we stop ignoring
92+
$timeout(function() {
93+
recentEvent = null;
94+
}, 100, false);
14695
}
14796

148-
/*
149-
* No event was handled, so setup a timeout to clear the events
150-
*
151-
* TODO: Use $mdUtil.debounce()?
152-
*/
153-
var resetEventsTimeout;
154-
155-
function eventUnhandled() {
156-
if (resetEventsTimeout) {
157-
window.clearTimeout(resetEventsTimeout);
158-
}
159-
160-
resetEventsTimeout = window.setTimeout(function() {
161-
resetEvents();
162-
}, 250);
97+
function isClick(event) {
98+
return event.type == '$md.pressdown' || event.type == 'click';
16399
}
164100

165101
function resetActionIndex() {
@@ -219,6 +155,9 @@
219155

220156
function enableKeyboard() {
221157
angular.element(document).on('keydown', keyPressed);
158+
159+
// TODO: On desktop, we should be able to reset the indexes so you cannot tab through
160+
//resetActionTabIndexes();
222161
}
223162

224163
function disableKeyboard() {
@@ -245,13 +184,7 @@
245184
}
246185

247186
function focusAction(event, direction) {
248-
// Grab all of the actions
249-
var actions = getActionsElement()[0].querySelectorAll('.md-fab-action-item');
250-
251-
// Disable all other actions for tabbing
252-
angular.forEach(actions, function(action) {
253-
angular.element(angular.element(action).children()[0]).attr('tabindex', -1);
254-
});
187+
var actions = resetActionTabIndexes();
255188

256189
// Increment/decrement the counter with restrictions
257190
vm.currentActionIndex = vm.currentActionIndex + direction;
@@ -268,6 +201,18 @@
268201
event.stopImmediatePropagation();
269202
}
270203

204+
function resetActionTabIndexes() {
205+
// Grab all of the actions
206+
var actions = getActionsElement()[0].querySelectorAll('.md-fab-action-item');
207+
208+
// Disable all other actions for tabbing
209+
angular.forEach(actions, function(action) {
210+
angular.element(angular.element(action).children()[0]).attr('tabindex', -1);
211+
});
212+
213+
return actions;
214+
}
215+
271216
function doKeyLeft(event) {
272217
if (vm.direction === 'left') {
273218
doActionNext(event);

src/components/fabSpeedDial/fabSpeedDial.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@
199199
offsetDelay = index * delay;
200200

201201
styles.opacity = ctrl.isOpen ? 1 : 0;
202-
styles.transform = styles.webkitTransform = ctrl.isOpen ? 'scale(1)' : 'scale(0)';
202+
styles.transform = styles.webkitTransform = ctrl.isOpen ? 'scale(1)' : 'scale(0.1)';
203203
styles.transitionDelay = (ctrl.isOpen ? offsetDelay : (items.length - offsetDelay)) + 'ms';
204204

205205
// Make the items closest to the trigger have the highest z-index

src/components/fabSpeedDial/fabSpeedDial.scss

+1-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ md-fab-speed-dial {
114114
&.md-scale {
115115
.md-fab-action-item {
116116
opacity: 0;
117-
transform: scale(0);
117+
transform: scale(0.1);
118118
transition: $swift-ease-in;
119119

120120
// Make the scale animation a bit faster since we are delaying each item

src/components/fabSpeedDial/fabSpeedDial.spec.js

+3
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ describe('<md-fab-speed-dial> directive', function() {
6868

6969
expect(controller.isOpen).toBe(true);
7070

71+
// Make sure to flush the timeout that ignores other events
72+
$timeout.flush();
73+
7174
// Click to close
7275
element.triggerHandler(clickEvent);
7376
pageScope.$digest();

src/components/tooltip/tooltip.spec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ describe('<md-tooltip> directive', function() {
141141
expect($rootScope.testModel.isVisible).toBe(false);
142142
});
143143

144-
it('should set visible on touchstart and touchend', function() {
144+
xit('should set visible on touchstart and touchend', function() {
145145
buildTooltip(
146146
'<md-button>' +
147147
'Hello' +

0 commit comments

Comments
 (0)