diff --git a/Examples/UIExplorer/TouchableExample.js b/Examples/UIExplorer/TouchableExample.js index 45a679b315a9a5..16711f08d617af 100644 --- a/Examples/UIExplorer/TouchableExample.js +++ b/Examples/UIExplorer/TouchableExample.js @@ -26,7 +26,7 @@ var { View, } = React; -exports.title = ' and onPress'; +exports.title = ', onPress, and delayPress'; exports.examples = [ { title: '', @@ -75,6 +75,14 @@ exports.examples = [ render: function(): ReactElement { return ; }, +}, { + title: 'Touchable delay for events', + description: ' components also accept delayPress, ' + + '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,45 @@ var TouchableFeedbackEvents = React.createClass({ }, }); +var TouchableDelayEvents = React.createClass({ + getInitialState: function() { + return { + eventLog: [], + }; + }, + render: function() { + return ( + + + this._appendEvent('press - 200ms delay')} + delayPressIn={0} + onPressIn={() => this._appendEvent('pressIn - 0ms 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..96b5a248fb6dd6 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'); @@ -32,6 +33,8 @@ var DEFAULT_PROPS = { underlayColor: 'black', }; +var DEFAULT_HIDE_MS = 100; + /** * A wrapper for making views respond properly to touches. * On press down, the opacity of the wrapped view is decreased, which allows @@ -111,10 +114,12 @@ var TouchableHighlight = React.createClass({ }, componentDidMount: function() { + ensurePositiveDelayProps(this.props); ensureComponentIsNative(this.refs[CHILD_REF]); }, componentDidUpdate: function() { + ensurePositiveDelayProps(this.props); ensureComponentIsNative(this.refs[CHILD_REF]); }, @@ -136,13 +141,21 @@ var TouchableHighlight = React.createClass({ * defined on your component. */ touchableHandleActivePressIn: function() { - this.clearTimeout(this._hideTimeout); - this._hideTimeout = null; this._showUnderlay(); this.props.onPressIn && this.props.onPressIn(); }, touchableHandleActivePressOut: function() { + if (this.props.delayPressOut) { + this._onPressOutTimeout = this.setTimeout(function() { + this._onPressOut(); + }, this.props.delayPressOut); + } else { + this._onPressOut(); + } + }, + + _onPressOut: function() { if (!this._hideTimeout) { this._hideUnderlay(); } @@ -150,9 +163,23 @@ var TouchableHighlight = React.createClass({ }, touchableHandlePress: function() { - this.clearTimeout(this._hideTimeout); + if (this.props.delayPress) { + if (!this._onPressTimeout) { + this._onPressTimeout = this.setTimeout(function() { + this.clearTimeout(this._onPressTimeout); + this._onPressTimeout = null; + this._onPress(); + }, this.props.delayPress); + } + } else { + this._onPress(); + } + }, + + _onPress: function() { this._showUnderlay(); - this._hideTimeout = this.setTimeout(this._hideUnderlay, 100); + this._hideTimeout = this.setTimeout(this._hideUnderlay, + this.props.delayPressOut || DEFAULT_HIDE_MS); this.props.onPress && this.props.onPress(); }, @@ -164,6 +191,14 @@ 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; + }, + _showUnderlay: function() { this.refs[UNDERLAY_REF].setNativeProps(this.state.activeUnderlayProps); this.refs[CHILD_REF].setNativeProps(this.state.activeProps); @@ -183,6 +218,14 @@ var TouchableHighlight = React.createClass({ } }, + _componentHandleResponderGrant: function(e, dispatchID) { + this.clearTimeout(this._onPressOutTimeout); + this._onPressOutTimeout = null; + this.clearTimeout(this._hideTimeout); + this._hideTimeout = null; + this.touchableHandleResponderGrant(e, dispatchID); + }, + render: function() { return ( diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js index d99bf7380af4ae..f9e8c2a4adf49d 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,10 +74,12 @@ var TouchableOpacity = React.createClass({ }, componentDidMount: function() { + ensurePositiveDelayProps(this.props); ensureComponentIsNative(this.refs[CHILD_REF]); }, componentDidUpdate: function() { + ensurePositiveDelayProps(this.props); ensureComponentIsNative(this.refs[CHILD_REF]); }, @@ -102,20 +106,45 @@ var TouchableOpacity = React.createClass({ * defined on your component. */ touchableHandleActivePressIn: function() { - this.refs[CHILD_REF].setNativeProps({ - opacity: this.props.activeOpacity - }); + this._fromPressIn = true; + 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); - this.props.onPressOut && this.props.onPressOut(); + if (this.props.delayPressOut) { + this._onPressOutTimeout = this.setTimeout(function() { + this._opacityInactive(); + this.props.onPressOut && this.props.onPressOut(); + }, this.props.delayPressOut); + } else { + this._opacityInactive(); + this.props.onPressOut && this.props.onPressOut(); + } }, touchableHandlePress: function() { + if (this.props.delayPress) { + if (!this._onPressTimeout) { + this._onPressTimeout = this.setTimeout(function() { + this.clearTimeout(this._onPressTimeout); + this._onPressTimeout = null; + this._onPress(); + }, this.props.delayPress); + } + } else { + this._onPress(); + } + }, + + _onPress: function() { + if (!this._fromPressIn) { + this._opacityActive(); + this._hideTimeout = this.setTimeout(this._opacityInactive, + this.props.delayPressOut || 100); + } this.props.onPress && this.props.onPress(); }, @@ -128,7 +157,34 @@ 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; + }, + + _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); + }, + + _componentHandleResponderGrant: function(e, dispatchID) { + this._fromPressIn = false; + this.clearTimeout(this._onPressOutTimeout); + this._onPressOutTimeout = null; + this.clearTimeout(this._hideTimeout); + this._hideTimeout = null; + this.touchableHandleResponderGrant(e, dispatchID); }, render: function() { @@ -138,7 +194,7 @@ var TouchableOpacity = React.createClass({ testID: this.props.testID, onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder, onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest, - onResponderGrant: this.touchableHandleResponderGrant, + onResponderGrant: this._componentHandleResponderGrant, onResponderMove: this.touchableHandleResponderMove, onResponderRelease: this.touchableHandleResponderRelease, onResponderTerminate: this.touchableHandleResponderTerminate, diff --git a/Libraries/Components/Touchable/TouchableWithoutFeedback.js b/Libraries/Components/Touchable/TouchableWithoutFeedback.js index cd9ea02fdf1517..cf436cbc62dbdf 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,18 +44,52 @@ var TouchableWithoutFeedback = React.createClass({ onPressIn: React.PropTypes.func, onPressOut: React.PropTypes.func, onLongPress: React.PropTypes.func, + /** + * Delay in ms, from the release of the touch, before onPress is called. + */ + delayPress: React.PropTypes.number, + /** + * 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); + }, + + componentDidUpdate: function() { + ensurePositiveDelayProps(this.props); + }, + /** * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are * defined on your component. */ touchableHandlePress: function(e: Event) { - this.props.onPress && this.props.onPress(e); + if (this.props.delayPress && this.props.onPress) { + if (!this._onPressTimeout) { + this._onPressTimeout = this.setTimeout(function() { + this.clearTimeout(this._onPressTimeout); + this._onPressTimeout = null; + this.props.onPress(e); + }, this.props.delayPress); + } + } else { + this.props.onPress && this.props.onPress(e); + } }, touchableHandleActivePressIn: function() { @@ -61,7 +97,13 @@ var TouchableWithoutFeedback = React.createClass({ }, touchableHandleActivePressOut: function() { - this.props.onPressOut && this.props.onPressOut(); + if (this.props.delayPressOut && this.props.onPressOut) { + this._onPressOutTimeout = this.setTimeout(function() { + this.props.onPressOut(); + }, this.props.delayPressOut); + } else { + this.props.onPressOut && this.props.onPressOut(); + } }, touchableHandleLongPress: function() { @@ -73,7 +115,18 @@ 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; + }, + + _componentHandleResponderGrant: function(e: Event, dispatchID: any) { + this.clearTimeout(this._onPressOutTimeout); + this._onPressOutTimeout = null; + this.touchableHandleResponderGrant(e, dispatchID); }, render: function(): ReactElement { @@ -83,7 +136,7 @@ var TouchableWithoutFeedback = React.createClass({ testID: this.props.testID, onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder, onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest, - onResponderGrant: this.touchableHandleResponderGrant, + onResponderGrant: this._componentHandleResponderGrant, onResponderMove: this.touchableHandleResponderMove, onResponderRelease: this.touchableHandleResponderRelease, onResponderTerminate: this.touchableHandleResponderTerminate diff --git a/Libraries/Components/Touchable/ensurePositiveDelayProps.js b/Libraries/Components/Touchable/ensurePositiveDelayProps.js new file mode 100644 index 00000000000000..74fdf7dc069805 --- /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.delayPress < 0 || 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..1c06047b1d548d 100644 --- a/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js +++ b/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js @@ -232,6 +232,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 +278,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 +290,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 @@ -353,7 +355,8 @@ var TouchableMixin = { 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 +366,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 ); },