From 88ae24f719d365b004696aff6461535188ca9f41 Mon Sep 17 00:00:00 2001 From: Tim Yung Date: Wed, 27 Nov 2019 01:44:39 -0800 Subject: [PATCH] RN: New `TouchableOpacity` Summary: Launches a new implementation of `TouchableOpacity`. It is implemented using `Pressability` and extends `React.Component`. Notably, `propTypes` no longer exist. Changelog: [General] [Changed] - TouchableOpacity overhauled as a class without propTypes. Reviewed By: TheSavior Differential Revision: D18715858 fbshipit-source-id: 06ba5fc7eaacdbb7dc12d1564cc5d04e1a991229 --- .../Components/Touchable/TouchableOpacity.js | 373 ++++++++---------- 1 file changed, 165 insertions(+), 208 deletions(-) diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js index fc070befea47e3..f06475dc98b002 100644 --- a/Libraries/Components/Touchable/TouchableOpacity.js +++ b/Libraries/Components/Touchable/TouchableOpacity.js @@ -4,32 +4,22 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * + * @flow strict-local * @format - * @flow */ 'use strict'; -import TouchableInjection from './TouchableInjection'; - -const Animated = require('../../Animated/src/Animated'); -const Easing = require('../../Animated/src/Easing'); -const NativeMethodsMixin = require('../../Renderer/shims/NativeMethodsMixin'); -const Platform = require('../../Utilities/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('../../StyleSheet/flattenStyle'); - -import type {Props as TouchableWithoutFeedbackProps} from './TouchableWithoutFeedback'; -import type {ViewStyleProp} from '../../StyleSheet/StyleSheet'; -import type {PressEvent} from '../../Types/CoreEventTypes'; - -const PRESS_RETENTION_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; +import Pressability from '../../Pressability/Pressability.js'; +import {PressabilityDebugView} from '../../Pressability/PressabilityDebug.js'; +import TVTouchable from './TVTouchable.js'; +import type {Props as TouchableWithoutFeedbackProps} from './TouchableWithoutFeedback.js'; +import Animated from 'react-native/Libraries/Animated/src/Animated'; +import Easing from 'react-native/Libraries/Animated/src/Easing'; +import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet'; +import flattenStyle from 'react-native/Libraries/StyleSheet/flattenStyle'; +import {Platform} from 'react-native'; +import * as React from 'react'; type TVProps = $ReadOnly<{| hasTVPreferredFocus?: ?boolean, @@ -40,11 +30,19 @@ type TVProps = $ReadOnly<{| nextFocusUp?: ?number, |}>; -export type Props = $ReadOnly<{| +type Props = $ReadOnly<{| ...TouchableWithoutFeedbackProps, ...TVProps, + activeOpacity?: ?number, style?: ?ViewStyleProp, + + hostRef: React.Ref, +|}>; + +type State = $ReadOnly<{| + anim: Animated.Value, + pressability: Pressability, |}>; /** @@ -81,24 +79,20 @@ export type Props = $ReadOnly<{| * } from 'react-native' * * class App extends Component { - * constructor(props) { - * super(props) - * this.state = { count: 0 } - * } + * state = { count: 0 } * * onPress = () => { - * this.setState({ - * count: this.state.count+1 - * }) - * } + * this.setState(state => ({ + * count: state.count + 1 + * })); + * }; * * render() { * return ( * * + * onPress={this.onPress}> * Touch Here * * @@ -135,171 +129,107 @@ export type Props = $ReadOnly<{| * ``` * */ -const TouchableOpacityImpl = ((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, - }, - - getDefaultProps: function() { - return { - activeOpacity: 0.2, - }; - }, - - getInitialState: function() { - return { - ...this.touchableGetInitialState(), - anim: new Animated.Value(this._getChildStyleOpacityWithDefault()), - }; - }, - - componentDidMount: function() { - ensurePositiveDelayProps(this.props); - }, - - UNSAFE_componentWillReceiveProps: function(nextProps) { - ensurePositiveDelayProps(nextProps); - }, - - componentDidUpdate: function(prevProps, prevState) { - if (this.props.disabled !== prevProps.disabled) { - this._opacityInactive(250); - } - }, +class TouchableOpacity extends React.Component { + _tvTouchable: ?TVTouchable; + + state: State = { + anim: new Animated.Value(this._getChildStyleOpacityWithDefault()), + pressability: new Pressability({ + getHitSlop: () => this.props.hitSlop, + getLongPressDelayMS: () => { + if (this.props.delayLongPress != null) { + const maybeNumber = this.props.delayLongPress; + if (typeof maybeNumber === 'number') { + return maybeNumber; + } + } + return 500; + }, + getPressDelayMS: () => this.props.delayPressIn, + getPressOutDelayMS: () => this.props.delayPressOut, + getPressRectOffset: () => this.props.pressRetentionOffset, + onBlur: event => { + if (Platform.isTV) { + this._opacityInactive(250); + } + if (this.props.onBlur != null) { + this.props.onBlur(event); + } + }, + onFocus: event => { + if (Platform.isTV) { + this._opacityActive(150); + } + if (this.props.onFocus != null) { + this.props.onFocus(event); + } + }, + onLongPress: event => { + if (this.props.onLongPress != null) { + this.props.onLongPress(event); + } + }, + onPress: event => { + if (this.props.onPress != null) { + this.props.onPress(event); + } + }, + onPressIn: event => { + this._opacityActive( + event.dispatchConfig.registrationName === 'onResponderGrant' + ? 0 + : 150, + ); + if (this.props.onPressIn != null) { + this.props.onPressIn(event); + } + }, + onPressOut: event => { + this._opacityInactive(250); + if (this.props.onPressOut != null) { + this.props.onPressOut(event); + } + }, + onResponderTerminationRequest: () => + !this.props.rejectResponderTermination, + onStartShouldSetResponder: () => !this.props.disabled, + }), + }; /** * Animate the touchable to a new opacity. */ - setOpacityTo: function(value: number, duration: number) { + _setOpacityTo(toValue: number, duration: number): void { Animated.timing(this.state.anim, { - toValue: value, - duration: duration, + toValue, + 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) { - if (e.dispatchConfig.registrationName === 'onResponderGrant') { - this._opacityActive(0); - } else { - this._opacityActive(150); - } - this.props.onPressIn && this.props.onPressIn(e); - }, - - touchableHandleActivePressOut: function(e: PressEvent) { - this._opacityInactive(250); - this.props.onPressOut && this.props.onPressOut(e); - }, - - touchableHandleFocus: function(e: Event) { - if (Platform.isTV) { - this._opacityActive(150); - } - this.props.onFocus && this.props.onFocus(e); - }, - - touchableHandleBlur: function(e: Event) { - if (Platform.isTV) { - this._opacityInactive(250); - } - this.props.onBlur && this.props.onBlur(e); - }, - - touchableHandlePress: function(e: PressEvent) { - this.props.onPress && this.props.onPress(e); - }, - - touchableHandleLongPress: function(e: PressEvent) { - 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; - }, + } + + _opacityActive(duration: number): void { + this._setOpacityTo(this.props.activeOpacity ?? 0.2, duration); + } + + _opacityInactive(duration: number): void { + this._setOpacityTo(this._getChildStyleOpacityWithDefault(), duration); + } + + _getChildStyleOpacityWithDefault(): number { + const opacity = flattenStyle(this.props.style)?.opacity; + return typeof opacity === 'number' ? opacity : 1; + } + + render(): React.Node { + // BACKWARD-COMPATIBILITY: Focus and blur events were never supported before + // adopting `Pressability`, so preserve that behavior. + const { + onBlur, + onFocus, + ...eventHandlersWithoutBlurAndFocus + } = this.state.pressability.getEventHandlers(); - touchableGetLongPressDelayMS: function() { - return this.props.delayLongPress === 0 - ? 0 - : this.props.delayLongPress || 500; - }, - - touchableGetPressOutDelayMS: function() { - return this.props.delayPressOut; - }, - - _opacityActive: function(duration: number) { - this.setOpacityTo(this.props.activeOpacity, duration); - }, - - _opacityInactive: function(duration: number) { - this.setOpacityTo(this._getChildStyleOpacityWithDefault(), duration); - }, - - _getChildStyleOpacityWithDefault: function() { - const childStyle = flattenStyle(this.props.style) || {}; - return childStyle.opacity == null ? 1 : childStyle.opacity; - }, - - render: function() { return ( + ref={this.props.hostRef} + {...eventHandlersWithoutBlurAndFocus}> {this.props.children} - {Touchable.renderDebugView({ - color: 'cyan', - hitSlop: this.props.hitSlop, - })} + {__DEV__ ? ( + + ) : null} ); - }, -}): any): React.ComponentType); + } -const TouchableOpacity: React.ComponentType = - TouchableInjection.unstable_TouchableOpacity == null - ? TouchableOpacityImpl - : TouchableInjection.unstable_TouchableOpacity; + componentDidMount(): void { + if (Platform.isTV) { + this._tvTouchable = new TVTouchable(this, { + getDisabled: () => this.props.disabled === true, + onBlur: event => { + if (this.props.onBlur != null) { + this.props.onBlur(event); + } + }, + onFocus: event => { + if (this.props.onFocus != null) { + this.props.onFocus(event); + } + }, + onPress: event => { + if (this.props.onPress != null) { + this.props.onPress(event); + } + }, + }); + } + } + + componentDidUpdate(prevProps: Props, prevState: State) { + if (this.props.disabled !== prevProps.disabled) { + this._opacityInactive(250); + } + } + + componentWillUnmount(): void { + if (Platform.isTV) { + if (this._tvTouchable != null) { + this._tvTouchable.destroy(); + } + } + this.state.pressability.reset(); + } +} -module.exports = TouchableOpacity; +module.exports = (React.forwardRef((props, hostRef) => ( + +)): React.ComponentType<$ReadOnly<$Diff>>);