1import React from 'react'; 2import { 3 Dimensions, 4 EventSubscription, 5 StyleSheet, 6 TouchableWithoutFeedback, 7 View, 8} from 'react-native'; 9import Animated from 'react-native-reanimated'; 10import BottomSheet from 'reanimated-bottom-sheet'; 11 12import DevMenuBottomSheetContext from './DevMenuBottomSheetContext'; 13import * as DevMenu from './DevMenuModule'; 14 15type Props = { 16 uuid: string; 17 children?: React.ReactNode; 18}; 19 20class DevMenuBottomSheet extends React.PureComponent<Props, any> { 21 // We need to track whether the bottom sheet is expanded to prevent 22 // collapsing on some unnecessary `onCloseEnd` calls. 23 hasExpandingFinished: boolean = false; 24 25 ref = React.createRef<BottomSheet>(); 26 27 snapPoints = [0, Math.max(BottomSheet.renumber('50%'), 600), '90%']; 28 29 callbackNode = new Animated.Value(0); 30 31 backgroundOpacity = this.callbackNode.interpolate({ 32 inputRange: [0, 1], 33 outputRange: [0.5, 0], 34 }); 35 36 closeSubscription: EventSubscription | null = null; 37 38 componentDidMount() { 39 this.expand(); 40 41 // Before the dev menu can be actually closed, we need to collapse its sheet view, 42 // and this listens for close requests that come from native side to start collapsing the view. 43 // The awaited return value of this listener is then send back as a response 44 // so the native module knows when it can fully close dev menu (detach its root view). 45 this.closeSubscription = DevMenu.listenForCloseRequests(() => { 46 // `collapse` returns a promise, so this `return` is important to finish the close event once the view is fully collapsed. 47 return this.collapse(); 48 }); 49 } 50 51 componentDidUpdate(prevProps: Props) { 52 // Make sure it gets expanded once we receive new identifier. 53 if (prevProps.uuid !== this.props.uuid) { 54 this.expand(); 55 } 56 } 57 58 componentWillUnmount() { 59 this.unsubscribeCloseSubscription(); 60 } 61 62 collapse = (): Promise<void> => { 63 this.hasExpandingFinished = false; 64 this.ref.current && this.ref.current.snapTo(0); 65 66 // Use setTimeout until there is a better solution to execute something once the sheet is fully collapsed. 67 return new Promise((resolve) => setTimeout(resolve, 300)); 68 }; 69 70 collapseAndClose = async () => { 71 await this.collapse(); 72 await DevMenu.closeAsync(); 73 }; 74 75 expand = () => { 76 this.ref.current && this.ref.current.snapTo(1); 77 78 setTimeout(() => { 79 this.hasExpandingFinished = true; 80 }, 300); 81 }; 82 83 unsubscribeCloseSubscription = () => { 84 if (this.closeSubscription) { 85 this.closeSubscription.remove(); 86 this.closeSubscription = null; 87 } 88 }; 89 90 onCloseEnd = () => { 91 if (this.hasExpandingFinished) { 92 this.collapseAndClose(); 93 } 94 }; 95 96 providedContext = { 97 expand: this.expand, 98 collapse: this.collapse, 99 }; 100 101 renderHeader = () => { 102 return ( 103 <View style={styles.bottomSheetHeader}> 104 <View style={styles.bottomSheetHeaderBar} /> 105 </View> 106 ); 107 }; 108 109 renderContent = () => { 110 return <View style={styles.bottomSheetContent}>{this.props.children}</View>; 111 }; 112 113 render() { 114 return ( 115 <DevMenuBottomSheetContext.Provider value={this.providedContext}> 116 <View style={styles.bottomSheetContainer}> 117 <TouchableWithoutFeedback onPress={this.collapseAndClose}> 118 <Animated.View 119 style={[styles.bottomSheetBackground, { opacity: this.backgroundOpacity }]} 120 /> 121 </TouchableWithoutFeedback> 122 <BottomSheet 123 ref={this.ref} 124 initialSnap={0} 125 enabledInnerScrolling={false} 126 overdragResistanceFactor={1.5} 127 snapPoints={this.snapPoints} 128 callbackNode={this.callbackNode} 129 renderHeader={this.renderHeader} 130 renderContent={this.renderContent} 131 onCloseEnd={this.onCloseEnd} 132 /> 133 </View> 134 </DevMenuBottomSheetContext.Provider> 135 ); 136 } 137} 138 139const styles = StyleSheet.create({ 140 bottomSheetContainer: { 141 flex: 1, 142 }, 143 bottomSheetBackground: { 144 flex: 1, 145 backgroundColor: '#000', 146 }, 147 bottomSheetHeader: { 148 alignItems: 'center', 149 paddingTop: 10, 150 paddingBottom: 6, 151 }, 152 bottomSheetHeaderBar: { 153 flex: 1, 154 width: 40, 155 height: 5, 156 backgroundColor: '#fff', 157 borderRadius: 40, 158 opacity: 0.8, 159 }, 160 bottomSheetContent: { 161 height: Dimensions.get('window').height, 162 }, 163}); 164 165export default DevMenuBottomSheet; 166