/** * Copyright (c) Meta Platforms, Inc. and 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 */ import type {ColorValue} from '../../StyleSheet/StyleSheet'; import typeof TouchableWithoutFeedback from './TouchableWithoutFeedback'; import View from '../../Components/View/View'; import Pressability, { type PressabilityConfig, } from '../../Pressability/Pressability'; import {PressabilityDebugView} from '../../Pressability/PressabilityDebug'; import StyleSheet, {type ViewStyleProp} from '../../StyleSheet/StyleSheet'; import Platform from '../../Utilities/Platform'; import * as React from 'react'; type AndroidProps = $ReadOnly<{| nextFocusDown?: ?number, nextFocusForward?: ?number, nextFocusLeft?: ?number, nextFocusRight?: ?number, nextFocusUp?: ?number, |}>; type IOSProps = $ReadOnly<{| hasTVPreferredFocus?: ?boolean, |}>; type Props = $ReadOnly<{| ...React.ElementConfig, ...AndroidProps, ...IOSProps, activeOpacity?: ?number, underlayColor?: ?ColorValue, style?: ?ViewStyleProp, onShowUnderlay?: ?() => void, onHideUnderlay?: ?() => void, testOnly_pressed?: ?boolean, hostRef: React.Ref, |}>; type ExtraStyles = $ReadOnly<{| child: ViewStyleProp, underlay: ViewStyleProp, |}>; type State = $ReadOnly<{| pressability: Pressability, extraStyles: ?ExtraStyles, |}>; /** * A wrapper for making views respond properly to touches. * On press down, the opacity of the wrapped view is decreased, which allows * the underlay color to show through, darkening or tinting the view. * * The underlay comes from wrapping the child in a new View, which can affect * layout, and sometimes cause unwanted visual artifacts if not used correctly, * for example if the backgroundColor of the wrapped view isn't explicitly set * to an opaque color. * * TouchableHighlight must have one child (not zero or more than one). * If you wish to have several child components, wrap them in a View. * * Example: * * ``` * renderButton: function() { * return ( * * * * ); * }, * ``` * * * ### Example * * ```ReactNativeWebPlayer * import React, { Component } from 'react' * import { * AppRegistry, * StyleSheet, * TouchableHighlight, * Text, * View, * } from 'react-native' * * class App extends Component { * constructor(props) { * super(props) * this.state = { count: 0 } * } * * onPress = () => { * this.setState({ * count: this.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 TouchableHighlight extends React.Component { _hideTimeout: ?TimeoutID; _isMounted: boolean = false; state: State = { pressability: new Pressability(this._createPressabilityConfig()), extraStyles: this.props.testOnly_pressed === true ? this._createExtraStyles() : null, }; _createPressabilityConfig(): PressabilityConfig { return { cancelable: !this.props.rejectResponderTermination, disabled: this.props.disabled != null ? this.props.disabled : this.props.accessibilityState?.disabled, hitSlop: this.props.hitSlop, delayLongPress: this.props.delayLongPress, delayPressIn: this.props.delayPressIn, delayPressOut: this.props.delayPressOut, minPressDuration: 0, pressRectOffset: this.props.pressRetentionOffset, android_disableSound: this.props.touchSoundDisabled, onBlur: event => { if (Platform.isTV) { this._hideUnderlay(); } if (this.props.onBlur != null) { this.props.onBlur(event); } }, onFocus: event => { if (Platform.isTV) { this._showUnderlay(); } if (this.props.onFocus != null) { this.props.onFocus(event); } }, onLongPress: this.props.onLongPress, onPress: event => { if (this._hideTimeout != null) { clearTimeout(this._hideTimeout); } if (!Platform.isTV) { this._showUnderlay(); this._hideTimeout = setTimeout(() => { this._hideUnderlay(); }, this.props.delayPressOut ?? 0); } if (this.props.onPress != null) { this.props.onPress(event); } }, onPressIn: event => { if (this._hideTimeout != null) { clearTimeout(this._hideTimeout); this._hideTimeout = null; } this._showUnderlay(); if (this.props.onPressIn != null) { this.props.onPressIn(event); } }, onPressOut: event => { if (this._hideTimeout == null) { this._hideUnderlay(); } if (this.props.onPressOut != null) { this.props.onPressOut(event); } }, }; } _createExtraStyles(): ExtraStyles { return { child: {opacity: this.props.activeOpacity ?? 0.85}, underlay: { backgroundColor: this.props.underlayColor === undefined ? 'black' : this.props.underlayColor, }, }; } _showUnderlay(): void { if (!this._isMounted || !this._hasPressHandler()) { return; } this.setState({extraStyles: this._createExtraStyles()}); if (this.props.onShowUnderlay != null) { this.props.onShowUnderlay(); } } _hideUnderlay(): void { if (this._hideTimeout != null) { clearTimeout(this._hideTimeout); this._hideTimeout = null; } if (this.props.testOnly_pressed === true) { return; } if (this._hasPressHandler()) { this.setState({extraStyles: null}); if (this.props.onHideUnderlay != null) { this.props.onHideUnderlay(); } } } _hasPressHandler(): boolean { return ( this.props.onPress != null || this.props.onPressIn != null || this.props.onPressOut != null || this.props.onLongPress != null ); } render(): React.Node { const child = React.Children.only<$FlowFixMe>(this.props.children); // BACKWARD-COMPATIBILITY: Focus and blur events were never supported before // adopting `Pressability`, so preserve that behavior. const {onBlur, onFocus, ...eventHandlersWithoutBlurAndFocus} = this.state.pressability.getEventHandlers(); const accessibilityState = this.props.disabled != null ? { ...this.props.accessibilityState, disabled: this.props.disabled, } : this.props.accessibilityState; const accessibilityValue = { max: this.props['aria-valuemax'] ?? this.props.accessibilityValue?.max, min: this.props['aria-valuemin'] ?? this.props.accessibilityValue?.min, now: this.props['aria-valuenow'] ?? this.props.accessibilityValue?.now, text: this.props['aria-valuetext'] ?? this.props.accessibilityValue?.text, }; const accessibilityLiveRegion = this.props['aria-live'] === 'off' ? 'none' : this.props['aria-live'] ?? this.props.accessibilityLiveRegion; const accessibilityLabel = this.props['aria-label'] ?? this.props.accessibilityLabel; return ( {React.cloneElement(child, { style: StyleSheet.compose( child.props.style, this.state.extraStyles?.child, ), })} {__DEV__ ? ( ) : null} ); } componentDidMount(): void { this._isMounted = true; } componentDidUpdate(prevProps: Props, prevState: State) { this.state.pressability.configure(this._createPressabilityConfig()); } componentWillUnmount(): void { this._isMounted = false; if (this._hideTimeout != null) { clearTimeout(this._hideTimeout); } this.state.pressability.reset(); } } const Touchable = (React.forwardRef((props, hostRef) => ( )): React.AbstractComponent< $ReadOnly<$Diff|}>>, React.ElementRef, >); Touchable.displayName = 'TouchableHighlight'; module.exports = Touchable;