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

Commit

Permalink
feat(mdGestures): greatly improve emulated click hijacking
Browse files Browse the repository at this point in the history
This commit greatly extends 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` previously `touchstart` and `touchend`
events and manually dispatches `click` events. This allows them to
happen 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)

**Soltuion:**: This PR makes `$mdGesture` perform hijacking of
`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
  • Loading branch information
rschmukler authored and ThomasBurleson committed Oct 16, 2015
1 parent 2ff4f9b commit 446df80
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 53 deletions.
2 changes: 2 additions & 0 deletions docs/app/js/preload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Used only to optionally test
// jQuery w/ docs (see docs/gulpfile#docs-js)
52 changes: 7 additions & 45 deletions src/components/fabSpeedDial/fabController.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@

function setupListeners() {
var eventTypes = [
'mousedown', 'mouseup', 'click', 'touchstart', 'touchend', 'focusin', 'focusout'
'click', 'focusin', 'focusout'
];

// Add our listeners
Expand Down Expand Up @@ -111,55 +111,17 @@
});
}

var lastSrc;
function parseEvents(latestEvent) {
events.push(latestEvent.type);

// Handle desktop click
if (equalsEvents(['mousedown', 'focusout?', 'focusin?', 'mouseup', 'click'])) {
handleItemClick(latestEvent);
resetEvents();
return;
}

// Handle mobile click/tap (and keyboard enter)
if (equalsEvents(['touchstart?', 'touchend?', 'click'])) {
if (latestEvent.srcEvent && latestEvent.srcEvent == lastSrc) return;
if (latestEvent.type == 'click') {
handleItemClick(latestEvent);
resetEvents();
return;
}

// Handle tab keys (focusin)
if (equalsEvents(['focusin'])) {
} else if (latestEvent.type == 'focusin') {
vm.open();
resetEvents();
return;
}

// Handle tab keys (focusout)
if (equalsEvents(['focusout'])) {
} else if (latestEvent.type == 'focusout') {
vm.close();
resetEvents();
return;
}

eventUnhandled();
}

/*
* No event was handled, so setup a timeout to clear the events
*
* TODO: Use $mdUtil.debounce()?
*/
var resetEventsTimeout;

function eventUnhandled() {
if (resetEventsTimeout) {
window.clearTimeout(resetEventsTimeout);
}

resetEventsTimeout = window.setTimeout(function() {
resetEvents();
}, 250);
lastSrc = latestEvent.srcEvent;
}

function resetActionIndex() {
Expand Down
6 changes: 3 additions & 3 deletions src/components/tooltip/tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,13 +168,13 @@ function MdTooltipDirective($timeout, $window, $$rAF, $document, $mdUtil, $mdThe
elementFocusedOnWindowBlur = false;
return;
}
parent.on('blur mouseleave touchend touchcancel', leaveHandler );
parent.on('blur mouseleave touchcancel', leaveHandler );
setVisible(true);
};
var leaveHandler = function () {
var leaveHandler = function (e) {
var autohide = scope.hasOwnProperty('autohide') ? scope.autohide : attr.hasOwnProperty('mdAutohide');
if (autohide || mouseActive || ($document[0].activeElement !== parent[0]) ) {
parent.off('blur mouseleave touchend touchcancel', leaveHandler );
parent.off('blur mouseleave touchcancel', leaveHandler );
parent.triggerHandler("blur");
setVisible(false);
}
Expand Down
76 changes: 71 additions & 5 deletions src/core/services/gesture/gesture.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,55 @@
};

if (self.isHijackingClicks) {
var maxClickDistance = 6;
self.handler('click', {
options: {
maxDistance: 6
maxDistance: maxClickDistance
},
onEnd: function (ev, pointer) {
onEnd: checkDistanceAndEmit('click')
});

self.handler('focus', {
options: {
maxDistance: maxClickDistance
},
onEnd: function(ev, pointer) {
if (pointer.distance < this.state.options.maxDistance) {
this.dispatchEvent(ev, 'click');
if (canFocus(ev.target)) {
this.dispatchEvent(ev, 'focus', pointer);
ev.target.focus();
}
}

function canFocus(element) {
return element.hasAttribute('href') ||
(( element.nodeName == 'INPUT' || element.nodeName == 'BUTTON' ||
element.nodeName == 'SELECT' || element.nodeName == 'TEXTAREA' ) && !element.hasAttribute('DISABLED')) ||
( element.hasAttribute('tabindex') && element.getAttribute('tabindex') != '-1' );
}
}
});

self.handler('mouseup', {
options: {
maxDistance: maxClickDistance
},
onEnd: checkDistanceAndEmit('mouseup')
});

self.handler('mousedown', {
onStart: function(ev) {
this.dispatchEvent(ev, 'mousedown');
}
});
}

function checkDistanceAndEmit(eventName) {
return function(ev, pointer) {
if (pointer.distance < this.state.options.maxDistance) {
this.dispatchEvent(ev, eventName, pointer);
}
};
}

/*
Expand Down Expand Up @@ -407,10 +446,10 @@
eventPointer = eventPointer || pointer;
var eventObj;

if (eventType === 'click') {
if (eventType === 'click' || eventType == 'mouseup' || eventType == 'mousedown' ) {
eventObj = document.createEvent('MouseEvents');
eventObj.initMouseEvent(
'click', true, true, window, srcEvent.detail,
eventType, true, true, window, srcEvent.detail,
eventPointer.x, eventPointer.y, eventPointer.x, eventPointer.y,
srcEvent.ctrlKey, srcEvent.altKey, srcEvent.shiftKey, srcEvent.metaKey,
srcEvent.button, srcEvent.relatedTarget || null
Expand Down Expand Up @@ -466,6 +505,33 @@
}
}
}, true);

document.addEventListener('mouseup', function clickHijacker(ev) {
var isKeyClick = !ev.clientX && !ev.clientY;
if (!isKeyClick && !ev.$material && !ev.isIonicTap
&& !isInputEventFromLabelClick(ev)) {
ev.preventDefault();
ev.stopPropagation();
}
}, true);

document.addEventListener('mousedown', function clickHijacker(ev) {
var isKeyClick = !ev.clientX && !ev.clientY;
if (!isKeyClick && !ev.$material && !ev.isIonicTap
&& !isInputEventFromLabelClick(ev)) {
ev.preventDefault();
ev.stopPropagation();
}
}, true);

document.addEventListener('focus', function clickHijacker(ev) {
var isKeyClick = !ev.clientX && !ev.clientY;
if (!isKeyClick && !ev.$material && !ev.isIonicTap
&& !isInputEventFromLabelClick(ev)) {
ev.preventDefault();
ev.stopPropagation();
}
}, true);

isInitialized = true;
}
Expand Down

0 comments on commit 446df80

Please sign in to comment.