diff --git a/Examples/UIExplorer/TouchableExample.js b/Examples/UIExplorer/TouchableExample.js index 45a679b315a9a5..acbba362921ea0 100644 --- a/Examples/UIExplorer/TouchableExample.js +++ b/Examples/UIExplorer/TouchableExample.js @@ -75,6 +75,14 @@ exports.examples = [ render: function(): ReactElement { return ; }, +}, { + title: 'Touchable delay for events', + description: ' components also accept delayPressIn, ' + + 'delayPressOut, and delayLongPress as props. These props impact the ' + + 'timing of feedback events.', + render: function(): ReactElement { + return ; + }, }]; var TextOnPressBox = React.createClass({ @@ -148,6 +156,44 @@ var TouchableFeedbackEvents = React.createClass({ }, }); +var TouchableDelayEvents = React.createClass({ + getInitialState: function() { + return { + eventLog: [], + }; + }, + render: function() { + return ( + + + this._appendEvent('press')} + delayPressIn={400} + onPressIn={() => this._appendEvent('pressIn - 400ms delay')} + delayPressOut={1000} + onPressOut={() => this._appendEvent('pressOut - 1000ms delay')} + delayLongPress={800} + onLongPress={() => this._appendEvent('longPress - 800ms delay')}> + + Press Me + + + + + {this.state.eventLog.map((e, ii) => {e})} + + + ); + }, + _appendEvent: function(eventName) { + var limit = 6; + var eventLog = this.state.eventLog.slice(0, limit - 1); + eventLog.unshift(eventName); + this.setState({eventLog}); + }, +}); + var heartImage = {uri: 'https://pbs.twimg.com/media/BlXBfT3CQAA6cVZ.png:small'}; var styles = StyleSheet.create({ diff --git a/Libraries/Components/Touchable/TouchableHighlight.js b/Libraries/Components/Touchable/TouchableHighlight.js index 533652f6504c91..dcbfbeee19d91e 100644 --- a/Libraries/Components/Touchable/TouchableHighlight.js +++ b/Libraries/Components/Touchable/TouchableHighlight.js @@ -23,6 +23,7 @@ var View = require('View'); var cloneWithProps = require('cloneWithProps'); var ensureComponentIsNative = require('ensureComponentIsNative'); +var ensurePositiveDelayProps = require('ensurePositiveDelayProps'); var keyOf = require('keyOf'); var merge = require('merge'); var onlyChild = require('onlyChild'); @@ -111,6 +112,7 @@ var TouchableHighlight = React.createClass({ }, componentDidMount: function() { + ensurePositiveDelayProps(this.props); ensureComponentIsNative(this.refs[CHILD_REF]); }, @@ -119,6 +121,7 @@ var TouchableHighlight = React.createClass({ }, componentWillReceiveProps: function(nextProps) { + ensurePositiveDelayProps(nextProps); if (nextProps.activeOpacity !== this.props.activeOpacity || nextProps.underlayColor !== this.props.underlayColor || nextProps.style !== this.props.style) { @@ -152,7 +155,8 @@ var TouchableHighlight = React.createClass({ touchableHandlePress: function() { this.clearTimeout(this._hideTimeout); this._showUnderlay(); - this._hideTimeout = this.setTimeout(this._hideUnderlay, 100); + this._hideTimeout = this.setTimeout(this._hideUnderlay, + this.props.delayPressOut || 100); this.props.onPress && this.props.onPress(); }, @@ -164,6 +168,18 @@ var TouchableHighlight = React.createClass({ return PRESS_RECT_OFFSET; // Always make sure to predeclare a constant! }, + touchableGetHighlightDelayMS: function() { + return this.props.delayPressIn; + }, + + touchableGetLongPressDelayMS: function() { + return this.props.delayLongPress; + }, + + touchableGetPressOutDelayMS: function() { + return this.props.delayPressOut; + }, + _showUnderlay: function() { this.refs[UNDERLAY_REF].setNativeProps(this.state.activeUnderlayProps); this.refs[CHILD_REF].setNativeProps(this.state.activeProps); diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js index d99bf7380af4ae..b13f8522880c66 100644 --- a/Libraries/Components/Touchable/TouchableOpacity.js +++ b/Libraries/Components/Touchable/TouchableOpacity.js @@ -15,11 +15,13 @@ var NativeMethodsMixin = require('NativeMethodsMixin'); var POPAnimationMixin = require('POPAnimationMixin'); var React = require('React'); +var TimerMixin = require('react-timer-mixin'); var Touchable = require('Touchable'); var TouchableWithoutFeedback = require('TouchableWithoutFeedback'); var cloneWithProps = require('cloneWithProps'); var ensureComponentIsNative = require('ensureComponentIsNative'); +var ensurePositiveDelayProps = require('ensurePositiveDelayProps'); var flattenStyle = require('flattenStyle'); var keyOf = require('keyOf'); var onlyChild = require('onlyChild'); @@ -50,7 +52,7 @@ var onlyChild = require('onlyChild'); */ var TouchableOpacity = React.createClass({ - mixins: [Touchable.Mixin, NativeMethodsMixin, POPAnimationMixin], + mixins: [TimerMixin, Touchable.Mixin, NativeMethodsMixin, POPAnimationMixin], propTypes: { ...TouchableWithoutFeedback.propTypes, @@ -72,6 +74,7 @@ var TouchableOpacity = React.createClass({ }, componentDidMount: function() { + ensurePositiveDelayProps(this.props); ensureComponentIsNative(this.refs[CHILD_REF]); }, @@ -79,6 +82,10 @@ var TouchableOpacity = React.createClass({ ensureComponentIsNative(this.refs[CHILD_REF]); }, + componentWillReceiveProps: function(nextProps) { + ensurePositiveDelayProps(nextProps); + }, + setOpacityTo: function(value) { if (POPAnimationMixin) { // Reset with animation if POP is available @@ -102,20 +109,24 @@ var TouchableOpacity = React.createClass({ * defined on your component. */ touchableHandleActivePressIn: function() { - this.refs[CHILD_REF].setNativeProps({ - opacity: this.props.activeOpacity - }); + this.clearTimeout(this._hideTimeout); + this._hideTimeout = null; + this._opacityActive(); this.props.onPressIn && this.props.onPressIn(); }, touchableHandleActivePressOut: function() { - var child = onlyChild(this.props.children); - var childStyle = flattenStyle(child.props.style) || {}; - this.setOpacityTo(childStyle.opacity === undefined ? 1 : childStyle.opacity); + if (!this._hideTimeout) { + this._opacityInactive(); + } this.props.onPressOut && this.props.onPressOut(); }, touchableHandlePress: function() { + this.clearTimeout(this._hideTimeout); + this._opacityActive(); + this._hideTimeout = this.setTimeout(this._opacityInactive, + this.props.delayPressOut || 100); this.props.onPress && this.props.onPress(); }, @@ -128,7 +139,29 @@ var TouchableOpacity = React.createClass({ }, touchableGetHighlightDelayMS: function() { - return 0; + return this.props.delayPressIn || 0; + }, + + touchableGetLongPressDelayMS: function() { + return this.props.delayLongPress === 0 ? 0 : + this.props.delayLongPress || 500; + }, + + touchableGetPressOutDelayMS: function() { + return this.props.delayPressOut; + }, + + _opacityActive: function() { + this.setOpacityTo(this.props.activeOpacity); + }, + + _opacityInactive: function() { + this.clearTimeout(this._hideTimeout); + this._hideTimeout = null; + var child = onlyChild(this.props.children); + var childStyle = flattenStyle(child.props.style) || {}; + this.setOpacityTo(childStyle.opacity === undefined ? 1 : + childStyle.opacity); }, render: function() { diff --git a/Libraries/Components/Touchable/TouchableWithoutFeedback.js b/Libraries/Components/Touchable/TouchableWithoutFeedback.js index cd9ea02fdf1517..3ee1bed8c73cb6 100755 --- a/Libraries/Components/Touchable/TouchableWithoutFeedback.js +++ b/Libraries/Components/Touchable/TouchableWithoutFeedback.js @@ -12,7 +12,9 @@ 'use strict'; var React = require('React'); +var TimerMixin = require('react-timer-mixin'); var Touchable = require('Touchable'); +var ensurePositiveDelayProps = require('ensurePositiveDelayProps'); var onlyChild = require('onlyChild'); /** @@ -31,7 +33,7 @@ type Event = Object; * one of the primary reason a "web" app doesn't feel "native". */ var TouchableWithoutFeedback = React.createClass({ - mixins: [Touchable.Mixin], + mixins: [TimerMixin, Touchable.Mixin], propTypes: { /** @@ -42,12 +44,32 @@ var TouchableWithoutFeedback = React.createClass({ onPressIn: React.PropTypes.func, onPressOut: React.PropTypes.func, onLongPress: React.PropTypes.func, + /** + * Delay in ms, from the start of the touch, before onPressIn is called. + */ + delayPressIn: React.PropTypes.number, + /** + * Delay in ms, from the release of the touch, before onPressOut is called. + */ + delayPressOut: React.PropTypes.number, + /** + * Delay in ms, from onPressIn, before onLongPress is called. + */ + delayLongPress: React.PropTypes.number, }, getInitialState: function() { return this.touchableGetInitialState(); }, + componentDidMount: function() { + ensurePositiveDelayProps(this.props); + }, + + componentWillReceiveProps: function(nextProps: Object) { + ensurePositiveDelayProps(nextProps); + }, + /** * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are * defined on your component. @@ -73,7 +95,16 @@ var TouchableWithoutFeedback = React.createClass({ }, touchableGetHighlightDelayMS: function(): number { - return 0; + return this.props.delayPressIn || 0; + }, + + touchableGetLongPressDelayMS: function(): number { + return this.props.delayLongPress === 0 ? 0 : + this.props.delayLongPress || 500; + }, + + touchableGetPressOutDelayMS: function(): ?number { + return this.props.delayPressOut; }, render: function(): ReactElement { diff --git a/Libraries/Components/Touchable/ensurePositiveDelayProps.js b/Libraries/Components/Touchable/ensurePositiveDelayProps.js new file mode 100644 index 00000000000000..76e5cfe2518a63 --- /dev/null +++ b/Libraries/Components/Touchable/ensurePositiveDelayProps.js @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ensurePositiveDelayProps + * @flow + */ +'use strict'; + +var invariant = require('invariant'); + +var ensurePositiveDelayProps = function(props: any) { + invariant( + !(props.delayPressIn < 0 || props.delayPressOut < 0 || + props.delayLongPress < 0), + 'Touchable components cannot have negative delay properties' + ); +}; + +module.exports = ensurePositiveDelayProps; diff --git a/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js b/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js index 37c42382712627..a57f8739bc01f7 100644 --- a/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js +++ b/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js @@ -89,7 +89,13 @@ var queryLayoutByID = require('queryLayoutByID'); * - There are more advanced methods you can implement (see documentation below): * touchableGetHighlightDelayMS: function() { * return 20; - * } + * }, + * touchableGetLongPressDelayMS: function() { + * return 500; + * }, + * touchableGetPressOutDelayMS: function() { + * return 200; + * }, * // In practice, *always* use a predeclared constant (conserve memory). * touchableGetPressRectOffset: function() { * return {top: 20, left: 20, right: 20, bottom: 100}; @@ -232,6 +238,8 @@ var PRESS_EXPAND_PX = 20; var LONG_PRESS_THRESHOLD = 500; +var LONG_PRESS_DELAY_MS = LONG_PRESS_THRESHOLD - HIGHLIGHT_DELAY_MS; + var LONG_PRESS_ALLOWED_MOVEMENT = 10; // Default amount "active" region protrudes beyond box @@ -276,7 +284,7 @@ var LONG_PRESS_ALLOWED_MOVEMENT = 10; * + * | RESPONDER_GRANT (HitRect) * v - * +---------------------------+ DELAY +-------------------------+ T - DELAY +------------------------------+ + * +---------------------------+ DELAY +-------------------------+ T + DELAY +------------------------------+ * |RESPONDER_INACTIVE_PRESS_IN|+-------->|RESPONDER_ACTIVE_PRESS_IN| +------------> |RESPONDER_ACTIVE_LONG_PRESS_IN| * +---------------------------+ +-------------------------+ +------------------------------+ * + ^ + ^ + ^ @@ -288,7 +296,7 @@ var LONG_PRESS_ALLOWED_MOVEMENT = 10; * |RESPONDER_INACTIVE_PRESS_OUT|+------->|RESPONDER_ACTIVE_PRESS_OUT| |RESPONDER_ACTIVE_LONG_PRESS_OUT| * +----------------------------+ +--------------------------+ +-------------------------------+ * - * T - DELAY => LONG_PRESS_THRESHOLD - DELAY + * T + DELAY => LONG_PRESS_DELAY_MS + DELAY * * Not drawn are the side effects of each transition. The most important side * effect is the `touchableHandlePress` abstract method invocation that occurs @@ -348,12 +356,16 @@ var TouchableMixin = { // event to make sure it doesn't get reused in the event object pool. e.persist(); + this.pressOutDelayTimeout && clearTimeout(this.pressOutDelayTimeout); + this.pressOutDelayTimeout = null; + this.state.touchable.touchState = States.NOT_RESPONDER; this.state.touchable.responderID = dispatchID; this._receiveSignal(Signals.RESPONDER_GRANT, e); var delayMS = this.touchableGetHighlightDelayMS !== undefined ? - this.touchableGetHighlightDelayMS() : HIGHLIGHT_DELAY_MS; + Math.max(this.touchableGetHighlightDelayMS(), 0) : HIGHLIGHT_DELAY_MS; + delayMS = isNaN(delayMS) ? HIGHLIGHT_DELAY_MS : delayMS; if (delayMS !== 0) { this.touchableDelayTimeout = setTimeout( this._handleDelay.bind(this, e), @@ -363,9 +375,13 @@ var TouchableMixin = { this._handleDelay(e); } + var longDelayMS = + this.touchableGetLongPressDelayMS !== undefined ? + Math.max(this.touchableGetLongPressDelayMS(), 10) : LONG_PRESS_DELAY_MS; + longDelayMS = isNaN(longDelayMS) ? LONG_PRESS_DELAY_MS : longDelayMS; this.longPressDelayTimeout = setTimeout( this._handleLongDelay.bind(this, e), - LONG_PRESS_THRESHOLD - delayMS + longDelayMS + delayMS ); }, @@ -632,8 +648,14 @@ var TouchableMixin = { if (newIsHighlight && !curIsHighlight) { this._savePressInLocation(e); this.touchableHandleActivePressIn && this.touchableHandleActivePressIn(); - } else if (!newIsHighlight && curIsHighlight) { - this.touchableHandleActivePressOut && this.touchableHandleActivePressOut(); + } else if (!newIsHighlight && curIsHighlight && this.touchableHandleActivePressOut) { + if (this.touchableGetPressOutDelayMS && this.touchableGetPressOutDelayMS()) { + this.pressOutDelayTimeout = this.setTimeout(function() { + this.touchableHandleActivePressOut(); + }, this.touchableGetPressOutDelayMS()); + } else { + this.touchableHandleActivePressOut(); + } } if (IsPressingIn[curState] && signal === Signals.RESPONDER_RELEASE) {