From 2f192937e33a4330fa8b9fdb915f935ef9c799f2 Mon Sep 17 00:00:00 2001 From: Aleksandr Gorbunov Date: Wed, 24 Jun 2026 10:28:02 +0300 Subject: [PATCH 1/3] fix: add sheet overlay support --- .../main/java/com/sheet2/AppFittedSheet.kt | 45 +++++++++++++- .../com/sheet2/FragmentModalBottomSheet.kt | 59 +++++++++++++++++++ .../java/com/sheet2/InlineSheetPresenter.kt | 21 +++++++ ios/HostFittedSheet.swift | 39 ++++++++++++ package-lock.json | 4 +- package.json | 2 +- src/FittedSheet.tsx | 2 + 7 files changed, 167 insertions(+), 5 deletions(-) diff --git a/android/src/main/java/com/sheet2/AppFittedSheet.kt b/android/src/main/java/com/sheet2/AppFittedSheet.kt index bf80de7..8ff69fe 100644 --- a/android/src/main/java/com/sheet2/AppFittedSheet.kt +++ b/android/src/main/java/com/sheet2/AppFittedSheet.kt @@ -39,6 +39,8 @@ open class AppFittedSheet(context: Context) : ViewGroup(context), LifecycleEvent private var stacked = true private val fragmentTag = "CCBottomSheet-${System.currentTimeMillis()}" var mHostView = DialogRootViewGroup(context) + private var inlineOverlayView: View? = null + private var dialogOverlayView: View? = null var dismissable = true set(value) { @@ -197,6 +199,7 @@ open class AppFittedSheet(context: Context) : ViewGroup(context), LifecycleEvent } getCurrentActivity()?.supportFragmentManager?.let { fragment.safeShow(it, fragmentTag) + fragment.setOverlayView(dialogOverlayView) if (stacked) { if (presentedSheets.contains(fragmentTag)) return presentedSheets.add(fragmentTag) @@ -222,20 +225,58 @@ open class AppFittedSheet(context: Context) : ViewGroup(context), LifecycleEvent override fun addView(child: View, index: Int) { UiThreadUtil.assertOnUiThread() + if (useInlinePresentation && index > 0) { + inlineOverlayView = child + inlinePresenter.setOverlayView(child) + return + } + if (!useInlinePresentation && index > 0) { + dialogOverlayView = child + sheet?.setOverlayView(child) + return + } mHostView.addView(child, index) } - override fun getChildCount(): Int = mHostView.childCount + override fun getChildCount(): Int = + mHostView.childCount + + (if (inlineOverlayView != null) 1 else 0) + + (if (dialogOverlayView != null) 1 else 0) - override fun getChildAt(index: Int): View? = mHostView.getChildAt(index) + override fun getChildAt(index: Int): View? { + if (index < mHostView.childCount) return mHostView.getChildAt(index) + val overlayIndex = index - mHostView.childCount + if (inlineOverlayView != null && overlayIndex == 0) return inlineOverlayView + return dialogOverlayView + } override fun removeView(child: View) { UiThreadUtil.assertOnUiThread() + if (child == inlineOverlayView) { + inlineOverlayView = null + inlinePresenter.setOverlayView(null) + return + } + if (child == dialogOverlayView) { + dialogOverlayView = null + sheet?.setOverlayView(null) + return + } dismiss() } override fun removeViewAt(index: Int) { UiThreadUtil.assertOnUiThread() + if (useInlinePresentation && index >= mHostView.childCount && inlineOverlayView != null) { + inlineOverlayView = null + inlinePresenter.setOverlayView(null) + return + } + if (!useInlinePresentation && index >= mHostView.childCount && dialogOverlayView != null) { + dialogOverlayView = null + sheet?.setOverlayView(null) + return + } dismiss() } diff --git a/android/src/main/java/com/sheet2/FragmentModalBottomSheet.kt b/android/src/main/java/com/sheet2/FragmentModalBottomSheet.kt index eb0105a..bd90306 100644 --- a/android/src/main/java/com/sheet2/FragmentModalBottomSheet.kt +++ b/android/src/main/java/com/sheet2/FragmentModalBottomSheet.kt @@ -5,15 +5,19 @@ import android.content.DialogInterface import android.graphics.Color import android.os.Bundle import android.view.LayoutInflater +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.Window +import android.widget.FrameLayout import com.google.android.material.bottomsheet.BottomSheetDialogFragment import java.lang.ref.WeakReference class FragmentModalBottomSheet() : BottomSheetDialogFragment() { private var modalView: ViewGroup? = null + private var overlayHost: ViewGroup? = null + private var overlayView: View? = null private var dismissable: Boolean = true private var isSystemUILight: Boolean = false private var onDismiss: ((dismissAll: Boolean) -> Unit)? = null @@ -64,6 +68,53 @@ class FragmentModalBottomSheet() : BottomSheetDialogFragment() { return dialog } + override fun onStart() { + super.onStart() + attachOverlayView() + } + + fun setOverlayView(view: View?) { + if (overlayView == view) return + detachOverlayView() + overlayView = view + attachOverlayView() + } + + private fun attachOverlayView() { + val view = overlayView ?: return + val decorView = dialog?.window?.decorView as? ViewGroup ?: return + detachOverlayHost() + (view.parent as? ViewGroup)?.removeView(view) + + val host = DialogPassThroughFrameLayout(decorView.context).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + isClickable = false + isFocusable = false + } + + view.layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT + ) + host.addView(view) + decorView.addView(host) + host.bringToFront() + overlayHost = host + } + + private fun detachOverlayHost() { + (overlayHost?.parent as? ViewGroup)?.removeView(overlayHost) + overlayHost = null + } + + private fun detachOverlayView() { + detachOverlayHost() + (overlayView?.parent as? ViewGroup)?.removeView(overlayView) + } + fun setNewNestedScrollView(view: View) { (dialog as CustomBottomSheetDialog).setNewNestedScrollView(view) } @@ -78,8 +129,16 @@ class FragmentModalBottomSheet() : BottomSheetDialogFragment() { override fun onDismiss(dialog: DialogInterface) { super.onDismiss(dialog) + detachOverlayView() + overlayView = null presentedWindow?.clear() presentedWindow = null onDismiss?.invoke(dismissAll) } } + +private class DialogPassThroughFrameLayout(context: android.content.Context) : FrameLayout(context) { + override fun dispatchTouchEvent(ev: MotionEvent): Boolean = false + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean = false + override fun onTouchEvent(event: MotionEvent): Boolean = false +} diff --git a/android/src/main/java/com/sheet2/InlineSheetPresenter.kt b/android/src/main/java/com/sheet2/InlineSheetPresenter.kt index b499d32..3649a03 100644 --- a/android/src/main/java/com/sheet2/InlineSheetPresenter.kt +++ b/android/src/main/java/com/sheet2/InlineSheetPresenter.kt @@ -32,6 +32,8 @@ internal class InlineSheetPresenter( ) { private var overlay: FrameLayout? = null + private var overlayHost: FrameLayout? = null + private var overlayView: View? = null private var behavior: BottomSheetBehavior? = null private var scrimAnimator: ValueAnimator? = null private var onDismiss: (() -> Unit)? = null @@ -145,6 +147,7 @@ internal class InlineSheetPresenter( scrimAnimator?.cancel() scrimAnimator = null this.behavior = null + this.overlayHost = null overlay = null (layout.parent as? ViewGroup)?.removeView(layout) (hostView.parent as? ViewGroup)?.removeView(hostView) @@ -156,6 +159,24 @@ internal class InlineSheetPresenter( behavior?.setDraggable(dismissable) } + fun setOverlayView(view: View?) { + if (overlayView == view) return + (overlayView?.parent as? ViewGroup)?.removeView(overlayView) + overlayView = view + attachOverlayView() + } + + private fun attachOverlayView() { + val host = overlayHost ?: return + val view = overlayView ?: return + (view.parent as? ViewGroup)?.removeView(view) + view.layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT, + ) + host.addView(view) + } + /** * Walks up from [anchor] looking for the closest react-native-screens Screen * ancestor and returns its parent (the ScreenStack / ScreensCoordinatorLayout). diff --git a/ios/HostFittedSheet.swift b/ios/HostFittedSheet.swift index fd7c4f3..b1e5ad4 100644 --- a/ios/HostFittedSheet.swift +++ b/ios/HostFittedSheet.swift @@ -20,6 +20,7 @@ public final class HostFittedSheet: UIView { @objc public var onSheetDismiss: (() -> Void)? private var _reactSubview: UIView? + private var _overlaySubview: UIView? private var _isPresented = false private var _sheetSize: CGFloat? public var sheetMaxWidthSize: CGFloat? @@ -76,6 +77,11 @@ public final class HostFittedSheet: UIView { @objc public func setUseInlinePresentation(_ value: Bool) { _useInlinePresentation = value + if value { + attachOverlaySubview() + } else { + _overlaySubview?.removeFromSuperview() + } } @objc @@ -116,6 +122,13 @@ public final class HostFittedSheet: UIView { } public override func insertReactSubview(_ subview: UIView!, at atIndex: Int) { + if atIndex > 0 { + _overlaySubview = subview + subview.isUserInteractionEnabled = false + attachOverlaySubview() + return + } + _touchHandler = RCTSurfaceTouchHandler() _touchHandler?.attach(to: subview) _touchHandlerAttachedView = subview @@ -124,11 +137,32 @@ public final class HostFittedSheet: UIView { } public override func removeReactSubview(_ subview: UIView!) { + if let overlaySubview = _overlaySubview, subview === overlaySubview { + _overlaySubview?.removeFromSuperview() + _overlaySubview = nil + return + } + detachTouchHandler() _reactSubview?.removeFromSuperview() _reactSubview = nil } + private func attachOverlaySubview() { + guard _useInlinePresentation else { return } + guard let overlaySubview = _overlaySubview, + let sheetView = _modalViewController?.view else { return } + + overlaySubview.removeFromSuperview() + overlaySubview.frame = sheetView.bounds + overlaySubview.autoresizingMask = [.flexibleWidth, .flexibleHeight] + overlaySubview.isUserInteractionEnabled = false + sheetView.addSubview(overlaySubview) + sheetView.bringSubviewToFront(overlaySubview) + overlaySubview.setNeedsLayout() + overlaySubview.layoutIfNeeded() + } + private func detachTouchHandler() { if let v = _touchHandlerAttachedView { _touchHandler?.detach(from: v) @@ -277,6 +311,9 @@ public final class HostFittedSheet: UIView { sheetVC.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] hostVC.view.addSubview(sheetVC.view) sheetVC.didMove(toParent: hostVC) + self.attachOverlaySubview() + sheetVC.view.setNeedsLayout() + sheetVC.view.layoutIfNeeded() // Don't attach our own RCTSurfaceTouchHandler in inline // containment: the sheet's subtree stays within the main Fabric @@ -383,6 +420,7 @@ public final class HostFittedSheet: UIView { self.onSheetDismiss = nil // Remove view hierarchy + self._overlaySubview?.removeFromSuperview() self._reactSubview?.removeFromSuperview() self.detachTouchHandler() self.viewController.view.removeFromSuperview() @@ -405,6 +443,7 @@ public final class HostFittedSheet: UIView { self._touchHandler = nil self._sheetSize = nil self.sheetMaxWidthSize = nil + self._overlaySubview = nil self._reactSubview = nil self._alertWindow = nil } diff --git a/package-lock.json b/package-lock.json index 6441f9e..6a0eb9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-native-sheet", - "version": "7.2.0", + "version": "7.9.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "react-native-sheet", - "version": "7.2.0", + "version": "7.9.1", "license": "MIT", "dependencies": { "@gorhom/portal": "^1.0.14" diff --git a/package.json b/package.json index 62f4c55..0e7aff1 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "example" ], "private": "true", - "version": "7.9.0", + "version": "7.9.1", "description": "Native implementation of Bottom sheet", "main": "src/index.tsx", "module": "src/index", diff --git a/src/FittedSheet.tsx b/src/FittedSheet.tsx index 004077f..4942bb9 100644 --- a/src/FittedSheet.tsx +++ b/src/FittedSheet.tsx @@ -48,6 +48,7 @@ export interface SheetProps { params?: FittedSheetParams; onSheetDismiss?: (passThroughParam?: any) => void; children?: FittedSheetChildren; + overlay?: React.ReactNode; rootViewStyle?: StyleProp>; } @@ -239,6 +240,7 @@ export class PrivateFittedSheet extends React.PureComponent { typeof this.props.children !== 'function' && this.props.children} + {this.props.overlay} ); } From e1d55e11be0435432779bcf052aafa5526fc357e Mon Sep 17 00:00:00 2001 From: Aleksandr Gorbunov Date: Wed, 24 Jun 2026 12:27:39 +0300 Subject: [PATCH 2/3] fix: stabilize Android sheet presses --- .../com/behavior/BottomSheetBehavior.java | 38 +++++++++++++++++-- .../main/java/com/sheet2/AppFittedSheet.kt | 4 ++ .../src/main/java/com/sheet2/BaseRNView.kt | 17 +++++---- .../java/com/sheet2/DialogRootViewGroup.kt | 11 +++++- package-lock.json | 4 +- package.json | 2 +- 6 files changed, 61 insertions(+), 15 deletions(-) diff --git a/android/src/main/java/com/behavior/BottomSheetBehavior.java b/android/src/main/java/com/behavior/BottomSheetBehavior.java index 47ab156..4746119 100644 --- a/android/src/main/java/com/behavior/BottomSheetBehavior.java +++ b/android/src/main/java/com/behavior/BottomSheetBehavior.java @@ -273,6 +273,7 @@ void onLayout(@NonNull View bottomSheet) {} @Nullable private ValueAnimator interpolatorAnimator; private static final int DEF_STYLE_RES = R.style.Widget_Design_BottomSheet_Modal; + private static final int SHEET_DRAG_TOUCH_SLOP_MULTIPLIER = 2; int expandedOffset; @@ -304,6 +305,7 @@ void onLayout(@NonNull View bottomSheet) {} private int lastNestedScrollDy; private boolean nestedScrolled; + private int nestedScrollDragDy; private float hideFriction = HIDE_FRICTION; @@ -662,6 +664,7 @@ public boolean onInterceptTouchEvent( } if (!ignoreEvents && viewDragHelper != null + && (action != MotionEvent.ACTION_MOVE || isPastSheetDragTouchSlop(event)) && viewDragHelper.shouldInterceptTouchEvent(event)) { return true; } @@ -675,7 +678,7 @@ public boolean onInterceptTouchEvent( && state != STATE_DRAGGING && (!parent.isPointInChildBounds(scroll, (int) event.getX(), (int) event.getY()) || (initialY - event.getY() < 10 && !canVisuallyScrollUp(scroll))) && viewDragHelper != null - && Math.abs(initialY - event.getY()) > viewDragHelper.getTouchSlop(); + && isPastSheetDragTouchSlop(event); return iss; } @@ -689,7 +692,7 @@ public boolean onTouchEvent( if (state == STATE_DRAGGING && action == MotionEvent.ACTION_DOWN) { return true; } - if (shouldHandleDraggingWithHelper()) { + if (shouldHandleDraggingWithHelper() && shouldForwardToDragHelper(event)) { viewDragHelper.processTouchEvent(event); } // Record the velocity @@ -703,7 +706,7 @@ public boolean onTouchEvent( // The ViewDragHelper tries to capture only the top-most View. We have to explicitly tell it // to capture the bottom sheet in case it is not captured and the touch slop is passed. if (shouldHandleDraggingWithHelper() && action == MotionEvent.ACTION_MOVE && !ignoreEvents) { - if (Math.abs(initialY - event.getY()) > viewDragHelper.getTouchSlop()) { + if (isPastSheetDragTouchSlop(event)) { viewDragHelper.captureChildView(child, event.getPointerId(event.getActionIndex())); } } @@ -720,6 +723,7 @@ public boolean onStartNestedScroll( int type) { lastNestedScrollDy = 0; nestedScrolled = false; + nestedScrollDragDy = 0; return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; } @@ -761,6 +765,9 @@ public void onNestedPreScroll( // Prevent dragging return; } + if (!isPastNestedScrollDragTouchSlop(physicalDy)) { + return; + } consumed[1] = dy; ViewCompat.offsetTopAndBottom(child, -physicalDy); @@ -773,6 +780,9 @@ public void onNestedPreScroll( // Prevent dragging return; } + if (!isPastNestedScrollDragTouchSlop(physicalDy)) { + return; + } consumed[1] = dy; ViewCompat.offsetTopAndBottom(child, -physicalDy); @@ -862,6 +872,7 @@ public void onStopNestedScroll( } startSettling(child, targetState, false); nestedScrolled = false; + nestedScrollDragDy = 0; } @Override @@ -1592,6 +1603,27 @@ private boolean shouldHandleDraggingWithHelper() { return viewDragHelper != null && (draggable || state == STATE_DRAGGING); } + private boolean shouldForwardToDragHelper(@NonNull MotionEvent event) { + return event.getActionMasked() != MotionEvent.ACTION_MOVE + || state == STATE_DRAGGING + || isPastSheetDragTouchSlop(event); + } + + private boolean isPastSheetDragTouchSlop(@NonNull MotionEvent event) { + return viewDragHelper != null + && Math.abs(initialY - event.getY()) > + viewDragHelper.getTouchSlop() * SHEET_DRAG_TOUCH_SLOP_MULTIPLIER; + } + + private boolean isPastNestedScrollDragTouchSlop(int dy) { + if (state == STATE_DRAGGING || viewDragHelper == null) { + return true; + } + nestedScrollDragDy += dy; + return Math.abs(nestedScrollDragDy) > + viewDragHelper.getTouchSlop() * SHEET_DRAG_TOUCH_SLOP_MULTIPLIER; + } + private void createMaterialShapeDrawableIfNeeded(@NonNull Context context) { if (shapeAppearanceModelDefault == null) { return; diff --git a/android/src/main/java/com/sheet2/AppFittedSheet.kt b/android/src/main/java/com/sheet2/AppFittedSheet.kt index 8ff69fe..bc04182 100644 --- a/android/src/main/java/com/sheet2/AppFittedSheet.kt +++ b/android/src/main/java/com/sheet2/AppFittedSheet.kt @@ -110,6 +110,10 @@ open class AppFittedSheet(context: Context) : ViewGroup(context), LifecycleEvent InlineSheetPresenter(this, mHostView) } + init { + mHostView.onSheetLayoutChanged = { pushContentOriginOffset() } + } + var maxWidth: Float = 0F set(value) { field = value diff --git a/android/src/main/java/com/sheet2/BaseRNView.kt b/android/src/main/java/com/sheet2/BaseRNView.kt index 78206d3..6c81ba1 100644 --- a/android/src/main/java/com/sheet2/BaseRNView.kt +++ b/android/src/main/java/com/sheet2/BaseRNView.kt @@ -75,10 +75,7 @@ open class BaseRNView(context: Context?) : ReactViewGroup(context), RootView { override fun onInterceptTouchEvent(event: MotionEvent): Boolean { if (!inlineMode) { - eventDispatcher?.let { eventDispatcher -> - jSTouchDispatcher.handleTouchEvent(event, eventDispatcher, reactContext) - jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, true) - } + dispatchTouchEventToJs(event, isCapture = true) } return super.onInterceptTouchEvent(event) } @@ -90,10 +87,7 @@ open class BaseRNView(context: Context?) : ReactViewGroup(context), RootView { // touch events or eat the event; let normal target finding run. return super.onTouchEvent(event) } - eventDispatcher?.let { eventDispatcher -> - jSTouchDispatcher.handleTouchEvent(event, eventDispatcher, reactContext) - jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, false) - } + dispatchTouchEventToJs(event, isCapture = false) super.onTouchEvent(event) // In case when there is no children interested in handling touch event, we return true from // the root view in order to receive subsequent events related to that gesture @@ -124,4 +118,11 @@ open class BaseRNView(context: Context?) : ReactViewGroup(context), RootView { // No-op - override in order to still receive events to onInterceptTouchEvent // even when some other view disallow that } + + private fun dispatchTouchEventToJs(event: MotionEvent, isCapture: Boolean) { + eventDispatcher?.let { eventDispatcher -> + jSTouchDispatcher.handleTouchEvent(event, eventDispatcher, reactContext) + jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, isCapture) + } + } } diff --git a/android/src/main/java/com/sheet2/DialogRootViewGroup.kt b/android/src/main/java/com/sheet2/DialogRootViewGroup.kt index e4a4fe9..349099f 100644 --- a/android/src/main/java/com/sheet2/DialogRootViewGroup.kt +++ b/android/src/main/java/com/sheet2/DialogRootViewGroup.kt @@ -12,6 +12,7 @@ import kotlin.math.min class DialogRootViewGroup(context: Context) : BaseRNView(context) { private var reactView: View? = null + var onSheetLayoutChanged: (() -> Unit)? = null var sheetMaxHeightSize = Float.MAX_VALUE var sheetMaxWidthSize = Float.MAX_VALUE @@ -56,6 +57,10 @@ class DialogRootViewGroup(context: Context) : BaseRNView(context) { } } + private fun notifySheetLayoutChanged() { + post { onSheetLayoutChanged?.invoke() } + } + fun setVirtualHeight(h: Float) { if (reactView == null) return this.sheetMaxHeightSize = h @@ -68,6 +73,7 @@ class DialogRootViewGroup(context: Context) : BaseRNView(context) { translationX = ((metrics.displayMetrics.widthPixels - newWidth) / 2).toFloat() layoutParams?.width = newWidth layout() + notifySheetLayoutChanged() } fun updateMaxWidth(value: Float) { @@ -76,6 +82,7 @@ class DialogRootViewGroup(context: Context) : BaseRNView(context) { translationX = ((metrics.displayMetrics.widthPixels - newWidth) / 2).toFloat() layoutParams?.width = newWidth layout() + notifySheetLayoutChanged() } override fun addView(child: View, index: Int, params: LayoutParams) { @@ -96,7 +103,9 @@ class DialogRootViewGroup(context: Context) : BaseRNView(context) { super.removeViewAt(index) } - override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {} + override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { + notifySheetLayoutChanged() + } private fun releaseReactView() { sheetMaxHeightSize = Float.MAX_VALUE diff --git a/package-lock.json b/package-lock.json index 6a0eb9f..726e938 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-native-sheet", - "version": "7.9.1", + "version": "7.9.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "react-native-sheet", - "version": "7.9.1", + "version": "7.9.2", "license": "MIT", "dependencies": { "@gorhom/portal": "^1.0.14" diff --git a/package.json b/package.json index 0e7aff1..dedfcdd 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "example" ], "private": "true", - "version": "7.9.1", + "version": "7.9.2", "description": "Native implementation of Bottom sheet", "main": "src/index.tsx", "module": "src/index", From 0a47d970d2b6208739e2e810223cccaf979e70b9 Mon Sep 17 00:00:00 2001 From: SergeyMild Date: Wed, 1 Jul 2026 09:02:40 +0300 Subject: [PATCH 3/3] fix: show sheet overlay in iOS modal presentation, add overlay example - iOS: attach the overlay subview for modal presentation, not only inline containment. attachOverlaySubview no longer guards on _useInlinePresentation and is invoked from the modal present completion, so `overlay` renders in both presentation modes (matching Android). - example: add OverlayExample demonstrating a single-node, pass-through overlay over interactive sheet content in both inline and modal modes. --- example/src/screens/index.tsx | 6 + example/src/screens/modal/OverlayExample.tsx | 148 +++++++++++++++++++ ios/HostFittedSheet.swift | 19 ++- 3 files changed, 166 insertions(+), 7 deletions(-) create mode 100644 example/src/screens/modal/OverlayExample.tsx diff --git a/example/src/screens/index.tsx b/example/src/screens/index.tsx index bdb43f5..8bf46b4 100644 --- a/example/src/screens/index.tsx +++ b/example/src/screens/index.tsx @@ -19,8 +19,14 @@ import { InvertedListExample } from './modal/InvertedListExample'; import E2ETestScreen from './modal/E2ETestScreen'; import { FullScreenModalOverSheetExample } from './modal/FullScreenModalOverSheetExample'; import { BigRedTouchExample } from './modal/BigRedTouchExample'; +import { OverlayExample } from './modal/OverlayExample'; export const screens = [ + { + name: 'Overlay', + slug: 'Modal/Overlay', + getScreen: () => OverlayExample, + }, { name: 'BigRedTouch', slug: 'Modal/BigRedTouch', diff --git a/example/src/screens/modal/OverlayExample.tsx b/example/src/screens/modal/OverlayExample.tsx new file mode 100644 index 0000000..bffcc7f --- /dev/null +++ b/example/src/screens/modal/OverlayExample.tsx @@ -0,0 +1,148 @@ +import { useCallback, useRef, useState } from 'react'; +import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; + +import { Button } from '../../components/button'; +import { FittedSheet, type FittedSheetRef } from 'react-native-sheet'; + +/** + * Demonstrates the `overlay` prop. + * + * `overlay` renders a visual, NON-interactive layer on top of the sheet + * content. Touches pass straight through it to the content underneath, so the + * buttons below keep working while the overlay is visible. Pass a SINGLE node + * (wrap multiple pieces in one parent View) — the overlay is tracked as one + * child on the native side. + * + * The same overlay works in both presentation modes: modal (default) and + * inline (`useInlinePresentation: true`). Toggle the switch to verify both. + */ +export const OverlayExample = () => { + const bottomSheetRef = useRef(null); + const [inline, setInline] = useState(false); + const [taps, setTaps] = useState(0); + + const handlePresentPress = useCallback(() => { + setTaps(0); + bottomSheetRef.current?.show(); + }, []); + + const handleContentPress = useCallback(() => { + // Fires even though the overlay is drawn on top — the overlay is + // pass-through (pointerEvents="none" / userInteractionEnabled = false). + setTaps((n) => n + 1); + }, []); + + // A single-node overlay: a translucent banner + a corner badge, wrapped in + // one parent View with pointerEvents="none" so it never eats touches. + const overlay = ( + + + OVERLAY (visual only) + + + NEW + + + ); + + return ( + +