diff --git a/packages/react-native/Libraries/Components/Pressable/Pressable.js b/packages/react-native/Libraries/Components/Pressable/Pressable.js index 78d32ecbf5f1..4a7d44112cf1 100644 --- a/packages/react-native/Libraries/Components/Pressable/Pressable.js +++ b/packages/react-native/Libraries/Components/Pressable/Pressable.js @@ -120,8 +120,10 @@ type PressableBaseProps = Readonly<{ onPressOut?: ?(event: GestureResponderEvent) => unknown, /** - * Whether to prevent any other native components from becoming responder - * while this pressable is responder. + * When true, prevents native ancestor views from receiving any touch events + * that begin on this Pressable, including pan gestures. Suppression is + * unconditional (not gated on JS responder state) to avoid timing races on + * fast taps. Does not affect UIGestureRecognizer-based interactions. */ blockNativeResponder?: ?boolean, @@ -334,12 +336,17 @@ function Pressable({ const eventHandlers = usePressability(config); return ( + // $FlowFixMe[incompatible-type] blockNativeResponder is an internal prop, intentionally absent from public ViewProps + collapsable={false} + // Must be false when disabled so touches reach native ancestors. Cannot + // guard this on the native side via _isJSResponder — it's set async and + // races with touchesBegan on fast taps. + blockNativeResponder={disabled !== true && blockNativeResponder}> {typeof children === 'function' ? children({pressed}) : children} {__DEV__ ? : null} diff --git a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js index d22a68642194..4e76214cb692 100644 --- a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js +++ b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js @@ -247,6 +247,7 @@ const validAttributesForNonEventProps = { hitSlop: {diff: require('../Utilities/differ/insetsDiffer').default}, collapsable: true, collapsableChildren: true, + blockNativeResponder: true, filter: filterAttribute, boxShadow: boxShadowAttribute, mixBlendMode: true, diff --git a/packages/react-native/Libraries/Pressability/Pressability.js b/packages/react-native/Libraries/Pressability/Pressability.js index 0be465bec546..4fd9dc84cd61 100644 --- a/packages/react-native/Libraries/Pressability/Pressability.js +++ b/packages/react-native/Libraries/Pressability/Pressability.js @@ -131,8 +131,8 @@ export type PressabilityConfig = Readonly<{ onPressOut?: ?(event: GestureResponderEvent) => unknown, /** - * Whether to prevent any other native components from becoming responder - * while this pressable is responder. + * When true, prevents native ancestor views (UIKit responder chain) from + * receiving touch events when this Pressable handles a press. */ blockNativeResponder?: ?boolean, }>; diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 5571b5350bff..6710d1d8afcc 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -672,6 +672,42 @@ - (void)setIsJSResponder:(BOOL)isJSResponder _isJSResponder = isJSResponder; } +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + const auto &viewProps = static_cast(*_props); + if (viewProps.blockNativeResponder) { + return; + } + [super touchesBegan:touches withEvent:event]; +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + const auto &viewProps = static_cast(*_props); + if (viewProps.blockNativeResponder) { + return; + } + [super touchesMoved:touches withEvent:event]; +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + const auto &viewProps = static_cast(*_props); + if (viewProps.blockNativeResponder) { + return; + } + [super touchesEnded:touches withEvent:event]; +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + const auto &viewProps = static_cast(*_props); + if (viewProps.blockNativeResponder) { + return; + } + [super touchesCancelled:touches withEvent:event]; +} + - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask { [super finalizeUpdates:updateMask]; diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp index d3bae3163c09..f27d268222c7 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp @@ -424,6 +424,15 @@ BaseViewProps::BaseViewProps( rawProps, "removeClippedSubviews", sourceProps.removeClippedSubviews, + false)), + blockNativeResponder( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.blockNativeResponder + : convertRawProp( + context, + rawProps, + "blockNativeResponder", + sourceProps.blockNativeResponder, false)) {} #define VIEW_EVENT_CASE(eventType) \ @@ -475,6 +484,7 @@ void BaseViewProps::setProp( RAW_SET_PROP_SWITCH_CASE_BASIC(collapsable); RAW_SET_PROP_SWITCH_CASE_BASIC(collapsableChildren); RAW_SET_PROP_SWITCH_CASE_BASIC(removeClippedSubviews); + RAW_SET_PROP_SWITCH_CASE_BASIC(blockNativeResponder); RAW_SET_PROP_SWITCH_CASE_BASIC(cursor); RAW_SET_PROP_SWITCH_CASE_BASIC(outlineColor); RAW_SET_PROP_SWITCH_CASE_BASIC(outlineOffset); diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h index 7554ba7cad86..978742beaf51 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h @@ -108,6 +108,7 @@ class BaseViewProps : public YogaStylableProps, public AccessibilityProps { bool collapsableChildren{true}; bool removeClippedSubviews{false}; + bool blockNativeResponder{false}; #pragma mark - Convenience Methods diff --git a/packages/rn-tester/RNTester/AppDelegate.mm b/packages/rn-tester/RNTester/AppDelegate.mm index 3f1ea7209bac..c12a5ec6b9ee 100644 --- a/packages/rn-tester/RNTester/AppDelegate.mm +++ b/packages/rn-tester/RNTester/AppDelegate.mm @@ -25,6 +25,7 @@ #ifndef RN_DISABLE_OSS_PLUGIN_HEADER #import #endif +#import "NativeExampleViews/RNTNativeTouchReceiverComponentView.h" #if __has_include() #define USE_OSS_CODEGEN 1 @@ -171,6 +172,9 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center if (!dict[@"SampleNativeComponent"]) { dict[@"SampleNativeComponent"] = NSClassFromString(@"RCTSampleNativeComponentComponentView"); } + if (!dict[@"RNTNativeTouchReceiver"]) { + dict[@"RNTNativeTouchReceiver"] = [RNTNativeTouchReceiverComponentView class]; + } return dict; } #endif diff --git a/packages/rn-tester/RNTester/NativeExampleViews/RNTNativeTouchReceiverComponentView.h b/packages/rn-tester/RNTester/NativeExampleViews/RNTNativeTouchReceiverComponentView.h new file mode 100644 index 000000000000..9631c034e1c8 --- /dev/null +++ b/packages/rn-tester/RNTester/NativeExampleViews/RNTNativeTouchReceiverComponentView.h @@ -0,0 +1,21 @@ +/* + * 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. + */ + +#pragma once + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Fabric component view that receives native touch events and emits onNativeTouch. + */ +@interface RNTNativeTouchReceiverComponentView : RCTViewComponentView +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/rn-tester/RNTester/NativeExampleViews/RNTNativeTouchReceiverComponentView.mm b/packages/rn-tester/RNTester/NativeExampleViews/RNTNativeTouchReceiverComponentView.mm new file mode 100644 index 000000000000..127f572a8274 --- /dev/null +++ b/packages/rn-tester/RNTester/NativeExampleViews/RNTNativeTouchReceiverComponentView.mm @@ -0,0 +1,58 @@ +/* + * 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. + */ + +#import "RNTNativeTouchReceiverComponentView.h" + +#import +#import +#import +#import + +#import + +using namespace facebook::react; + +@implementation RNTNativeTouchReceiverComponentView + ++ (ComponentDescriptorProvider)componentDescriptorProvider +{ + return concreteComponentDescriptorProvider(); +} + ++ (void)load +{ + [super load]; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + static const auto defaultProps = std::make_shared(); + _props = defaultProps; + } + return self; +} + +/** + * Emits onNativeTouch when UIKit delivers the touch-up event. + */ +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + if (_eventEmitter) { + auto const &emitter = + *std::static_pointer_cast(_eventEmitter); + emitter.onNativeTouch({}); + } + [super touchesEnded:touches withEvent:event]; +} + +@end + +Class RNTNativeTouchReceiverCls(void) +{ + return RNTNativeTouchReceiverComponentView.class; +} diff --git a/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj b/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj index 898cd14e1ca5..cd086800567e 100644 --- a/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj +++ b/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 8145AE06241172D900A3F8DA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8145AE05241172D900A3F8DA /* LaunchScreen.storyboard */; }; 832F45BB2A8A6E1F0097B4E6 /* SwiftTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832F45BA2A8A6E1F0097B4E6 /* SwiftTest.swift */; }; A975CA6C2C05EADF0043F72A /* RCTNetworkTaskTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A975CA6B2C05EADE0043F72A /* RCTNetworkTaskTests.m */; }; + AA000003AABB0003CCDD0003 /* RNTNativeTouchReceiverComponentView.mm in Sources */ = {isa = PBXBuildFile; fileRef = AA000002AABB0002CCDD0002 /* RNTNativeTouchReceiverComponentView.mm */; }; C175B6D9ED9336FB66637943 /* libPods-RNTester.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C706D402EE4AF9BE838CBA9 /* libPods-RNTester.a */; }; CD10C7A5290BD4EB0033E1ED /* RCTEventEmitterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CD10C7A4290BD4EB0033E1ED /* RCTEventEmitterTests.m */; }; E62F11832A5C6580000BF1C8 /* FlexibleSizeExampleView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 27F441E81BEBE5030039B79C /* FlexibleSizeExampleView.mm */; }; @@ -95,6 +96,8 @@ 832F45BA2A8A6E1F0097B4E6 /* SwiftTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SwiftTest.swift; path = RNTester/SwiftTest.swift; sourceTree = ""; }; 93A243F0D4D5C54911E811C4 /* libPods-RNTesterIntegrationTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RNTesterIntegrationTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; A975CA6B2C05EADE0043F72A /* RCTNetworkTaskTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTNetworkTaskTests.m; sourceTree = ""; }; + AA000001AABB0001CCDD0001 /* RNTNativeTouchReceiverComponentView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNTNativeTouchReceiverComponentView.h; path = RNTester/NativeExampleViews/RNTNativeTouchReceiverComponentView.h; sourceTree = ""; }; + AA000002AABB0002CCDD0002 /* RNTNativeTouchReceiverComponentView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = RNTNativeTouchReceiverComponentView.mm; path = RNTester/NativeExampleViews/RNTNativeTouchReceiverComponentView.mm; sourceTree = ""; }; AC474BFB29BBD4A1002BDAED /* RNTester.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = RNTester.xctestplan; path = RNTester/RNTester.xctestplan; sourceTree = ""; }; B0E70A8A05E03E868F8703FE /* Pods-RNTesterIntegrationTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNTesterIntegrationTests.release.xcconfig"; path = "Target Support Files/Pods-RNTesterIntegrationTests/Pods-RNTesterIntegrationTests.release.xcconfig"; sourceTree = ""; }; CA59C9994B1822826D8983F0 /* Pods-RNTester.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNTester.debug.xcconfig"; path = "Target Support Files/Pods-RNTester/Pods-RNTester.debug.xcconfig"; sourceTree = ""; }; @@ -224,6 +227,8 @@ 27F441EA1BEBE5030039B79C /* FlexibleSizeExampleView.h */, 272E6B3B1BEA849E001FCF37 /* UpdatePropertiesExampleView.h */, 272E6B3C1BEA849E001FCF37 /* UpdatePropertiesExampleView.mm */, + AA000001AABB0001CCDD0001 /* RNTNativeTouchReceiverComponentView.h */, + AA000002AABB0002CCDD0002 /* RNTNativeTouchReceiverComponentView.mm */, ); name = NativeExampleViews; sourceTree = ""; @@ -727,6 +732,7 @@ files = ( E62F11842A5C6584000BF1C8 /* UpdatePropertiesExampleView.mm in Sources */, E62F11832A5C6580000BF1C8 /* FlexibleSizeExampleView.mm in Sources */, + AA000003AABB0003CCDD0003 /* RNTNativeTouchReceiverComponentView.mm in Sources */, 832F45BB2A8A6E1F0097B4E6 /* SwiftTest.swift in Sources */, 5C60EB1C226440DB0018C04F /* AppDelegate.mm in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, diff --git a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.kt b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.kt index 2a162c4c2493..bd8d28178afb 100644 --- a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.kt +++ b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.kt @@ -27,6 +27,7 @@ import com.facebook.react.module.model.ReactModuleInfo import com.facebook.react.module.model.ReactModuleInfoProvider import com.facebook.react.uiapp.component.MyLegacyViewManager import com.facebook.react.uiapp.component.MyNativeViewManager +import com.facebook.react.uiapp.component.RNTNativeTouchReceiverManager import com.facebook.react.uiapp.component.ReportFullyDrawnViewManager import com.facebook.react.uimanager.ReactShadowNode import com.facebook.react.uimanager.ViewManager @@ -83,6 +84,7 @@ internal class RNTesterApplication : Application(), ReactApplication { "RNTMyNativeView", "RNTMyLegacyNativeView", "RNTReportFullyDrawnView", + "RNTNativeTouchReceiver", ) override fun createViewManagers( @@ -92,6 +94,7 @@ internal class RNTesterApplication : Application(), ReactApplication { MyNativeViewManager(), MyLegacyViewManager(reactContext), ReportFullyDrawnViewManager(), + RNTNativeTouchReceiverManager(), ) override fun createViewManager( @@ -102,6 +105,7 @@ internal class RNTesterApplication : Application(), ReactApplication { "RNTMyNativeView" -> MyNativeViewManager() "RNTMyLegacyNativeView" -> MyLegacyViewManager(reactContext) "RNTReportFullyDrawnView" -> ReportFullyDrawnViewManager() + "RNTNativeTouchReceiver" -> RNTNativeTouchReceiverManager() else -> null } } diff --git a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/component/RNTNativeTouchReceiverManager.kt b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/component/RNTNativeTouchReceiverManager.kt new file mode 100644 index 000000000000..590b0ec30419 --- /dev/null +++ b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/component/RNTNativeTouchReceiverManager.kt @@ -0,0 +1,35 @@ +/* + * 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. + */ + +package com.facebook.react.uiapp.component + +import com.facebook.react.module.annotations.ReactModule +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.ViewGroupManager +import com.facebook.react.uimanager.ViewManagerDelegate +import com.facebook.react.viewmanagers.RNTNativeTouchReceiverManagerDelegate +import com.facebook.react.viewmanagers.RNTNativeTouchReceiverManagerInterface + +@ReactModule(name = RNTNativeTouchReceiverManager.REACT_CLASS) +internal class RNTNativeTouchReceiverManager : + ViewGroupManager(), + RNTNativeTouchReceiverManagerInterface { + + companion object { + const val REACT_CLASS = "RNTNativeTouchReceiver" + } + + private val delegate: ViewManagerDelegate = + RNTNativeTouchReceiverManagerDelegate(this) + + override fun getDelegate(): ViewManagerDelegate = delegate + + override fun getName(): String = REACT_CLASS + + override fun createViewInstance(reactContext: ThemedReactContext): RNTNativeTouchReceiverView = + RNTNativeTouchReceiverView(reactContext) +} diff --git a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/component/RNTNativeTouchReceiverView.kt b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/component/RNTNativeTouchReceiverView.kt new file mode 100644 index 000000000000..0f0c90bf5908 --- /dev/null +++ b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/component/RNTNativeTouchReceiverView.kt @@ -0,0 +1,54 @@ +/* + * 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. + */ + +package com.facebook.react.uiapp.component + +import android.view.MotionEvent +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.ReactContext +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.UIManagerHelper +import com.facebook.react.uimanager.events.Event +import com.facebook.react.views.view.ReactViewGroup + +/** + * Native ViewGroup that receives touch events through Android's touch dispatch + * and emits onNativeTouch. + */ +internal class RNTNativeTouchReceiverView(context: ThemedReactContext) : ReactViewGroup(context) { + + override fun onInterceptTouchEvent(event: MotionEvent): Boolean { + val intercepted = super.onInterceptTouchEvent(event) + if (event.action == MotionEvent.ACTION_UP) { + emitNativeTouchEvent() + } + return intercepted + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + val handled = super.onTouchEvent(event) + if (event.action == MotionEvent.ACTION_UP) { + emitNativeTouchEvent() + } + return handled + } + + private fun emitNativeTouchEvent() { + val reactContext = context as ReactContext + val surfaceId = UIManagerHelper.getSurfaceId(reactContext) + val eventDispatcher = UIManagerHelper.getEventDispatcher(reactContext) + eventDispatcher?.dispatchEvent(OnNativeTouchEvent(surfaceId, id)) + } + + private inner class OnNativeTouchEvent(surfaceId: Int, viewId: Int) : + Event(surfaceId, viewId) { + override fun getEventName(): String = "topNativeTouch" + + override fun getEventData(): WritableMap = Arguments.createMap() + } +} diff --git a/packages/rn-tester/js/examples/Pressable/NativeTouchReceiverNativeComponent.js b/packages/rn-tester/js/examples/Pressable/NativeTouchReceiverNativeComponent.js new file mode 100644 index 000000000000..f4b22e8e9f52 --- /dev/null +++ b/packages/rn-tester/js/examples/Pressable/NativeTouchReceiverNativeComponent.js @@ -0,0 +1,24 @@ +/** + * 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 {CodegenTypes, HostComponent, ViewProps} from 'react-native'; + +import {codegenNativeComponent} from 'react-native'; + +type NativeProps = Readonly<{ + ...ViewProps, + onNativeTouch?: ?CodegenTypes.DirectEventHandler>, +}>; + +export type NativeTouchReceiverType = HostComponent; + +export default codegenNativeComponent( + 'RNTNativeTouchReceiver', +) as NativeTouchReceiverType; diff --git a/packages/rn-tester/js/examples/Pressable/PressableExample.js b/packages/rn-tester/js/examples/Pressable/PressableExample.js index b52f9f9ba053..84d713c3a00b 100644 --- a/packages/rn-tester/js/examples/Pressable/PressableExample.js +++ b/packages/rn-tester/js/examples/Pressable/PressableExample.js @@ -10,6 +10,7 @@ import type {RNTesterModule} from '../../types/RNTesterTypes'; +import RNTNativeTouchReceiver from './NativeTouchReceiverNativeComponent'; import * as PressableExampleFbInternal from './PressableExampleFbInternal'; import * as React from 'react'; import { @@ -20,6 +21,7 @@ import { PlatformColor, Pressable, StyleSheet, + Switch, Text, View, } from 'react-native'; @@ -275,6 +277,89 @@ function PressableDisabled() { ); } +const scenarioReceiverStyle = { + padding: 16, + backgroundColor: '#fff3cd', + borderRadius: 8, + borderWidth: StyleSheet.hairlineWidth, + borderColor: '#e0bb5a', + marginBottom: 4, + alignItems: 'flex-start' as const, +}; + +function PressableBlockNativeResponderExample() { + const [blockNativeResponder, setBlockNativeResponder] = useState(false); + const [log, setLog] = useState>([]); + const onNativeTouch = () => + setLog(prev => [...prev, 'NativeTouchReceiver.onNativeTouch']); + const onPress = () => setLog(prev => [...prev, 'Pressable.onPress']); + + return ( + + + + blockNativeResponder + + + + + + + Press Me + + + + + setLog([])}> + Clear log + + + + + ); +} + +const monoFont = Platform.OS === 'ios' ? 'Menlo' : 'monospace'; + +function LogBox({lines}: {lines: Array}) { + return ( + + {lines.length === 0 ? ( + + tap to see events + + ) : ( + lines.map((line, i) => ( + + {line} + + )) + )} + + ); +} + const styles = StyleSheet.create({ row: { justifyContent: 'center', @@ -661,6 +746,15 @@ const examples = [ ); }, }, + { + title: 'Pressable with blockNativeResponder', + name: 'prevent-native-propagation', + description: + 'Pressable inside a native parent view. Without blockNativeResponder the touch leaks up the native touch event system to the parent.' as string, + render: function (): React.Node { + return ; + }, + }, ...PressableExampleFbInternal.examples, ]; diff --git a/packages/rn-tester/package.json b/packages/rn-tester/package.json index 8056b295df1c..8a5517c9e52d 100644 --- a/packages/rn-tester/package.json +++ b/packages/rn-tester/package.json @@ -47,6 +47,9 @@ "components": { "RNTMyNativeView": { "className": "RNTMyNativeViewComponentView" + }, + "RNTNativeTouchReceiver": { + "className": "RNTNativeTouchReceiverComponentView" } } } diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 205202f898f6..69d0a0abf2a2 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -1804,6 +1804,7 @@ class facebook::react::BaseViewEventEmitter : public facebook::react::TouchEvent class facebook::react::BaseViewProps : public facebook::react::YogaStylableProps, public facebook::react::AccessibilityProps { public BaseViewProps() = default; public BaseViewProps(const facebook::react::PropsParserContext& context, const facebook::react::BaseViewProps& sourceProps, const facebook::react::RawProps& rawProps, const std::function& filterObjectKeys = nullptr); + public bool blockNativeResponder; public bool collapsable; public bool collapsableChildren; public bool getClipsContentToBounds() const; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index 59443fbd585d..ea9953eb1dd7 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -1798,6 +1798,7 @@ class facebook::react::BaseViewEventEmitter : public facebook::react::TouchEvent class facebook::react::BaseViewProps : public facebook::react::YogaStylableProps, public facebook::react::AccessibilityProps { public BaseViewProps() = default; public BaseViewProps(const facebook::react::PropsParserContext& context, const facebook::react::BaseViewProps& sourceProps, const facebook::react::RawProps& rawProps, const std::function& filterObjectKeys = nullptr); + public bool blockNativeResponder; public bool collapsable; public bool collapsableChildren; public bool getClipsContentToBounds() const; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 0f5f39b20abf..b030139446bd 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -1802,6 +1802,7 @@ class facebook::react::BaseViewEventEmitter : public facebook::react::TouchEvent class facebook::react::BaseViewProps : public facebook::react::YogaStylableProps, public facebook::react::AccessibilityProps { public BaseViewProps() = default; public BaseViewProps(const facebook::react::PropsParserContext& context, const facebook::react::BaseViewProps& sourceProps, const facebook::react::RawProps& rawProps, const std::function& filterObjectKeys = nullptr); + public bool blockNativeResponder; public bool collapsable; public bool collapsableChildren; public bool getClipsContentToBounds() const; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index cc3c6c0e38da..dfc72cdac8aa 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -4391,6 +4391,7 @@ class facebook::react::BaseViewEventEmitter : public facebook::react::TouchEvent class facebook::react::BaseViewProps : public facebook::react::YogaStylableProps, public facebook::react::AccessibilityProps { public BaseViewProps() = default; public BaseViewProps(const facebook::react::PropsParserContext& context, const facebook::react::BaseViewProps& sourceProps, const facebook::react::RawProps& rawProps, const std::function& filterObjectKeys = nullptr); + public bool blockNativeResponder; public bool collapsable; public bool collapsableChildren; public bool getClipsContentToBounds() const; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index bcd1ef1969f3..4bef41ed4f17 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -4374,6 +4374,7 @@ class facebook::react::BaseViewEventEmitter : public facebook::react::TouchEvent class facebook::react::BaseViewProps : public facebook::react::YogaStylableProps, public facebook::react::AccessibilityProps { public BaseViewProps() = default; public BaseViewProps(const facebook::react::PropsParserContext& context, const facebook::react::BaseViewProps& sourceProps, const facebook::react::RawProps& rawProps, const std::function& filterObjectKeys = nullptr); + public bool blockNativeResponder; public bool collapsable; public bool collapsableChildren; public bool getClipsContentToBounds() const; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index baa00077d2b5..5196f1d6a153 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -4389,6 +4389,7 @@ class facebook::react::BaseViewEventEmitter : public facebook::react::TouchEvent class facebook::react::BaseViewProps : public facebook::react::YogaStylableProps, public facebook::react::AccessibilityProps { public BaseViewProps() = default; public BaseViewProps(const facebook::react::PropsParserContext& context, const facebook::react::BaseViewProps& sourceProps, const facebook::react::RawProps& rawProps, const std::function& filterObjectKeys = nullptr); + public bool blockNativeResponder; public bool collapsable; public bool collapsableChildren; public bool getClipsContentToBounds() const; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index 575079ae0c35..3281b715918b 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -1133,6 +1133,7 @@ class facebook::react::BaseViewProps : public facebook::react::YogaStylableProps public Float outlineWidth; public Float shadowOpacity; public Float shadowRadius; + public bool blockNativeResponder; public bool collapsable; public bool collapsableChildren; public bool getClipsContentToBounds() const; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api index 81906b49e192..39e6a4cea6bb 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -1128,6 +1128,7 @@ class facebook::react::BaseViewProps : public facebook::react::YogaStylableProps public Float outlineWidth; public Float shadowOpacity; public Float shadowRadius; + public bool blockNativeResponder; public bool collapsable; public bool collapsableChildren; public bool getClipsContentToBounds() const; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index ccc6128c0ef7..aebec6f73246 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -1131,6 +1131,7 @@ class facebook::react::BaseViewProps : public facebook::react::YogaStylableProps public Float outlineWidth; public Float shadowOpacity; public Float shadowRadius; + public bool blockNativeResponder; public bool collapsable; public bool collapsableChildren; public bool getClipsContentToBounds() const;