Skip to content

Commit

Permalink
feat: add backdrop handling (#124)
Browse files Browse the repository at this point in the history
* chore: added BottomSheetBackdrop

* chore: updated BottomSheet implementation to handle backdrops

* chore: updated examples
  • Loading branch information
gorhom authored Dec 20, 2020
1 parent 3454fdd commit 9650a48
Show file tree
Hide file tree
Showing 16 changed files with 281 additions and 43 deletions.
4 changes: 2 additions & 2 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ function App() {
getComponent={() => require('./screens/CustomHandleExample').default}
/>
<Stack.Screen
name="ShadowOverlayExample"
getComponent={() => require('./screens/ShadowOverlayExample').default}
name="BackdropExample"
getComponent={() => require('./screens/BackdropExample').default}
/>
<Stack.Screen
name="MapExample"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,17 @@
import React, { useCallback, useMemo, useRef } from 'react';
import { View, StyleSheet, Text } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
interpolate,
Extrapolate,
} from 'react-native-reanimated';
import BottomSheet from '@gorhom/bottom-sheet';
import BottomSheet, { BottomSheetBackdrop } from '@gorhom/bottom-sheet';
import Button from '../components/button';
import ContactList from '../components/contactList';

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

// variables
const snapPoints = useMemo(() => ['25%', '50%', '90%'], []);
const animatedPositionIndex = useSharedValue<number>(0);

// styles

const shadowOverlayAnimatedStyle = useAnimatedStyle(
() => ({
opacity: interpolate(
animatedPositionIndex.value,
[0, 2],
[0, 1],
Extrapolate.CLAMP
),
}),
[]
);

// callbacks
const handleSheetChanges = useCallback((index: number) => {
console.log('handleSheetChanges', index);
}, []);
const handleSnapPress = useCallback(index => {
bottomSheetRef.current?.snapTo(index);
}, []);
Expand All @@ -53,7 +29,7 @@ const ShadowOverlayExample = () => {
const renderHeader = useCallback(() => {
return (
<View style={styles.headerContainer}>
<Text style={styles.title}>Shadow Overlay Example</Text>
<Text style={styles.title}>Backdrop Example</Text>
</View>
);
}, []);
Expand Down Expand Up @@ -90,16 +66,11 @@ const ShadowOverlayExample = () => {
style={styles.buttonContainer}
onPress={() => handleClosePress()}
/>
<Animated.View
pointerEvents="none"
style={[styles.shadowOverlay, shadowOverlayAnimatedStyle]}
/>
<BottomSheet
ref={bottomSheetRef}
index={1}
snapPoints={snapPoints}
animatedIndex={animatedPositionIndex}
onChange={handleSheetChanges}
backdropComponent={BottomSheetBackdrop}
>
<ContactList type="View" count={3} header={renderHeader} />
</BottomSheet>
Expand All @@ -117,7 +88,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 24,
backgroundColor: 'white',
},
shadowOverlay: {
shadowBackdrop: {
position: 'absolute',
top: 0,
left: 0,
Expand All @@ -139,4 +110,4 @@ const styles = StyleSheet.create({
},
});

export default ShadowOverlayExample;
export default BackdropExample;
4 changes: 2 additions & 2 deletions example/src/screens/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ const data = [
slug: 'CustomHandleExample',
},
{
name: 'Shadow Overlay',
slug: 'ShadowOverlayExample',
name: 'Backdrop',
slug: 'BackdropExample',
},
{
name: 'Map',
Expand Down
2 changes: 1 addition & 1 deletion example/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export type AppStackParamsList = {
ScrollViewExample: undefined;
ViewExample: undefined;
CustomHandleExample: undefined;
ShadowOverlayExample: undefined;
BackdropExample: undefined;
MapExample: undefined;
DynamicSnapPointExample: undefined;
};
8 changes: 8 additions & 0 deletions src/components/bottomSheet/BottomSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
BottomSheetProvider,
} from '../../contexts';
import BottomSheetContainer from '../bottomSheetContainer';
import BottomSheetBackdropContainer from '../bottomSheetBackdropContainer';
import BottomSheetHandleContainer from '../bottomSheetHandleContainer';
import BottomSheetBackgroundContainer from '../bottomSheetBackgroundContainer';
import BottomSheetContentWrapper from '../bottomSheetContentWrapper';
Expand Down Expand Up @@ -89,6 +90,7 @@ const BottomSheetComponent = forwardRef<BottomSheet, BottomSheetProps>(
onChange: _providedOnChange,
// components
handleComponent,
backdropComponent,
backgroundComponent,
children,
} = props;
Expand Down Expand Up @@ -481,6 +483,12 @@ const BottomSheetComponent = forwardRef<BottomSheet, BottomSheetProps>(
console.log('BottomSheet', 'render', snapPoints, isLayoutCalculated);
return (
<BottomSheetProvider value={externalContextVariables}>
<BottomSheetBackdropContainer
key="BottomSheetBackdropContainer"
animatedIndex={animatedIndex}
animatedPosition={animatedPosition}
backdropComponent={backdropComponent}
/>
<BottomSheetContainer
key="BottomSheetContainer"
shouldMeasureHeight={shouldMeasureContainerHeight}
Expand Down
15 changes: 12 additions & 3 deletions src/components/bottomSheet/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type React from 'react';
import type Animated from 'react-native-reanimated';
import type { BottomSheetHandleProps } from '../bottomSheetHandle';
import type { ViewProps } from 'react-native';
import type { BottomSheetBackdropProps } from '../bottomSheetBackdrop';
import type { BottomSheetBackgroundProps } from '../bottomSheetBackground';

export interface BottomSheetProps extends BottomSheetAnimationConfigs {
// configuration
Expand Down Expand Up @@ -88,11 +89,19 @@ export interface BottomSheetProps extends BottomSheetAnimationConfigs {
* @type React.FC\<BottomSheetHandleProps\>
*/
handleComponent?: React.FC<BottomSheetHandleProps> | null;
/**
* Component to be placed as a sheet backdrop.
* @see {BottomSheetBackdropProps}
* @type React.FC\<BottomSheetBackdropProps\>
* @default null
*/
backdropComponent?: React.FC<BottomSheetBackdropProps> | null;
/**
* Component to be placed as a background.
* @type React.FC\<ViewProps\>
* @see {BottomSheetBackgroundProps}
* @type React.FC\<BottomSheetBackgroundProps\>
*/
backgroundComponent?: React.FC<ViewProps> | null;
backgroundComponent?: React.FC<BottomSheetBackgroundProps> | null;
/**
* A scrollable node or normal view.
* @type React.ReactNode[] | React.ReactNode
Expand Down
126 changes: 126 additions & 0 deletions src/components/bottomSheetBackdrop/BottomSheetBackdrop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import React, { memo, useCallback, useMemo, useRef } from 'react';
import { TouchableWithoutFeedback } from 'react-native';
import Animated, {
interpolate,
Extrapolate,
useAnimatedStyle,
useAnimatedProps,
useSharedValue,
useAnimatedReaction,
} from 'react-native-reanimated';
import isEqual from 'lodash.isequal';
import { useBottomSheet } from '../../hooks/useBottomSheet';
import {
DEFAULT_OPACITY,
DEFAULT_APPEARS_ON_INDEX,
DEFAULT_DISAPPEARS_ON_INDEX,
DEFAULT_ENABLE_TOUCH_THROUGH,
DEFAULT_CLOSE_ON_PRESS,
} from './constants';
import { WINDOW_HEIGHT } from '../../constants';
import type { BottomSheetDefaultBackdropProps } from './types';
import { styles } from './styles';

const AnimatedTouchableWithoutFeedback = Animated.createAnimatedComponent(
TouchableWithoutFeedback
);

const BottomSheetBackdropComponent = ({
animatedIndex,
opacity = DEFAULT_OPACITY,
appearsOnIndex = DEFAULT_APPEARS_ON_INDEX,
disappearsOnIndex = DEFAULT_DISAPPEARS_ON_INDEX,
enableTouchThrough = DEFAULT_ENABLE_TOUCH_THROUGH,
closeOnPress = DEFAULT_CLOSE_ON_PRESS,
style,
}: BottomSheetDefaultBackdropProps) => {
//#region hooks
const { close } = useBottomSheet();
//#endregion

//#region variables
const containerRef = useRef<Animated.View>(null);
const pointerEvents = useMemo(() => (enableTouchThrough ? 'none' : 'auto'), [
enableTouchThrough,
]);
//#endregion

//#region callbacks
const handleOnPress = useCallback(() => {
close();
}, [close]);
//#endregion

//#region animated props
const isContainerTouchable = useSharedValue<boolean>(closeOnPress, true);
const containerAnimatedProps = useAnimatedProps(() => ({
pointerEvents: isContainerTouchable.value ? 'auto' : 'none',
}));
//#endregion

//#region styles
const buttonAnimatedStyle = useAnimatedStyle(
() => ({
top: animatedIndex.value === disappearsOnIndex ? WINDOW_HEIGHT : 0,
}),
[disappearsOnIndex]
);
const buttonStyle = useMemo(() => [style, buttonAnimatedStyle], [
style,
buttonAnimatedStyle,
]);
const containerAnimatedStyle = useAnimatedStyle(
() => ({
opacity: interpolate(
animatedIndex.value,
[disappearsOnIndex, appearsOnIndex],
[0, opacity],
Extrapolate.CLAMP
),
}),
[]
);
const containerStyle = useMemo(
() => [style, styles.container, containerAnimatedStyle],
[style, containerAnimatedStyle]
);
//#endregion

//#region effects
useAnimatedReaction(
() => animatedIndex.value === disappearsOnIndex,
shouldDisableTouchability => {
if (shouldDisableTouchability) {
isContainerTouchable.value = false;
} else {
isContainerTouchable.value = true;
}
},
[disappearsOnIndex]
);
//#endregion

return closeOnPress ? (
<AnimatedTouchableWithoutFeedback
accessible={true}
accessibilityRole="button"
accessibilityLabel="Bottom Sheet backdrop"
accessibilityHint="Tap to close the Bottom Sheet"
onPress={handleOnPress}
style={buttonStyle}
>
<Animated.View
ref={containerRef}
style={containerStyle}
// @ts-ignore
animatedProps={containerAnimatedProps}
/>
</AnimatedTouchableWithoutFeedback>
) : (
<Animated.View pointerEvents={pointerEvents} style={containerStyle} />
);
};

const BottomSheetBackdrop = memo(BottomSheetBackdropComponent, isEqual);

export default BottomSheetBackdrop;
13 changes: 13 additions & 0 deletions src/components/bottomSheetBackdrop/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const DEFAULT_OPACITY = 0.5;
const DEFAULT_APPEARS_ON_INDEX = 1;
const DEFAULT_DISAPPEARS_ON_INDEX = 0;
const DEFAULT_ENABLE_TOUCH_THROUGH = false;
const DEFAULT_CLOSE_ON_PRESS = true;

export {
DEFAULT_OPACITY,
DEFAULT_APPEARS_ON_INDEX,
DEFAULT_DISAPPEARS_ON_INDEX,
DEFAULT_ENABLE_TOUCH_THROUGH,
DEFAULT_CLOSE_ON_PRESS,
};
2 changes: 2 additions & 0 deletions src/components/bottomSheetBackdrop/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './BottomSheetBackdrop';
export type { BottomSheetBackdropProps } from './types';
7 changes: 7 additions & 0 deletions src/components/bottomSheetBackdrop/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { StyleSheet } from 'react-native';

export const styles = StyleSheet.create({
container: {
backgroundColor: 'black',
},
});
49 changes: 49 additions & 0 deletions src/components/bottomSheetBackdrop/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { ViewProps } from 'react-native';
import type Animated from 'react-native-reanimated';

export interface BottomSheetBackdropProps extends Pick<ViewProps, 'style'> {
/**
* Current sheet position index.
* @type Animated.SharedValue<number>
*/
animatedIndex: Animated.SharedValue<number>;
/**
* Current sheet position.
* @type Animated.SharedValue<number>
*/
animatedPosition: Animated.SharedValue<number>;
}

export interface BottomSheetDefaultBackdropProps
extends BottomSheetBackdropProps {
/**
* Backdrop opacity.
* @type number
* @default 0.5
*/
opacity?: number;
/**
* Snap point index when backdrop will appears on.
* @type number
* @default 1
*/
appearsOnIndex?: number;
/**
* Snap point index when backdrop will disappears on.
* @type number
* @default 0
*/
disappearsOnIndex?: number;
/**
* Enable touch through backdrop component.
* @type boolean
* @default false
*/
enableTouchThrough?: boolean;
/**
* Close sheet when user press on backdrop.
* @type boolean
* @default true
*/
closeOnPress?: boolean;
}
Loading

0 comments on commit 9650a48

Please sign in to comment.