/** * Copyright (c) Facebook, Inc. and its affiliates. * * 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 */ 'use strict'; import Pressability, { type PressabilityConfig, } from '../../Pressability/Pressability'; import {PressabilityDebugView} from '../../Pressability/PressabilityDebug'; import TVTouchable from './TVTouchable'; import typeof TouchableWithoutFeedback from './TouchableWithoutFeedback'; 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 '../../Utilities/Platform'; import * as React from 'react'; type TVProps = $ReadOnly<{| hasTVPreferredFocus?: ?boolean, nextFocusDown?: ?number, nextFocusForward?: ?number, nextFocusLeft?: ?number, nextFocusRight?: ?number, nextFocusUp?: ?number, |}>; type Props = $ReadOnly<{| ...React.ElementConfig, ...TVProps, activeOpacity?: ?number, style?: ?ViewStyleProp, hostRef: React.Ref, |}>; type State = $ReadOnly<{| anim: Animated.Value, pressability: Pressability, |}>; /** * A wrapper for making views respond properly to touches. * On press down, the opacity of the wrapped view is decreased, dimming it. * * Opacity is controlled by wrapping the children in an Animated.View, which is * added to the view hierarchy. Be aware that this can affect layout. * * Example: * * ``` * renderButton: function() { * return ( * * * * ); * }, * ``` * ### Example * * ```ReactNativeWebPlayer * import React, { Component } from 'react' * import { * AppRegistry, * StyleSheet, * TouchableOpacity, * Text, * View, * } from 'react-native' * * class App extends Component { * state = { count: 0 } * * onPress = () => { * this.setState(state => ({ * count: state.count + 1 * })); * }; * * render() { * return ( * * * Touch Here * * * * { this.state.count !== 0 ? this.state.count: null} * * * * ) * } * } * * const styles = StyleSheet.create({ * container: { * flex: 1, * justifyContent: 'center', * paddingHorizontal: 10 * }, * button: { * alignItems: 'center', * backgroundColor: '#DDDDDD', * padding: 10 * }, * countContainer: { * alignItems: 'center', * padding: 10 * }, * countText: { * color: '#FF00FF' * } * }) * * AppRegistry.registerComponent('App', () => App) * ``` * */ class TouchableOpacity extends React.Component { _tvTouchable: ?TVTouchable; state: State = { anim: new Animated.Value(this._getChildStyleOpacityWithDefault()), pressability: new Pressability(this._createPressabilityConfig()), }; _createPressabilityConfig(): PressabilityConfig { return { cancelable: !this.props.rejectResponderTermination, disabled: this.props.disabled, hitSlop: this.props.hitSlop, delayLongPress: this.props.delayLongPress, delayPressIn: this.props.delayPressIn, delayPressOut: this.props.delayPressOut, minPressDuration: 0, pressRectOffset: 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: this.props.onLongPress, onPress: this.props.onPress, 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); } }, }; } /** * Animate the touchable to a new opacity. */ _setOpacityTo(toValue: number, duration: number): void { Animated.timing(this.state.anim, { toValue, duration, easing: Easing.inOut(Easing.quad), useNativeDriver: true, }).start(); } _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(); return ( {this.props.children} {__DEV__ ? ( ) : null} ); } 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) { this.state.pressability.configure(this._createPressabilityConfig()); 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 = (React.forwardRef((props, hostRef) => ( )): React.ComponentType<$ReadOnly<$Diff>>);