Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f0dcd8b
test setup
mdjastrzebski Jun 25, 2026
6ddc120
basic fix
mdjastrzebski Jun 25, 2026
2f5d043
simplify
mdjastrzebski Jun 25, 2026
2954034
rename to `preventNativePropagation`
mdjastrzebski Jun 25, 2026
dee0df0
.
mdjastrzebski Jun 25, 2026
5b00795
cleanup
mdjastrzebski Jun 25, 2026
4f4c0a2
docs
mdjastrzebski Jun 25, 2026
6882003
revert the name change
mdjastrzebski Jun 25, 2026
c40b104
naming
mdjastrzebski Jun 25, 2026
5d9beef
revert unintended changes
mdjastrzebski Jun 25, 2026
5a4fa7c
simplify the example
mdjastrzebski Jun 25, 2026
b73f938
simplify
mdjastrzebski Jun 25, 2026
fe7c258
fix format
mdjastrzebski Jun 25, 2026
eed43c0
fix lint
mdjastrzebski Jun 25, 2026
a422fb8
fix edge case: disabled pressable
mdjastrzebski Jun 25, 2026
2574f0c
android support
mdjastrzebski Jun 25, 2026
2ccbd6a
clean up ios only annotations
mdjastrzebski Jun 25, 2026
0f9691b
cleanup
mdjastrzebski Jun 25, 2026
8b2b1d8
fix yarn build-types
mdjastrzebski Jun 25, 2026
2b6abcb
fix api snapshots
mdjastrzebski Jun 25, 2026
884730f
fix ios: move disabled check to JS side to avoid setIsJSResponder tim…
mdjastrzebski Jun 25, 2026
67ffbf2
add comment explaining blockNativeResponder disabled guard
mdjastrzebski Jun 26, 2026
9fa06bf
tighten comment on blockNativeResponder disabled guard
mdjastrzebski Jun 26, 2026
b208056
reduce public type churn
mdjastrzebski Jun 26, 2026
07d001f
fix flow typecheck
mdjastrzebski Jun 26, 2026
44e89d9
fix flow check
mdjastrzebski Jun 26, 2026
b0ea2e1
tweaks
mdjastrzebski Jun 26, 2026
f38462e
fix android build warnings
mdjastrzebski Jun 26, 2026
1c1602b
improve comment
mdjastrzebski Jun 26, 2026
9d3c17e
fix prettier
mdjastrzebski Jun 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions packages/react-native/Libraries/Components/Pressable/Pressable.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down Expand Up @@ -334,12 +336,17 @@ function Pressable({
const eventHandlers = usePressability(config);

return (
// $FlowFixMe[incompatible-type] blockNativeResponder is an internal prop, intentionally absent from public ViewProps
<View
{...restPropsWithDefaults}
{...eventHandlers}
ref={mergedRef}
style={typeof style === 'function' ? style({pressed}) : style}
collapsable={false}>
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__ ? <PressabilityDebugView color="red" hitSlop={hitSlop} /> : null}
</View>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions packages/react-native/Libraries/Pressability/Pressability.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,42 @@ - (void)setIsJSResponder:(BOOL)isJSResponder
_isJSResponder = isJSResponder;
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
const auto &viewProps = static_cast<const ViewProps &>(*_props);
if (viewProps.blockNativeResponder) {
return;
}
[super touchesBegan:touches withEvent:event];
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
const auto &viewProps = static_cast<const ViewProps &>(*_props);
if (viewProps.blockNativeResponder) {
return;
}
[super touchesMoved:touches withEvent:event];
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
const auto &viewProps = static_cast<const ViewProps &>(*_props);
if (viewProps.blockNativeResponder) {
return;
}
[super touchesEnded:touches withEvent:event];
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
const auto &viewProps = static_cast<const ViewProps &>(*_props);
if (viewProps.blockNativeResponder) {
return;
}
[super touchesCancelled:touches withEvent:event];
}

- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
{
[super finalizeUpdates:updateMask];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) \
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ class BaseViewProps : public YogaStylableProps, public AccessibilityProps {
bool collapsableChildren{true};

bool removeClippedSubviews{false};
bool blockNativeResponder{false};

#pragma mark - Convenience Methods

Expand Down
4 changes: 4 additions & 0 deletions packages/rn-tester/RNTester/AppDelegate.mm
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#ifndef RN_DISABLE_OSS_PLUGIN_HEADER
#import <RNTMyNativeViewComponentView.h>
#endif
#import "NativeExampleViews/RNTNativeTouchReceiverComponentView.h"

#if __has_include(<ReactAppDependencyProvider/RCTAppDependencyProvider.h>)
#define USE_OSS_CODEGEN 1
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <React/RCTViewComponentView.h>
#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

/**
* Fabric component view that receives native touch events and emits onNativeTouch.
*/
@interface RNTNativeTouchReceiverComponentView : RCTViewComponentView
@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
@@ -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 <react/renderer/components/AppSpecs/ComponentDescriptors.h>
#import <react/renderer/components/AppSpecs/EventEmitters.h>
#import <react/renderer/components/AppSpecs/Props.h>
#import <react/renderer/components/AppSpecs/RCTComponentViewHelpers.h>

#import <React/RCTFabricComponentsPlugins.h>

using namespace facebook::react;

@implementation RNTNativeTouchReceiverComponentView

+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<RNTNativeTouchReceiverComponentDescriptor>();
}

+ (void)load
{
[super load];
}

- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared<const RNTNativeTouchReceiverProps>();
_props = defaultProps;
}
return self;
}

/**
* Emits onNativeTouch when UIKit delivers the touch-up event.
*/
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
if (_eventEmitter) {
auto const &emitter =
*std::static_pointer_cast<const RNTNativeTouchReceiverEventEmitter>(_eventEmitter);
emitter.onNativeTouch({});
}
[super touchesEnded:touches withEvent:event];
}

@end

Class<RCTComponentViewProtocol> RNTNativeTouchReceiverCls(void)
{
return RNTNativeTouchReceiverComponentView.class;
}
6 changes: 6 additions & 0 deletions packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -95,6 +96,8 @@
832F45BA2A8A6E1F0097B4E6 /* SwiftTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SwiftTest.swift; path = RNTester/SwiftTest.swift; sourceTree = "<group>"; };
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 = "<group>"; };
AA000001AABB0001CCDD0001 /* RNTNativeTouchReceiverComponentView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNTNativeTouchReceiverComponentView.h; path = RNTester/NativeExampleViews/RNTNativeTouchReceiverComponentView.h; sourceTree = "<group>"; };
AA000002AABB0002CCDD0002 /* RNTNativeTouchReceiverComponentView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = RNTNativeTouchReceiverComponentView.mm; path = RNTester/NativeExampleViews/RNTNativeTouchReceiverComponentView.mm; sourceTree = "<group>"; };
AC474BFB29BBD4A1002BDAED /* RNTester.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = RNTester.xctestplan; path = RNTester/RNTester.xctestplan; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
Expand Down Expand Up @@ -224,6 +227,8 @@
27F441EA1BEBE5030039B79C /* FlexibleSizeExampleView.h */,
272E6B3B1BEA849E001FCF37 /* UpdatePropertiesExampleView.h */,
272E6B3C1BEA849E001FCF37 /* UpdatePropertiesExampleView.mm */,
AA000001AABB0001CCDD0001 /* RNTNativeTouchReceiverComponentView.h */,
AA000002AABB0002CCDD0002 /* RNTNativeTouchReceiverComponentView.mm */,
);
name = NativeExampleViews;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -83,6 +84,7 @@ internal class RNTesterApplication : Application(), ReactApplication {
"RNTMyNativeView",
"RNTMyLegacyNativeView",
"RNTReportFullyDrawnView",
"RNTNativeTouchReceiver",
)

override fun createViewManagers(
Expand All @@ -92,6 +94,7 @@ internal class RNTesterApplication : Application(), ReactApplication {
MyNativeViewManager(),
MyLegacyViewManager(reactContext),
ReportFullyDrawnViewManager(),
RNTNativeTouchReceiverManager(),
)

override fun createViewManager(
Expand All @@ -102,6 +105,7 @@ internal class RNTesterApplication : Application(), ReactApplication {
"RNTMyNativeView" -> MyNativeViewManager()
"RNTMyLegacyNativeView" -> MyLegacyViewManager(reactContext)
"RNTReportFullyDrawnView" -> ReportFullyDrawnViewManager()
"RNTNativeTouchReceiver" -> RNTNativeTouchReceiverManager()
else -> null
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<RNTNativeTouchReceiverView>(),
RNTNativeTouchReceiverManagerInterface<RNTNativeTouchReceiverView> {

companion object {
const val REACT_CLASS = "RNTNativeTouchReceiver"
}

private val delegate: ViewManagerDelegate<RNTNativeTouchReceiverView> =
RNTNativeTouchReceiverManagerDelegate(this)

override fun getDelegate(): ViewManagerDelegate<RNTNativeTouchReceiverView> = delegate

override fun getName(): String = REACT_CLASS

override fun createViewInstance(reactContext: ThemedReactContext): RNTNativeTouchReceiverView =
RNTNativeTouchReceiverView(reactContext)
}
Original file line number Diff line number Diff line change
@@ -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<OnNativeTouchEvent>(surfaceId, viewId) {
override fun getEventName(): String = "topNativeTouch"

override fun getEventData(): WritableMap = Arguments.createMap()
}
}
Loading
Loading