Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic Bottom Sheet height based on Children height #32

Closed
SatyaFariz opened this issue Sep 21, 2020 · 43 comments · Fixed by #81 or #1513
Closed

Dynamic Bottom Sheet height based on Children height #32

SatyaFariz opened this issue Sep 21, 2020 · 43 comments · Fixed by #81 or #1513
Assignees
Labels
enhancement New feature or request v2 Written in Reanimated v1

Comments

@SatyaFariz
Copy link

Feature Request

It would be awesome if you add dynamic height feature based on the children height (and still able to define max height of the bottom sheet). Also the snapPoints prop should be relative to the total Bottom Sheet height, not the screen height.

Why it is needed

Sometimes we only need to render only a few items inside the bottom sheet. So there's no need to define the bottom sheet height. It should adjust itself according to the children height. And the snapPoints should be relative to the height of the bottom sheet, not the screen height like in the example.

@SatyaFariz SatyaFariz added the enhancement New feature or request label Sep 21, 2020
@gorhom
Copy link
Owner

gorhom commented Sep 24, 2020

hi @SatyaFariz , thanks for submitting this feature request, i will pick it up as soon as i get free 👍

@sa8ab
Copy link
Contributor

sa8ab commented Oct 11, 2020

This is a good one.

@EricPKerr
Copy link

Another similar enhancement would be to allow the snapPoints to change (with something like an onLayout callback) and then have the sheet handle animating to the new position

@mrmarktyy
Copy link

I have sort of a singleton Bottom Sheet component and snapPoints prop is a local state.
Depends on the use cases, I'd update snapPoints before expand the bottom sheet, But i found it is somehow problematic ?

So I am wondering whether dynamic snapPoints is supported ?

@fabcall
Copy link

fabcall commented Nov 2, 2020

@mrmarktyy I've managed to get it working by setting a key prop to BottomSheet component. So, when it's children view is indeed calculated, it invalidates the layout:

<BottomSheet
      ref={sheetRef}
      initialSnapIndex={0}
      snapPoints={snapPoints}
      topInset={StatusBar.currentHeight}
      key={layout.measured.toString()}>
    {{ children }}
</BottomSheet>

@mrmarktyy
Copy link

Thanks @fabcall good one, i'll give it try.
I actually ended up getting it work with a hacky setTimeout after updating snapPoints, I guess it worked because timeout gives the react to recalculate the new layout, etc.

@EricPKerr
Copy link

The only issue is that it goes instantly to the new point instead of animating there. It would be nice to have the useTransition internal hook find the closest new snapPoint when those change and update the position via transition (or have that be an available prop option).

@gorhom
Copy link
Owner

gorhom commented Nov 3, 2020

my initial idea is to add a new value type auto to snapPoints, which will measure content height and uses it value as a snap point before animating.

i'll look into it this weekend :)

@fabcall
Copy link

fabcall commented Nov 3, 2020

@gorhom sounds nice to me. Keep in mind that, if content height is bigger than screen, you could replace "auto" with "100%" and keep the default behavior ;)

@gorhom
Copy link
Owner

gorhom commented Nov 4, 2020

hi guys @SatyaFariz, @EricPKerr, @fabcall, @mrmarktyy

i have submitted a draft pr for this feature and while i finish it up, i would appreciate if you could test it

yarn add ssh://git@github.com:gorhom/react-native-bottom-sheet#feature/dynamic-snap-point

Sample Code

/* eslint-disable react-native/no-inline-styles */
import React, { useMemo, useRef } from 'react';
import { View, StyleSheet } from 'react-native';
import BottomSheet from '@gorhom/bottom-sheet';

const BasicExample = () => {
  // hooks
  const bottomSheetRef = useRef<BottomSheet>(null);

  // variables
  const snapPoints = useMemo(() => [100, 200], []);

  return (
    <View style={styles.container}>
      <BottomSheet
        ref={bottomSheetRef}
        snapPoints={snapPoints}
        initialSnapIndex={0}
        shouldMeasureContentHeight={true} // < new prop 🔥
      >
        <View
          style={{
            height: 500,
            backgroundColor: 'white',
          }}
        />
      </BottomSheet>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 24,
    backgroundColor: 'grey',
  },
  contentContainer: {
    height: 500,
    backgroundColor: 'white',
  },
});

export default BasicExample;

@avet-m
Copy link

avet-m commented Nov 5, 2020

@gorhom thank you
it works perfect on static view but seems like there are some problem with scrollable components like BottomSheetScrollView, the bottom sheet doesn't reach last point, just jumping up and down

Code example


import React, { useMemo, useRef } from 'react';
import { View, StyleSheet } from 'react-native';
import BottomSheet from '@gorhom/bottom-sheet';

const BasicExample = () => {
  // hooks
  const bottomSheetRef = useRef<BottomSheet>(null);

  // variables
  const snapPoints = useMemo(() => [100, 200], []);

  return (
    <View style={styles.container}>
      <BottomSheet
        ref={bottomSheetRef}
        snapPoints={snapPoints}
        initialSnapIndex={0}
        shouldMeasureContentHeight={true} // < new prop 🔥
      >
       <BottomSheetScrollView>
          <View
            style={{
              height: 500,
              backgroundColor: 'white',
            }}
          />
        </BottomSheetScrollView>
      </BottomSheet>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 24,
    backgroundColor: 'grey',
  },
  contentContainer: {
    height: 500,
    backgroundColor: 'white',
  },
});

export default BasicExample;

@gorhom
Copy link
Owner

gorhom commented Nov 5, 2020

thanks @avet-m for spotting this issue,, i have pushed the fix already, please try again and let me know.

⚠️ you will need to reinstall the branch again:

yarn add ssh://git@github.com:gorhom/react-native-bottom-sheet#feature/dynamic-snap-point

@gorhom gorhom added the v1 Written in Reanimated v1 label Nov 5, 2020
@avet-m
Copy link

avet-m commented Nov 5, 2020

@gorhom now the scroll of BottomSheetScrollView doesn't work, and also it doesn't work with shouldMeasureContentHeight={flase}

@fabcall
Copy link

fabcall commented Nov 6, 2020

@gorhom using useBottomSheetModal also shows an erratic behavior. For instance,

presentPasswordRecovery(
    <PasswordRecovery dismissPasswordRecovery={dismissPasswordRecovery} />,
    {
      animationDuration: 300,
      animatedPosition: snapPoints.passwordRecovery,
      handleComponent: Grabber,
      snapPoints: [20],
      shouldMeasureContentHeight: true,
    },
  );

will show the component at the specified point (20). Even though you can drag it up to its measured height, it animates back to point 20.

@gorhom
Copy link
Owner

gorhom commented Nov 17, 2020

a quick update, managed to get snapPoints to be reactive, so the sheet will listen to all its updates.

here is a preview

@fabcall
Copy link

fabcall commented Nov 17, 2020

This ia simply beautiful 😍. Amazing work, @gorhom

@gorhom gorhom self-assigned this Nov 18, 2020
@gorhom
Copy link
Owner

gorhom commented Nov 18, 2020

quick update:

  • Because of the complexity of implementing this feature and other breaking changes, it will be released with the next major release v2.0 along with the reanimated v2 version.
  • Current implementation works perfectly with BottomSheetView, however it could works with Scrollable but with more extra steps, since VirtualizedList items height is unpredictable.
  • I am working now im cleaning up this PR - only for BottomSheetView - and will post it here so you guys can test it and then i'll start preparing for the release v2.0.

i can't promise a specific date,, but i will try my best to get out asap :)

@gorhom gorhom added v2 Written in Reanimated v1 and removed v1 Written in Reanimated v1 labels Nov 20, 2020
@gorhom gorhom linked a pull request Nov 22, 2020 that will close this issue
@gorhom
Copy link
Owner

gorhom commented Nov 22, 2020

hi there 👋

here is a final pr to test this feature: #81

@gorhom
Copy link
Owner

gorhom commented Nov 30, 2020

I just pushed an alpha release for v2, please try it and let me know :)

also check out the new documents website ( still in progress ) 🎉

yarn add @gorhom/bottom-sheet@2.0.0-alpha.2

@nandorojo
Copy link

Hey @gorhom is this available in v2? I don't see it on the docs. Thanks!

@gorhom
Copy link
Owner

gorhom commented Mar 28, 2021

@nandorojo yes! it is available for v2 & v3, look at the example Dynamic Snap Point Example.

@dorw123
Copy link

dorw123 commented May 24, 2021

Still not working for BottomSheetScrollView?
onContentSizeChange not fireing at all, onLayout only fires on the first render.

@ngoel37
Copy link

ngoel37 commented Jul 14, 2021

Hey there - I may be totally missing something on the example @gorhom, but is contentHeight being set when the size of the content changes? As far as I can tell, onLayout only fires on the first render, not when the content size changes.

@Isaacmeedinaa
Copy link

@gorhom Thank you very much for this fix... I would like to point out 2 issues with dynamic heights.

  1. There isn't an "out-of-the-box" fix to set a height limit. I had to do it with Dimensions.
  2. This is one is the important... Scrollables are still not working with dynamic heights. Will there be a fix for this soon? Thanks!

@VladSenko
Copy link

VladSenko commented Jul 22, 2021

@gorhom I have BottomSheet with dynamic height content inside. This content is actually a separate component that shows some data. I set this data by clicking on an element outside the bottom sheet and then open the bottom sheet. Dynamic height is calculated but BottomSheet behaves very oddly: it jumps up/down or even closes immediately after content height change.

Please help.

@anhquan291
Copy link

anhquan291 commented Jul 29, 2021

@gorhom I have BottomSheet with dynamic height content inside. This content is actually a separate component that shows some data. I set this data by clicking on an element outside the bottom sheet and then open the bottom sheet. Dynamic height is calculated but BottomSheet behaves very oddly: it jumps up/down or even closes immediately after content height change.

Please help.

I'm also facing the problem(v3). The bottom sheet goes down immediately when calculating the new height. Please help. Thanks

@gorhom
Copy link
Owner

gorhom commented Aug 8, 2021

Could you guys test on ‘v4’ ? Thanks

@ktamir
Copy link

ktamir commented Aug 12, 2021

Could you guys test on ‘v4’ ? Thanks

I had some issues with v3, tried v4 with the new hook and it seems to work well
Thank you so much for the great work on this library!

@doougui
Copy link

doougui commented Nov 17, 2021

For me scrollables with CONTENT_HEGIHT still aren't working

My code is looking like this:

interface ContentHeightBottomSheetProps {
  children: any;
  bottomSheetModalRef: React.MutableRefObject<BottomSheetModalMethods>;
  initialSnapPoints?: (string | number)[];
  allowClosingOnPanDown?: boolean;
}

export function ContentHeightBottomSheet({ 
  children, 
  bottomSheetModalRef, 
  initialSnapPoints = ['5%', 'CONTENT_HEIGHT'],
  allowClosingOnPanDown = true,
}: ContentHeightBottomSheetProps) {
  const snapPoints = React.useMemo(() => [...initialSnapPoints], []);

  const {
    animatedHandleHeight,
    animatedSnapPoints,
    animatedContentHeight,
    handleContentLayout,
  } = useBottomSheetDynamicSnapPoints(snapPoints);

  return (
    <BottomSheetModal
      ref={bottomSheetModalRef}
      index={1}
      backdropComponent={BottomSheetBackdrop}
      snapPoints={animatedSnapPoints}
      handleHeight={animatedHandleHeight}
      contentHeight={animatedContentHeight}
      enablePanDownToClose={allowClosingOnPanDown}
    >
      <BottomSheetScrollView onLayout={handleContentLayout}>
        {children}
      </BottomSheetScrollView>
    </BottomSheetModal>
  );
}

It works perfectly when the content doesn't cover 100%+ of the screen, but when it does I cannot scroll. Seems like this is the same issue I am experiencing

Note that I am using BottomSheetScrollView, but it still doesn't scroll at all

@doougui
Copy link

doougui commented Nov 23, 2021

@gorhom Any updates on this? Tbh I don't really know if I am doing something I am not supposed to or if scrollables with content height are not available yet :/

Thanks in advance :)

@vitorsilvalima
Copy link

vitorsilvalima commented Dec 15, 2021

Hey guys, I was able to make the scrollable content to work by setting the maxHeight for the BottomSheetScrollView component

import React, { useMemo, forwardRef, useCallback } from 'react'
import { Dimensions, ScrollView, StyleSheet } from 'react-native'
import RNBottomSheet, {
  useBottomSheetDynamicSnapPoints,
  BottomSheetBackdrop,
  BottomSheetBackdropProps,
  BottomSheetScrollView,
} from '@gorhom/bottom-sheet'

interface BottomSheetProps {
  children: React.ReactNode
  onClose(): void
}

const styles = StyleSheet.create({
  backgroundStyle: {
    backgroundColor: 'transparent',
  },
  handleIndicator: {
    backgroundColor: 'white',
  },
})

const { height } = Dimensions.get('window')
function CustomBottomSheet(
  { children, onClose }: BottomSheetProps,
  ref: React.Ref<RNBottomSheet>,
): React.ReactElement {
  const initialSnapPoints = useMemo(() => ['CONTENT_HEIGHT'], [])

  const {
    animatedHandleHeight,
    animatedSnapPoints,
    animatedContentHeight,
    handleContentLayout,
  } = useBottomSheetDynamicSnapPoints(initialSnapPoints)

  const renderBackdrop = useCallback(
    (props: BottomSheetBackdropProps) => (
      <BottomSheetBackdrop
        {...props}
        animatedIndex={animatedContentHeight}
        pressBehavior="close"
      />
    ),
    [],
  )

  return (
    <RNBottomSheet
      ref={ref}
      snapPoints={animatedSnapPoints}
      handleHeight={animatedHandleHeight}
      contentHeight={animatedContentHeight}
      animateOnMount={false}
      onClose={onClose}
      backdropComponent={renderBackdrop}
      backgroundStyle={styles.backgroundStyle}
      handleIndicatorStyle={styles.handleIndicator}
      enablePanDownToClose
    >
      <BottomSheetScrollView
        onLayout={handleContentLayout}
        style={{ maxHeight: height * 0.9 }}
      >
        {children}
      </BottomSheetScrollView>
    </RNBottomSheet>
  )
}

@tambadiya
Copy link

I have many screen in same BottomSheet and also I use bottomsheet on map so how can I give dynamic height of all screens.?

@taiyrbegeyev
Copy link

Hey guys, I was able to make the scrollable content to work by setting the maxHeight for the BottomSheetScrollView component

import React, { useMemo, forwardRef, useCallback } from 'react'
import { Dimensions, ScrollView, StyleSheet } from 'react-native'
import RNBottomSheet, {
  useBottomSheetDynamicSnapPoints,
  BottomSheetBackdrop,
  BottomSheetBackdropProps,
  BottomSheetScrollView,
} from '@gorhom/bottom-sheet'

interface BottomSheetProps {
  children: React.ReactNode
  onClose(): void
}

const styles = StyleSheet.create({
  backgroundStyle: {
    backgroundColor: 'transparent',
  },
  handleIndicator: {
    backgroundColor: 'white',
  },
})

const { height } = Dimensions.get('window')
function CustomBottomSheet(
  { children, onClose }: BottomSheetProps,
  ref: React.Ref<RNBottomSheet>,
): React.ReactElement {
  const initialSnapPoints = useMemo(() => ['CONTENT_HEIGHT'], [])

  const {
    animatedHandleHeight,
    animatedSnapPoints,
    animatedContentHeight,
    handleContentLayout,
  } = useBottomSheetDynamicSnapPoints(initialSnapPoints)

  const renderBackdrop = useCallback(
    (props: BottomSheetBackdropProps) => (
      <BottomSheetBackdrop
        {...props}
        animatedIndex={animatedContentHeight}
        pressBehavior="close"
      />
    ),
    [],
  )

  return (
    <RNBottomSheet
      ref={ref}
      snapPoints={animatedSnapPoints}
      handleHeight={animatedHandleHeight}
      contentHeight={animatedContentHeight}
      animateOnMount={false}
      onClose={onClose}
      backdropComponent={renderBackdrop}
      backgroundStyle={styles.backgroundStyle}
      handleIndicatorStyle={styles.handleIndicator}
      enablePanDownToClose
    >
      <BottomSheetScrollView
        onLayout={handleContentLayout}
        style={{ maxHeight: height * 0.9 }}
      >
        {children}
      </BottomSheetScrollView>
    </RNBottomSheet>
  )
}

This approach seemed to work but the bottom sheet start crashing after opening it multiple times. Is there a way to fix it?

@quicksilverr
Copy link

Hey guys, I was able to make the scrollable content to work by setting the maxHeight for the BottomSheetScrollView component

import React, { useMemo, forwardRef, useCallback } from 'react'
import { Dimensions, ScrollView, StyleSheet } from 'react-native'
import RNBottomSheet, {
  useBottomSheetDynamicSnapPoints,
  BottomSheetBackdrop,
  BottomSheetBackdropProps,
  BottomSheetScrollView,
} from '@gorhom/bottom-sheet'

interface BottomSheetProps {
  children: React.ReactNode
  onClose(): void
}

const styles = StyleSheet.create({
  backgroundStyle: {
    backgroundColor: 'transparent',
  },
  handleIndicator: {
    backgroundColor: 'white',
  },
})

const { height } = Dimensions.get('window')
function CustomBottomSheet(
  { children, onClose }: BottomSheetProps,
  ref: React.Ref<RNBottomSheet>,
): React.ReactElement {
  const initialSnapPoints = useMemo(() => ['CONTENT_HEIGHT'], [])

  const {
    animatedHandleHeight,
    animatedSnapPoints,
    animatedContentHeight,
    handleContentLayout,
  } = useBottomSheetDynamicSnapPoints(initialSnapPoints)

  const renderBackdrop = useCallback(
    (props: BottomSheetBackdropProps) => (
      <BottomSheetBackdrop
        {...props}
        animatedIndex={animatedContentHeight}
        pressBehavior="close"
      />
    ),
    [],
  )

  return (
    <RNBottomSheet
      ref={ref}
      snapPoints={animatedSnapPoints}
      handleHeight={animatedHandleHeight}
      contentHeight={animatedContentHeight}
      animateOnMount={false}
      onClose={onClose}
      backdropComponent={renderBackdrop}
      backgroundStyle={styles.backgroundStyle}
      handleIndicatorStyle={styles.handleIndicator}
      enablePanDownToClose
    >
      <BottomSheetScrollView
        onLayout={handleContentLayout}
        style={{ maxHeight: height * 0.9 }}
      >
        {children}
      </BottomSheetScrollView>
    </RNBottomSheet>
  )
}

Hey @vitorsilvalima Please could you explain, why does this work? making this maxHeight * 0.9 does what?

@bryanltobing
Copy link

bryanltobing commented Mar 28, 2023

is this feature available already? i don't see any in the docs.

I have tried this https://gorhom.github.io/react-native-bottom-sheet/hooks/#usebottomsheetdynamicsnappoints

but it doesn't work with BottomSheetScrollView

also, i use this example

and it doesn't work well with footerComponent prop

@norbusonam
Copy link

I am confused on this as well, CONTENT_HEIGHT is being rejected for me

@ororsatti
Copy link

Seems like scrollable + dynamically sized container is a big request around here.
Help me push it to @gorhom !
Here is the discussion I started about implementing it, TL;DR - its working!
#1363

@ororsatti
Copy link

Just sharing here that there is a PR ready for this exact feature.

#1402

@Eli-Nathan
Copy link

The solution I opted for with a dynamic snap point bottom sheet with max height is:

// ....
const initialSnapPoints = useMemo(() => ["CONTENT_HEIGHT"], [])
const {
    animatedHandleHeight,
    animatedSnapPoints,
    animatedContentHeight,
    handleContentLayout,
} = useBottomSheetDynamicSnapPoints(initialSnapPoints)

const maxHeightStyle = useBottomSheetMaxHeight(maxHeight)

return (
    <BottomSheetModal
        // .... other props
        snapPoints={animatedSnapPoints as any}
        handleHeight={animatedHandleHeight}
        contentHeight={animatedContentHeight}
    >
        <BottomSheetScrollView
            onLayout={handleContentLayout}
            style={{
                maxHeight: maxHeightStyle,
            }}
        >
            {children}
        </BottomSheetScrollView>
    </BottomSheetModal>
)
// ...

With the custom useBottomSheetMaxHeight hook looking like this:

/*
    Designed to take the specified max height, take away top safe
    area inset and convert to a decimal
*/
import { useWindowDimensions } from "react-native"
import { useSafeAreaInsets } from "react-native-safe-area-context"

export const useBottomSheetMaxHeight = (maxHeight = "85%"): number => {
    const { height: deviceHeight } = useWindowDimensions()
    const maxHeightDecimal = parseFloat(`${maxHeight}`) / 100.0
    const { top: topSafeAreaInset } = useSafeAreaInsets()

    return (deviceHeight - topSafeAreaInset) * maxHeightDecimal
}

Hope that helps someone 😄

@puniker
Copy link

puniker commented Sep 12, 2023

The solution I opted for with a dynamic snap point bottom sheet with max height is:

// ....
const initialSnapPoints = useMemo(() => ["CONTENT_HEIGHT"], [])
const {
    animatedHandleHeight,
    animatedSnapPoints,
    animatedContentHeight,
    handleContentLayout,
} = useBottomSheetDynamicSnapPoints(initialSnapPoints)

const maxHeightStyle = useBottomSheetMaxHeight(maxHeight)

return (
    <BottomSheetModal
        // .... other props
        snapPoints={animatedSnapPoints as any}
        handleHeight={animatedHandleHeight}
        contentHeight={animatedContentHeight}
    >
        <BottomSheetScrollView
            onLayout={handleContentLayout}
            style={{
                maxHeight: maxHeightStyle,
            }}
        >
            {children}
        </BottomSheetScrollView>
    </BottomSheetModal>
)
// ...

With the custom useBottomSheetMaxHeight hook looking like this:

/*
    Designed to take the specified max height, take away top safe
    area inset and convert to a decimal
*/
import { useWindowDimensions } from "react-native"
import { useSafeAreaInsets } from "react-native-safe-area-context"

export const useBottomSheetMaxHeight = (maxHeight = "85%"): number => {
    const { height: deviceHeight } = useWindowDimensions()
    const maxHeightDecimal = parseFloat(`${maxHeight}`) / 100.0
    const { top: topSafeAreaInset } = useSafeAreaInsets()

    return (deviceHeight - topSafeAreaInset) * maxHeightDecimal
}

Hope that helps someone 😄

and now that the function useBottomSheetDynamicSnapPoints() becomes deprecated? There is no documentation about it

@Eli-Nathan
Copy link

and now that the function useBottomSheetDynamicSnapPoints() becomes deprecated? There is no documentation about it

@puniker
General docs are here but it is not yet deprecated. It will likely be deprecated in v5 which is still in Alpha. When the V5 docs are released it will hopefully just not include that section at all.

When using the hooks, your IDE should show the JSDoc annotation to inform you that it will soon be deprecated.

@alitsvin
Copy link

One can try to use enableDynamicSizing prop

@mustapha-ghlissi
Copy link

Please refer to this comment:
#1513 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment