Skip to content

Instantly share code, notes, and snippets.

@mizanxali
Created November 16, 2023 07:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mizanxali/df7bc82a1dadf3723c15603cd385d53b to your computer and use it in GitHub Desktop.
Save mizanxali/df7bc82a1dadf3723c15603cd385d53b to your computer and use it in GitHub Desktop.
React Native 101: The only bottom sheet guide you'll ever need
import React, {
ReactNode,
forwardRef,
useImperativeHandle,
useState,
} from "react";
import {
Animated,
KeyboardAvoidingView,
LayoutChangeEvent,
Modal,
PanResponder,
Platform,
TouchableOpacity,
View,
ViewStyle,
} from "react-native";
export interface Props {
animationType?: "none" | "fade" | "slide";
height?: number;
duration?: number;
closeOnSwipeDown?: boolean;
closeOnPressMask?: boolean;
showHandler?: boolean;
keyboardAvoidingViewEnabled?: boolean;
children?: ReactNode;
customStyles?: {
wrapper?: ViewStyle;
container?: ViewStyle;
draggableIcon?: ViewStyle;
};
onClose?: () => void;
renderContent?: () => React.ReactElement;
}
const defaultProps: Props = {
animationType: "none",
duration: 200,
closeOnSwipeDown: true,
closeOnPressMask: true,
showHandler: true,
keyboardAvoidingViewEnabled: Platform.OS === "ios",
};
export type BottomSheetRef = {
show: (params: Props) => void;
hide: () => void;
};
const BottomSheet: React.ForwardRefRenderFunction<BottomSheetRef, Props> = (
initialProps,
ref,
) => {
const [modalVisible, setModalVisibility] = useState(false);
const [props, setProps] = useState<Props>({
...defaultProps,
...initialProps,
});
const [currentHeight, setCurrentHeight] = useState(props.height ?? 260);
const [pan] = useState(
new Animated.ValueXY({
x: 0,
y: currentHeight,
}),
);
const setModalVisible = (visible: boolean) => {
if (visible) {
setModalVisibility(true);
Animated.timing(pan, {
toValue: { x: 0, y: 0 },
duration: props.duration,
useNativeDriver: false,
}).start();
} else {
Animated.timing(pan, {
toValue: { x: 0, y: currentHeight },
duration: props.duration,
useNativeDriver: false,
}).start(() => {
setModalVisibility(false);
if (typeof props.onClose === "function") {
props.onClose();
}
});
}
};
const panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => !!props.closeOnSwipeDown,
onPanResponderMove: (e, gestureState) => {
if (gestureState.dy > 0) {
Animated.event([null, { dy: pan.y }], { useNativeDriver: false })(
e,
gestureState,
);
}
},
onPanResponderRelease: (_e, gestureState) => {
const distanceToClose = currentHeight * 0.4;
if (gestureState.dy > distanceToClose ?? gestureState.vy > 0.5) {
setModalVisible(false);
} else {
Animated.spring(pan, {
toValue: { x: 0, y: 0 },
useNativeDriver: false,
}).start();
}
},
});
const handleChildrenLayout = (event: LayoutChangeEvent) => {
setCurrentHeight(event.nativeEvent.layout.height);
};
const show = (params: Props) => {
setProps({
...defaultProps,
...initialProps,
...params,
});
setModalVisible(true);
};
const hide = () => {
setModalVisible(false);
};
useImperativeHandle(ref, () => ({
hide,
show,
}));
return (
<Modal
transparent
animationType={props.animationType ?? "none"}
visible={modalVisible}
onRequestClose={() => {
setModalVisible(false);
}}>
<KeyboardAvoidingView
enabled={props.keyboardAvoidingViewEnabled}
behavior="padding"
className="absolute top-0 left-0 w-full h-full bg-[#00000077]"
style={props.customStyles?.wrapper}>
<TouchableOpacity
className="flex-1 bg-transparent"
activeOpacity={1}
onPress={() => (props.closeOnPressMask ? hide() : {})}
/>
<View className="max-h-[80%]" onLayout={handleChildrenLayout}>
<Animated.View
className="mb-7 mx-3 rounded-3xl overflow-hidden bg-[#181A20]"
style={[
{ transform: pan.getTranslateTransform() },
(props.customStyles ?? {}).container,
]}>
{(props.closeOnSwipeDown ?? props.showHandler) && (
<View
{...panResponder.panHandlers}
className="w-full items-center bg-transparent">
<View
className="w-12 h-1 rounded-full m-1 mt-3 bg-[#5E6272]"
style={props.customStyles?.draggableIcon}
/>
</View>
)}
<View style={{ height: props.height }}>
{props.children ?? <View />}
{props.renderContent && props.renderContent()}
</View>
</Animated.View>
</View>
</KeyboardAvoidingView>
</Modal>
);
};
export default forwardRef(BottomSheet);
import React from "react";
import { BottomSheetRef } from "../BottomSheet";
export type BottomSheetType = BottomSheetRef;
const BottomSheetContext = React.createContext({} as BottomSheetType);
export default BottomSheetContext;
import React, { FC, useEffect, useRef, useState } from "react";
import BottomSheet, { BottomSheetRef, Props } from "../BottomSheet";
import BottomSheetContext from "./context";
type PropsWithChildren = Props & {
children: React.ReactNode;
};
const BottomSheetProvider: FC<PropsWithChildren> = ({ children, ...props }) => {
const bottomSheetRef = useRef<BottomSheetRef>(null);
const [refState, setRefState] = useState<BottomSheetRef>({
show: () => {},
hide: () => {},
});
useEffect(() => {
setRefState(bottomSheetRef.current as BottomSheetRef);
}, []);
return (
<BottomSheetContext.Provider value={refState}>
{children}
<BottomSheet ref={bottomSheetRef} {...props} />
</BottomSheetContext.Provider>
);
};
export default BottomSheetProvider;
import { useContext } from "react";
import BottomSheetContext, { BottomSheetType } from "./context";
const useBottomSheet = (): BottomSheetType => useContext(BottomSheetContext);
export default useBottomSheet;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment