Skip to content

Commit 4eef373

Browse files
Merge pull request #14 from arekkubaczkowski/fix/nested-portals-fix
fix: nested portal in persistent
2 parents 3c7ddc0 + 87787ad commit 4eef373

7 files changed

Lines changed: 262 additions & 20 deletions

File tree

example/src/App.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { SafeAreaProvider } from 'react-native-safe-area-context';
99

1010
import { UserProvider } from './context/UserContext';
1111
import { HomeScreen } from './screens';
12-
import { ScannerSheet } from './sheets';
12+
import { PersistentWithPortalSheet, ScannerSheet } from './sheets';
1313
import { sharedStyles } from './styles/theme';
1414

1515
export default function App() {
@@ -30,6 +30,10 @@ export default function App() {
3030
<BottomSheetPersistent id="scanner-sheet">
3131
<ScannerSheet />
3232
</BottomSheetPersistent>
33+
{/* Persistent sheet with nested portal sheet inside */}
34+
<BottomSheetPersistent id="persistent-with-portal">
35+
<PersistentWithPortalSheet />
36+
</BottomSheetPersistent>
3337
</BottomSheetManagerProvider>
3438
</GestureHandlerRootView>
3539
</SafeAreaProvider>

example/src/bottom-sheet.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,9 @@ declare module 'react-native-bottom-sheet-stack' {
1111
source: 'home' | 'navigation';
1212
title?: string;
1313
};
14+
'persistent-with-portal': true;
15+
'nested-portal-in-persistent': {
16+
message: string;
17+
};
1418
}
1519
}

example/src/screens/HomeScreen.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ export function HomeScreen() {
2424
const portalSheetControl = useBottomSheetControl('context-portal-sheet');
2525
const portalModeSheetA = useBottomSheetControl('portal-mode-sheet-a');
2626
const scannerControl = useBottomSheetControl('scanner-sheet');
27+
const persistentWithPortalControl = useBottomSheetControl(
28+
'persistent-with-portal'
29+
);
2730

2831
return (
2932
<View style={sharedStyles.container}>
@@ -125,6 +128,15 @@ export function HomeScreen() {
125128
})
126129
}
127130
/>
131+
132+
<DemoCard
133+
title="Persistent + Nested Portal"
134+
description="Persistent sheet with portal-based sheet defined inside"
135+
color={colors.purple}
136+
onPress={() =>
137+
persistentWithPortalControl.open({ scaleBackground: true })
138+
}
139+
/>
128140
</View>
129141

130142
{/* Features */}
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import type { BottomSheetMethods } from '@gorhom/bottom-sheet/lib/typescript/types';
2+
import { forwardRef, useState } from 'react';
3+
import { StyleSheet, Text, View } from 'react-native';
4+
import {
5+
BottomSheetPortal,
6+
useBottomSheetContext,
7+
useBottomSheetControl,
8+
} from 'react-native-bottom-sheet-stack';
9+
10+
import { Badge, Button, SecondaryButton, Sheet } from '../components';
11+
import { colors, sharedStyles } from '../styles/theme';
12+
13+
/**
14+
* Nested portal sheet content - defined inside the persistent sheet
15+
* to demonstrate portal sheets can be declared within other sheets
16+
*/
17+
const NestedPortalSheetContent = forwardRef<BottomSheetMethods>((_, ref) => {
18+
const { close, params } =
19+
useBottomSheetContext<'nested-portal-in-persistent'>();
20+
const [counter, setCounter] = useState(0);
21+
22+
return (
23+
<Sheet ref={ref} enableDynamicSizing>
24+
<View style={styles.badgeRow}>
25+
<Badge label="Portal" color={colors.warning} />
26+
<Badge label="Nested" color={colors.purple} />
27+
</View>
28+
<Text style={sharedStyles.h1}>Nested Portal Sheet</Text>
29+
<Text style={sharedStyles.text}>
30+
This portal-based sheet is defined inside the persistent sheet. It has
31+
access to the same React context as its parent.
32+
</Text>
33+
34+
{params?.message && (
35+
<View style={styles.paramBox}>
36+
<Text style={styles.paramLabel}>Message from parent:</Text>
37+
<Text style={styles.paramValue}>{params.message}</Text>
38+
</View>
39+
)}
40+
41+
<View style={styles.counterBox}>
42+
<Text style={styles.counterLabel}>Local counter:</Text>
43+
<Text style={styles.counterValue}>{counter}</Text>
44+
</View>
45+
46+
<View style={styles.actions}>
47+
<Button
48+
title="Increment Counter"
49+
onPress={() => setCounter((c) => c + 1)}
50+
/>
51+
<SecondaryButton title="Close" onPress={close} />
52+
</View>
53+
54+
<View style={styles.infoBox}>
55+
<Text style={styles.infoText}>
56+
Note: This sheet's state resets on close because it's a portal sheet
57+
(not persistent). The parent persistent sheet keeps its state.
58+
</Text>
59+
</View>
60+
</Sheet>
61+
);
62+
});
63+
64+
NestedPortalSheetContent.displayName = 'NestedPortalSheetContent';
65+
66+
/**
67+
* Persistent sheet that contains a portal-based sheet definition inside
68+
*/
69+
export const PersistentWithPortalSheet = forwardRef<BottomSheetMethods>(
70+
(_, ref) => {
71+
const { close } = useBottomSheetContext<'persistent-with-portal'>();
72+
const nestedPortalControl = useBottomSheetControl(
73+
'nested-portal-in-persistent'
74+
);
75+
const [openCount, setOpenCount] = useState(0);
76+
77+
const handleOpenNestedPortal = () => {
78+
setOpenCount((c) => c + 1);
79+
nestedPortalControl.open({
80+
scaleBackground: true,
81+
mode: 'push',
82+
params: {
83+
message: `Opened ${openCount + 1} time(s) from persistent sheet`,
84+
},
85+
});
86+
};
87+
88+
return (
89+
<>
90+
{/* Portal sheet defined inside the persistent sheet */}
91+
<BottomSheetPortal id="nested-portal-in-persistent">
92+
<NestedPortalSheetContent />
93+
</BottomSheetPortal>
94+
95+
<Sheet ref={ref} enableDynamicSizing>
96+
<View style={styles.badgeRow}>
97+
<Badge label="Persistent" color={colors.cyan} />
98+
<Badge label="Has Nested Portal" color={colors.purple} />
99+
</View>
100+
<Text style={sharedStyles.h1}>Persistent + Portal Demo</Text>
101+
<Text style={sharedStyles.text}>
102+
This persistent sheet contains a portal-based sheet definition
103+
inside. The persistent sheet keeps its state across open/close
104+
cycles, while the nested portal sheet resets.
105+
</Text>
106+
107+
<View style={styles.stateBox}>
108+
<Text style={styles.stateLabel}>Nested portal opened:</Text>
109+
<Text style={styles.stateValue}>{openCount} time(s)</Text>
110+
</View>
111+
112+
<View style={styles.actions}>
113+
<Button
114+
title="Open Nested Portal Sheet"
115+
onPress={handleOpenNestedPortal}
116+
/>
117+
<SecondaryButton title="Close" onPress={close} />
118+
</View>
119+
120+
<View style={styles.infoBox}>
121+
<Text style={styles.infoText}>
122+
Close this sheet and reopen it - the "opened count" persists
123+
because this is a persistent sheet. The nested portal sheet's
124+
counter will reset each time it opens.
125+
</Text>
126+
</View>
127+
</Sheet>
128+
</>
129+
);
130+
}
131+
);
132+
133+
PersistentWithPortalSheet.displayName = 'PersistentWithPortalSheet';
134+
135+
const styles = StyleSheet.create({
136+
badgeRow: {
137+
flexDirection: 'row',
138+
gap: 8,
139+
},
140+
actions: {
141+
gap: 12,
142+
marginTop: 16,
143+
},
144+
paramBox: {
145+
backgroundColor: colors.primaryDark,
146+
borderRadius: 12,
147+
padding: 14,
148+
marginTop: 16,
149+
},
150+
paramLabel: {
151+
color: colors.textSecondary,
152+
fontSize: 12,
153+
marginBottom: 4,
154+
},
155+
paramValue: {
156+
color: colors.primary,
157+
fontSize: 16,
158+
fontWeight: '600',
159+
},
160+
counterBox: {
161+
backgroundColor: colors.background,
162+
borderRadius: 12,
163+
padding: 14,
164+
marginTop: 12,
165+
flexDirection: 'row',
166+
justifyContent: 'space-between',
167+
alignItems: 'center',
168+
},
169+
counterLabel: {
170+
color: colors.textSecondary,
171+
fontSize: 14,
172+
},
173+
counterValue: {
174+
color: colors.warning,
175+
fontSize: 24,
176+
fontWeight: '700',
177+
},
178+
stateBox: {
179+
backgroundColor: colors.background,
180+
borderRadius: 12,
181+
padding: 14,
182+
marginTop: 16,
183+
flexDirection: 'row',
184+
justifyContent: 'space-between',
185+
alignItems: 'center',
186+
borderWidth: 1,
187+
borderColor: colors.border,
188+
},
189+
stateLabel: {
190+
color: colors.textSecondary,
191+
fontSize: 14,
192+
},
193+
stateValue: {
194+
color: colors.cyan,
195+
fontSize: 20,
196+
fontWeight: '700',
197+
},
198+
infoBox: {
199+
backgroundColor: colors.background,
200+
borderRadius: 12,
201+
padding: 14,
202+
marginTop: 16,
203+
borderWidth: 1,
204+
borderColor: colors.border,
205+
},
206+
infoText: {
207+
color: colors.textSecondary,
208+
fontSize: 13,
209+
lineHeight: 18,
210+
textAlign: 'center',
211+
},
212+
});

example/src/sheets/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ export {
77
export { HeavySheet } from './DynamicContentSheet';
88
export { SheetA, SheetB, SheetC, SheetD } from './NavigationSheets';
99
export { NestedSheet1, NestedSheet2, NestedSheet3 } from './NestedScaleSheets';
10+
export { PersistentWithPortalSheet } from './PersistentWithNestedPortal';
1011
export { PortalModeSheetA, PortalModeSheetB } from './PortalModeSheets';
1112
export { ScannerSheet } from './ScannerSheet';

src/BottomSheetPersistent.tsx

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import type { BottomSheetMethods } from '@gorhom/bottom-sheet/lib/typescript/types';
22
import React, { useEffect, useRef } from 'react';
33

4-
import { useMount, useSheetExists, useUnmount } from './bottomSheet.store';
4+
import {
5+
useMount,
6+
useSheetExists,
7+
useSheetPortalSession,
8+
useUnmount,
9+
} from './bottomSheet.store';
510
import { BottomSheetDefaultIndexContext } from './BottomSheetDefaultIndex.context';
611
import { useMaybeBottomSheetManagerContext } from './BottomSheetManager.provider';
7-
import { BottomSheetPortal } from './BottomSheetPortal';
812
import { BottomSheetRefContext } from './BottomSheetRef.context';
913
import type { BottomSheetPortalId } from './portal.types';
1014
import { setSheetRef } from './refsMap';
1115
import { useEvent } from './useEvent';
16+
import { Portal } from 'react-native-teleport';
17+
import { BottomSheetContext } from './BottomSheet.context';
1218

1319
interface BottomSheetPersistentProps {
1420
id: BottomSheetPortalId;
@@ -23,7 +29,7 @@ export function BottomSheetPersistent({
2329
const mount = useMount();
2430
const unmount = useUnmount();
2531
const sheetExists = useSheetExists(id);
26-
32+
const portalSession = useSheetPortalSession(id);
2733
const sheetRef = useRef<BottomSheetMethods>(null);
2834
const groupId = bottomSheetManagerContext?.groupId || 'default';
2935

@@ -49,12 +55,14 @@ export function BottomSheetPersistent({
4955
}
5056

5157
return (
52-
<BottomSheetPortal id={id}>
53-
<BottomSheetDefaultIndexContext.Provider value={{ defaultIndex: -1 }}>
54-
<BottomSheetRefContext.Provider value={sheetRef}>
55-
{children}
56-
</BottomSheetRefContext.Provider>
57-
</BottomSheetDefaultIndexContext.Provider>
58-
</BottomSheetPortal>
58+
<Portal hostName={`bottomsheet-${id}-${portalSession}`}>
59+
<BottomSheetContext.Provider value={{ id }}>
60+
<BottomSheetDefaultIndexContext.Provider value={{ defaultIndex: -1 }}>
61+
<BottomSheetRefContext.Provider value={sheetRef}>
62+
{children}
63+
</BottomSheetRefContext.Provider>
64+
</BottomSheetDefaultIndexContext.Provider>
65+
</BottomSheetContext.Provider>
66+
</Portal>
5967
);
6068
}

src/BottomSheetPortal.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import React from 'react';
44
import { Portal } from 'react-native-teleport';
55

66
import { BottomSheetContext } from './BottomSheet.context';
7-
import { useSheetUsePortal, useSheetPortalSession } from './bottomSheet.store';
7+
import { BottomSheetDefaultIndexContext } from './BottomSheetDefaultIndex.context';
8+
import { BottomSheetRefContext } from './BottomSheetRef.context';
9+
import { useSheetPortalSession } from './bottomSheet.store';
810
import type { BottomSheetPortalId } from './portal.types';
911
import { getSheetRef } from './refsMap';
1012

@@ -14,22 +16,21 @@ interface BottomSheetPortalProps {
1416
}
1517

1618
export function BottomSheetPortal({ id, children }: BottomSheetPortalProps) {
17-
const usePortal = useSheetUsePortal(id);
1819
const portalSession = useSheetPortalSession(id);
20+
const ref = getSheetRef(id);
1921

20-
if (!usePortal || portalSession === undefined) {
22+
if (!portalSession || !ref) {
2123
return null;
2224
}
2325

24-
const ref = getSheetRef(id);
25-
const childWithRef = React.cloneElement(children, {
26-
ref,
27-
} as { ref: typeof ref });
28-
2926
return (
3027
<Portal hostName={`bottomsheet-${id}-${portalSession}`}>
3128
<BottomSheetContext.Provider value={{ id }}>
32-
{childWithRef}
29+
<BottomSheetDefaultIndexContext.Provider value={{ defaultIndex: 0 }}>
30+
<BottomSheetRefContext.Provider value={ref}>
31+
{children}
32+
</BottomSheetRefContext.Provider>
33+
</BottomSheetDefaultIndexContext.Provider>
3334
</BottomSheetContext.Provider>
3435
</Portal>
3536
);

0 commit comments

Comments
 (0)