From 3c4d7655db65c3ffaefda4b81a94dc5a7b33bfa6 Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Tue, 15 Mar 2016 14:18:08 -0700 Subject: [PATCH] [fix] Touchable: adapt RN touchables for Web --- src/components/Touchable/Touchable.js | 16 +- src/components/Touchable/TouchableBounce.js | 164 +++++++++++ .../Touchable/TouchableHighlight.js | 278 ++++++++++++++++++ src/components/Touchable/TouchableOpacity.js | 200 +++++++++++++ .../Touchable/TouchableWithoutFeedback.js | 166 +++++++++++ .../Touchable/__tests__/index-test.js | 32 +- .../Touchable/ensureComponentIsNative.js | 25 ++ .../Touchable/ensurePositiveDelayProps.js | 25 ++ src/components/Touchable/index.js | 131 --------- 9 files changed, 869 insertions(+), 168 deletions(-) create mode 100644 src/components/Touchable/TouchableBounce.js create mode 100644 src/components/Touchable/TouchableHighlight.js create mode 100644 src/components/Touchable/TouchableOpacity.js create mode 100644 src/components/Touchable/TouchableWithoutFeedback.js create mode 100644 src/components/Touchable/ensureComponentIsNative.js create mode 100644 src/components/Touchable/ensurePositiveDelayProps.js delete mode 100644 src/components/Touchable/index.js diff --git a/src/components/Touchable/Touchable.js b/src/components/Touchable/Touchable.js index 5f07831d..d9903e1a 100644 --- a/src/components/Touchable/Touchable.js +++ b/src/components/Touchable/Touchable.js @@ -340,7 +340,7 @@ var TouchableMixin = { * Must return true to start the process of `Touchable`. */ touchableHandleStartShouldSetResponder: function() { - return true; + return !this.props.disabled; }, /** @@ -558,10 +558,10 @@ var TouchableMixin = { * @sideeffects * @private */ - _remeasureMetricsOnActivation: function() { + _remeasureMetricsOnActivation: function(e) { /* @edit begin */ UIManager.measure( - this.state.touchable.responderID, + e.nativeEvent.target, this._handleQueryLayout ); /* @edit end */ @@ -603,18 +603,22 @@ var TouchableMixin = { * @sideeffects */ _receiveSignal: function(signal, e) { + var responderID = this.state.touchable.responderID; var curState = this.state.touchable.touchState; var nextState = Transitions[curState] && Transitions[curState][signal]; + if (!responderID && signal === Signals.RESPONDER_RELEASE) { + return; + } if (!nextState) { throw new Error( 'Unrecognized signal `' + signal + '` or state `' + curState + - '` for Touchable responder `' + this.state.touchable.responderID + '`' + '` for Touchable responder `' + responderID + '`' ); } if (nextState === States.ERROR) { throw new Error( 'Touchable cannot transition from `' + curState + '` to `' + signal + - '` for responder `' + this.state.touchable.responderID + '`' + '` for responder `' + responderID + '`' ); } if (curState !== nextState) { @@ -672,7 +676,7 @@ var TouchableMixin = { } if (!IsActive[curState] && IsActive[nextState]) { - this._remeasureMetricsOnActivation(); + this._remeasureMetricsOnActivation(e); } if (IsPressingIn[curState] && signal === Signals.LONG_PRESS_DETECTED) { diff --git a/src/components/Touchable/TouchableBounce.js b/src/components/Touchable/TouchableBounce.js new file mode 100644 index 00000000..7aec0b39 --- /dev/null +++ b/src/components/Touchable/TouchableBounce.js @@ -0,0 +1,164 @@ +/* eslint-disable */ +/** + * 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 TouchableBounce + * @flow + */ +'use strict'; + +var Animated = require('../../apis/Animated'); +var EdgeInsetsPropType = require('../../apis/StyleSheet/EdgeInsetsPropType'); +var NativeMethodsMixin = require('../../modules/NativeMethodsMixin'); +var React = require('react'); +var StyleSheet = require('../../apis/StyleSheet'); +var Touchable = require('./Touchable'); + +type Event = Object; + +type State = { + animationID: ?number; + scale: Animated.Value; +}; + +var PRESS_RETENTION_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; + +/** + * Example of using the `TouchableMixin` to play well with other responder + * locking views including `ScrollView`. `TouchableMixin` provides touchable + * hooks (`this.touchableHandle*`) that we forward events to. In turn, + * `TouchableMixin` expects us to implement some abstract methods to handle + * interesting interactions such as `handleTouchablePress`. + */ +var TouchableBounce = React.createClass({ + mixins: [Touchable.Mixin, NativeMethodsMixin], + + propTypes: { + onPress: React.PropTypes.func, + onPressIn: React.PropTypes.func, + onPressOut: React.PropTypes.func, + // The function passed takes a callback to start the animation which should + // be run after this onPress handler is done. You can use this (for example) + // to update UI before starting the animation. + onPressWithCompletion: React.PropTypes.func, + // the function passed is called after the animation is complete + onPressAnimationComplete: React.PropTypes.func, + /** + * When the scroll view is disabled, this defines how far your touch may + * move off of the button, before deactivating the button. Once deactivated, + * try moving it back and you'll see that the button is once again + * reactivated! Move it back and forth several times while the scroll view + * is disabled. Ensure you pass in a constant to reduce memory allocations. + */ + pressRetentionOffset: EdgeInsetsPropType, + /** + * This defines how far your touch can start away from the button. This is + * added to `pressRetentionOffset` when moving off of the button. + * ** NOTE ** + * The touch area never extends past the parent view bounds and the Z-index + * of sibling views always takes precedence if a touch hits two overlapping + * views. + */ + hitSlop: EdgeInsetsPropType, + }, + + getInitialState: function(): State { + return { + ...this.touchableGetInitialState(), + scale: new Animated.Value(1), + }; + }, + + bounceTo: function( + value: number, + velocity: number, + bounciness: number, + callback?: ?Function + ) { + Animated.spring(this.state.scale, { + toValue: value, + velocity, + bounciness, + }).start(callback); + }, + + /** + * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are + * defined on your component. + */ + touchableHandleActivePressIn: function(e: Event) { + this.bounceTo(0.93, 0.1, 0); + this.props.onPressIn && this.props.onPressIn(e); + }, + + touchableHandleActivePressOut: function(e: Event) { + this.bounceTo(1, 0.4, 0); + this.props.onPressOut && this.props.onPressOut(e); + }, + + touchableHandlePress: function(e: Event) { + var onPressWithCompletion = this.props.onPressWithCompletion; + if (onPressWithCompletion) { + onPressWithCompletion(() => { + this.state.scale.setValue(0.93); + this.bounceTo(1, 10, 10, this.props.onPressAnimationComplete); + }); + return; + } + + this.bounceTo(1, 10, 10, this.props.onPressAnimationComplete); + this.props.onPress && this.props.onPress(e); + }, + + touchableGetPressRectOffset: function(): typeof PRESS_RETENTION_OFFSET { + return this.props.pressRetentionOffset || PRESS_RETENTION_OFFSET; + }, + + touchableGetHitSlop: function(): ?Object { + return this.props.hitSlop; + }, + + touchableGetHighlightDelayMS: function(): number { + return 0; + }, + + render: function(): ReactElement { + const scaleTransform = [{ scale: this.state.scale }]; + const propsTransform = this.props.style.transform; + const transform = propsTransform && Array.isArray(propsTransform) ? propsTransform.concat(scaleTransform) : scaleTransform; + + return ( + + {this.props.children} + + ); + } +}); + +const styles = StyleSheet.create({ + root: { + cursor: 'pointer', + userSelect: 'none' + } +}); + +module.exports = TouchableBounce; diff --git a/src/components/Touchable/TouchableHighlight.js b/src/components/Touchable/TouchableHighlight.js new file mode 100644 index 00000000..5f2594c1 --- /dev/null +++ b/src/components/Touchable/TouchableHighlight.js @@ -0,0 +1,278 @@ +/* eslint-disable */ +/** + * 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 TouchableHighlight + * @noflow + */ +'use strict'; + +// Note (avik): add @flow when Flow supports spread properties in propTypes + +var ColorPropType = require('../../apis/StyleSheet/ColorPropType'); +var NativeMethodsMixin = require('../../modules/NativeMethodsMixin'); +var React = require('react'); +var StyleSheet = require('../../apis/StyleSheet'); +var TimerMixin = require('react-timer-mixin'); +var Touchable = require('./Touchable'); +var TouchableWithoutFeedback = require('./TouchableWithoutFeedback'); +var View = require('../View'); + +var ensureComponentIsNative = require('./ensureComponentIsNative'); +var ensurePositiveDelayProps = require('./ensurePositiveDelayProps'); +var keyOf = require('fbjs/lib/keyOf'); +var merge = require('../../modules/merge'); + +type Event = Object; + +var DEFAULT_PROPS = { + activeOpacity: 0.8, + underlayColor: 'black', +}; + +var PRESS_RETENTION_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; + +/** + * A wrapper for making views respond properly to touches. + * On press down, the opacity of the wrapped view is decreased, which allows + * the underlay color to show through, darkening or tinting the view. The + * underlay comes from adding a view to the view hierarchy, which can sometimes + * cause unwanted visual artifacts if not used correctly, for example if the + * backgroundColor of the wrapped view isn't explicitly set to an opaque color. + * + * Example: + * + * ``` + * renderButton: function() { + * return ( + * + * + * + * ); + * }, + * ``` + * > **NOTE**: TouchableHighlight supports only one child + * > + * > If you wish to have several child components, wrap them in a View. + */ + +var TouchableHighlight = React.createClass({ + propTypes: { + ...TouchableWithoutFeedback.propTypes, + /** + * Determines what the opacity of the wrapped view should be when touch is + * active. + */ + activeOpacity: React.PropTypes.number, + /** + * The color of the underlay that will show through when the touch is + * active. + */ + underlayColor: ColorPropType, + style: View.propTypes.style, + /** + * Called immediately after the underlay is shown + */ + onShowUnderlay: React.PropTypes.func, + /** + * Called immediately after the underlay is hidden + */ + onHideUnderlay: React.PropTypes.func, + }, + + mixins: [NativeMethodsMixin, TimerMixin, Touchable.Mixin], + + getDefaultProps: () => DEFAULT_PROPS, + + // Performance optimization to avoid constantly re-generating these objects. + computeSyntheticState: function(props) { + return { + activeProps: { + style: { + opacity: props.activeOpacity, + } + }, + activeUnderlayProps: { + style: { + backgroundColor: props.underlayColor, + } + }, + underlayStyle: [ + INACTIVE_UNDERLAY_PROPS.style + ] + }; + }, + + getInitialState: function() { + return merge(this.touchableGetInitialState(), this.computeSyntheticState(this.props)) + }, + + componentDidMount: function() { + ensurePositiveDelayProps(this.props); + ensureComponentIsNative(this.refs[CHILD_REF]); + }, + + componentDidUpdate: function() { + ensureComponentIsNative(this.refs[CHILD_REF]); + }, + + componentWillReceiveProps: function(nextProps) { + ensurePositiveDelayProps(nextProps); + if (nextProps.activeOpacity !== this.props.activeOpacity || + nextProps.underlayColor !== this.props.underlayColor || + nextProps.style !== this.props.style) { + this.setState(this.computeSyntheticState(nextProps)); + } + }, + + // viewConfig: { + // uiViewClassName: 'RCTView', + // validAttributes: ReactNativeViewAttributes.RCTView + // }, + + /** + * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are + * defined on your component. + */ + touchableHandleActivePressIn: function(e: Event) { + this.clearTimeout(this._hideTimeout); + this._hideTimeout = null; + this._showUnderlay(); + this.props.onPressIn && this.props.onPressIn(e); + }, + + touchableHandleActivePressOut: function(e: Event) { + if (!this._hideTimeout) { + this._hideUnderlay(); + } + this.props.onPressOut && this.props.onPressOut(e); + }, + + touchableHandlePress: function(e: Event) { + this.clearTimeout(this._hideTimeout); + this._showUnderlay(); + this._hideTimeout = this.setTimeout(this._hideUnderlay, + this.props.delayPressOut || 100); + this.props.onPress && this.props.onPress(e); + }, + + touchableHandleLongPress: function(e: Event) { + this.props.onLongPress && this.props.onLongPress(e); + }, + + touchableGetPressRectOffset: function() { + return this.props.pressRetentionOffset || PRESS_RETENTION_OFFSET; + }, + + touchableGetHitSlop: function() { + return this.props.hitSlop; + }, + + touchableGetHighlightDelayMS: function() { + return this.props.delayPressIn; + }, + + touchableGetLongPressDelayMS: function() { + return this.props.delayLongPress; + }, + + touchableGetPressOutDelayMS: function() { + return this.props.delayPressOut; + }, + + _showUnderlay: function() { + if (!this.isMounted() || !this._hasPressHandler()) { + return; + } + + this.refs[UNDERLAY_REF].setNativeProps(this.state.activeUnderlayProps); + this.refs[CHILD_REF].setNativeProps(this.state.activeProps); + this.props.onShowUnderlay && this.props.onShowUnderlay(); + }, + + _hideUnderlay: function() { + this.clearTimeout(this._hideTimeout); + this._hideTimeout = null; + if (this._hasPressHandler() && this.refs[UNDERLAY_REF]) { + this.refs[CHILD_REF].setNativeProps(INACTIVE_CHILD_PROPS); + this.refs[UNDERLAY_REF].setNativeProps({ + ...INACTIVE_UNDERLAY_PROPS, + style: this.state.underlayStyle, + }); + this.props.onHideUnderlay && this.props.onHideUnderlay(); + } + }, + + _hasPressHandler: function() { + return !!( + this.props.onPress || + this.props.onPressIn || + this.props.onPressOut || + this.props.onLongPress + ); + }, + + _onKeyEnter(e, callback) { + var ENTER = 13 + if (e.keyCode === ENTER) { + callback && callback(e) + } + }, + + render: function() { + return ( + { this._onKeyEnter(e, this.touchableHandleActivePressIn) }} + onKeyPress={(e) => { this._onKeyEnter(e, this.touchableHandlePress) }} + onKeyUp={(e) => { this._onKeyEnter(e, this.touchableHandleActivePressOut) }} + onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder} + onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest} + onResponderGrant={this.touchableHandleResponderGrant} + onResponderMove={this.touchableHandleResponderMove} + onResponderRelease={this.touchableHandleResponderRelease} + onResponderTerminate={this.touchableHandleResponderTerminate} + tabIndex='0' + testID={this.props.testID}> + {React.cloneElement( + React.Children.only(this.props.children), + { + ref: CHILD_REF, + } + )} + + ); + } +}); + +var CHILD_REF = keyOf({childRef: null}); +var UNDERLAY_REF = keyOf({underlayRef: null}); +var INACTIVE_CHILD_PROPS = { + style: StyleSheet.create({x: {opacity: 1.0}}).x, +}; +var INACTIVE_UNDERLAY_PROPS = { + style: {backgroundColor: null} +}; + +var styles = StyleSheet.create({ + root: { + cursor: 'pointer', + userSelect: 'none' + } +}); + +module.exports = TouchableHighlight; diff --git a/src/components/Touchable/TouchableOpacity.js b/src/components/Touchable/TouchableOpacity.js new file mode 100644 index 00000000..2f6c6d91 --- /dev/null +++ b/src/components/Touchable/TouchableOpacity.js @@ -0,0 +1,200 @@ +/* eslint-disable */ +/** + * 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 TouchableOpacity + * @noflow + */ +'use strict'; + +// Note (avik): add @flow when Flow supports spread properties in propTypes + +var Animated = require('../../apis/Animated'); +var NativeMethodsMixin = require('../../modules/NativeMethodsMixin'); +var React = require('react'); +var StyleSheet = require('../../apis/StyleSheet'); +var TimerMixin = require('react-timer-mixin'); +var Touchable = require('./Touchable'); +var TouchableWithoutFeedback = require('./TouchableWithoutFeedback'); + +var ensurePositiveDelayProps = require('./ensurePositiveDelayProps'); +var flattenStyle = require('../../apis/StyleSheet/flattenStyle'); + +type Event = Object; + +var PRESS_RETENTION_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; + +/** + * A wrapper for making views respond properly to touches. + * On press down, the opacity of the wrapped view is decreased, dimming it. + * This is done without actually changing the view hierarchy, and in general is + * easy to add to an app without weird side-effects. + * + * Example: + * + * ``` + * renderButton: function() { + * return ( + * + * + * + * ); + * }, + * ``` + */ +var TouchableOpacity = React.createClass({ + mixins: [TimerMixin, Touchable.Mixin, NativeMethodsMixin], + + propTypes: { + ...TouchableWithoutFeedback.propTypes, + /** + * Determines what the opacity of the wrapped view should be when touch is + * active. + */ + activeOpacity: React.PropTypes.number, + }, + + getDefaultProps: function() { + return { + activeOpacity: 0.2, + }; + }, + + getInitialState: function() { + return { + ...this.touchableGetInitialState(), + anim: new Animated.Value(1), + }; + }, + + componentDidMount: function() { + ensurePositiveDelayProps(this.props); + }, + + componentWillReceiveProps: function(nextProps) { + ensurePositiveDelayProps(nextProps); + }, + + setOpacityTo: function(value) { + Animated.timing( + this.state.anim, + {toValue: value, duration: 150} + ).start(); + }, + + /** + * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are + * defined on your component. + */ + touchableHandleActivePressIn: function(e: Event) { + this.clearTimeout(this._hideTimeout); + this._hideTimeout = null; + this._opacityActive(); + this.props.onPressIn && this.props.onPressIn(e); + }, + + touchableHandleActivePressOut: function(e: Event) { + if (!this._hideTimeout) { + this._opacityInactive(); + } + this.props.onPressOut && this.props.onPressOut(e); + }, + + touchableHandlePress: function(e: Event) { + this.clearTimeout(this._hideTimeout); + this._opacityActive(); + this._hideTimeout = this.setTimeout( + this._opacityInactive, + this.props.delayPressOut || 100 + ); + this.props.onPress && this.props.onPress(e); + }, + + touchableHandleLongPress: function(e: Event) { + this.props.onLongPress && this.props.onLongPress(e); + }, + + touchableGetPressRectOffset: function() { + return this.props.pressRetentionOffset || PRESS_RETENTION_OFFSET; + }, + + touchableGetHitSlop: function() { + return this.props.hitSlop; + }, + + touchableGetHighlightDelayMS: function() { + 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 childStyle = flattenStyle(this.props.style) || {}; + this.setOpacityTo( + childStyle.opacity === undefined ? 1 : childStyle.opacity + ); + }, + + _onKeyEnter(e, callback) { + var ENTER = 13 + if (e.keyCode === ENTER) { + callback && callback(e) + } + }, + + render: function() { + return ( + { this._onKeyEnter(e, this.touchableHandleActivePressIn) }} + onKeyPress={(e) => { this._onKeyEnter(e, this.touchableHandlePress) }} + onKeyUp={(e) => { this._onKeyEnter(e, this.touchableHandleActivePressOut) }} + onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder} + onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest} + onResponderGrant={this.touchableHandleResponderGrant} + onResponderMove={this.touchableHandleResponderMove} + onResponderRelease={this.touchableHandleResponderRelease} + onResponderTerminate={this.touchableHandleResponderTerminate} + tabIndex='0' + > + {this.props.children} + + ); + }, +}); + +var styles = StyleSheet.create({ + root: { + cursor: 'pointer', + userSelect: 'none' + } +}); + +module.exports = TouchableOpacity; diff --git a/src/components/Touchable/TouchableWithoutFeedback.js b/src/components/Touchable/TouchableWithoutFeedback.js new file mode 100644 index 00000000..6bfa4151 --- /dev/null +++ b/src/components/Touchable/TouchableWithoutFeedback.js @@ -0,0 +1,166 @@ +/* eslint-disable */ +/** + * 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 TouchableWithoutFeedback + * @flow + */ +'use strict'; + +var EdgeInsetsPropType = require('../../apis/StyleSheet/EdgeInsetsPropType'); +var React = require('react'); +var TimerMixin = require('react-timer-mixin'); +var Touchable = require('./Touchable'); +var View = require('../View'); +var ensurePositiveDelayProps = require('./ensurePositiveDelayProps'); + +type Event = Object; + +var PRESS_RETENTION_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; + +/** + * Do not use unless you have a very good reason. All the elements that + * respond to press should have a visual feedback when touched. This is + * one of the primary reason a "web" app doesn't feel "native". + * + * > **NOTE**: TouchableWithoutFeedback supports only one child + * > + * > If you wish to have several child components, wrap them in a View. + */ +var TouchableWithoutFeedback = React.createClass({ + mixins: [TimerMixin, Touchable.Mixin], + + propTypes: { + accessible: React.PropTypes.bool, + accessibilityLabel: View.propTypes.accessibilityLabel, + accessibilityRole: View.propTypes.accessibilityRole, + /** + * If true, disable all interactions for this component. + */ + disabled: React.PropTypes.bool, + /** + * Called when the touch is released, but not if cancelled (e.g. by a scroll + * that steals the responder lock). + */ + onPress: React.PropTypes.func, + onPressIn: React.PropTypes.func, + onPressOut: React.PropTypes.func, + /** + * Invoked on mount and layout changes with + * + * `{nativeEvent: {layout: {x, y, width, height}}}` + */ + onLayout: 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, + /** + * When the scroll view is disabled, this defines how far your touch may + * move off of the button, before deactivating the button. Once deactivated, + * try moving it back and you'll see that the button is once again + * reactivated! Move it back and forth several times while the scroll view + * is disabled. Ensure you pass in a constant to reduce memory allocations. + */ + pressRetentionOffset: EdgeInsetsPropType, + /** + * This defines how far your touch can start away from the button. This is + * added to `pressRetentionOffset` when moving off of the button. + * ** NOTE ** + * The touch area never extends past the parent view bounds and the Z-index + * of sibling views always takes precedence if a touch hits two overlapping + * views. + */ + hitSlop: EdgeInsetsPropType, + }, + + 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. + */ + touchableHandlePress: function(e: Event) { + this.props.onPress && this.props.onPress(e); + }, + + touchableHandleActivePressIn: function(e: Event) { + this.props.onPressIn && this.props.onPressIn(e); + }, + + touchableHandleActivePressOut: function(e: Event) { + this.props.onPressOut && this.props.onPressOut(e); + }, + + touchableHandleLongPress: function(e: Event) { + this.props.onLongPress && this.props.onLongPress(e); + }, + + touchableGetPressRectOffset: function(): typeof PRESS_RETENTION_OFFSET { + return this.props.pressRetentionOffset || PRESS_RETENTION_OFFSET; + }, + + touchableGetHitSlop: function(): ?Object { + return this.props.hitSlop; + }, + + touchableGetHighlightDelayMS: function(): number { + 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 || 0; + }, + + render: function(): ReactElement { + // Note(avik): remove dynamic typecast once Flow has been upgraded + return (React: any).cloneElement(React.children.only(this.props.children), { + accessible: this.props.accessible !== false, + accessibilityLabel: this.props.accessibilityLabel, + accessibilityRole: this.props.accessibilityRole, + testID: this.props.testID, + onLayout: this.props.onLayout, + hitSlop: this.props.hitSlop, + onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder, + onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest, + onResponderGrant: this.touchableHandleResponderGrant, + onResponderMove: this.touchableHandleResponderMove, + onResponderRelease: this.touchableHandleResponderRelease, + onResponderTerminate: this.touchableHandleResponderTerminate, + tabIndex: '0' + }); + } +}); + +module.exports = TouchableWithoutFeedback; diff --git a/src/components/Touchable/__tests__/index-test.js b/src/components/Touchable/__tests__/index-test.js index 1c4e489f..aac36bfc 100644 --- a/src/components/Touchable/__tests__/index-test.js +++ b/src/components/Touchable/__tests__/index-test.js @@ -1,35 +1,5 @@ /* eslint-env mocha */ -import * as utils from '../../../modules/specHelpers' -import assert from 'assert' -import React from 'react' - -import Touchable from '../' - -const children = children -const requiredProps = { children } - suite('components/Touchable', () => { - test('prop "accessibilityLabel"', () => { - const accessibilityLabel = 'accessibilityLabel' - const result = utils.shallowRender() - assert.equal(result.props.accessibilityLabel, accessibilityLabel) - }) - - test('prop "accessibilityRole"', () => { - const accessibilityRole = 'accessibilityRole' - const result = utils.shallowRender() - assert.equal(result.props.accessibilityRole, accessibilityRole) - }) - - test('prop "accessible"', () => { - const accessible = false - const result = utils.shallowRender() - assert.equal(result.props.accessible, accessible) - }) - - test('prop "children"', () => { - const result = utils.shallowRender() - assert.deepEqual(result.props.children, children) - }) + test.skip('NO TEST COVERAGE', () => {}) }) diff --git a/src/components/Touchable/ensureComponentIsNative.js b/src/components/Touchable/ensureComponentIsNative.js new file mode 100644 index 00000000..f8eee4b4 --- /dev/null +++ b/src/components/Touchable/ensureComponentIsNative.js @@ -0,0 +1,25 @@ +/* eslint-disable */ +/** + * 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 ensureComponentIsNative + * @flow + */ +'use strict'; + +var invariant = require('fbjs/lib/invariant'); + +var ensureComponentIsNative = function(component: any) { + invariant( + component && typeof component.setNativeProps === 'function', + 'Touchable child must either be native or forward setNativeProps to a ' + + 'native component' + ); +}; + +module.exports = ensureComponentIsNative; diff --git a/src/components/Touchable/ensurePositiveDelayProps.js b/src/components/Touchable/ensurePositiveDelayProps.js new file mode 100644 index 00000000..2be15328 --- /dev/null +++ b/src/components/Touchable/ensurePositiveDelayProps.js @@ -0,0 +1,25 @@ +/* eslint-disable */ +/** + * 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('fbjs/lib/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/src/components/Touchable/index.js b/src/components/Touchable/index.js deleted file mode 100644 index 8f68fa27..00000000 --- a/src/components/Touchable/index.js +++ /dev/null @@ -1,131 +0,0 @@ -import React, { Component, PropTypes } from 'react' -import StyleSheet from '../../apis/StyleSheet' -import Tappable from 'react-tappable' -import View from '../View' - -export default class Touchable extends Component { - constructor(props, context) { - super(props, context) - this.state = { - isActive: false - } - - this._onLongPress = this._onLongPress.bind(this) - this._onPress = this._onPress.bind(this) - this._onPressIn = this._onPressIn.bind(this) - this._onPressOut = this._onPressOut.bind(this) - } - - static propTypes = { - accessibilityLabel: View.propTypes.accessibilityLabel, - accessibilityRole: View.propTypes.accessibilityRole, - accessible: View.propTypes.accessible, - activeOpacity: PropTypes.number, - activeUnderlayColor: PropTypes.string, - children: PropTypes.element, - delayLongPress: PropTypes.number, - delayPressIn: PropTypes.number, - delayPressOut: PropTypes.number, - onLongPress: PropTypes.func, - onPress: PropTypes.func, - onPressIn: PropTypes.func, - onPressOut: PropTypes.func, - style: View.propTypes.style - }; - - static defaultProps = { - accessibilityRole: 'button', - activeOpacity: 0.8, - activeUnderlayColor: 'black', - delayLongPress: 500, - delayPressIn: 0, - delayPressOut: 100, - style: {} - }; - - _getChildren() { - const { activeOpacity, children } = this.props - return React.cloneElement(React.Children.only(children), { - style: [ - children.props.style, - this.state.isActive && { opacity: activeOpacity } - ] - }) - } - - _onKeyEnter(e, callback) { - var ENTER = 13 - if (e.keyCode === ENTER) { - callback(e) - } - } - - _onLongPress(e) { - if (this.props.onLongPress) this.props.onLongPress(e) - } - - _onPress(e) { - if (this.props.onPress) this.props.onPress(e) - } - - _onPressIn(e) { - this.setState({ isActive: true }) - if (this.props.onPressIn) this.props.onPressIn(e) - } - - _onPressOut(e) { - this.setState({ isActive: false }) - if (this.props.onPressOut) this.props.onPressOut(e) - } - - render() { - const { - accessibilityLabel, - accessibilityRole, - accessible, - activeUnderlayColor, - delayLongPress, - style - } = this.props - - /** - * Creates a wrapping element that can receive keyboard focus. The - * highlight is applied as a background color on this wrapper. The opacity - * is set on the child element, allowing it to have its own background - * color. - */ - return ( - { this._onKeyEnter(e, this._onPressIn) }} - onKeyPress={this._onPress} - onKeyUp={(e) => { this._onKeyEnter(e, this._onPressOut) }} - onMouseDown={this._onPressIn} - onMouseUp={this._onPressOut} - onPress={this._onLongPress} - onTap={this._onPress} - onTouchEnd={this._onPressOut} - onTouchStart={this._onPressIn} - pressDelay={delayLongPress} - pressMoveThreshold={5} - style={StyleSheet.flatten([ - styles.initial, - style, - activeUnderlayColor && this.state.isActive && { backgroundColor: activeUnderlayColor } - ])} - tabIndex='0' - /> - ) - } -} - -const styles = StyleSheet.create({ - initial: { - cursor: 'pointer', - userSelect: undefined - } -})