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

[v4] | [v2] [iOS] Backdrop flashes when content height is dynamic in BottomSheetModal #1763

Open
badalsaibo opened this issue Mar 4, 2024 · 9 comments
Labels
bug Something isn't working

Comments

@badalsaibo
Copy link

badalsaibo commented Mar 4, 2024

Bug

  • For a BottomSheetModal with dynamic content height, the backdrop flashes when the content height is being changed.
  • The change in content height could be done in any way. For my usecase, an API call with a loader was set. During the API call. The BottomSheetModal renders a list of skeletons which changes the backdrop height and causes the backdrop to flicker

Environment info

Library Version
@gorhom/bottom-sheet 4.6.1
react-native 0.72.5
react-native-reanimated 3.5.4
react-native-gesture-handler 2.13.4

Steps To Reproduce

import { BottomSheetModal } from '@gorhom/bottom-sheet';
import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types';
import { ForwardedRef, forwardRef, useMemo } from 'react';
import { useBottomSheetDynamicSnapPoints } from './hooks/useBottomSheetDynamicSnapPoints';
import CustomBackdrop from './custom-backdrop.component';
import { View } from 'react-native';
import { useBottomSheetBackHandler } from './hooks/useBottomSheetBackHandler';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

const GenericBottomSheetModal = (
  props: {
    children: React.ReactNode;
    enableDismissOnClose?: boolean;
    backdropComponent?: (props: any) => React.JSX.Element;
  },
  ref: ForwardedRef<BottomSheetModalMethods>,
) => {
  const { handleSheetPositionChange } = useBottomSheetBackHandler(ref);

  const insets = useSafeAreaInsets();

  const { children, ...rest } = props;
  const initialSnapPoints = useMemo(() => ['CONTENT_HEIGHT'], []);
  const {
    animatedHandleHeight,
    animatedSnapPoints,
    animatedContentHeight,
    handleContentLayout,
  } = useBottomSheetDynamicSnapPoints(initialSnapPoints);

  return (
    <BottomSheetModal
      ref={ref}
      snapPoints={animatedSnapPoints as any}
      handleHeight={animatedHandleHeight}
      contentHeight={animatedContentHeight}
      handleIndicatorStyle={{ display: 'none' }}
      backdropComponent={CustomBackdrop}
      onChange={handleSheetPositionChange}
      {...rest}>
      <View
        className="flex-1"
        onLayout={handleContentLayout}
        style={{ paddingBottom: insets.bottom }}>
        {children}
      </View>
    </BottomSheetModal>
  );
};

export default forwardRef(GenericBottomSheetModal);
// useBottomSheetDynamicSnapPoints

import { useCallback } from 'react';
import { useDerivedValue, useSharedValue } from 'react-native-reanimated';

const INITIAL_HANDLE_HEIGHT = -999;
const INITIAL_SNAP_POINT = -999;

/**
 * Provides dynamic content height calculating functionalities, by
 * replacing the placeholder `CONTENT_HEIGHT` with calculated layout.
 * @example
 * [0, 'CONTENT_HEIGHT', '100%']
 * @param initialSnapPoints your snap point with content height placeholder.
 * @returns {
 *  - animatedSnapPoints: an animated snap points to be set on `BottomSheet` or `BottomSheetModal`.
 *  - animatedHandleHeight: an animated handle height callback node to be set on `BottomSheet` or `BottomSheetModal`.
 *  - animatedContentHeight: an animated content height callback node to be set on `BottomSheet` or `BottomSheetModal`.
 *  - handleContentLayout: a `onLayout` callback method to be set on `BottomSheetView` component.
 * }
 */
export const useBottomSheetDynamicSnapPoints = (
  initialSnapPoints: Array<string | number>,
) => {
  // variables
  const animatedContentHeight = useSharedValue(0);
  const animatedHandleHeight = useSharedValue(INITIAL_HANDLE_HEIGHT);
  const animatedSnapPoints = useDerivedValue(() => {
    if (
      animatedHandleHeight.value === INITIAL_HANDLE_HEIGHT ||
      animatedContentHeight.value === 0
    ) {
      return initialSnapPoints.map(() => INITIAL_SNAP_POINT);
    }
    const contentWithHandleHeight =
      animatedContentHeight.value + animatedHandleHeight.value;

    return initialSnapPoints.map((snapPoint) =>
      snapPoint === 'CONTENT_HEIGHT' ? contentWithHandleHeight : snapPoint,
    );
  }, []);

  type HandleContentLayoutProps = {
    nativeEvent: {
      layout: { height: number };
    };
  };
  // callbacks
  const handleContentLayout = useCallback(
    ({
      nativeEvent: {
        layout: { height },
      },
    }: HandleContentLayoutProps) => {
      animatedContentHeight.value = height;
    },
    [animatedContentHeight],
  );

  return {
    animatedSnapPoints,
    animatedHandleHeight,
    animatedContentHeight,
    handleContentLayout,
  };
};

Describe what you expected to happen:

  1. The backdrop shouldn't flicker.
  2. The dynamic height shouldn't change.

Reproducible sample code

Videos / Images

Simulator.Screen.Recording.-.iPhone.SE.3rd.generation.-.2024-03-04.at.13.52.53.mp4
@badalsaibo badalsaibo added the bug Something isn't working label Mar 4, 2024
@VictorioMolina
Copy link

+1

@Acetyld
Copy link

Acetyld commented Mar 11, 2024

Exactly the same issue here, @badalsaibo did you found a solution to this?

Copy link

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.

@severinferard
Copy link

Having the same issue here

@lexi-stein
Copy link

also having the same issue!

@Karthik-B-06
Copy link

I am also having the same issue, has anyone found a solution?

@stichingsd-vitrion
Copy link

Also having same issue here

@badalsaibo
Copy link
Author

badalsaibo commented May 15, 2024

@Acetyld I think for a fix I gave it a fixed height for iOS only.

  const initialSnapPoints = useMemo(() => ['50%'], []);

I created a non adaptive component for iOS separately too

{
  Platform.OS === "ios" ? (
    <SearchTutorBottomSheetNonAdaptive
      bottomSheetModalRef={tutorSearchBottomSheetRef}
      handleClose={handleClose}
    />
  ) : (
    <SearchTutorBottomSheet
      bottomSheetModalRef={tutorSearchBottomSheetRef}
      handleClose={handleClose}
    />
  );
}

@freddy-eturi
Copy link

+1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

8 participants