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..919cf63e8e13a --- /dev/null +++ b/src/defaultClickRejectionStrategy.js @@ -0,0 +1,5 @@ +module.exports = function(lastTouchEvent, clickTimestamp) { + if (lastTouchEvent && (clickTimestamp - lastTouchEvent) < 750) { + return true; + } +}; 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) }); };