From f0c70ffe9240d0b7838a906b192419cdde1c1666 Mon Sep 17 00:00:00 2001 From: Tom Hicks Date: Tue, 26 Jan 2016 09:11:49 +0000 Subject: [PATCH 1/2] Allow custom click rejection strategy --- README.md | 28 +++++++ src/TapEventPlugin.js | 108 ++++++++++++++------------- src/defaultClickRejectionStrategy.js | 5 ++ src/injectTapEventPlugin.js | 9 ++- 4 files changed, 95 insertions(+), 55 deletions(-) create mode 100644 src/defaultClickRejectionStrategy.js diff --git a/README.md b/README.md index 430424a8aa577..7cb9e65ae4014 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,34 @@ var Main = React.createClass({ ReactDOM.render(
, document.getElementById("container")); ``` +### Ignoring ghost clicks + +When a tap happens, the browser sends a `touchstart` and `touchend`, and then +300ms later, a `click` event. This plugin ignores the click event if it has +been immediately preceeded by a touch event (within 750ms of the last touch +event). + +Occasionally, there may be times when the 750ms threshold is exceeded due to +slow rendering or garbage collection, and this causes the dreaded ghost click. + +The 750ms threshold is pretty good, but sometimes you might want to override +that behaviour. You can do this by supplying your own `shouldRejectClick` +function when you inject the plugin. + +The following example will simply reject all click events, which you might +want to do if you are always using `onTouchTap` and only building for touch +devices: + +```js +var React = require('react'), +injectTapEventPlugin = require("react-tap-event-plugin"); +injectTapEventPlugin({ + shouldRejectClick: function (lastTouchEventTimestamp, clickEventTimestamp) { + return true; + } +}); +``` + ## Build standalone version Use the demo project and it's README instructions to build a version of React with the tap event plugin included. diff --git a/src/TapEventPlugin.js b/src/TapEventPlugin.js index e0b1a457f75ca..0513517c1bc12 100644 --- a/src/TapEventPlugin.js +++ b/src/TapEventPlugin.js @@ -109,61 +109,63 @@ var now = (function() { } })(); -var TapEventPlugin = { - - tapMoveThreshold: tapMoveThreshold, - - ignoreMouseThreshold: ignoreMouseThreshold, - - eventTypes: eventTypes, - - /** - * @param {string} topLevelType Record from `EventConstants`. - * @param {DOMEventTarget} topLevelTarget The listening component root node. - * @param {string} topLevelTargetID ID of `topLevelTarget`. - * @param {object} nativeEvent Native browser event. - * @return {*} An accumulation of synthetic events. - * @see {EventPluginHub.extractEvents} - */ - extractEvents: function( - topLevelType, - topLevelTarget, - topLevelTargetID, - nativeEvent, - nativeEventTarget) { - - if (isTouch(topLevelType)) { - lastTouchEvent = now(); - } else { - if (lastTouchEvent && (now() - lastTouchEvent) < ignoreMouseThreshold) { - return null; - } - } - - if (!isStartish(topLevelType) && !isEndish(topLevelType)) { - return null; - } - var event = null; - var distance = getDistance(startCoords, nativeEvent); - if (isEndish(topLevelType) && distance < tapMoveThreshold) { - event = SyntheticUIEvent.getPooled( - eventTypes.touchTap, +function createTapEventPlugin(shouldRejectClick) { + return { + + tapMoveThreshold: tapMoveThreshold, + + ignoreMouseThreshold: ignoreMouseThreshold, + + eventTypes: eventTypes, + + /** + * @param {string} topLevelType Record from `EventConstants`. + * @param {DOMEventTarget} topLevelTarget The listening component root node. + * @param {string} topLevelTargetID ID of `topLevelTarget`. + * @param {object} nativeEvent Native browser event. + * @return {*} An accumulation of synthetic events. + * @see {EventPluginHub.extractEvents} + */ + extractEvents: function( + topLevelType, + topLevelTarget, topLevelTargetID, nativeEvent, - nativeEventTarget - ); - } - if (isStartish(topLevelType)) { - startCoords.x = getAxisCoordOfEvent(Axis.x, nativeEvent); - startCoords.y = getAxisCoordOfEvent(Axis.y, nativeEvent); - } else if (isEndish(topLevelType)) { - startCoords.x = 0; - startCoords.y = 0; + nativeEventTarget) { + + if (isTouch(topLevelType)) { + lastTouchEvent = now(); + } else { + if (shouldRejectClick(lastTouchEvent, now())) { + return null; + } + } + + if (!isStartish(topLevelType) && !isEndish(topLevelType)) { + return null; + } + var event = null; + var distance = getDistance(startCoords, nativeEvent); + if (isEndish(topLevelType) && distance < tapMoveThreshold) { + event = SyntheticUIEvent.getPooled( + eventTypes.touchTap, + topLevelTargetID, + nativeEvent, + nativeEventTarget + ); + } + if (isStartish(topLevelType)) { + startCoords.x = getAxisCoordOfEvent(Axis.x, nativeEvent); + startCoords.y = getAxisCoordOfEvent(Axis.y, nativeEvent); + } else if (isEndish(topLevelType)) { + startCoords.x = 0; + startCoords.y = 0; + } + EventPropagators.accumulateTwoPhaseDispatches(event); + return event; } - EventPropagators.accumulateTwoPhaseDispatches(event); - return event; - } -}; + }; +} -module.exports = TapEventPlugin; +module.exports = createTapEventPlugin; diff --git a/src/defaultClickRejectionStrategy.js b/src/defaultClickRejectionStrategy.js new file mode 100644 index 0000000000000..5dbfe6e6121cb --- /dev/null +++ b/src/defaultClickRejectionStrategy.js @@ -0,0 +1,5 @@ +module.exports = function(lastTouchEvent, clickTimestamp) { + if (lastTouchEvent && (clickTimestamp - lastTouchEvent) < 750) { + return null; + } +}; diff --git a/src/injectTapEventPlugin.js b/src/injectTapEventPlugin.js index c9c9d866ef4fd..9d057c975de58 100644 --- a/src/injectTapEventPlugin.js +++ b/src/injectTapEventPlugin.js @@ -1,5 +1,10 @@ -module.exports = function injectTapEventPlugin () { +var defaultClickRejectionStrategy = require("./defaultClickRejectionStrategy"); + +module.exports = function injectTapEventPlugin (strategyOverrides) { + strategyOverrides = strategyOverrides || {} + var shouldRejectClick = strategyOverrides.shouldRejectClick || defaultClickRejectionStrategy; + require('react/lib/EventPluginHub').injection.injectEventPluginsByName({ - "TapEventPlugin": require('./TapEventPlugin.js') + "TapEventPlugin": require('./TapEventPlugin.js')(shouldRejectClick) }); }; From f7c59f0eef4ed611b649bb0a3159352ecc24dc00 Mon Sep 17 00:00:00 2001 From: Tom Hicks Date: Fri, 29 Jan 2016 08:58:32 +0000 Subject: [PATCH 2/2] Fix default rejection strategy It needs to return true so the calling function can return null. --- src/defaultClickRejectionStrategy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/defaultClickRejectionStrategy.js b/src/defaultClickRejectionStrategy.js index 5dbfe6e6121cb..919cf63e8e13a 100644 --- a/src/defaultClickRejectionStrategy.js +++ b/src/defaultClickRejectionStrategy.js @@ -1,5 +1,5 @@ module.exports = function(lastTouchEvent, clickTimestamp) { if (lastTouchEvent && (clickTimestamp - lastTouchEvent) < 750) { - return null; + return true; } };