From 133bb610b986324a9eff8fa744ea9b3e7e46347c Mon Sep 17 00:00:00 2001 From: Hein Rutjes Date: Thu, 8 Oct 2026 08:57:18 +0200 Subject: [PATCH 1/4] fix: hide a sheet hosted in a tab screen when an enclosing stack pushes a screen A sheet whose nearest react-native-screens screen is a bottom-tab screen stayed presented when the stack enclosing the tabs pushed a screen, so the pushed screen ended up behind the sheet. iOS: the presenter was only compared against the top of its nearest navigation controller, which for a tab is the tabs container where the active tab is always on top. Also walk the enclosing navigation controllers. Android: react-native-screens dispatches lifecycle events for stack screens only, so a tab screen never receives them. Also listen on the enclosing screens, and let stacked sheets inherit them from their parent. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../com/lodev09/truesheet/TrueSheetView.kt | 4 +++- .../truesheet/core/RNScreensEventObserver.kt | 18 +++++++++++++++--- ios/core/RNScreensEventObserver.mm | 19 ++++++++++++++++++- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt b/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt index aac79b36..e267d77d 100644 --- a/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +++ b/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt @@ -380,10 +380,12 @@ class TrueSheetView(private val reactContext: ThemedReactContext) : // For stacked sheets on the same screen, inherit parent's presenter screen tag. // If parent was hidden by screen navigation, this sheet is on a different screen. - val parentScreenTag = viewController.parentSheetView?.screensEventObserver?.presenterScreenTag ?: 0 + val parentObserver = viewController.parentSheetView?.screensEventObserver + val parentScreenTag = parentObserver?.presenterScreenTag ?: 0 val parentHiddenByScreen = viewController.parentSheetView?.viewController?.wasHiddenByScreen == true if (parentScreenTag != 0 && !parentHiddenByScreen) { presenterScreenTag = parentScreenTag + ancestorScreenTags = parentObserver?.ancestorScreenTags ?: emptySet() } else { capturePresenterScreenFromView(this@TrueSheetView) } diff --git a/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt b/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt index 0c9f6651..75319216 100644 --- a/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt +++ b/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt @@ -22,6 +22,11 @@ class RNScreensEventObserver : EventDispatcherListener { private var eventDispatcher: EventDispatcher? = null var presenterScreenTag: Int = 0 + // Screens enclosing the presenter. react-native-screens only dispatches lifecycle + // events for stack screens, so a presenter inside a plain screen container (e.g. a + // bottom tab) never receives them and an enclosing stack screen has to stand in. + var ancestorScreenTags: Set = emptySet() + fun startObserving(dispatcher: EventDispatcher?) { if (eventDispatcher != null || dispatcher == null) return @@ -36,20 +41,27 @@ class RNScreensEventObserver : EventDispatcherListener { fun capturePresenterScreenFromView(view: View?) { presenterScreenTag = 0 + val ancestors = mutableSetOf() var current: View? = view while (current != null) { if (isScreenView(current)) { - presenterScreenTag = current.id - break + if (presenterScreenTag == 0) { + presenterScreenTag = current.id + } else { + ancestors.add(current.id) + } } current = (current.parent as? View) } + + ancestorScreenTags = ancestors } override fun onEventDispatch(event: Event<*>) { // Only process events for the presenter screen - if (presenterScreenTag == 0 || event.viewTag != presenterScreenTag) return + if (presenterScreenTag == 0) return + if (event.viewTag != presenterScreenTag && event.viewTag !in ancestorScreenTags) return when (event.eventName) { "topWillDisappear" -> delegate?.presenterScreenWillDisappear() diff --git a/ios/core/RNScreensEventObserver.mm b/ios/core/RNScreensEventObserver.mm index dd3c3748..5eb04bbf 100644 --- a/ios/core/RNScreensEventObserver.mm +++ b/ios/core/RNScreensEventObserver.mm @@ -272,7 +272,24 @@ - (BOOL)shouldDismissForScreenTag:(NSInteger)screenTag { // Dismiss if presenter is no longer top of nav stack (pushed/popped) // Skip if still top (e.g. modal dismiss - sheet dismisses naturally) - return navController.topViewController != _presenterScreenController; + if (navController.topViewController != _presenterScreenController) { + return YES; + } + + // A presenter inside a screen container (e.g. a bottom tab) stays the top of + // that container while an enclosing stack pushes a screen over it. + for (UINavigationController *outerNav = navController.navigationController; outerNav; + outerNav = outerNav.navigationController) { + UIViewController *ancestor = _presenterScreenController; + while (ancestor && ancestor != outerNav.topViewController) { + ancestor = ancestor.parentViewController; + } + if (!ancestor) { + return YES; + } + } + + return NO; } @end From dd0a92b353e07749db32f1b737321a089257532d Mon Sep 17 00:00:00 2001 From: Hein Rutjes Date: Thu, 8 Oct 2026 08:57:50 +0200 Subject: [PATCH 2/4] docs: add changelog entry Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67605003..173ffa68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ - The peek detent no longer collapses when swapping between two screens that each render a peek component — the last-mounted peek wins. ([#851](https://github.com/lodev09/react-native-true-sheet/pull/851) by [@SamuelBrucksch](https://github.com/SamuelBrucksch) and [@lodev09](https://github.com/lodev09)) - Sheet navigator: a sheet pushed while another is still dismissing is no longer dropped. ([#898](https://github.com/lodev09/react-native-true-sheet/pull/898) by [@lodev09](https://github.com/lodev09)) - Dismissing a sheet before its present finishes now always dismisses it and settles `present()`, instead of leaving the sheet open or `present()` hanging. On web, `present()`/`dismiss()` now resolve when the animation ends and ignore calls on an already presented or dismissed sheet, like native. ([#900](https://github.com/lodev09/react-native-true-sheet/pull/900) by [@lodev09](https://github.com/lodev09)) +- A sheet hosted in a bottom-tab screen is now hidden when the stack enclosing the tabs pushes a screen, and restored on return, instead of covering the pushed screen. ([#910](https://github.com/lodev09/react-native-true-sheet/pull/910) by [@IjzerenHein](https://github.com/IjzerenHein)) ### 💥 Breaking changes From 71659779ac5eabb4a63088ab74b36e63643d1a11 Mon Sep 17 00:00:00 2001 From: Hein Rutjes Date: Mon, 12 Oct 2026 10:47:23 +0200 Subject: [PATCH 3/4] fix(android): only restore a sheet for the screen that hid it Listening to the enclosing screens meant any of them reappearing restored the sheet. A sheet whose own screen was still covered by a push inside a nested stack came back on top of that screen after the enclosing stack pushed and popped. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../truesheet/core/RNScreensEventObserver.kt | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt b/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt index 75319216..da290613 100644 --- a/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt +++ b/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt @@ -27,6 +27,10 @@ class RNScreensEventObserver : EventDispatcherListener { // bottom tab) never receives them and an enclosing stack screen has to stand in. var ancestorScreenTags: Set = emptySet() + // Only the screen that hid the sheet brings it back. An enclosing screen reappearing + // must not reveal a sheet whose own screen is still covered inside it. + private var disappearedScreenTag: Int = 0 + fun startObserving(dispatcher: EventDispatcher?) { if (eventDispatcher != null || dispatcher == null) return @@ -59,13 +63,22 @@ class RNScreensEventObserver : EventDispatcherListener { } override fun onEventDispatch(event: Event<*>) { - // Only process events for the presenter screen + // Only process events for the presenter screen and the screens enclosing it if (presenterScreenTag == 0) return if (event.viewTag != presenterScreenTag && event.viewTag !in ancestorScreenTags) return when (event.eventName) { - "topWillDisappear" -> delegate?.presenterScreenWillDisappear() - "topWillAppear" -> delegate?.presenterScreenWillAppear() + "topWillDisappear" -> { + if (disappearedScreenTag != 0) return + disappearedScreenTag = event.viewTag + delegate?.presenterScreenWillDisappear() + } + + "topWillAppear" -> { + if (event.viewTag != disappearedScreenTag) return + disappearedScreenTag = 0 + delegate?.presenterScreenWillAppear() + } } } From 7f7dbce5d88f018f07a784ff6967a4277bb233da Mon Sep 17 00:00:00 2001 From: Hein Rutjes Date: Mon, 12 Oct 2026 10:47:25 +0200 Subject: [PATCH 4/4] chore(example): add tab and nested stack repro screens Co-Authored-By: Claude Opus 5.5 (1M context) --- example/expo/app/repro/_layout.tsx | 3 +++ example/expo/app/repro/nested/_layout.tsx | 17 +++++++++++++ example/expo/app/repro/nested/details.tsx | 19 ++++++++++++++ example/expo/app/repro/nested/index.tsx | 25 +++++++++++++++++++ example/expo/app/repro/pushed.tsx | 18 +++++++++++++ example/expo/app/repro/tabs/_layout.tsx | 19 ++++++++++++++ example/expo/app/repro/tabs/index.tsx | 25 +++++++++++++++++++ example/expo/app/repro/tabs/stack/_layout.tsx | 17 +++++++++++++ example/expo/app/repro/tabs/stack/details.tsx | 19 ++++++++++++++ example/expo/app/repro/tabs/stack/index.tsx | 25 +++++++++++++++++++ 10 files changed, 187 insertions(+) create mode 100644 example/expo/app/repro/nested/_layout.tsx create mode 100644 example/expo/app/repro/nested/details.tsx create mode 100644 example/expo/app/repro/nested/index.tsx create mode 100644 example/expo/app/repro/pushed.tsx create mode 100644 example/expo/app/repro/tabs/_layout.tsx create mode 100644 example/expo/app/repro/tabs/index.tsx create mode 100644 example/expo/app/repro/tabs/stack/_layout.tsx create mode 100644 example/expo/app/repro/tabs/stack/details.tsx create mode 100644 example/expo/app/repro/tabs/stack/index.tsx diff --git a/example/expo/app/repro/_layout.tsx b/example/expo/app/repro/_layout.tsx index 433752ed..3f6cd3b0 100644 --- a/example/expo/app/repro/_layout.tsx +++ b/example/expo/app/repro/_layout.tsx @@ -23,6 +23,9 @@ export default function ReproLayout() { /> + + + ); } diff --git a/example/expo/app/repro/nested/_layout.tsx b/example/expo/app/repro/nested/_layout.tsx new file mode 100644 index 00000000..5850eb0f --- /dev/null +++ b/example/expo/app/repro/nested/_layout.tsx @@ -0,0 +1,17 @@ +import { Stack } from 'expo-router'; + +import { DARK_BLUE } from '@example/shared/utils'; + +export default function ReproNestedLayout() { + return ( + + + + + ); +} diff --git a/example/expo/app/repro/nested/details.tsx b/example/expo/app/repro/nested/details.tsx new file mode 100644 index 00000000..47d7e55f --- /dev/null +++ b/example/expo/app/repro/nested/details.tsx @@ -0,0 +1,19 @@ +import { useRouter } from 'expo-router'; + +import { Button } from '@example/shared/components'; + +import { ReproScreen } from '../../../components/ReproScreen'; + +export default function ReproNestedDetails() { + const router = useRouter(); + + return ( + +