Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Unreleased

### 🐛 Bug fixes

- **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))
- **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))

## 3.10.0

### 🎉 New features
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1267,6 +1267,19 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
// MARK: - RootView Touch Handling
// =============================================================================

/**
* Check if a touch event is within the footer's screen bounds.
*/
private fun isTouchInFooter(event: MotionEvent): Boolean {
val footer = containerView?.footerView ?: return false
if (!footer.isShown) return false
val loc = ScreenUtils.getScreenLocation(footer)
val x = event.rawX.toInt()
val y = event.rawY.toInt()
return x >= loc[0] && x <= loc[0] + footer.width &&
y >= loc[1] && y <= loc[1] + footer.height
}

override fun dispatchTouchEvent(event: MotionEvent): Boolean {
val footer = containerView?.footerView
if (footer != null && footer.isShown) {
Expand All @@ -1293,17 +1306,24 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
}

override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
eventDispatcher?.let {
jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
jsPointerDispatcher.handleMotionEvent(event, it, true)
// Skip JS dispatch for footer touches — the footer's own RootView handles them.
// This prevents the same touch event being dispatched to JS twice.
if (!isTouchInFooter(event)) {
eventDispatcher?.let {
jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
jsPointerDispatcher.handleMotionEvent(event, it, true)
}
}
return super.onInterceptTouchEvent(event)
}

override fun onTouchEvent(event: MotionEvent): Boolean {
eventDispatcher?.let {
jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
jsPointerDispatcher.handleMotionEvent(event, it, false)
// Skip JS dispatch for footer touches — handled by footer's own RootView.
if (!isTouchInFooter(event)) {
eventDispatcher?.let {
jsTouchDispatcher.handleTouchEvent(event, it, reactContext)
jsPointerDispatcher.handleMotionEvent(event, it, false)
}
}
super.onTouchEvent(event)
return true
Expand Down
5 changes: 4 additions & 1 deletion ios/TrueSheetFooterView.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ NS_ASSUME_NONNULL_BEGIN
- (void)footerViewDidChangeSize:(CGSize)size;
@end

@interface TrueSheetFooterView : RCTViewComponentView <TrueSheetKeyboardObserverDelegate>
@interface TrueSheetFooterView : RCTViewComponentView <TrueSheetKeyboardObserverDelegate> {
RCTSurfaceTouchHandler *_footerTouchHandler;
BOOL _footerTouchHandlerAttached;
}

@property (nonatomic, weak, nullable) TrueSheetKeyboardObserver *keyboardObserver;
@property (nonatomic, weak, nullable) id<TrueSheetFooterViewDelegate> delegate;
Expand Down
33 changes: 33 additions & 0 deletions ios/TrueSheetFooterView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,34 @@ - (instancetype)initWithFrame:(CGRect)frame {
_didInitialLayout = NO;
_bottomConstraint = nil;
_currentKeyboardOffset = 0;

// Dedicated touch handler so touches are hit-tested against the footer's
// actual AutoLayout frame, not the stale Yoga frame in the container's
// touch handler.
_footerTouchHandler = [[RCTSurfaceTouchHandler alloc] init];
_footerTouchHandlerAttached = NO;
}
return self;
}

#pragma mark - Touch Handling

- (void)attachFooterTouchHandler {
if (_footerTouchHandlerAttached) {
return;
}
[_footerTouchHandler attachToView:self];
_footerTouchHandlerAttached = YES;
}

- (void)detachFooterTouchHandler {
if (!_footerTouchHandlerAttached) {
return;
}
[_footerTouchHandler detachFromView:self];
_footerTouchHandlerAttached = NO;
}

#pragma mark - Layout

- (void)setupConstraintsWithHeight:(CGFloat)height {
Expand Down Expand Up @@ -78,6 +102,12 @@ - (void)didMoveToSuperview {
if (self.superview) {
CGFloat initialHeight = self.frame.size.height;
[self setupConstraintsWithHeight:initialHeight];

// Attach the footer's own touch handler so it has an independent
// coordinate space for hit-testing (bypasses container's Yoga tree).
[self attachFooterTouchHandler];
} else {
[self detachFooterTouchHandler];
}
}

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

- (void)prepareForRecycle {
// Guarded: no-op if didMoveToSuperview:nil already detached.
[self detachFooterTouchHandler];

[super prepareForRecycle];

[LayoutUtil unpinView:self fromParentView:self.superview];
Expand Down
Loading