From ecbdf69b8040618a96adde00153ede6f09985f33 Mon Sep 17 00:00:00 2001 From: Jyrno Ader Date: Sat, 30 Mar 2019 21:16:33 +0200 Subject: [PATCH] WIP: Make Touchables strict mode compatible Convert all Touchables to be class based and remove UNSAFE props. Also renamed Touchable.Mixin.withoutDefaultFocusAndBlur to Touchable.MixinWithoutDefaultFocusAndBlur to improve flow automatic typings. Note: TouchableNativeFeedback uses ReactNative.findNodeHandle which triggers a warning in strict mode during a tap. I could not figure out how to remove it yet. Related to https://github.com/facebook/react-native/issues/22186 --- .../Animated/src/createAnimatedComponent.js | 12 +- Libraries/Components/Touchable/Touchable.js | 40 +- .../Components/Touchable/TouchableBounce.js | 192 +++++---- .../Touchable/TouchableHighlight.js | 350 +++++++++-------- .../TouchableNativeFeedback.android.js | 370 +++++++++--------- .../Components/Touchable/TouchableOpacity.js | 268 +++++++------ .../Touchable/TouchableWithoutFeedback.js | 282 +++++++------ RNTester/js/StrictModeExample.js | 68 +++- 8 files changed, 890 insertions(+), 692 deletions(-) diff --git a/Libraries/Animated/src/createAnimatedComponent.js b/Libraries/Animated/src/createAnimatedComponent.js index 327070de59da40..8872a594d60e25 100644 --- a/Libraries/Animated/src/createAnimatedComponent.js +++ b/Libraries/Animated/src/createAnimatedComponent.js @@ -35,6 +35,8 @@ function createAnimatedComponent(Component: any): any { constructor(props: Object) { super(props); + + this._attachProps(this.props); } componentWillUnmount() { @@ -46,10 +48,6 @@ function createAnimatedComponent(Component: any): any { this._component.setNativeProps(props); } - UNSAFE_componentWillMount() { - this._attachProps(this.props); - } - componentDidMount() { if (this._invokeAnimatedPropsCallbackOnMount) { this._invokeAnimatedPropsCallbackOnMount = false; @@ -131,11 +129,9 @@ function createAnimatedComponent(Component: any): any { oldPropsAnimated && oldPropsAnimated.__detach(); } - UNSAFE_componentWillReceiveProps(newProps) { - this._attachProps(newProps); - } - componentDidUpdate(prevProps) { + this._attachProps(this.props); + if (this._component !== this._prevComponent) { this._propsAnimated.setNativeView(this._component); } diff --git a/Libraries/Components/Touchable/Touchable.js b/Libraries/Components/Touchable/Touchable.js index 72f7e4c35033f5..e5cea837dba1e4 100644 --- a/Libraries/Components/Touchable/Touchable.js +++ b/Libraries/Components/Touchable/Touchable.js @@ -23,7 +23,7 @@ const View = require('View'); const keyMirror = require('fbjs/lib/keyMirror'); const normalizeColor = require('normalizeColor'); -import type {PressEvent} from 'CoreEventTypes'; +import type {SyntheticEvent, PressEvent} from 'CoreEventTypes'; import type {EdgeInsetsProp} from 'EdgeInsetsPropType'; const extractSingleTouch = nativeEvent => { @@ -149,6 +149,22 @@ type State = | typeof States.RESPONDER_ACTIVE_LONG_PRESS_OUT | typeof States.ERROR; +export type TouchableState = {| + touchable: { + touchState: ?State, + responderID: ?number, + }, +|}; + +type TargetEvent = SyntheticEvent< + $ReadOnly<{| + target: number, + |}>, +>; + +export type BlurEvent = TargetEvent; +export type FocusEvent = TargetEvent; + /* * Quick lookup map for states that are considered to be "active" */ @@ -576,9 +592,9 @@ const TouchableMixin = { * currently has the focus. Most platforms only support a single element being * focused at a time, in which case there may have been a previously focused * element that was blurred just prior to this. This can be overridden when - * using `Touchable.Mixin.withoutDefaultFocusAndBlur`. + * using `Touchable.MixinWithoutDefaultFocusAndBlur`. */ - touchableHandleFocus: function(e: Event) { + touchableHandleFocus: function(e: FocusEvent) { this.props.onFocus && this.props.onFocus(e); }, @@ -588,9 +604,9 @@ const TouchableMixin = { * no longer has focus. Most platforms only support a single element being * focused at a time, in which case the focus may have moved to another. * This can be overridden when using - * `Touchable.Mixin.withoutDefaultFocusAndBlur`. + * `Touchable.MixinWithoutDefaultFocusAndBlur`. */ - touchableHandleBlur: function(e: Event) { + touchableHandleBlur: function(e: BlurEvent) { this.props.onBlur && this.props.onBlur(e); }, @@ -900,8 +916,6 @@ const TouchableMixin = { } } }, - - withoutDefaultFocusAndBlur: {}, }; /** @@ -910,15 +924,13 @@ const TouchableMixin = { * be set on TV platforms, without breaking existing implementations of * `Touchable`. */ -const { - touchableHandleFocus, - touchableHandleBlur, - ...TouchableMixinWithoutDefaultFocusAndBlur -} = TouchableMixin; -TouchableMixin.withoutDefaultFocusAndBlur = TouchableMixinWithoutDefaultFocusAndBlur; +const TouchableMixinWithoutDefaultFocusAndBlur = {...TouchableMixin}; +delete TouchableMixinWithoutDefaultFocusAndBlur.touchableHandleFocus; +delete TouchableMixinWithoutDefaultFocusAndBlur.touchableHandleBlur; const Touchable = { Mixin: TouchableMixin, + MixinWithoutDefaultFocusAndBlur: TouchableMixinWithoutDefaultFocusAndBlur, TOUCH_TARGET_DEBUG: false, // Highlights all touchable targets. Toggle with Inspector. /** * Renders a debugging overlay to visualize touch target with hitSlop (might not work on Android). @@ -928,7 +940,7 @@ const Touchable = { hitSlop, }: { color: string | number, - hitSlop: EdgeInsetsProp, + hitSlop: ?EdgeInsetsProp, }) => { if (!Touchable.TOUCH_TARGET_DEBUG) { return null; diff --git a/Libraries/Components/Touchable/TouchableBounce.js b/Libraries/Components/Touchable/TouchableBounce.js index 04817aef02bacf..16ef42f85c87ee 100644 --- a/Libraries/Components/Touchable/TouchableBounce.js +++ b/Libraries/Components/Touchable/TouchableBounce.js @@ -10,40 +10,64 @@ 'use strict'; const Animated = require('Animated'); -const DeprecatedViewPropTypes = require('DeprecatedViewPropTypes'); -const DeprecatedEdgeInsetsPropType = require('DeprecatedEdgeInsetsPropType'); -const NativeMethodsMixin = require('NativeMethodsMixin'); const Platform = require('Platform'); -const PropTypes = require('prop-types'); const React = require('React'); const Touchable = require('Touchable'); -const TouchableWithoutFeedback = require('TouchableWithoutFeedback'); - -const createReactClass = require('create-react-class'); +import type {PressEvent} from 'CoreEventTypes'; import type {EdgeInsetsProp} from 'EdgeInsetsPropType'; import type {ViewStyleProp} from 'StyleSheet'; +import type {BlurEvent, FocusEvent, TouchableState} from 'Touchable'; import type {Props as TouchableWithoutFeedbackProps} from 'TouchableWithoutFeedback'; -import type {PressEvent} from 'CoreEventTypes'; - -type State = { - animationID: ?number, - scale: Animated.Value, -}; const PRESS_RETENTION_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; type Props = $ReadOnly<{| ...TouchableWithoutFeedbackProps, + // 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?: ?(fn: () => void) => void, + // the function passed is called after the animation is complete onPressAnimationComplete?: ?() => void, + /** + * 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?: ?EdgeInsetsProp, - releaseVelocity?: ?number, - releaseBounciness?: ?number, + releaseVelocity: number, + releaseBounciness: number, + /** + * Style to apply to the container/underlay. Most commonly used to make sure + * rounded corners match the wrapped component. + */ style?: ?ViewStyleProp, |}>; +type State = {| + scale: Animated.Value, + + ...TouchableState, +|}; + +function createTouchMixin( + node: React.ElementRef, +): typeof Touchable.MixinWithoutDefaultFocusAndBlur { + const touchMixin = {...Touchable.MixinWithoutDefaultFocusAndBlur}; + + for (const key in touchMixin) { + if (typeof touchMixin[key] === 'function') { + touchMixin[key] = touchMixin[key].bind(node); + } + } + + return touchMixin; +} + /** * Example of using the `TouchableMixin` to play well with other responder * locking views including `ScrollView`. `TouchableMixin` provides touchable @@ -51,50 +75,50 @@ type Props = $ReadOnly<{| * `TouchableMixin` expects us to implement some abstract methods to handle * interesting interactions such as `handleTouchablePress`. */ -const TouchableBounce = ((createReactClass({ - displayName: 'TouchableBounce', - mixins: [Touchable.Mixin.withoutDefaultFocusAndBlur, NativeMethodsMixin], - - propTypes: { - /* $FlowFixMe(>=0.89.0 site=react_native_fb) This comment suppresses an - * error found when Flow v0.89 was deployed. To see the error, delete this - * comment and run Flow. */ - ...TouchableWithoutFeedback.propTypes, - // 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: PropTypes.func, - // the function passed is called after the animation is complete - onPressAnimationComplete: 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: DeprecatedEdgeInsetsPropType, - releaseVelocity: PropTypes.number.isRequired, - releaseBounciness: PropTypes.number.isRequired, - /** - * Style to apply to the container/underlay. Most commonly used to make sure - * rounded corners match the wrapped component. - */ - style: DeprecatedViewPropTypes.style, - }, - - getDefaultProps: function() { - return {releaseBounciness: 10, releaseVelocity: 10}; - }, - - getInitialState: function(): State { - return { - ...this.touchableGetInitialState(), +class TouchableBounce extends React.Component { + static defaultProps = { + releaseBounciness: 10, + releaseVelocity: 10, + }; + + _touchMixin: typeof Touchable.MixinWithoutDefaultFocusAndBlur = createTouchMixin(this); + + constructor(props: Props) { + super(props); + + const touchMixin = Touchable.MixinWithoutDefaultFocusAndBlur; + for (const key in touchMixin) { + if ( + typeof touchMixin[key] === 'function' && + (key.startsWith('_') || key.startsWith('touchable')) + ) { + // $FlowFixMe - dynamically adding properties to a class + (this: any)[key] = touchMixin[key].bind(this); + } + } + + Object.keys(touchMixin) + .filter(key => typeof touchMixin[key] !== 'function') + .forEach(key => { + // $FlowFixMe - dynamically adding properties to a class + (this: any)[key] = touchMixin[key]; + }); + + this.state = { scale: new Animated.Value(1), + ...this._touchMixin.touchableGetInitialState(), }; - }, + } + + componentDidMount() { + this._touchMixin.componentDidMount(); + } - bounceTo: function( + componentWillUnmount() { + this._touchMixin.componentWillUnmount(); + } + + bounceTo( value: number, velocity: number, bounciness: number, @@ -106,37 +130,37 @@ const TouchableBounce = ((createReactClass({ bounciness, useNativeDriver: true, }).start(callback); - }, + } /** * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are * defined on your component. */ - touchableHandleActivePressIn: function(e: PressEvent) { + touchableHandleActivePressIn(e: PressEvent) { this.bounceTo(0.93, 0.1, 0); this.props.onPressIn && this.props.onPressIn(e); - }, + } - touchableHandleActivePressOut: function(e: PressEvent) { + touchableHandleActivePressOut(e: PressEvent) { this.bounceTo(1, 0.4, 0); this.props.onPressOut && this.props.onPressOut(e); - }, + } - touchableHandleFocus: function(e: Event) { + touchableHandleFocus(e: FocusEvent) { if (Platform.isTV) { this.bounceTo(0.93, 0.1, 0); } this.props.onFocus && this.props.onFocus(e); - }, + } - touchableHandleBlur: function(e: Event) { + touchableHandleBlur(e: BlurEvent) { if (Platform.isTV) { this.bounceTo(1, 0.4, 0); } this.props.onBlur && this.props.onBlur(e); - }, + } - touchableHandlePress: function(e: PressEvent) { + touchableHandlePress(e: PressEvent) { const onPressWithCompletion = this.props.onPressWithCompletion; if (onPressWithCompletion) { onPressWithCompletion(() => { @@ -158,21 +182,21 @@ const TouchableBounce = ((createReactClass({ this.props.onPressAnimationComplete, ); this.props.onPress && this.props.onPress(e); - }, + } - touchableGetPressRectOffset: function(): typeof PRESS_RETENTION_OFFSET { + touchableGetPressRectOffset(): EdgeInsetsProp { return this.props.pressRetentionOffset || PRESS_RETENTION_OFFSET; - }, + } - touchableGetHitSlop: function(): ?EdgeInsetsProp { + touchableGetHitSlop(): ?EdgeInsetsProp { return this.props.hitSlop; - }, + } - touchableGetHighlightDelayMS: function(): number { + touchableGetHighlightDelayMS(): number { return 0; - }, + } - render: function(): React.Element { + render(): React.Element { return ( + onResponderGrant={this._touchMixin.touchableHandleResponderGrant} + onResponderMove={this._touchMixin.touchableHandleResponderMove} + onResponderRelease={this._touchMixin.touchableHandleResponderRelease} + onResponderTerminate={ + this._touchMixin.touchableHandleResponderTerminate + }> {this.props.children} {Touchable.renderDebugView({ color: 'orange', @@ -199,7 +227,7 @@ const TouchableBounce = ((createReactClass({ })} ); - }, -}): any): React.ComponentType); + } +} module.exports = TouchableBounce; diff --git a/Libraries/Components/Touchable/TouchableHighlight.js b/Libraries/Components/Touchable/TouchableHighlight.js index fa0e083fa8a985..48228c5138b6db 100644 --- a/Libraries/Components/Touchable/TouchableHighlight.js +++ b/Libraries/Components/Touchable/TouchableHighlight.js @@ -9,45 +9,78 @@ */ 'use strict'; -const DeprecatedColorPropType = require('DeprecatedColorPropType'); -const DeprecatedViewPropTypes = require('DeprecatedViewPropTypes'); -const NativeMethodsMixin = require('NativeMethodsMixin'); const Platform = require('Platform'); -const PropTypes = require('prop-types'); const React = require('React'); const ReactNativeViewAttributes = require('ReactNativeViewAttributes'); const StyleSheet = require('StyleSheet'); const Touchable = require('Touchable'); -const TouchableWithoutFeedback = require('TouchableWithoutFeedback'); const View = require('View'); -const createReactClass = require('create-react-class'); const ensurePositiveDelayProps = require('ensurePositiveDelayProps'); import type {PressEvent} from 'CoreEventTypes'; import type {ViewStyleProp} from 'StyleSheet'; import type {ColorValue} from 'StyleSheetTypes'; +import type {BlurEvent, FocusEvent, TouchableState} from 'Touchable'; import type {Props as TouchableWithoutFeedbackProps} from 'TouchableWithoutFeedback'; import type {TVParallaxPropertiesType} from 'TVViewPropTypes'; -const DEFAULT_PROPS = { - activeOpacity: 0.85, - delayPressOut: 100, - underlayColor: 'black', -}; - const PRESS_RETENTION_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; type IOSProps = $ReadOnly<{| - hasTVPreferredFocus?: ?boolean, - tvParallaxProperties?: ?TVParallaxPropertiesType, + /** + * *(Apple TV only)* TV preferred focus (see documentation for the View component). + * + * @platform ios + */ + hasTVPreferredFocus?: boolean, + /** + * *(Apple TV only)* Object with properties to control Apple TV parallax effects. + * + * enabled: If true, parallax effects are enabled. Defaults to true. + * shiftDistanceX: Defaults to 2.0. + * shiftDistanceY: Defaults to 2.0. + * tiltAngle: Defaults to 0.05. + * magnification: Defaults to 1.0. + * pressMagnification: Defaults to 1.0. + * pressDuration: Defaults to 0.3. + * pressDelay: Defaults to 0.0. + * + * @platform ios + */ + tvParallaxProperties?: TVParallaxPropertiesType, |}>; type AndroidProps = $ReadOnly<{| + /** + * TV next focus down (see documentation for the View component). + * + * @platform android + */ nextFocusDown?: ?number, + /** + * TV next focus forward (see documentation for the View component). + * + * @platform android + */ nextFocusForward?: ?number, + /** + * TV next focus left (see documentation for the View component). + * + * @platform android + */ nextFocusLeft?: ?number, + /** + * TV next focus right (see documentation for the View component). + * + * @platform android + */ nextFocusRight?: ?number, + /** + * TV next focus up (see documentation for the View component). + * + * @platform android + */ nextFocusUp?: ?number, |}>; @@ -56,14 +89,64 @@ type Props = $ReadOnly<{| ...IOSProps, ...AndroidProps, - activeOpacity?: ?number, - underlayColor?: ?ColorValue, + /** + * Determines what the opacity of the wrapped view should be when touch is + * active. + */ + activeOpacity: number, + /** + * The color of the underlay that will show through when the touch is + * active. + */ + underlayColor: ColorValue, + /** + * Delay in ms, from the release of the touch, before onPressOut is called. + */ + delayPressOut: number, + /** + * Style to apply to the container/underlay. Most commonly used to make sure + * rounded corners match the wrapped component. + */ style?: ?ViewStyleProp, + /** + * Called immediately after the underlay is shown + */ onShowUnderlay?: ?() => void, + /** + * Called immediately after the underlay is hidden + */ onHideUnderlay?: ?() => void, + /** + * Handy for snapshot tests. + */ testOnly_pressed?: ?boolean, |}>; +type State = {| + ...TouchableState, + + extraChildStyle: ?{ + opacity: number, + }, + extraUnderlayStyle: ?{| + backgroundColor: ColorValue, + |}, +|}; + +function createTouchMixin( + node: React.ElementRef, +): typeof Touchable.MixinWithoutDefaultFocusAndBlur { + const touchMixin = {...Touchable.MixinWithoutDefaultFocusAndBlur}; + + for (const key in touchMixin) { + if (typeof touchMixin[key] === 'function') { + touchMixin[key] = touchMixin[key].bind(node); + } + } + + return touchMixin; +} + /** * A wrapper for making views respond properly to touches. * On press down, the opacity of the wrapped view is decreased, which allows @@ -160,103 +243,46 @@ type Props = $ReadOnly<{| * ``` * */ +class TouchableHighlight extends React.Component { + static defaultProps = { + activeOpacity: 0.85, + delayPressOut: 100, + underlayColor: 'black', + }; + + _touchMixin: typeof Touchable.MixinWithoutDefaultFocusAndBlur = createTouchMixin( + this, + ); + + _isMounted: boolean; + + _hideTimeout: ?TimeoutID; + + constructor(props: Props) { + super(props); + + const touchMixin = Touchable.MixinWithoutDefaultFocusAndBlur; + for (const key in touchMixin) { + if ( + typeof touchMixin[key] === 'function' && + (key.startsWith('_') || key.startsWith('touchable')) + ) { + // $FlowFixMe - dynamically adding properties to a class + (this: any)[key] = touchMixin[key].bind(this); + } + } -const TouchableHighlight = ((createReactClass({ - displayName: 'TouchableHighlight', - propTypes: { - /* $FlowFixMe(>=0.89.0 site=react_native_fb) This comment suppresses an - * error found when Flow v0.89 was deployed. To see the error, delete this - * comment and run Flow. */ - ...TouchableWithoutFeedback.propTypes, - /** - * Determines what the opacity of the wrapped view should be when touch is - * active. - */ - activeOpacity: PropTypes.number, - /** - * The color of the underlay that will show through when the touch is - * active. - */ - underlayColor: DeprecatedColorPropType, - /** - * Style to apply to the container/underlay. Most commonly used to make sure - * rounded corners match the wrapped component. - */ - style: DeprecatedViewPropTypes.style, - /** - * Called immediately after the underlay is shown - */ - onShowUnderlay: PropTypes.func, - /** - * Called immediately after the underlay is hidden - */ - onHideUnderlay: PropTypes.func, - /** - * *(Apple TV only)* TV preferred focus (see documentation for the View component). - * - * @platform ios - */ - hasTVPreferredFocus: PropTypes.bool, - /** - * TV next focus down (see documentation for the View component). - * - * @platform android - */ - nextFocusDown: PropTypes.number, - /** - * TV next focus forward (see documentation for the View component). - * - * @platform android - */ - nextFocusForward: PropTypes.number, - /** - * TV next focus left (see documentation for the View component). - * - * @platform android - */ - nextFocusLeft: PropTypes.number, - /** - * TV next focus right (see documentation for the View component). - * - * @platform android - */ - nextFocusRight: PropTypes.number, - /** - * TV next focus up (see documentation for the View component). - * - * @platform android - */ - nextFocusUp: PropTypes.number, - /** - * *(Apple TV only)* Object with properties to control Apple TV parallax effects. - * - * enabled: If true, parallax effects are enabled. Defaults to true. - * shiftDistanceX: Defaults to 2.0. - * shiftDistanceY: Defaults to 2.0. - * tiltAngle: Defaults to 0.05. - * magnification: Defaults to 1.0. - * pressMagnification: Defaults to 1.0. - * pressDuration: Defaults to 0.3. - * pressDelay: Defaults to 0.0. - * - * @platform ios - */ - tvParallaxProperties: PropTypes.object, - /** - * Handy for snapshot tests. - */ - testOnly_pressed: PropTypes.bool, - }, - - mixins: [NativeMethodsMixin, Touchable.Mixin.withoutDefaultFocusAndBlur], - - getDefaultProps: () => DEFAULT_PROPS, + Object.keys(touchMixin) + .filter(key => typeof touchMixin[key] !== 'function') + .forEach(key => { + // $FlowFixMe - dynamically adding properties to a class + (this: any)[key] = touchMixin[key]; + }); - getInitialState: function() { this._isMounted = false; if (this.props.testOnly_pressed) { - return { - ...this.touchableGetInitialState(), + this.state = { + ...this._touchMixin.touchableGetInitialState(), extraChildStyle: { opacity: this.props.activeOpacity, }, @@ -265,66 +291,70 @@ const TouchableHighlight = ((createReactClass({ }, }; } else { - return { - ...this.touchableGetInitialState(), + this.state = { + ...this._touchMixin.touchableGetInitialState(), extraChildStyle: null, extraUnderlayStyle: null, }; } - }, + } - componentDidMount: function() { + componentDidMount() { this._isMounted = true; ensurePositiveDelayProps(this.props); - }, - componentWillUnmount: function() { + this._touchMixin.componentDidMount(); + } + + componentDidUpdate(prevProps: Props, prevState: State) { + ensurePositiveDelayProps(this.props); + } + + componentWillUnmount() { this._isMounted = false; clearTimeout(this._hideTimeout); - }, - UNSAFE_componentWillReceiveProps: function(nextProps) { - ensurePositiveDelayProps(nextProps); - }, + this._touchMixin.componentWillUnmount(); + } - viewConfig: { + static 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: PressEvent) { + touchableHandleActivePressIn(e: PressEvent) { clearTimeout(this._hideTimeout); this._hideTimeout = null; this._showUnderlay(); this.props.onPressIn && this.props.onPressIn(e); - }, + } - touchableHandleActivePressOut: function(e: PressEvent) { + touchableHandleActivePressOut(e: PressEvent) { if (!this._hideTimeout) { this._hideUnderlay(); } this.props.onPressOut && this.props.onPressOut(e); - }, + } - touchableHandleFocus: function(e: Event) { + touchableHandleFocus(e: FocusEvent) { if (Platform.isTV) { this._showUnderlay(); } this.props.onFocus && this.props.onFocus(e); - }, + } - touchableHandleBlur: function(e: Event) { + touchableHandleBlur(e: BlurEvent) { if (Platform.isTV) { this._hideUnderlay(); } this.props.onBlur && this.props.onBlur(e); - }, + } - touchableHandlePress: function(e: PressEvent) { + touchableHandlePress(e: PressEvent) { clearTimeout(this._hideTimeout); if (!Platform.isTV) { this._showUnderlay(); @@ -334,33 +364,33 @@ const TouchableHighlight = ((createReactClass({ ); } this.props.onPress && this.props.onPress(e); - }, + } - touchableHandleLongPress: function(e: PressEvent) { + touchableHandleLongPress(e: PressEvent) { this.props.onLongPress && this.props.onLongPress(e); - }, + } - touchableGetPressRectOffset: function() { + touchableGetPressRectOffset() { return this.props.pressRetentionOffset || PRESS_RETENTION_OFFSET; - }, + } - touchableGetHitSlop: function() { + touchableGetHitSlop() { return this.props.hitSlop; - }, + } - touchableGetHighlightDelayMS: function() { + touchableGetHighlightDelayMS() { return this.props.delayPressIn; - }, + } - touchableGetLongPressDelayMS: function() { + touchableGetLongPressDelayMS() { return this.props.delayLongPress; - }, + } - touchableGetPressOutDelayMS: function() { + touchableGetPressOutDelayMS() { return this.props.delayPressOut; - }, + } - _showUnderlay: function() { + _showUnderlay() { if (!this._isMounted || !this._hasPressHandler()) { return; } @@ -373,9 +403,9 @@ const TouchableHighlight = ((createReactClass({ }, }); this.props.onShowUnderlay && this.props.onShowUnderlay(); - }, + } - _hideUnderlay: function() { + _hideUnderlay = () => { clearTimeout(this._hideTimeout); this._hideTimeout = null; if (this.props.testOnly_pressed) { @@ -388,18 +418,18 @@ const TouchableHighlight = ((createReactClass({ }); this.props.onHideUnderlay && this.props.onHideUnderlay(); } - }, + }; - _hasPressHandler: function() { + _hasPressHandler() { return !!( this.props.onPress || this.props.onPressIn || this.props.onPressOut || this.props.onLongPress ); - }, + } - render: function() { + render() { const child = React.Children.only(this.props.children); return ( {React.cloneElement(child, { @@ -444,7 +478,7 @@ const TouchableHighlight = ((createReactClass({ })} ); - }, -}): any): React.ComponentType); + } +} module.exports = TouchableHighlight; diff --git a/Libraries/Components/Touchable/TouchableNativeFeedback.android.js b/Libraries/Components/Touchable/TouchableNativeFeedback.android.js index 77b6e84ea7c60f..15dfefce3a4345 100644 --- a/Libraries/Components/Touchable/TouchableNativeFeedback.android.js +++ b/Libraries/Components/Touchable/TouchableNativeFeedback.android.js @@ -12,36 +12,85 @@ const Platform = require('Platform'); const React = require('React'); -const PropTypes = require('prop-types'); const ReactNative = require('ReactNative'); const Touchable = require('Touchable'); -const TouchableWithoutFeedback = require('TouchableWithoutFeedback'); const UIManager = require('UIManager'); const View = require('View'); -const createReactClass = require('create-react-class'); const ensurePositiveDelayProps = require('ensurePositiveDelayProps'); const processColor = require('processColor'); import type {PressEvent} from 'CoreEventTypes'; +import type {TouchableState} from 'Touchable'; -const rippleBackgroundPropType = PropTypes.shape({ - type: PropTypes.oneOf(['RippleAndroid']), - color: PropTypes.number, - borderless: PropTypes.bool, -}); +const PRESS_RETENTION_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; -const themeAttributeBackgroundPropType = PropTypes.shape({ - type: PropTypes.oneOf(['ThemeAttrAndroid']), - attribute: PropTypes.string.isRequired, -}); +type Props = $ReadOnly<{| + ...TouchableWithoutFeedbackProps, -const backgroundPropType = PropTypes.oneOfType([ - rippleBackgroundPropType, - themeAttributeBackgroundPropType, -]); + /** + * Determines the type of background drawable that's going to be used to + * display feedback. It takes an object with `type` property and extra data + * depending on the `type`. It's recommended to use one of the static + * methods to generate that dictionary. + */ + background?: ?AndroidDrawableRipple, -const PRESS_RETENTION_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; + /** + * TV preferred focus (see documentation for the View component). + */ + hasTVPreferredFocus?: ?boolean, + + /** + * TV next focus down (see documentation for the View component). + */ + nextFocusDown?: ?number, + + /** + * TV next focus forward (see documentation for the View component). + */ + nextFocusForward?: ?number, + + /** + * TV next focus left (see documentation for the View component). + */ + nextFocusLeft?: ?number, + + /** + * TV next focus right (see documentation for the View component). + */ + nextFocusRight?: ?number, + + /** + * TV next focus up (see documentation for the View component). + */ + nextFocusUp?: ?number, + + /** + * Set to true to add the ripple effect to the foreground of the view, instead of the + * background. This is useful if one of your child views has a background of its own, or you're + * e.g. displaying images, and you don't want the ripple to be covered by them. + * + * Check TouchableNativeFeedback.canUseNativeForeground() first, as this is only available on + * Android 6.0 and above. If you try to use this on older versions you will get a warning and + * fallback to background. + */ + useForeground?: ?boolean, +|}>; + +function createTouchMixin( + node: React.ElementRef, +): typeof Touchable.Mixin { + const touchMixin = {...Touchable.Mixin}; + + for (const key in touchMixin) { + if (typeof touchMixin[key] === 'function') { + touchMixin[key] = touchMixin[key].bind(node); + } + } + + return touchMixin; +} /** * A wrapper for making views respond properly to touches (Android only). @@ -71,145 +120,56 @@ const PRESS_RETENTION_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; * }, * ``` */ +class TouchableNativeFeedback extends React.Component { + static defaultProps = { + background: TouchableNativeFeedback.SelectableBackground(), + }; + + _touchMixin: typeof Touchable.Mixin = createTouchMixin(this); + + constructor(props: Props) { + super(props); + + const touchMixin = Touchable.Mixin; + for (const key in touchMixin) { + if ( + typeof touchMixin[key] === 'function' && + (key.startsWith('_') || key.startsWith('touchable')) + ) { + // $FlowFixMe - dynamically adding properties to a class + (this: any)[key] = touchMixin[key].bind(this); + } + } -const TouchableNativeFeedback = createReactClass({ - displayName: 'TouchableNativeFeedback', - propTypes: { - /* $FlowFixMe(>=0.89.0 site=react_native_android_fb) This comment - * suppresses an error found when Flow v0.89 was deployed. To see the - * error, delete this comment and run Flow. */ - ...TouchableWithoutFeedback.propTypes, - - /** - * Determines the type of background drawable that's going to be used to - * display feedback. It takes an object with `type` property and extra data - * depending on the `type`. It's recommended to use one of the static - * methods to generate that dictionary. - */ - background: backgroundPropType, - - /** - * TV preferred focus (see documentation for the View component). - */ - hasTVPreferredFocus: PropTypes.bool, - - /** - * TV next focus down (see documentation for the View component). - */ - nextFocusDown: PropTypes.number, - - /** - * TV next focus forward (see documentation for the View component). - */ - nextFocusForward: PropTypes.number, - - /** - * TV next focus left (see documentation for the View component). - */ - nextFocusLeft: PropTypes.number, - - /** - * TV next focus right (see documentation for the View component). - */ - nextFocusRight: PropTypes.number, - - /** - * TV next focus up (see documentation for the View component). - */ - nextFocusUp: PropTypes.number, - - /** - * Set to true to add the ripple effect to the foreground of the view, instead of the - * background. This is useful if one of your child views has a background of its own, or you're - * e.g. displaying images, and you don't want the ripple to be covered by them. - * - * Check TouchableNativeFeedback.canUseNativeForeground() first, as this is only available on - * Android 6.0 and above. If you try to use this on older versions you will get a warning and - * fallback to background. - */ - useForeground: PropTypes.bool, - }, - - statics: { - /** - * Creates an object that represents android theme's default background for - * selectable elements (?android:attr/selectableItemBackground). - */ - SelectableBackground: function(): { - type: 'ThemeAttrAndroid', - attribute: 'selectableItemBackground', - } { - return {type: 'ThemeAttrAndroid', attribute: 'selectableItemBackground'}; - }, - /** - * Creates an object that represent android theme's default background for borderless - * selectable elements (?android:attr/selectableItemBackgroundBorderless). - * Available on android API level 21+. - */ - SelectableBackgroundBorderless: function(): { - type: 'ThemeAttrAndroid', - attribute: 'selectableItemBackgroundBorderless', - } { - return { - type: 'ThemeAttrAndroid', - attribute: 'selectableItemBackgroundBorderless', - }; - }, - /** - * Creates an object that represents ripple drawable with specified color (as a - * string). If property `borderless` evaluates to true the ripple will - * render outside of the view bounds (see native actionbar buttons as an - * example of that behavior). This background type is available on Android - * API level 21+. - * - * @param color The ripple color - * @param borderless If the ripple can render outside it's bounds - */ - Ripple: function( - color: string, - borderless: boolean, - ): { - type: 'RippleAndroid', - color: ?number, - borderless: boolean, - } { - return { - type: 'RippleAndroid', - color: processColor(color), - borderless: borderless, - }; - }, - - canUseNativeForeground: function(): boolean { - return Platform.OS === 'android' && Platform.Version >= 23; - }, - }, - - mixins: [Touchable.Mixin], - - getDefaultProps: function() { - return { - background: this.SelectableBackground(), - }; - }, + Object.keys(touchMixin) + .filter(key => typeof touchMixin[key] !== 'function') + .forEach(key => { + // $FlowFixMe - dynamically adding properties to a class + (this: any)[key] = touchMixin[key]; + }); + + this.state = this._touchMixin.touchableGetInitialState(); + } + + componentDidMount() { + ensurePositiveDelayProps(this.props); - getInitialState: function() { - return this.touchableGetInitialState(); - }, + this._touchMixin.componentDidMount(); + } - componentDidMount: function() { + componentDidUpdate(prevProps, prevState) { ensurePositiveDelayProps(this.props); - }, + } - UNSAFE_componentWillReceiveProps: function(nextProps) { - ensurePositiveDelayProps(nextProps); - }, + componentWillUnmount() { + this._touchMixin.componentWillUnmount(); + } /** * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are * defined on your component. */ - touchableHandleActivePressIn: function(e: PressEvent) { + touchableHandleActivePressIn(e: PressEvent) { this.props.onPressIn && this.props.onPressIn(e); this._dispatchPressedStateChange(true); if (this.pressInLocation) { @@ -218,67 +178,67 @@ const TouchableNativeFeedback = createReactClass({ this.pressInLocation.locationY, ); } - }, + } - touchableHandleActivePressOut: function(e: PressEvent) { + touchableHandleActivePressOut(e: PressEvent) { this.props.onPressOut && this.props.onPressOut(e); this._dispatchPressedStateChange(false); - }, + } - touchableHandlePress: function(e: PressEvent) { + touchableHandlePress(e: PressEvent) { this.props.onPress && this.props.onPress(e); - }, + } - touchableHandleLongPress: function(e: PressEvent) { + touchableHandleLongPress(e: PressEvent) { this.props.onLongPress && this.props.onLongPress(e); - }, + } - touchableGetPressRectOffset: function() { + touchableGetPressRectOffset() { // Always make sure to predeclare a constant! return this.props.pressRetentionOffset || PRESS_RETENTION_OFFSET; - }, + } - touchableGetHitSlop: function() { + touchableGetHitSlop() { return this.props.hitSlop; - }, + } - touchableGetHighlightDelayMS: function() { + touchableGetHighlightDelayMS() { return this.props.delayPressIn; - }, + } - touchableGetLongPressDelayMS: function() { + touchableGetLongPressDelayMS() { return this.props.delayLongPress; - }, + } - touchableGetPressOutDelayMS: function() { + touchableGetPressOutDelayMS() { return this.props.delayPressOut; - }, + } - _handleResponderMove: function(e) { + _handleResponderMove = e => { this.touchableHandleResponderMove(e); this._dispatchHotspotUpdate( e.nativeEvent.locationX, e.nativeEvent.locationY, ); - }, + }; - _dispatchHotspotUpdate: function(destX, destY) { + _dispatchHotspotUpdate(destX, destY) { UIManager.dispatchViewManagerCommand( ReactNative.findNodeHandle(this), UIManager.getViewManagerConfig('RCTView').Commands.hotspotUpdate, [destX || 0, destY || 0], ); - }, + } - _dispatchPressedStateChange: function(pressed) { + _dispatchPressedStateChange(pressed) { UIManager.dispatchViewManagerCommand( ReactNative.findNodeHandle(this), UIManager.getViewManagerConfig('RCTView').Commands.setPressed, [pressed], ); - }, + } - render: function() { + render() { const child = React.Children.only(this.props.children); let children = child.props.children; if (Touchable.TOUCH_TARGET_DEBUG && child.type === View) { @@ -325,20 +285,76 @@ const TouchableNativeFeedback = createReactClass({ nextFocusRight: this.props.nextFocusRight, nextFocusUp: this.props.nextFocusUp, hasTVPreferredFocus: this.props.hasTVPreferredFocus, - onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder, - onResponderTerminationRequest: this + onStartShouldSetResponder: this._touchMixin + .touchableHandleStartShouldSetResponder, + onResponderTerminationRequest: this._touchMixin .touchableHandleResponderTerminationRequest, - onResponderGrant: this.touchableHandleResponderGrant, + onResponderGrant: this._touchMixin.touchableHandleResponderGrant, onResponderMove: this._handleResponderMove, - onResponderRelease: this.touchableHandleResponderRelease, - onResponderTerminate: this.touchableHandleResponderTerminate, + onResponderRelease: this._touchMixin.touchableHandleResponderRelease, + onResponderTerminate: this._touchMixin.touchableHandleResponderTerminate, }; // We need to clone the actual element so that the ripple background drawable // can be applied directly to the background of this element rather than to // a wrapper view as done in other Touchable* return React.cloneElement(child, childProps); - }, -}); + } + + /** + * Creates an object that represents android theme's default background for + * selectable elements (?android:attr/selectableItemBackground). + */ + static SelectableBackground(): { + type: 'ThemeAttrAndroid', + attribute: 'selectableItemBackground', + } { + return {type: 'ThemeAttrAndroid', attribute: 'selectableItemBackground'}; + } + + /** + * Creates an object that represent android theme's default background for borderless + * selectable elements (?android:attr/selectableItemBackgroundBorderless). + * Available on android API level 21+. + */ + static SelectableBackgroundBorderless(): { + type: 'ThemeAttrAndroid', + attribute: 'selectableItemBackgroundBorderless', + } { + return { + type: 'ThemeAttrAndroid', + attribute: 'selectableItemBackgroundBorderless', + }; + } + + /** + * Creates an object that represents ripple drawable with specified color (as a + * string). If property `borderless` evaluates to true the ripple will + * render outside of the view bounds (see native actionbar buttons as an + * example of that behavior). This background type is available on Android + * API level 21+. + * + * @param color The ripple color + * @param borderless If the ripple can render outside it's bounds + */ + static Ripple( + color: string, + borderless: boolean, + ): { + type: 'RippleAndroid', + color: ?number, + borderless: boolean, + } { + return { + type: 'RippleAndroid', + color: processColor(color), + borderless: borderless, + }; + } + + static canUseNativeForeground(): boolean { + return Platform.OS === 'android' && Platform.Version >= 23; + } +} module.exports = TouchableNativeFeedback; diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js index 98a2c5d0afd099..4228e47d95c07a 100644 --- a/Libraries/Components/Touchable/TouchableOpacity.js +++ b/Libraries/Components/Touchable/TouchableOpacity.js @@ -11,18 +11,16 @@ 'use strict'; const Animated = require('Animated'); +const AnimatedNode = require('AnimatedNode'); const Easing = require('Easing'); -const NativeMethodsMixin = require('NativeMethodsMixin'); const Platform = require('Platform'); const React = require('React'); -const PropTypes = require('prop-types'); const Touchable = require('Touchable'); -const TouchableWithoutFeedback = require('TouchableWithoutFeedback'); -const createReactClass = require('create-react-class'); const ensurePositiveDelayProps = require('ensurePositiveDelayProps'); const flattenStyle = require('flattenStyle'); +import type {BlurEvent, FocusEvent, TouchableState} from 'Touchable'; import type {Props as TouchableWithoutFeedbackProps} from 'TouchableWithoutFeedback'; import type {ViewStyleProp} from 'StyleSheet'; import type {TVParallaxPropertiesType} from 'TVViewPropTypes'; @@ -31,22 +29,78 @@ import type {PressEvent} from 'CoreEventTypes'; const PRESS_RETENTION_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; type TVProps = $ReadOnly<{| + /** + * TV preferred focus (see documentation for the View component). + */ hasTVPreferredFocus?: ?boolean, + /** + * TV next focus down (see documentation for the View component). + * + * @platform android + */ nextFocusDown?: ?number, + /** + * TV next focus forward (see documentation for the View component). + * + * @platform android + */ nextFocusForward?: ?number, + /** + * TV next focus left (see documentation for the View component). + * + * @platform android + */ nextFocusLeft?: ?number, + /** + * TV next focus right (see documentation for the View component). + * + * @platform android + */ nextFocusRight?: ?number, + /** + * TV next focus up (see documentation for the View component). + * + * @platform android + */ nextFocusUp?: ?number, + /** + * Apple TV parallax effects + */ tvParallaxProperties?: ?TVParallaxPropertiesType, |}>; type Props = $ReadOnly<{| ...TouchableWithoutFeedbackProps, ...TVProps, - activeOpacity?: ?number, + + /** + * Determines what the opacity of the wrapped view should be when touch is + * active. Defaults to 0.2. + */ + activeOpacity: number, style?: ?ViewStyleProp, |}>; +type State = {| + ...TouchableState, + + anim: Animated.Value, +|}; + +function createTouchMixin( + node: React.ElementRef, +): typeof Touchable.MixinWithoutDefaultFocusAndBlur { + const touchMixin = {...Touchable.MixinWithoutDefaultFocusAndBlur}; + + for (const key in touchMixin) { + if (typeof touchMixin[key] === 'function') { + touchMixin[key] = touchMixin[key].bind(node); + } + } + + return touchMixin; +} + /** * A wrapper for making views respond properly to touches. * On press down, the opacity of the wrapped view is decreased, dimming it. @@ -135,175 +189,157 @@ type Props = $ReadOnly<{| * ``` * */ -const TouchableOpacity = ((createReactClass({ - displayName: 'TouchableOpacity', - mixins: [Touchable.Mixin.withoutDefaultFocusAndBlur, NativeMethodsMixin], - - propTypes: { - /* $FlowFixMe(>=0.89.0 site=react_native_fb) This comment suppresses an - * error found when Flow v0.89 was deployed. To see the error, delete this - * comment and run Flow. */ - ...TouchableWithoutFeedback.propTypes, - /** - * Determines what the opacity of the wrapped view should be when touch is - * active. Defaults to 0.2. - */ - activeOpacity: PropTypes.number, - /** - * TV preferred focus (see documentation for the View component). - */ - hasTVPreferredFocus: PropTypes.bool, - /** - * TV next focus down (see documentation for the View component). - * - * @platform android - */ - nextFocusDown: PropTypes.number, - /** - * TV next focus forward (see documentation for the View component). - * - * @platform android - */ - nextFocusForward: PropTypes.number, - /** - * TV next focus left (see documentation for the View component). - * - * @platform android - */ - nextFocusLeft: PropTypes.number, - /** - * TV next focus right (see documentation for the View component). - * - * @platform android - */ - nextFocusRight: PropTypes.number, - /** - * TV next focus up (see documentation for the View component). - * - * @platform android - */ - nextFocusUp: PropTypes.number, - /** - * Apple TV parallax effects - */ - tvParallaxProperties: PropTypes.object, - }, - - getDefaultProps: function() { - return { - activeOpacity: 0.2, - }; - }, +class TouchableOpacity extends React.Component { + static defaultProps = { + activeOpacity: 0.2, + }; + + _touchMixin: typeof Touchable.MixinWithoutDefaultFocusAndBlur = createTouchMixin( + this, + ); + + _isMounted: boolean; + + constructor(props: Props) { + super(props); + + const touchMixin = Touchable.MixinWithoutDefaultFocusAndBlur; + for (const key in touchMixin) { + if ( + typeof touchMixin[key] === 'function' && + (key.startsWith('_') || key.startsWith('touchable')) + ) { + // $FlowFixMe - dynamically adding properties to a class + (this: any)[key] = touchMixin[key].bind(this); + } + } + + Object.keys(touchMixin) + .filter(key => typeof touchMixin[key] !== 'function') + .forEach(key => { + // $FlowFixMe - dynamically adding properties to a class + (this: any)[key] = touchMixin[key]; + }); + + this.state = { + ...this._touchMixin.touchableGetInitialState(), - getInitialState: function() { - return { - ...this.touchableGetInitialState(), anim: new Animated.Value(this._getChildStyleOpacityWithDefault()), }; - }, + } - componentDidMount: function() { + componentDidMount() { ensurePositiveDelayProps(this.props); - }, - UNSAFE_componentWillReceiveProps: function(nextProps) { - ensurePositiveDelayProps(nextProps); - }, + this._touchMixin.componentDidMount(); + } + + componentDidUpdate(prevProps: Props, prevState: State) { + ensurePositiveDelayProps(this.props); - componentDidUpdate: function(prevProps, prevState) { if (this.props.disabled !== prevProps.disabled) { this._opacityInactive(250); } - }, + } + + componentWillUnmount() { + this._touchMixin.componentWillUnmount(); + } /** * Animate the touchable to a new opacity. */ - setOpacityTo: function(value: number, duration: number) { + setOpacityTo(value: number, duration: number) { Animated.timing(this.state.anim, { toValue: value, duration: duration, easing: Easing.inOut(Easing.quad), useNativeDriver: true, }).start(); - }, + } /** * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are * defined on your component. */ - touchableHandleActivePressIn: function(e: PressEvent) { + touchableHandleActivePressIn(e: PressEvent) { if (e.dispatchConfig.registrationName === 'onResponderGrant') { this._opacityActive(0); } else { this._opacityActive(150); } this.props.onPressIn && this.props.onPressIn(e); - }, + } - touchableHandleActivePressOut: function(e: PressEvent) { + touchableHandleActivePressOut(e: PressEvent) { this._opacityInactive(250); this.props.onPressOut && this.props.onPressOut(e); - }, + } - touchableHandleFocus: function(e: Event) { + touchableHandleFocus(e: FocusEvent) { if (Platform.isTV) { this._opacityActive(150); } this.props.onFocus && this.props.onFocus(e); - }, + } - touchableHandleBlur: function(e: Event) { + touchableHandleBlur(e: BlurEvent) { if (Platform.isTV) { this._opacityInactive(250); } this.props.onBlur && this.props.onBlur(e); - }, + } - touchableHandlePress: function(e: PressEvent) { + touchableHandlePress(e: PressEvent) { this.props.onPress && this.props.onPress(e); - }, + } - touchableHandleLongPress: function(e: PressEvent) { + touchableHandleLongPress(e: PressEvent) { this.props.onLongPress && this.props.onLongPress(e); - }, + } - touchableGetPressRectOffset: function() { + touchableGetPressRectOffset() { return this.props.pressRetentionOffset || PRESS_RETENTION_OFFSET; - }, + } - touchableGetHitSlop: function() { + touchableGetHitSlop() { return this.props.hitSlop; - }, + } - touchableGetHighlightDelayMS: function() { + touchableGetHighlightDelayMS() { return this.props.delayPressIn || 0; - }, + } - touchableGetLongPressDelayMS: function() { + touchableGetLongPressDelayMS() { return this.props.delayLongPress === 0 ? 0 : this.props.delayLongPress || 500; - }, + } - touchableGetPressOutDelayMS: function() { + touchableGetPressOutDelayMS() { return this.props.delayPressOut; - }, + } - _opacityActive: function(duration: number) { + _opacityActive(duration: number) { this.setOpacityTo(this.props.activeOpacity, duration); - }, + } - _opacityInactive: function(duration: number) { + _opacityInactive(duration: number) { this.setOpacityTo(this._getChildStyleOpacityWithDefault(), duration); - }, + } - _getChildStyleOpacityWithDefault: function() { + _getChildStyleOpacityWithDefault(): number { const childStyle = flattenStyle(this.props.style) || {}; + + // childStyle.opacity could be an AnimatedNode + if (typeof childStyle.opacity !== 'number') { + return 1; + } + return childStyle.opacity == null ? 1 : childStyle.opacity; - }, + } - render: function() { + render() { return ( + onResponderGrant={this._touchMixin.touchableHandleResponderGrant} + onResponderMove={this._touchMixin.touchableHandleResponderMove} + onResponderRelease={this._touchMixin.touchableHandleResponderRelease} + onResponderTerminate={ + this._touchMixin.touchableHandleResponderTerminate + }> {this.props.children} {Touchable.renderDebugView({ color: 'cyan', @@ -339,7 +379,7 @@ const TouchableOpacity = ((createReactClass({ })} ); - }, -}): any): React.ComponentType); + } +} module.exports = TouchableOpacity; diff --git a/Libraries/Components/Touchable/TouchableWithoutFeedback.js b/Libraries/Components/Touchable/TouchableWithoutFeedback.js index a78de287e41e77..3e17ed79066bc7 100755 --- a/Libraries/Components/Touchable/TouchableWithoutFeedback.js +++ b/Libraries/Components/Touchable/TouchableWithoutFeedback.js @@ -10,24 +10,15 @@ 'use strict'; -const DeprecatedEdgeInsetsPropType = require('DeprecatedEdgeInsetsPropType'); const React = require('React'); -const PropTypes = require('prop-types'); const Touchable = require('Touchable'); const View = require('View'); -const createReactClass = require('create-react-class'); const ensurePositiveDelayProps = require('ensurePositiveDelayProps'); -const { - DeprecatedAccessibilityComponentTypes, - DeprecatedAccessibilityRoles, - DeprecatedAccessibilityStates, - DeprecatedAccessibilityTraits, -} = require('DeprecatedViewAccessibility'); - import type {SyntheticEvent, LayoutEvent, PressEvent} from 'CoreEventTypes'; import type {EdgeInsetsProp} from 'EdgeInsetsPropType'; +import type {TouchableState} from 'Touchable'; import type { AccessibilityComponentType, AccessibilityRole, @@ -72,24 +63,91 @@ export type Props = $ReadOnly<{| accessibilityStates?: ?AccessibilityStates, accessibilityTraits?: ?AccessibilityTraits, children?: ?React.Node, + /** + * Delay in ms, from onPressIn, before onLongPress is called. + */ delayLongPress?: ?number, + /** + * Delay in ms, from the start of the touch, before onPressIn is called. + */ delayPressIn?: ?number, + /** + * Delay in ms, from the release of the touch, before onPressOut is called. + */ delayPressOut?: ?number, + /** + * If true, disable all interactions for this component. + */ disabled?: ?boolean, + /** + * 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?: ?EdgeInsetsProp, nativeID?: ?string, + /** + * When `accessible` is true (which is the default) this may be called when + * the OS-specific concept of "blur" occurs, meaning the element lost focus. + * Some platforms may not have the concept of blur. + */ onBlur?: ?(e: BlurEvent) => void, + /** + * When `accessible` is true (which is the default) this may be called when + * the OS-specific concept of "focus" occurs. Some platforms may not have + * the concept of focus. + */ onFocus?: ?(e: FocusEvent) => void, + /** + * Invoked on mount and layout changes with + * + * `{nativeEvent: {layout: {x, y, width, height}}}` + */ onLayout?: ?(event: LayoutEvent) => mixed, onLongPress?: ?(event: PressEvent) => mixed, + /** + * Called when the touch is released, but not if cancelled (e.g. by a scroll + * that steals the responder lock). + */ onPress?: ?(event: PressEvent) => mixed, + /** + * Called as soon as the touchable element is pressed and invoked even before onPress. + * This can be useful when making network requests. + */ onPressIn?: ?(event: PressEvent) => mixed, + /** + * Called as soon as the touch is released even before onPress. + */ onPressOut?: ?(event: PressEvent) => mixed, + /** + * 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?: ?EdgeInsetsProp, rejectResponderTermination?: ?boolean, testID?: ?string, |}>; +function createTouchMixin( + node: React.ElementRef, +): typeof Touchable.Mixin { + const touchMixin = {...Touchable.Mixin}; + + for (const key in touchMixin) { + if (typeof touchMixin[key] === 'function') { + touchMixin[key] = touchMixin[key].bind(node); + } + } + + return touchMixin; +} + /** * Do not use unless you have a very good reason. All elements that * respond to press should have a visual feedback when touched. @@ -97,155 +155,118 @@ export type Props = $ReadOnly<{| * TouchableWithoutFeedback supports only one child. * If you wish to have several child components, wrap them in a View. */ -const TouchableWithoutFeedback = ((createReactClass({ - displayName: 'TouchableWithoutFeedback', - mixins: [Touchable.Mixin], - - propTypes: { - accessible: PropTypes.bool, - accessibilityLabel: PropTypes.node, - accessibilityHint: PropTypes.string, - accessibilityComponentType: PropTypes.oneOf( - DeprecatedAccessibilityComponentTypes, - ), - accessibilityIgnoresInvertColors: PropTypes.bool, - accessibilityRole: PropTypes.oneOf(DeprecatedAccessibilityRoles), - accessibilityStates: PropTypes.arrayOf( - PropTypes.oneOf(DeprecatedAccessibilityStates), - ), - accessibilityTraits: PropTypes.oneOfType([ - PropTypes.oneOf(DeprecatedAccessibilityTraits), - PropTypes.arrayOf(PropTypes.oneOf(DeprecatedAccessibilityTraits)), - ]), - /** - * When `accessible` is true (which is the default) this may be called when - * the OS-specific concept of "focus" occurs. Some platforms may not have - * the concept of focus. - */ - onFocus: PropTypes.func, - /** - * When `accessible` is true (which is the default) this may be called when - * the OS-specific concept of "blur" occurs, meaning the element lost focus. - * Some platforms may not have the concept of blur. - */ - onBlur: PropTypes.func, - /** - * If true, disable all interactions for this component. - */ - disabled: PropTypes.bool, - /** - * Called when the touch is released, but not if cancelled (e.g. by a scroll - * that steals the responder lock). - */ - onPress: PropTypes.func, - /** - * Called as soon as the touchable element is pressed and invoked even before onPress. - * This can be useful when making network requests. - */ - onPressIn: PropTypes.func, - /** - * Called as soon as the touch is released even before onPress. - */ - onPressOut: PropTypes.func, - /** - * Invoked on mount and layout changes with - * - * `{nativeEvent: {layout: {x, y, width, height}}}` - */ - onLayout: PropTypes.func, +class TouchableWithoutFeedback extends React.Component { + /** + * Part 1: Removing Touchable.Mixin: + * + * 1. Mixin methods should be flow typed. That's why we create a + * copy of Touchable.Mixin and attach it to this._touchMixin. + * Otherwise, we'd have to manually declare each method on the component + * class and assign it a flow type. + * 2. Mixin methods can call component methods, and access the component's + * props and state. So, we need to bind all mixin methods to the + * component instance. + * 3. Continued... + */ + _touchMixin: typeof Touchable.Mixin = createTouchMixin(this); - onLongPress: PropTypes.func, + _isMounted: boolean; - nativeID: PropTypes.string, - testID: PropTypes.string, + constructor(props: Props) { + super(props); /** - * Delay in ms, from the start of the touch, before onPressIn is called. - */ - delayPressIn: PropTypes.number, - /** - * Delay in ms, from the release of the touch, before onPressOut is called. - */ - delayPressOut: PropTypes.number, - /** - * Delay in ms, from onPressIn, before onLongPress is called. - */ - delayLongPress: 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. + * Part 2: Removing Touchable.Mixin + * + * 3. Mixin methods access other mixin methods via dynamic dispatch using + * this. Since mixin methods are bound to the component instance, we need + * to copy all mixin methods to the component instance. */ - pressRetentionOffset: DeprecatedEdgeInsetsPropType, + const touchMixin = Touchable.Mixin; + for (const key in touchMixin) { + if ( + typeof touchMixin[key] === 'function' && + (key.startsWith('_') || key.startsWith('touchable')) + ) { + // $FlowFixMe - dynamically adding properties to a class + (this: any)[key] = touchMixin[key].bind(this); + } + } + /** - * 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. + * Part 3: Removing Touchable.Mixin + * + * 4. Mixins can initialize properties and use properties on the component + * instance. */ - hitSlop: DeprecatedEdgeInsetsPropType, - }, + Object.keys(touchMixin) + .filter(key => typeof touchMixin[key] !== 'function') + .forEach(key => { + // $FlowFixMe - dynamically adding properties to a class + (this: any)[key] = touchMixin[key]; + }); + + this.state = this._touchMixin.touchableGetInitialState(); + } + + componentDidMount() { + ensurePositiveDelayProps(this.props); - getInitialState: function() { - return this.touchableGetInitialState(); - }, + this._touchMixin.componentDidMount(); + } - componentDidMount: function() { + componentDidUpdate(prevProps: Props, prevState: TouchableState) { ensurePositiveDelayProps(this.props); - }, + } - UNSAFE_componentWillReceiveProps: function(nextProps: Object) { - ensurePositiveDelayProps(nextProps); - }, + componentWillUnmount() { + this._touchMixin.componentWillUnmount(); + } /** * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are * defined on your component. */ - touchableHandlePress: function(e: PressEvent) { + touchableHandlePress(e: PressEvent) { this.props.onPress && this.props.onPress(e); - }, + } - touchableHandleActivePressIn: function(e: PressEvent) { + touchableHandleActivePressIn(e: PressEvent) { this.props.onPressIn && this.props.onPressIn(e); - }, + } - touchableHandleActivePressOut: function(e: PressEvent) { + touchableHandleActivePressOut(e: PressEvent) { this.props.onPressOut && this.props.onPressOut(e); - }, + } - touchableHandleLongPress: function(e: PressEvent) { + touchableHandleLongPress(e: PressEvent) { this.props.onLongPress && this.props.onLongPress(e); - }, + } - touchableGetPressRectOffset: function(): typeof PRESS_RETENTION_OFFSET { + touchableGetPressRectOffset(): EdgeInsetsProp { // $FlowFixMe Invalid prop usage return this.props.pressRetentionOffset || PRESS_RETENTION_OFFSET; - }, + } - touchableGetHitSlop: function(): ?Object { + touchableGetHitSlop(): ?EdgeInsetsProp { return this.props.hitSlop; - }, + } - touchableGetHighlightDelayMS: function(): number { + touchableGetHighlightDelayMS(): number { return this.props.delayPressIn || 0; - }, + } - touchableGetLongPressDelayMS: function(): number { + touchableGetLongPressDelayMS(): number { return this.props.delayLongPress === 0 ? 0 : this.props.delayLongPress || 500; - }, + } - touchableGetPressOutDelayMS: function(): number { + touchableGetPressOutDelayMS(): number { return this.props.delayPressOut || 0; - }, + } - render: function(): React.Element { + render(): React.Element { // Note(avik): remove dynamic typecast once Flow has been upgraded // $FlowFixMe(>=0.41.0) const child = React.Children.only(this.props.children); @@ -267,16 +288,17 @@ const TouchableWithoutFeedback = ((createReactClass({ return (React: any).cloneElement(child, { ...overrides, accessible: this.props.accessible !== false, - onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder, - onResponderTerminationRequest: this + onStartShouldSetResponder: this._touchMixin + .touchableHandleStartShouldSetResponder, + onResponderTerminationRequest: this._touchMixin .touchableHandleResponderTerminationRequest, - onResponderGrant: this.touchableHandleResponderGrant, - onResponderMove: this.touchableHandleResponderMove, - onResponderRelease: this.touchableHandleResponderRelease, - onResponderTerminate: this.touchableHandleResponderTerminate, + onResponderGrant: this._touchMixin.touchableHandleResponderGrant, + onResponderMove: this._touchMixin.touchableHandleResponderMove, + onResponderRelease: this._touchMixin.touchableHandleResponderRelease, + onResponderTerminate: this._touchMixin.touchableHandleResponderTerminate, children, }); - }, -}): any): React.ComponentType); + } +} module.exports = TouchableWithoutFeedback; diff --git a/RNTester/js/StrictModeExample.js b/RNTester/js/StrictModeExample.js index 1ca3964d76c263..83e1dfca4fdcb7 100644 --- a/RNTester/js/StrictModeExample.js +++ b/RNTester/js/StrictModeExample.js @@ -13,23 +13,73 @@ const React = require('react'); const {StrictMode} = React; const ReactNative = require('react-native'); -const {ScrollView, Text} = ReactNative; +const { + ScrollView, + Text, + TouchableHighlight, + TouchableNativeFeedback, + TouchableOpacity, + TouchableWithoutFeedback, + View, +} = ReactNative; +const TouchableBounce = require('TouchableBounce'); type Props = $ReadOnly<{||}>; type State = {|result: string|}; -const componentsToTest = [ScrollView]; +const componentsToTest = [ + [ScrollView, {}], + [ + TouchableBounce, + { + onPress: () => console.warn('[press]'), + }, + ], + [ + TouchableHighlight, + { + onPress: () => console.warn('[press]'), + }, + ], + [ + // Caveat: Contains ReactNative.findNodeHandle which is not allowed in strict mode + TouchableNativeFeedback, + { + onPress: () => console.warn('[press]'), + background: TouchableNativeFeedback.Ripple('rgba(0, 0, 255, 0.4)', true), + children: ( + + I am a TouchableNativeFeedback + + ), + }, + ], + [ + TouchableOpacity, + { + onPress: () => console.warn('[press]'), + }, + ], + [ + TouchableWithoutFeedback, + { + onPress: () => console.warn('[press]'), + }, + ], +]; class StrictModeExample extends React.Component { render() { return ( - - {componentsToTest.map(Component => ( - - {Component.displayName} - - ))} - + + + {componentsToTest.map(([Component, props]) => ( + + {props.children || I am a {Component.displayName}} + + ))} + + ); } }