Created
November 16, 2023 07:13
-
-
Save mizanxali/df7bc82a1dadf3723c15603cd385d53b to your computer and use it in GitHub Desktop.
React Native 101: The only bottom sheet guide you'll ever need
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from "react"; | |
import { BottomSheetRef } from "../BottomSheet"; | |
export type BottomSheetType = BottomSheetRef; | |
const BottomSheetContext = React.createContext({} as BottomSheetType); | |
export default BottomSheetContext; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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