/** * 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 * @format */ import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; import RNTesterText from '../../components/RNTesterText'; import React from 'react'; import {useEffect, useRef, useState} from 'react'; import { Animated, Image, Platform, StyleSheet, TouchableHighlight, TouchableNativeFeedback, TouchableOpacity, TouchableWithoutFeedback, View, } from 'react-native'; const forceTouchAvailable = (Platform.OS === 'ios' && Platform.constants.forceTouchAvailable) || false; class TouchableHighlightBox extends React.Component<{...}, $FlowFixMe> { state: any | {timesPressed: number} = { timesPressed: 0, }; touchableOnPress = () => { this.setState({ timesPressed: this.state.timesPressed + 1, }); }; render(): React.Node { let textLog = ''; if (this.state.timesPressed > 1) { textLog = this.state.timesPressed + 'x TouchableHighlight onPress'; } else if (this.state.timesPressed > 0) { textLog = 'TouchableHighlight onPress'; } return ( Tap Here For Custom Highlight! {textLog} ); } } class TouchableWithoutFeedbackBox extends React.Component<{...}, $FlowFixMe> { state: any | {timesPressed: number} = { timesPressed: 0, }; textOnPress = () => { this.setState({ timesPressed: this.state.timesPressed + 1, }); }; render(): React.Node { let textLog = ''; if (this.state.timesPressed > 1) { textLog = this.state.timesPressed + 'x TouchableWithoutFeedback onPress'; } else if (this.state.timesPressed > 0) { textLog = 'TouchableWithoutFeedback onPress'; } return ( Tap Here For No Feedback! {textLog} ); } } class TextOnPressBox extends React.Component<{...}, $FlowFixMe> { state: any | {timesPressed: number} = { timesPressed: 0, }; textOnPress = () => { this.setState({ timesPressed: this.state.timesPressed + 1, }); }; render(): React.Node { let textLog = ''; if (this.state.timesPressed > 1) { textLog = this.state.timesPressed + 'x text onPress'; } else if (this.state.timesPressed > 0) { textLog = 'text onPress'; } return ( Text has built-in onPress handling {textLog} ); } } class TouchableFeedbackEvents extends React.Component<{...}, $FlowFixMe> { state: any | {eventLog: Array} = { eventLog: [], }; render(): React.Node { 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 = (eventName: string) => { const limit = 6; const eventLog = this.state.eventLog.slice(0, limit - 1); eventLog.unshift(eventName); this.setState({eventLog}); }; } class TouchableDelayEvents extends React.Component<{...}, $FlowFixMe> { state: any | {eventLog: Array} = { eventLog: [], }; render(): React.Node { return ( this._appendEvent('press')} delayPressIn={400} onPressIn={() => this._appendEvent('pressIn - 400ms delay')} delayPressOut={1000} onPressOut={() => this._appendEvent('pressOut - 1000ms delay')} delayLongPress={800} onLongPress={() => this._appendEvent('longPress - 800ms delay')}> Press Me {this.state.eventLog.map((e, ii) => ( {e} ))} ); } _appendEvent = (eventName: string) => { const limit = 6; const eventLog = this.state.eventLog.slice(0, limit - 1); eventLog.unshift(eventName); this.setState({eventLog}); }; } class ForceTouchExample extends React.Component<{...}, $FlowFixMe> { state: any | {force: number} = { force: 0, }; _renderConsoleText = (): string => { return forceTouchAvailable ? 'Force: ' + this.state.force.toFixed(3) : '3D Touch is not available on this device'; }; render(): React.Node { return ( {this._renderConsoleText()} true} onResponderMove={event => this.setState({force: event.nativeEvent.force}) } onResponderRelease={event => this.setState({force: 0})}> Press Me ); } } class TouchableHitSlop extends React.Component<{...}, $FlowFixMe> { state: any | {timesPressed: number} = { timesPressed: 0, }; onPress = () => { this.setState({ timesPressed: this.state.timesPressed + 1, }); }; render(): React.Node { let log = ''; if (this.state.timesPressed > 1) { log = this.state.timesPressed + 'x onPress'; } else if (this.state.timesPressed > 0) { log = 'onPress'; } return ( Press Outside This View {log} ); } } function TouchableNativeMethodChecker< T: component(ref?: React.RefSetter, ...any), >(props: {Component: T, name: string}): React.Node { const [status, setStatus] = useState(null); const ref = useRef(null); useEffect(() => { setStatus(ref.current != null && typeof ref.current.measure === 'function'); }, []); return ( {props.name + ': '} {status == null ? 'Missing Ref!' : status === true ? 'Native Methods Exist' : 'Native Methods Missing!'} ); } function TouchableNativeMethods() { return ( ); } class TouchableDisabled extends React.Component<{...}> { render(): React.Node { return ( Disabled TouchableOpacity Enabled TouchableOpacity console.log('custom THW text - highlight')}> Disabled TouchableHighlight console.log('custom THW text - highlight')}> Enabled TouchableHighlight console.log('TWOF has been clicked')} disabled={true}> Disabled TouchableWithoutFeedback console.log('TWOF has been clicked')} disabled={false}> Enabled TouchableWithoutFeedback {Platform.OS === 'android' && ( <> console.log('custom TNF has been clicked')} background={TouchableNativeFeedback.SelectableBackground()}> Enabled TouchableNativeFeedback console.log('custom TNF has been clicked')} background={TouchableNativeFeedback.SelectableBackground()}> Disabled TouchableNativeFeedback )} ); } } function CustomRippleRadius() { if (Platform.OS !== 'android') { return null; } return ( console.log('custom TNF has been clicked')} background={TouchableNativeFeedback.Ripple('orange', true, 30)}> radius 30 console.log('custom TNF has been clicked')} background={TouchableNativeFeedback.SelectableBackgroundBorderless( 150, )}> radius 150 console.log('custom TNF has been clicked')} background={TouchableNativeFeedback.SelectableBackground(70)}> radius 70, with border ); } const remoteImage = { uri: 'https://www.facebook.com/favicon.ico', }; const TouchableHighlightUnderlayMethods = () => { const [underlayVisible, setUnderlayVisible] = useState( 'Underlay not visible', ); const hiddenUnderlay = () => { setUnderlayVisible('Press to make underlay visible'); }; const shownUnderlay = () => { setUnderlayVisible('Underlay visible'); }; return ( { console.log('TouchableHighlight underlay shown!'); }}> {underlayVisible} ); }; const TouchableTouchSoundDisabled = () => { const [soundEnabled, setSoundEnabled] = useState(false); const toggleTouchableSound = () => { soundEnabled ? setSoundEnabled(false) : setSoundEnabled(true); }; return ( <> {Platform.OS === 'android' ? ( <> console.log('touchSoundDisabled pressed!')}> Touchables make a sound on Android, which can be turned off. {soundEnabled ? 'Disable Touchable Sound' : 'Enable Touchable Sound'} ) : null} ); }; function TouchableOnFocus() { const ref = useRef>(null); const [isFocused, setIsFocused] = useState(false); const [focusStatus, setFocusStatus] = useState( 'This touchable is not focused.', ); const [isBlurred, setIsBlurred] = useState( 'This item still has focus, onBlur is not called', ); const toggleFocus = () => { isFocused ? setFocusStatus('This touchable is focused') : /* $FlowFixMe[constant-condition] Error discovered during Constant * Condition roll out. See https://fburl.com/workplace/1v97vimq. */ setIsFocused('This touchable is not focused') && setIsBlurred('This item has lost focus, onBlur called'); }; const focusTouchable = () => { if (ref.current) { ref.current.focus(); } }; return ( {focusStatus} {'\n'} {isBlurred} ); } const styles = StyleSheet.create({ row: { justifyContent: 'center', flexDirection: 'row', }, centered: { justifyContent: 'center', }, image: { width: 50, height: 50, }, text: { fontSize: 16, }, block: { padding: 10, }, button: { color: '#007AFF', }, disabledButton: { color: '#007AFF', opacity: 0.5, }, nativeFeedbackButton: { textAlign: 'center', margin: 10, }, hitSlopButton: { color: 'white', }, wrapper: { borderRadius: 8, }, wrapperCustom: { borderRadius: 8, padding: 6, }, hitSlopWrapper: { backgroundColor: 'red', marginVertical: 30, }, logBox: { padding: 20, margin: 10, borderWidth: StyleSheet.hairlineWidth, borderColor: '#f0f0f0', }, eventLogBox: { padding: 10, margin: 10, height: 120, borderWidth: StyleSheet.hairlineWidth, borderColor: '#f0f0f0', }, forceTouchBox: { padding: 10, margin: 10, borderWidth: StyleSheet.hairlineWidth, borderColor: '#f0f0f0', backgroundColor: '#f9f9f9', alignItems: 'center', }, textBlock: { fontWeight: '500', color: 'blue', }, }); exports.displayName = (undefined: ?string); exports.description = 'Touchable and onPress examples.'; exports.title = 'Touchable* and onPress'; exports.category = 'UI'; exports.documentationURL = 'https://reactnative.dev/docs/touchablehighlight'; exports.examples = [ { title: '', description: ('TouchableHighlight works by adding an extra view with a ' + 'black background under the single child view. This works best when the ' + 'child view is fully opaque, although it can be made to work as a simple ' + 'background color change as well with the activeOpacity and ' + 'underlayColor props.': string), render(): React.Node { return ; }, }, { title: '', render(): React.Node { return ; }, }, { title: 'TouchableNativeFeedback with Animated child', description: ('TouchableNativeFeedback can have an AnimatedComponent as a' + 'direct child.': string), platform: 'android', render(): React.Node { const mScale = new Animated.Value(1); Animated.timing(mScale, { toValue: 0.3, duration: 1000, useNativeDriver: false, }).start(); const style = { backgroundColor: 'rgb(180, 64, 119)', width: 200, height: 100, transform: [{scale: mScale}], }; return ( ); }, }, { title: 'TouchableHighlight Underlay Visibility', render(): React.Node { return ; }, }, { title: 'Touchable Touch Sound', render(): React.Node { return ; }, }, { title: 'Touchable onFocus', render(): React.Node { return ; }, }, { title: ' with highlight', render(): React.MixedElement { return ; }, }, { title: 'Touchable feedback events', description: (' components accept onPress, onPressIn, ' + 'onPressOut, and onLongPress as props.': string), render(): React.MixedElement { return ; }, }, { title: 'Touchable delay for events', description: (' components also accept delayPressIn, ' + 'delayPressOut, and delayLongPress as props. These props impact the ' + 'timing of feedback events.': string), render(): React.MixedElement { return ; }, }, { title: '3D Touch / Force Touch', description: 'iPhone 8 and 8 plus support 3D touch, which adds a force property to touches', render(): React.MixedElement { return ; }, platform: 'ios', }, { title: 'Touchable Hit Slop', description: (' components accept hitSlop prop which extends the touch area ' + 'without changing the view bounds.': string), render(): React.MixedElement { return ; }, }, { title: 'Touchable Native Methods', description: ('Some components expose native methods like `measure`.': string), render(): React.MixedElement { return ; }, }, { title: 'Custom Ripple Radius (Android-only)', description: ('Ripple radius on TouchableNativeFeedback can be controlled': string), render(): React.MixedElement { return ; }, }, { title: 'Disabled Touchable*', description: (' components accept disabled prop which prevents ' + 'any interaction with component': string), render(): React.MixedElement { return ; }, }, ] as Array;