Skip to content

Commit 3585881

Browse files
isaacrowntreeclaude
andcommitted
fix: footer swallowing touch events
iOS: Give the footer its own RCTSurfaceTouchHandler so touches are hit-tested against the footer's actual AutoLayout frame instead of the stale Yoga frame in the container's touch handler. Attach/detach is guarded by a BOOL so prepareForRecycle is a no-op when didMoveToSuperview has already detached the handler (previously crashed inside RCTSurfaceTouchHandler on double-detach, reported at TrueSheetFooterView.mm:111). Android: Skip JS touch dispatch in ViewController.onInterceptTouchEvent and onTouchEvent when the touch lands in footer bounds, preventing double dispatch — the footer's own RootView handles it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 626a04c commit 3585881

4 files changed

Lines changed: 68 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## Unreleased
44

5+
### 🐛 Bug fixes
6+
7+
- **iOS**: Fixed footer swallowing touch events by giving it a dedicated `RCTSurfaceTouchHandler`, bypassing stale Yoga frame hit-testing. ([#589](https://github.com/lodev09/react-native-true-sheet/pull/589) by [@isaacrowntree](https://github.com/isaacrowntree))
8+
- **Android**: Fixed double JS touch dispatch for footer touches — footer's own `RootView` now exclusively handles events in its bounds. ([#589](https://github.com/lodev09/react-native-true-sheet/pull/589) by [@isaacrowntree](https://github.com/isaacrowntree))
9+
510
## 3.10.0
611

712
### 🎉 New features

android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1267,6 +1267,19 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
12671267
// MARK: - RootView Touch Handling
12681268
// =============================================================================
12691269

1270+
/**
1271+
* Check if a touch event is within the footer's screen bounds.
1272+
*/
1273+
private fun isTouchInFooter(event: MotionEvent): Boolean {
1274+
val footer = containerView?.footerView ?: return false
1275+
if (!footer.isShown) return false
1276+
val loc = ScreenUtils.getScreenLocation(footer)
1277+
val x = event.rawX.toInt()
1278+
val y = event.rawY.toInt()
1279+
return x >= loc[0] && x <= loc[0] + footer.width &&
1280+
y >= loc[1] && y <= loc[1] + footer.height
1281+
}
1282+
12701283
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
12711284
val footer = containerView?.footerView
12721285
if (footer != null && footer.isShown) {
@@ -1293,17 +1306,24 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
12931306
}
12941307

12951308
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
1296-
eventDispatcher?.let {
1297-
jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
1298-
jsPointerDispatcher.handleMotionEvent(event, it, true)
1309+
// Skip JS dispatch for footer touches — the footer's own RootView handles them.
1310+
// This prevents the same touch event being dispatched to JS twice.
1311+
if (!isTouchInFooter(event)) {
1312+
eventDispatcher?.let {
1313+
jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
1314+
jsPointerDispatcher.handleMotionEvent(event, it, true)
1315+
}
12991316
}
13001317
return super.onInterceptTouchEvent(event)
13011318
}
13021319

13031320
override fun onTouchEvent(event: MotionEvent): Boolean {
1304-
eventDispatcher?.let {
1305-
jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
1306-
jsPointerDispatcher.handleMotionEvent(event, it, false)
1321+
// Skip JS dispatch for footer touches — handled by footer's own RootView.
1322+
if (!isTouchInFooter(event)) {
1323+
eventDispatcher?.let {
1324+
jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
1325+
jsPointerDispatcher.handleMotionEvent(event, it, false)
1326+
}
13071327
}
13081328
super.onTouchEvent(event)
13091329
return true

ios/TrueSheetFooterView.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ NS_ASSUME_NONNULL_BEGIN
2222
- (void)footerViewDidChangeSize:(CGSize)size;
2323
@end
2424

25-
@interface TrueSheetFooterView : RCTViewComponentView <TrueSheetKeyboardObserverDelegate>
25+
@interface TrueSheetFooterView : RCTViewComponentView <TrueSheetKeyboardObserverDelegate> {
26+
RCTSurfaceTouchHandler *_footerTouchHandler;
27+
BOOL _footerTouchHandlerAttached;
28+
}
2629

2730
@property (nonatomic, weak, nullable) TrueSheetKeyboardObserver *keyboardObserver;
2831
@property (nonatomic, weak, nullable) id<TrueSheetFooterViewDelegate> delegate;

ios/TrueSheetFooterView.mm

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,34 @@ - (instancetype)initWithFrame:(CGRect)frame {
4141
_didInitialLayout = NO;
4242
_bottomConstraint = nil;
4343
_currentKeyboardOffset = 0;
44+
45+
// Dedicated touch handler so touches are hit-tested against the footer's
46+
// actual AutoLayout frame, not the stale Yoga frame in the container's
47+
// touch handler.
48+
_footerTouchHandler = [[RCTSurfaceTouchHandler alloc] init];
49+
_footerTouchHandlerAttached = NO;
4450
}
4551
return self;
4652
}
4753

54+
#pragma mark - Touch Handling
55+
56+
- (void)attachFooterTouchHandler {
57+
if (_footerTouchHandlerAttached) {
58+
return;
59+
}
60+
[_footerTouchHandler attachToView:self];
61+
_footerTouchHandlerAttached = YES;
62+
}
63+
64+
- (void)detachFooterTouchHandler {
65+
if (!_footerTouchHandlerAttached) {
66+
return;
67+
}
68+
[_footerTouchHandler detachFromView:self];
69+
_footerTouchHandlerAttached = NO;
70+
}
71+
4872
#pragma mark - Layout
4973

5074
- (void)setupConstraintsWithHeight:(CGFloat)height {
@@ -78,6 +102,12 @@ - (void)didMoveToSuperview {
78102
if (self.superview) {
79103
CGFloat initialHeight = self.frame.size.height;
80104
[self setupConstraintsWithHeight:initialHeight];
105+
106+
// Attach the footer's own touch handler so it has an independent
107+
// coordinate space for hit-testing (bypasses container's Yoga tree).
108+
[self attachFooterTouchHandler];
109+
} else {
110+
[self detachFooterTouchHandler];
81111
}
82112
}
83113

@@ -97,6 +127,9 @@ - (void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetric
97127
}
98128

99129
- (void)prepareForRecycle {
130+
// Guarded: no-op if didMoveToSuperview:nil already detached.
131+
[self detachFooterTouchHandler];
132+
100133
[super prepareForRecycle];
101134

102135
[LayoutUtil unpinView:self fromParentView:self.superview];

0 commit comments

Comments
 (0)