xref: /expo/home/menu/DevMenuBottomSheet.tsx (revision dfd15ebd)
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