diff --git a/Examples/UIExplorer/TouchableExample.js b/Examples/UIExplorer/TouchableExample.js index 7aa2be01c68be3..66a919a3cdd29b 100644 --- a/Examples/UIExplorer/TouchableExample.js +++ b/Examples/UIExplorer/TouchableExample.js @@ -12,6 +12,7 @@ var { StyleSheet, Text, TouchableHighlight, + TouchableOpacity, View, } = React; @@ -57,6 +58,13 @@ exports.examples = [ render: function() { return ; }, +}, { + title: 'Touchable feedback events', + description: ' components accept onPress, onPressIn, ' + + 'onPressOut, and onLongPress as props.', + render: function() { + return ; + }, }]; var TextOnPressBox = React.createClass({ @@ -95,11 +103,46 @@ var TextOnPressBox = React.createClass({ } }); +var TouchableFeedbackEvents = React.createClass({ + getInitialState: function() { + return { + eventLog: [], + }; + }, + render: function() { + return ( + + + this._appendEvent('press')} + onPressIn={() => this._appendEvent('pressIn')} + onPressOut={() => this._appendEvent('pressOut')} + onLongPress={() => this._appendEvent('longPress')}> + + 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({ row: { - alignItems: 'center', + justifyContent: 'center', flexDirection: 'row', }, icon: { @@ -113,6 +156,9 @@ var styles = StyleSheet.create({ text: { fontSize: 16, }, + button: { + color: '#007AFF', + }, wrapper: { borderRadius: 8, }, @@ -127,6 +173,14 @@ var styles = StyleSheet.create({ borderColor: '#f0f0f0', backgroundColor: '#f9f9f9', }, + eventLogBox: { + padding: 10, + margin: 10, + height: 120, + borderWidth: 1 / PixelRatio.get(), + borderColor: '#f0f0f0', + backgroundColor: '#f9f9f9', + }, textBlock: { fontWeight: 'bold', color: 'blue', diff --git a/Libraries/Components/TextInput/TextInput.ios.js b/Libraries/Components/TextInput/TextInput.ios.js index 2bd194d17ce310..fef9c16f01b80a 100644 --- a/Libraries/Components/TextInput/TextInput.ios.js +++ b/Libraries/Components/TextInput/TextInput.ios.js @@ -68,6 +68,15 @@ var autoCapitalizeMode = { characters: nativeConstants.AllCharacters }; +var clearButtonModeConstants = NativeModulesDeprecated.RKUIManager.UITextField.clearButtonMode; + +var clearButtonModeTypes = { + never: clearButtonModeConstants.Never, + whileEditing: clearButtonModeConstants.WhileEditing, + unlessEditing: clearButtonModeConstants.UnlessEditing, + always: clearButtonModeConstants.Always, +}; + var keyboardType = { default: 'default', numeric: 'numeric', @@ -90,6 +99,7 @@ var RKTextViewAttributes = merge(ReactIOSViewAttributes.UIView, { var RKTextFieldAttributes = merge(RKTextViewAttributes, { caretHidden: true, enabled: true, + clearButtonMode: true, }); var onlyMultiline = { @@ -105,6 +115,7 @@ var notMultiline = { var TextInput = React.createClass({ statics: { autoCapitalizeMode: autoCapitalizeMode, + clearButtonModeTypes: clearButtonModeTypes, keyboardType: keyboardType, }, @@ -188,6 +199,10 @@ var TextInput = React.createClass({ * and/or laggy typing, depending on how you process onChange events. */ controlled: PropTypes.bool, + /** + * When the clear button should appear on the right side of the text view + */ + clearButtonMode: PropTypes.oneOf(getObjectValues(clearButtonModeTypes)), style: Text.stylePropType, }, @@ -316,6 +331,7 @@ var TextInput = React.createClass({ text={this.state.bufferedValue} autoCapitalize={this.props.autoCapitalize} autoCorrect={this.props.autoCorrect} + clearButtonMode={this.props.clearButtonMode} />; } else { for (var propKey in notMultiline) { diff --git a/Libraries/Components/Touchable/TouchableFeedbackPropType.js b/Libraries/Components/Touchable/TouchableFeedbackPropType.js new file mode 100644 index 00000000000000..336b091c629fdb --- /dev/null +++ b/Libraries/Components/Touchable/TouchableFeedbackPropType.js @@ -0,0 +1,22 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule TouchableFeedbackPropType + * @flow + */ +'use strict'; + +var { PropTypes } = require('React'); + +var TouchableFeedbackPropType = { + /** + * Called when the touch is released, but not if cancelled (e.g. by a scroll + * that steals the responder lock). + */ + onPress: PropTypes.func, + onPressIn: PropTypes.func, + onPressOut: PropTypes.func, + onLongPress: PropTypes.func, +}; + +module.exports = TouchableFeedbackPropType; diff --git a/Libraries/Components/Touchable/TouchableHighlight.js b/Libraries/Components/Touchable/TouchableHighlight.js index da721cff8a6d3c..9381f52d628178 100644 --- a/Libraries/Components/Touchable/TouchableHighlight.js +++ b/Libraries/Components/Touchable/TouchableHighlight.js @@ -11,6 +11,7 @@ var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); var StyleSheet = require('StyleSheet'); var TimerMixin = require('TimerMixin'); var Touchable = require('Touchable'); +var TouchableFeedbackPropType = require('TouchableFeedbackPropType'); var View = require('View'); var cloneWithProps = require('cloneWithProps'); @@ -50,6 +51,7 @@ var DEFAULT_PROPS = { var TouchableHighlight = React.createClass({ propTypes: { + ...TouchableFeedbackPropType, /** * Called when the touch is released, but not if cancelled (e.g. by * a scroll that steals the responder lock). @@ -127,12 +129,14 @@ var TouchableHighlight = React.createClass({ this.clearTimeout(this._hideTimeout); this._hideTimeout = null; this._showUnderlay(); + this.props.onPressIn && this.props.onPressIn(); }, touchableHandleActivePressOut: function() { if (!this._hideTimeout) { this._hideUnderlay(); } + this.props.onPressOut && this.props.onPressOut(); }, touchableHandlePress: function() { @@ -142,6 +146,10 @@ var TouchableHighlight = React.createClass({ this.props.onPress && this.props.onPress(); }, + touchableHandleLongPress: function() { + this.props.onLongPress && this.props.onLongPress(); + }, + touchableGetPressRectOffset: function() { return PRESS_RECT_OFFSET; // Always make sure to predeclare a constant! }, diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js index a1bd8f4e359eee..cb68d6df37cfd2 100644 --- a/Libraries/Components/Touchable/TouchableOpacity.js +++ b/Libraries/Components/Touchable/TouchableOpacity.js @@ -9,6 +9,7 @@ var NativeMethodsMixin = require('NativeMethodsMixin'); var POPAnimationMixin = require('POPAnimationMixin'); var React = require('React'); var Touchable = require('Touchable'); +var TouchableFeedbackPropType = require('TouchableFeedbackPropType'); var cloneWithProps = require('cloneWithProps'); var ensureComponentIsNative = require('ensureComponentIsNative'); @@ -41,11 +42,7 @@ var TouchableOpacity = React.createClass({ mixins: [Touchable.Mixin, NativeMethodsMixin, POPAnimationMixin], propTypes: { - /** - * Called when the touch is released, but not if cancelled (e.g. by - * a scroll that steals the responder lock). - */ - onPress: React.PropTypes.func, + ...TouchableFeedbackPropType, /** * Determines what the opacity of the wrapped view should be when touch is * active. @@ -97,10 +94,12 @@ var TouchableOpacity = React.createClass({ this.refs[CHILD_REF].setNativeProps({ opacity: this.props.activeOpacity }); + this.props.onPressIn && this.props.onPressIn(); }, touchableHandleActivePressOut: function() { this.setOpacityTo(1.0); + this.props.onPressOut && this.props.onPressOut(); }, touchableHandlePress: function() { @@ -108,6 +107,10 @@ var TouchableOpacity = React.createClass({ this.props.onPress && this.props.onPress(); }, + touchableHandleLongPress: function() { + this.props.onLongPress && this.props.onLongPress(); + }, + touchableGetPressRectOffset: function() { return PRESS_RECT_OFFSET; // Always make sure to predeclare a constant! }, diff --git a/Libraries/Components/Touchable/TouchableWithoutFeedback.js b/Libraries/Components/Touchable/TouchableWithoutFeedback.js index 66a82d59c75cbb..74fbf43a1e1e24 100644 --- a/Libraries/Components/Touchable/TouchableWithoutFeedback.js +++ b/Libraries/Components/Touchable/TouchableWithoutFeedback.js @@ -7,7 +7,7 @@ var React = require('React'); var Touchable = require('Touchable'); -var View = require('View'); +var TouchableFeedbackPropType = require('TouchableFeedbackPropType'); var copyProperties = require('copyProperties'); var onlyChild = require('onlyChild'); @@ -29,12 +29,7 @@ var PRESS_RECT_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; var TouchableWithoutFeedback = React.createClass({ mixins: [Touchable.Mixin], - propTypes: { - onPress: React.PropTypes.func, - onPressIn: React.PropTypes.func, - onPressOut: React.PropTypes.func, - onLongPress: React.PropTypes.func, - }, + propTypes: TouchableFeedbackPropType, getInitialState: function() { return this.touchableGetInitialState(); diff --git a/Libraries/ReactIOS/ReactIOS.js b/Libraries/ReactIOS/ReactIOS.js index f0a1ed782839ba..0022e09bd25e64 100644 --- a/Libraries/ReactIOS/ReactIOS.js +++ b/Libraries/ReactIOS/ReactIOS.js @@ -6,6 +6,7 @@ "use strict"; +var ReactChildren = require('ReactChildren'); var ReactComponent = require('ReactComponent'); var ReactCompositeComponent = require('ReactCompositeComponent'); var ReactContext = require('ReactContext'); @@ -20,6 +21,7 @@ var ReactPropTypes = require('ReactPropTypes'); var deprecated = require('deprecated'); var invariant = require('invariant'); +var onlyChild = require('onlyChild'); ReactIOSDefaultInjection.inject(); @@ -73,6 +75,12 @@ var render = function(component, mountInto) { var ReactIOS = { hasReactIOSInitialized: false, + Children: { + map: ReactChildren.map, + forEach: ReactChildren.forEach, + count: ReactChildren.count, + only: onlyChild + }, PropTypes: ReactPropTypes, createClass: ReactCompositeComponent.createClass, createElement: createElement, diff --git a/ReactKit/Base/RCTConvert.m b/ReactKit/Base/RCTConvert.m index 752d03ced0bde9..ab79cd8617742b 100644 --- a/ReactKit/Base/RCTConvert.m +++ b/ReactKit/Base/RCTConvert.m @@ -446,6 +446,9 @@ + (CAKeyframeAnimation *)GIF:(id)json } imageSource = CGImageSourceCreateWithData((CFDataRef)data, NULL); + } else { + RCTLogMustFix(@"Expected NSString or NSData for GIF, received %@: %@", [json class], json); + return nil; } if (!UTTypeConformsTo(CGImageSourceGetType(imageSource), kUTTypeGIF)) { diff --git a/ReactKit/Base/RCTTouchHandler.m b/ReactKit/Base/RCTTouchHandler.m index c56f996cc9b515..a23cd70c7deaac 100644 --- a/ReactKit/Base/RCTTouchHandler.m +++ b/ReactKit/Base/RCTTouchHandler.m @@ -77,9 +77,10 @@ - (void)_recordNewTouches:(NSSet *)touches } targetView = targetView.superview; } - - RCTAssert(targetView.reactTag && targetView.userInteractionEnabled, - @"No react view found for touch - something went wrong."); + + if (!targetView.reactTag || !targetView.userInteractionEnabled) { + return; + } // Get new, unique touch id const NSUInteger RCTMaxTouches = 11; // This is the maximum supported by iDevices @@ -113,7 +114,10 @@ - (void)_recordRemovedTouches:(NSSet *)touches { for (UITouch *touch in touches) { NSUInteger index = [_nativeTouches indexOfObject:touch]; - RCTAssert(index != NSNotFound, @"Touch is already removed. This is a critical bug."); + if(index == NSNotFound) { + continue; + } + [_touchViews removeObjectAtIndex:index]; [_nativeTouches removeObjectAtIndex:index]; [_reactTouches removeObjectAtIndex:index]; @@ -159,10 +163,17 @@ - (void)_updateAndDispatchTouches:(NSSet *)touches eventName:(NSString *)eventNa NSMutableArray *changedIndexes = [[NSMutableArray alloc] init]; for (UITouch *touch in touches) { NSInteger index = [_nativeTouches indexOfObject:touch]; - RCTAssert(index != NSNotFound, @"Touch not found. This is a critical bug."); + if (index == NSNotFound) { + continue; + } + [self _updateReactTouchAtIndex:index]; [changedIndexes addObject:@(index)]; } + + if (changedIndexes.count == 0) { + return; + } // Deep copy the touches because they will be accessed from another thread // TODO: would it be safer to do this in the bridge or executor, rather than trusting caller?