gorhom / react-native-bottom-sheet

A performant interactive bottom sheet with fully configurable options 🚀
https://gorhom.dev/react-native-bottom-sheet/
MIT License
6.99k stars 766 forks source link

[v4] BottomSheetModal in react navigation modal #832

Closed Livijn closed 2 years ago

Livijn commented 2 years ago

Bug

The sheet is placed behind the @react-navigation/native modal.

Environment info

Library Version
@gorhom/bottom-sheet 4.1.5
react-native 0.67.2
react-native-reanimated 2.3.1
react-native-gesture-handler 2.2.0

Steps To Reproduce

  1. Create a NativeStack
  2. Place the BottomSheetModalProvider like this:
    <NavigationContainer>
    <BottomSheetModalProvider>
    <StackNavigator />
    </BottomSheetModalProvider>
    </NavigationContainer>
  3. Use the <BottomSheetModal> in a screen with presentation: "modal".

Describe what you expected to happen:

The sheet is placed behind the modal. I expect the sheet to be in front of the modal that hosts the sheet. I've also tried wrapping the modal-component with the provider, but then I get 'BottomSheetModalInternalContext' cannot be null!.

Reproducible sample code

The snack template throws an error: Cannot convert undefined or null to object

philipp-wat commented 2 years ago

Hi Livijn,

we had the same bug with "createNativeStackNavigator".

Using "createStackNavigator" from "@react-navigation/stack" fixed it for us.

Hope that helps! Philipp

Livijn commented 2 years ago

I don't want to drop the benefits of using the native stack for this purpose. Thanks though.

lghimfus commented 2 years ago

hi @Livijn, did you find a workaround for this one?

Livijn commented 2 years ago

Nope, not yet. Still waiting for @gorhom to reply.

lghimfus commented 2 years ago

Nope, not yet. Still waiting for @gorhom to reply.

right, I ended up using <FullWindowOverlay style={StyleSheet.absoluteFill}> from react-native-screens to wrap the bottom sheet; that brings it above the navigators. I found out about it here https://github.com/gorhom/react-native-portal, in case you also need to use a portal

hope this helps a bit in the meantime

Livijn commented 2 years ago

If I wrap it like this: <FullWindowOverlay><BottomSheetModal /></FullWindowOverlay> nothing changes. However, if I wrap the content like: <BottomSheetModal><FullWindowOverlay /></BottomSheetModal> the content is placed correctly above the navigators. However, it breaks the animation etc of the sheet.

What way did you do it?

lghimfus commented 2 years ago

FullWindowOverlay

If I wrap it like this: <FullWindowOverlay><BottomSheetModal /></FullWindowOverlay> nothing changes. However, if I wrap the content like: <BottomSheetModal><FullWindowOverlay /></BottomSheetModal> the content is placed correctly above the navigators. However, it breaks the animation etc of the sheet.

What way did you do it?

`

` it might be either that you didn't add style={[StyleSheet.absoluteFill]} or that I'm not using the modal
Livijn commented 2 years ago

I did add the correct styling. For now, I guess I'll go with the non-native stack navigator.

mariomendonca commented 2 years ago

Guys, it worked for me <FullWindowOverlay style={StyleSheet.absoluteFill}><Portal><BottomSheet> {...}

nihilenz commented 2 years ago

Portal usage didn't help me, I preferred to switch to the non-modal component (BottomSheet) waiting for a better solution with BottomSheetModal

github-actions[bot] commented 2 years ago

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.

github-actions[bot] commented 2 years ago

This issue was closed because it has been stalled for 5 days with no activity.

thomasgrivet commented 2 years ago

Hello! I figured a workaround which allow the use of any BottomSheetModal inside react-navigation's native modal screen:

For it to work, you must place a PortalProvider inside your base BottomSheetModalProvider (the one which is usually located inside you App.tsx file), this will be useful for later. Then, for each modal Screen in which you want to use your BottomSheetModal, you have to place yet another BottomSheetModalProvider and place another PortalProvider inside it. You may also create a generic component which will have the same purpose but cleaner.

Example:

import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
import { PortalProvider } from '@gorhom/portal';
import { useMemo } from 'react';

import { NavigationModalScreenContainerContext } from './NavigationModalScreenContainer.context';
import {
    NavigationModalScreenContainerContextProps,
    NavigationModalScreenContainerProps
} from './NavigationModalScreenContainer.types';

export const NavigationModalScreenContainer = ({ children }: NavigationModalScreenContainerProps) => {
    const contextValue = useMemo<NavigationModalScreenContainerContextProps>(
        () => ({
            isNavigationModal: true
        }),
        []
    );

    return (
        <BottomSheetModalProvider>
            <PortalProvider>
                <NavigationModalScreenContainerContext.Provider value={contextValue}>
                    {children}
                </NavigationModalScreenContainerContext.Provider>
            </PortalProvider>
        </BottomSheetModalProvider>
    );
};

Then, wrap every one of your BottomSheetModal inside a Portal component. This will force the sheet to render inside the react-navigation's native modal and not higher in the tree. From then, your sheets should work as expected with the only downside being that the backdrop won't be able to fill the entire screen (as it is only rendered inside your native modal screen).

See:

My solution was to use a Context.Provider inside NavigationModalScreenContainer with a variable like isNavigationModal, you can use this value to edit your backdrop opacity and / or add shadow if this value is true. Once again, I advise you to create a generic component for all your BottomSheetModal which contains all the previous components and logic.

Hope this will help, have a nice day ☀️

wbercx commented 2 years ago

Adding a <PortalProvider> to the root of my app seemed to interfere with the <BottomSheetModalProvider> that was already there (which already uses PortalProvider under the hood). Only every other bottom sheet I opened would become visible.

I got mine working simply by wrapping my react-navigation modal screen's contents in a <BottomSheetModalProvider>:

function AppStack() {
    return (
        <NativeStack.Navigator>
            {/* ... */}
            <NativeStack.Group screenOptions={{ presentation: 'modal' }}>
                <NativeStack.Screen 
                    name="MyScreenThatNeedsToBeAbleToOpenABottomSheetModal" 
                    component={MyScreenThatNeedsToBeAbleToOpenABottomSheetModal} 
                />
            </NativeStack.Group>
        </NativeStack.Navigator>
    )
}

function MyScreenThatNeedsToBeAbleToOpenABottomSheetModal() {
    return (
        <BottomSheetModalProvider>
            {/* Screen content here, and one of these children can now open a BottomSheetModal */}
        </BottomSheetModalProvider>
    )
}
magrinj commented 2 years ago

I made a PR #1086 to fix this issue, hope it will be merged soon :)

nhuesmann commented 11 months ago

Adding a <PortalProvider> to the root of my app seemed to interfere with the <BottomSheetModalProvider> that was already there (which already uses PortalProvider under the hood). Only every other bottom sheet I opened would become visible.

I got mine working simply by wrapping my react-navigation modal screen's contents in a <BottomSheetModalProvider>:

function AppStack() {
    return (
        <NativeStack.Navigator>
            {/* ... */}
            <NativeStack.Group screenOptions={{ presentation: 'modal' }}>
                <NativeStack.Screen 
                    name="MyScreenThatNeedsToBeAbleToOpenABottomSheetModal" 
                    component={MyScreenThatNeedsToBeAbleToOpenABottomSheetModal} 
                />
            </NativeStack.Group>
        </NativeStack.Navigator>
    )
}

function MyScreenThatNeedsToBeAbleToOpenABottomSheetModal() {
    return (
        <BottomSheetModalProvider>
            {/* Screen content here, and one of these children can now open a BottomSheetModal */}
        </BottomSheetModalProvider>
    )
}

Not sure why this works but it does, thank you so much!

fiizzy commented 10 months ago

In my case, instead of using a react native navigation modal screen, I instead used a normal screen, but modified the animation to look like that of a modal. I know this might be the case for some of us.

In other words, instead of

 <Stack.Screen
        name="ModalScreen"
        component={ModalScreen}
        options={{presentation: 'fullScreenModal', headerShown: false}}
      />

I changed the configuration to this:

      <Stack.Screen
        name={ModalScreen}
        component={ModalScreen}
        options={{
          headerShown: false,
          animationTypeForReplace: 'push',
          animation: 'slide_from_bottom',
        }}
      />

Again, this doesn't directly solve the issue, but it is a work around I am very comfortable with, as it doesn't break any existing structure I have in place. Hope it helps ❤️

joshsmith commented 9 months ago

Adding a <PortalProvider> to the root of my app seemed to interfere with the <BottomSheetModalProvider> that was already there (which already uses PortalProvider under the hood). Only every other bottom sheet I opened would become visible.

I got mine working simply by wrapping my react-navigation modal screen's contents in a <BottomSheetModalProvider>:

function AppStack() {
    return (
        <NativeStack.Navigator>
            {/* ... */}
            <NativeStack.Group screenOptions={{ presentation: 'modal' }}>
                <NativeStack.Screen 
                    name="MyScreenThatNeedsToBeAbleToOpenABottomSheetModal" 
                    component={MyScreenThatNeedsToBeAbleToOpenABottomSheetModal} 
                />
            </NativeStack.Group>
        </NativeStack.Navigator>
    )
}

function MyScreenThatNeedsToBeAbleToOpenABottomSheetModal() {
    return (
        <BottomSheetModalProvider>
            {/* Screen content here, and one of these children can now open a BottomSheetModal */}
        </BottomSheetModalProvider>
    )
}

Using this approach in the context of a presentation: 'modal' for me resulted in the backdrop being contained within the screen of that modal (as you might expect). Not a reliable solution if you expect the backdrop to cover the entire screen.

joshsmith commented 9 months ago

Ultimately I did something like:

import { FullWindowOverlay } from 'react-native-screens'

...

const renderContainerComponent = useCallback(
  ({ children }) => <FullWindowOverlay>{children}</FullWindowOverlay>,
  [],
)

...

<BottomSheetModal containerComponent={renderContainerComponent}>

...
giaset commented 8 months ago

Adding containerComponent={({ children }) => <FullWindowOverlay>{children}</FullWindowOverlay>} to my BottomSheetModal weirdly causes the modal to flicker on open

https://github.com/gorhom/react-native-bottom-sheet/assets/2816603/cff55a50-a172-48a8-837d-3a806d9d9a20

giaset commented 8 months ago

Fixed by wrapping it in useCallback and only setting it on iOS

const containerComponent = useCallback((props: any) => <FullWindowOverlay>{props.children}</FullWindowOverlay>, []);

return <BottomSheetModal containerComponent={IS_IOS ? containerComponent : undefined}>...</BottomSheetModal>
danbeo95 commented 6 months ago

Ultimately I did something like:

import { FullWindowOverlay } from 'react-native-screens'

...

const renderContainerComponent = useCallback(
  ({ children }) => <FullWindowOverlay>{children}</FullWindowOverlay>,
  [],
)

...

<BottomSheetModal containerComponent={renderContainerComponent}>

...

It works for me Why it works with FullWindowOverlay?

levibuzolic commented 6 months ago

Why it works with FullWindowOverlay?

Because React Navigation's native stack renders modals in a new native view above the existing app, so anything rendered in the original root view will appear behind the modal.

Wrapping with a FullWindowOverlay will add another native overlay above the modal, so now there's 3 native root views:

1) Your primary app 2) React navigation's modal overlay 3) BottomSheetModal, wrapped wrapped in a FullWindowOverlay

danbeo95 commented 6 months ago

Why it works with FullWindowOverlay?

Because React Navigation's native stack renders modals in a new native view above the existing app, so anything rendered in the original root view will appear behind the modal.

Wrapping with a FullWindowOverlay will add another native overlay above the modal, so now there's 3 native root views:

  1. Your primary app
  2. React navigation's modal overlay
  3. BottomSheetModal, wrapped wrapped in a FullWindowOverlay

It dose make sense Thank for your reply