Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
- 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))
- `scrollableRef` now resolves scrollables that expose their scroll node through an imperative handle, such as LegendList. ([#914](https://github.com/lodev09/react-native-true-sheet/pull/914) by [@cpruijsen](https://github.com/cpruijsen))
- 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

Expand Down
4 changes: 3 additions & 1 deletion android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -399,10 +399,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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ 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<Int> = 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

Expand All @@ -36,24 +45,40 @@ class RNScreensEventObserver : EventDispatcherListener {

fun capturePresenterScreenFromView(view: View?) {
presenterScreenTag = 0
val ancestors = mutableSetOf<Int>()

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
// 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()
}
}
}

Expand Down
3 changes: 3 additions & 0 deletions example/expo/app/repro/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ export default function ReproLayout() {
/>
<Stack.Screen name="page-sheet" options={{ presentation: 'modal', headerShown: false }} />
<Stack.Screen name="sheet" options={{ headerShown: false }} />
<Stack.Screen name="tabs" options={{ headerShown: false }} />
<Stack.Screen name="nested" options={{ headerShown: false }} />
<Stack.Screen name="pushed" options={{ title: 'Pushed' }} />
</Stack>
);
}
17 changes: 17 additions & 0 deletions example/expo/app/repro/nested/_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Stack } from 'expo-router';

import { DARK_BLUE } from '@example/shared/utils';

export default function ReproNestedLayout() {
return (
<Stack
screenOptions={{
headerTintColor: 'white',
headerStyle: { backgroundColor: DARK_BLUE },
}}
>
<Stack.Screen name="index" options={{ title: 'Nested Stack' }} />
<Stack.Screen name="details" options={{ title: 'Details' }} />
</Stack>
);
}
19 changes: 19 additions & 0 deletions example/expo/app/repro/nested/details.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<ReproScreen
title="Repro Nested Details"
description="No sheet should be on top of this screen, also after pushing a screen on the enclosing stack and going back."
>
<Button text="Push Screen" onPress={() => router.push('/repro/pushed')} />
<Button text="Go Back" onPress={() => router.back()} />
</ReproScreen>
);
}
25 changes: 25 additions & 0 deletions example/expo/app/repro/nested/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useRef } from 'react';
import { useRouter } from 'expo-router';
import type { TrueSheet } from '@lodev09/react-native-true-sheet';

import { Button } from '@example/shared/components';
import { BlankSheet } from '@example/shared/sheets';

import { ReproScreen } from '../../../components/ReproScreen';

export default function ReproNested() {
const router = useRouter();
const sheet = useRef<TrueSheet>(null);

return (
<ReproScreen
title="Repro Nested Stack"
description="Hosts a sheet in a stack nested in a stack screen. The sheet should hide for a push on either stack, and only come back when this screen does."
>
<Button text="Present Sheet" onPress={() => sheet.current?.present()} />
<Button text="Push Details" onPress={() => router.push('/repro/nested/details')} />
<Button text="Push Screen" onPress={() => router.push('/repro/pushed')} />
<BlankSheet ref={sheet} detents={['auto']} dimmed={false} />
</ReproScreen>
);
}
18 changes: 18 additions & 0 deletions example/expo/app/repro/pushed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useRouter } from 'expo-router';

import { Button } from '@example/shared/components';

import { ReproScreen } from '../../components/ReproScreen';

export default function ReproPushed() {
const router = useRouter();

return (
<ReproScreen
title="Repro Pushed"
description="Pushed on the stack enclosing the tabs. No sheet should be on top of this screen."
>
<Button text="Go Back" onPress={() => router.back()} />
</ReproScreen>
);
}
19 changes: 19 additions & 0 deletions example/expo/app/repro/tabs/_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Tabs } from 'expo-router/js-tabs';

import { DARK_BLUE } from '@example/shared/utils';

// Sheets hosted in a tab screen, a stack inside a tab, and a nested stack, with
// the enclosing stack pushing over them. Open with `yarn expo open repro/tabs`.
export default function ReproTabsLayout() {
return (
<Tabs
screenOptions={{
headerTintColor: 'white',
headerStyle: { backgroundColor: DARK_BLUE },
}}
>
<Tabs.Screen name="index" options={{ title: 'Tab' }} />
<Tabs.Screen name="stack" options={{ title: 'Stack', headerShown: false }} />
</Tabs>
);
}
25 changes: 25 additions & 0 deletions example/expo/app/repro/tabs/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useRef } from 'react';
import { useRouter } from 'expo-router';
import type { TrueSheet } from '@lodev09/react-native-true-sheet';

import { Button } from '@example/shared/components';
import { BlankSheet } from '@example/shared/sheets';

import { ReproScreen } from '../../../components/ReproScreen';

export default function ReproTab() {
const router = useRouter();
const sheet = useRef<TrueSheet>(null);

return (
<ReproScreen
title="Repro Tab"
description="Hosts a sheet in a tab screen. Pushing a screen on the enclosing stack should hide the sheet, and going back should restore it."
>
<Button text="Present Sheet" onPress={() => sheet.current?.present()} />
<Button text="Push Screen" onPress={() => router.push('/repro/pushed')} />
<Button text="Open Nested Stack" onPress={() => router.push('/repro/nested')} />
<BlankSheet ref={sheet} detents={['auto']} dimmed={false} />
</ReproScreen>
);
}
17 changes: 17 additions & 0 deletions example/expo/app/repro/tabs/stack/_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Stack } from 'expo-router';

import { DARK_BLUE } from '@example/shared/utils';

export default function ReproTabStackLayout() {
return (
<Stack
screenOptions={{
headerTintColor: 'white',
headerStyle: { backgroundColor: DARK_BLUE },
}}
>
<Stack.Screen name="index" options={{ title: 'Stack in Tab' }} />
<Stack.Screen name="details" options={{ title: 'Details' }} />
</Stack>
);
}
19 changes: 19 additions & 0 deletions example/expo/app/repro/tabs/stack/details.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useRouter } from 'expo-router';

import { Button } from '@example/shared/components';

import { ReproScreen } from '../../../../components/ReproScreen';

export default function ReproTabStackDetails() {
const router = useRouter();

return (
<ReproScreen
title="Repro Details"
description="No sheet should be on top of this screen, also after pushing a screen on the enclosing stack and going back."
>
<Button text="Push Screen" onPress={() => router.push('/repro/pushed')} />
<Button text="Go Back" onPress={() => router.back()} />
</ReproScreen>
);
}
25 changes: 25 additions & 0 deletions example/expo/app/repro/tabs/stack/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useRef } from 'react';
import { useRouter } from 'expo-router';
import type { TrueSheet } from '@lodev09/react-native-true-sheet';

import { Button } from '@example/shared/components';
import { BlankSheet } from '@example/shared/sheets';

import { ReproScreen } from '../../../../components/ReproScreen';

export default function ReproTabStack() {
const router = useRouter();
const sheet = useRef<TrueSheet>(null);

return (
<ReproScreen
title="Repro Stack in Tab"
description="Hosts a sheet in a stack inside a tab. The sheet should hide for a push on either stack, and only come back when this screen does."
>
<Button text="Present Sheet" onPress={() => sheet.current?.present()} />
<Button text="Push Details" onPress={() => router.push('/repro/tabs/stack/details')} />
<Button text="Push Screen" onPress={() => router.push('/repro/pushed')} />
<BlankSheet ref={sheet} detents={['auto']} dimmed={false} />
</ReproScreen>
);
}
19 changes: 18 additions & 1 deletion ios/core/RNScreensEventObserver.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading