Browse Source

Merge pull request #2932 from saghul/refactor-bottomsheet

[RN] Refactor SimpleBottomSheet
j8
Zoltan Bettenbuk 7 years ago
parent
commit
ab7e572162
No account linked to committer's email address

+ 84
- 0
react/features/base/dialog/components/BottomSheet.native.js View File

@@ -0,0 +1,84 @@
1
+// @flow
2
+
3
+import React, { Component, type Node } from 'react';
4
+import { Modal, TouchableWithoutFeedback, View } from 'react-native';
5
+
6
+import { bottomSheetStyles as styles } from './styles';
7
+
8
+/**
9
+ * The type of {@code BottomSheet}'s React {@code Component} prop types.
10
+ */
11
+type Props = {
12
+
13
+    /**
14
+     * The children to be displayed within this component.
15
+     */
16
+    children: Node,
17
+
18
+    /**
19
+     * Handler for the cancel event, which happens when the user dismisses
20
+     * the sheet.
21
+     */
22
+    onCancel: ?Function
23
+};
24
+
25
+/**
26
+ * A component emulating Android's BottomSheet. For all intents and purposes,
27
+ * this component has been designed to work and behave as a {@code Dialog}.
28
+ */
29
+export default class BottomSheet extends Component<Props> {
30
+    /**
31
+     * Initializes a new {@code BottomSheet} instance.
32
+     *
33
+     * @inheritdoc
34
+     */
35
+    constructor(props: Props) {
36
+        super(props);
37
+
38
+        this._onCancel = this._onCancel.bind(this);
39
+    }
40
+
41
+    /**
42
+     * Implements React's {@link Component#render()}.
43
+     *
44
+     * @inheritdoc
45
+     * @returns {ReactElement}
46
+     */
47
+    render() {
48
+        return [
49
+            <View
50
+                key = 'overlay'
51
+                style = { styles.overlay } />,
52
+            <Modal
53
+                animationType = { 'slide' }
54
+                key = 'modal'
55
+                onRequestClose = { this._onCancel }
56
+                transparent = { true }
57
+                visible = { true }>
58
+                <View style = { styles.container }>
59
+                    <TouchableWithoutFeedback
60
+                        onPress = { this._onCancel } >
61
+                        <View style = { styles.backdrop } />
62
+                    </TouchableWithoutFeedback>
63
+                    <View style = { styles.sheet }>
64
+                        { this.props.children }
65
+                    </View>
66
+                </View>
67
+            </Modal>
68
+        ];
69
+    }
70
+
71
+    _onCancel: () => void;
72
+
73
+    /**
74
+     * Cancels the dialog by calling the onCancel prop callback.
75
+     *
76
+     * @private
77
+     * @returns {void}
78
+     */
79
+    _onCancel() {
80
+        const { onCancel } = this.props;
81
+
82
+        onCancel && onCancel();
83
+    }
84
+}

react/features/base/dialog/components/SimpleBottomSheet.web.js → react/features/base/dialog/components/BottomSheet.web.js View File


+ 0
- 206
react/features/base/dialog/components/SimpleBottomSheet.native.js View File

@@ -1,206 +0,0 @@
1
-// @flow
2
-
3
-import React, { Component } from 'react';
4
-import {
5
-    Modal,
6
-    Text,
7
-    TouchableHighlight,
8
-    TouchableWithoutFeedback,
9
-    View
10
-} from 'react-native';
11
-import { connect } from 'react-redux';
12
-
13
-import { Icon } from '../../font-icons';
14
-
15
-import { simpleBottomSheet as styles } from './styles';
16
-
17
-/**
18
- * Underlay color for the buttons on the sheet.
19
- *
20
- * @type {string}
21
- */
22
-const BUTTON_UNDERLAY_COLOR = '#eee';
23
-
24
-type Option = {
25
-
26
-    /**
27
-     * Name of the icon which will be rendered on the right.
28
-     */
29
-    iconName: string,
30
-
31
-    /**
32
-     * True if the element is selected (will be highlighted in blue),
33
-     * false otherwise.
34
-     */
35
-    selected: boolean,
36
-
37
-    /**
38
-     * Text which will be rendered in the row.
39
-     */
40
-    text: string
41
-};
42
-
43
-
44
-/**
45
- * The type of {@code SimpleBottomSheet}'s React {@code Component} prop types.
46
- */
47
-type Props = {
48
-
49
-    /**
50
-     * Handler for the cancel event, which happens when the user dismisses
51
-     * the sheet.
52
-     */
53
-    onCancel: Function,
54
-
55
-    /**
56
-     * Handler for the event when an option has been selected in the sheet.
57
-     */
58
-    onSubmit: Function,
59
-
60
-    /**
61
-     * Array of options which will be rendered as rows.
62
-     */
63
-    options: Array<Option>
64
-};
65
-
66
-/**
67
- * A component emulating Android's BottomSheet, in a simplified form.
68
- * It supports text options with an icon, which the user can tap. The style has
69
- * been implemented following the Material Design guidelines for bottom
70
- * sheets: https://material.io/guidelines/components/bottom-sheets.html
71
- *
72
- * For all intents and purposes, this component has been designed to work and
73
- * behave as a {@code Dialog}.
74
- */
75
-class SimpleBottomSheet extends Component<Props> {
76
-    /**
77
-     * Initializes a new {@code SimpleBottomSheet} instance.
78
-     *
79
-     * @param {Object} props - The read-only React {@code Component} props with
80
-     * which the new instance is to be initialized.
81
-     */
82
-    constructor(props) {
83
-        super(props);
84
-
85
-        this._onButtonPress = this._onButtonPress.bind(this);
86
-        this._onCancel = this._onCancel.bind(this);
87
-    }
88
-
89
-    /**
90
-     * Implements React's {@link Component#render()}.
91
-     *
92
-     * @inheritdoc
93
-     * @returns {ReactElement}
94
-     */
95
-    render() {
96
-        return (
97
-            <Modal
98
-                animationType = { 'slide' }
99
-                onRequestClose = { this._onCancel }
100
-                transparent = { true }
101
-                visible = { true }>
102
-                <View style = { styles.container }>
103
-                    <TouchableWithoutFeedback
104
-                        onPress = { this._onCancel } >
105
-                        <View style = { styles.overlay } />
106
-                    </TouchableWithoutFeedback>
107
-                    <View style = { styles.sheet }>
108
-                        <View style = { styles.rowsWrapper }>
109
-                            { this._renderOptions() }
110
-                        </View>
111
-                    </View>
112
-                </View>
113
-            </Modal>
114
-        );
115
-    }
116
-
117
-    _onButtonPress: (?Object) => void;
118
-
119
-    /**
120
-     * Handle pressing of one of the options. The sheet will be hidden and the
121
-     * onSubmit prop will be called with the selected option.
122
-     *
123
-     * @param {Object} option - The option which the user selected.
124
-     * @private
125
-     * @returns {void}
126
-     */
127
-    _onButtonPress(option) {
128
-        const { onSubmit } = this.props;
129
-
130
-        onSubmit && onSubmit(option);
131
-    }
132
-
133
-    _onCancel: () => void;
134
-
135
-    /**
136
-     * Cancels the dialog by calling the onCancel prop callback.
137
-     *
138
-     * @private
139
-     * @returns {void}
140
-     */
141
-    _onCancel() {
142
-        const { onCancel } = this.props;
143
-
144
-        onCancel && onCancel();
145
-    }
146
-
147
-    /**
148
-     * Renders sheet rows based on the options prop.
149
-     *
150
-     * @private
151
-     * @returns {Array} - Array of rows to be rendered in the sheet.
152
-     */
153
-    _renderOptions() {
154
-        return this.props.options.map(
155
-            (option, index) => this._renderRow(option, index));
156
-    }
157
-
158
-    /**
159
-     * Renders a single row of the sheet.
160
-     *
161
-     * @param {Object} option - Single option which needs to be rendered.
162
-     * @param {int} index - Option index, used as a key for React.
163
-     * @private
164
-     * @returns {ReactElement} - A row element with an icon and text.
165
-     */
166
-    _renderRow(option, index) {
167
-        const { iconName, selected, text } = option;
168
-        const selectedStyle = selected ? styles.rowSelectedText : {};
169
-
170
-        return (
171
-            <TouchableHighlight
172
-                key = { index }
173
-
174
-                // TODO The following disables an eslint error alerting about a
175
-                // known potential/theoretical performance pernalty:
176
-                //
177
-                // A bind call or arrow function in a JSX prop will create a
178
-                // brand new function on every single render. This is bad for
179
-                // performance, as it will result in the garbage collector being
180
-                // invoked way more than is necessary. It may also cause
181
-                // unnecessary re-renders if a brand new function is passed as a
182
-                // prop to a component that uses reference equality check on the
183
-                // prop to determine if it should update.
184
-                //
185
-                // I'm not addressing the potential/theoretical performance
186
-                // penalty at the time of this writing because it doesn't seem
187
-                // to me that it's a practical performance penalty in the case.
188
-                //
189
-                // eslint-disable-next-line react/jsx-no-bind
190
-                onPress = { this._onButtonPress.bind(this, option) }
191
-                underlayColor = { BUTTON_UNDERLAY_COLOR } >
192
-                <View style = { styles.row } >
193
-                    <Icon
194
-                        name = { iconName }
195
-                        style = { [ styles.rowIcon, selectedStyle ] } />
196
-                    <View style = { styles.rowPadding } />
197
-                    <Text style = { [ styles.rowText, selectedStyle ] } >
198
-                        { text }
199
-                    </Text>
200
-                </View>
201
-            </TouchableHighlight>
202
-        );
203
-    }
204
-}
205
-
206
-export default connect()(SimpleBottomSheet);

+ 1
- 1
react/features/base/dialog/components/index.js View File

@@ -1,4 +1,4 @@
1
+export { default as BottomSheet } from './BottomSheet';
1 2
 export { default as DialogContainer } from './DialogContainer';
2 3
 export { default as Dialog } from './Dialog';
3
-export { default as SimpleBottomSheet } from './SimpleBottomSheet';
4 4
 export { default as StatelessDialog } from './StatelessDialog';

+ 22
- 51
react/features/base/dialog/components/styles.js View File

@@ -21,24 +21,17 @@ export const dialog = createStyleSheet({
21 21
 });
22 22
 
23 23
 /**
24
- * The React {@code Component} styles of {@code SimpleBottomSheet}. These have
24
+ * The React {@code Component} styles of {@code BottomSheet}. These have
25 25
  * been implemented as per the Material Design guidelines:
26 26
  * {@link https://material.io/guidelines/components/bottom-sheets.html}.
27 27
  */
28
-export const simpleBottomSheet = createStyleSheet({
28
+export const bottomSheetStyles = createStyleSheet({
29 29
     /**
30
-     * Style for the container of the sheet.
31
-     */
32
-    container: {
33
-        flex: 1,
34
-        flexDirection: 'row'
35
-    },
36
-
37
-    /**
38
-     * Style for a backdrop overlay covering the screen while the
30
+     * Style for a backdrop which dims the view in the background. This view
31
+     * will also be clickable. The backgroundColor is applied to the overlay
32
+     * view instead, so the modal animation doesn't affect the backdrop.
39 33
      */
40
-    overlay: {
41
-        backgroundColor: 'rgba(0, 0, 0, 0.8)',
34
+    backdrop: {
42 35
         bottom: 0,
43 36
         left: 0,
44 37
         position: 'absolute',
@@ -47,56 +40,34 @@ export const simpleBottomSheet = createStyleSheet({
47 40
     },
48 41
 
49 42
     /**
50
-     * Base style for each row.
43
+     * Style for the container of the sheet.
51 44
      */
52
-    row: {
53
-        alignItems: 'center',
45
+    container: {
46
+        alignItems: 'flex-end',
47
+        flex: 1,
54 48
         flexDirection: 'row',
55
-        height: 48
49
+        justifyContent: 'center'
56 50
     },
57 51
 
58 52
     /**
59
-     * Style for the {@code Icon} element in a row.
53
+     * Style for an overlay on top of which the sheet will be displayed.
60 54
      */
61
-    rowIcon: {
62
-        fontSize: 24
63
-    },
64
-
65
-    /**
66
-     * Helper for adding some padding between the icon and text in a row.
67
-     */
68
-    rowPadding: {
69
-        width: 32
70
-    },
71
-
72
-    /**
73
-     * Style for a row which is marked as selected.
74
-     */
75
-    rowSelectedText: {
76
-        color: ColorPalette.blue
77
-    },
78
-
79
-    /**
80
-     * Style for the {@code Text} element in a row.
81
-     */
82
-    rowText: {
83
-        fontSize: 16
84
-    },
85
-
86
-    /**
87
-     * Wrapper for all rows, it adds a margin to the sheet container.
88
-     */
89
-    rowsWrapper: {
90
-        marginHorizontal: 16,
91
-        marginVertical: 8
55
+    overlay: {
56
+        backgroundColor: 'rgba(0, 0, 0, 0.8)',
57
+        bottom: 0,
58
+        left: 0,
59
+        position: 'absolute',
60
+        right: 0,
61
+        top: 0
92 62
     },
93 63
 
94 64
     /**
95 65
      * Bottom sheet's base style.
96 66
      */
97 67
     sheet: {
98
-        alignSelf: 'flex-end',
68
+        flex: 1,
99 69
         backgroundColor: ColorPalette.white,
100
-        flex: 1
70
+        paddingHorizontal: 16,
71
+        paddingVertical: 8
101 72
     }
102 73
 });

+ 79
- 18
react/features/mobile/audio-mode/components/AudioRoutePickerDialog.js View File

@@ -2,15 +2,45 @@
2 2
 
3 3
 import _ from 'lodash';
4 4
 import React, { Component } from 'react';
5
-import { NativeModules } from 'react-native';
5
+import { NativeModules, Text, TouchableHighlight, View } from 'react-native';
6 6
 import { connect } from 'react-redux';
7 7
 
8
-import { hideDialog, SimpleBottomSheet } from '../../../base/dialog';
8
+import { hideDialog, BottomSheet } from '../../../base/dialog';
9 9
 import { translate } from '../../../base/i18n';
10 10
 
11
+import { Icon } from '../../../base/font-icons';
12
+
13
+import styles, { UNDERLAY_COLOR } from './styles';
11 14
 
12 15
 /**
13
- * {@code PasswordRequiredPrompt}'s React {@code Component} prop types.
16
+ * Type definition for a single entry in the device list.
17
+ */
18
+type Device = {
19
+
20
+    /**
21
+     * Name of the icon which will be rendered on the right.
22
+     */
23
+    iconName: string,
24
+
25
+    /**
26
+     * True if the element is selected (will be highlighted in blue),
27
+     * false otherwise.
28
+     */
29
+    selected: boolean,
30
+
31
+    /**
32
+     * Text which will be rendered in the row.
33
+     */
34
+    text: string,
35
+
36
+    /**
37
+     * Device type.
38
+     */
39
+    type: string
40
+};
41
+
42
+/**
43
+ * {@code AudioRoutePickerDialog}'s React {@code Component} prop types.
14 44
  */
15 45
 type Props = {
16 46
 
@@ -25,12 +55,15 @@ type Props = {
25 55
     t: Function
26 56
 };
27 57
 
58
+/**
59
+ * {@code AudioRoutePickerDialog}'s React {@code Component} state types.
60
+ */
28 61
 type State = {
29 62
 
30 63
     /**
31 64
      * Array of available devices.
32 65
      */
33
-    devices: Array<string>
66
+    devices: Array<Device>
34 67
 };
35 68
 
36 69
 const { AudioMode } = NativeModules;
@@ -87,12 +120,11 @@ class AudioRoutePickerDialog extends Component<Props, State> {
87 120
      * @param {Props} props - The read-only React {@code Component} props with
88 121
      * which the new instance is to be initialized.
89 122
      */
90
-    constructor(props) {
123
+    constructor(props: Props) {
91 124
         super(props);
92 125
 
93 126
         // Bind event handlers so they are only bound once per instance.
94 127
         this._onCancel = this._onCancel.bind(this);
95
-        this._onSubmit = this._onSubmit.bind(this);
96 128
     }
97 129
 
98 130
     /**
@@ -146,19 +178,49 @@ class AudioRoutePickerDialog extends Component<Props, State> {
146 178
         this._hide();
147 179
     }
148 180
 
149
-    _onSubmit: (?Object) => void;
181
+    _onSelectDeviceFn: (Device) => Function;
150 182
 
151 183
     /**
152
-     * Handles the selection of a device on the sheet. The selected device will
153
-     * be used by {@code AudioMode}.
184
+     * Builds and returns a function which handles the selection of a device
185
+     * on the sheet. The selected device will be used by {@code AudioMode}.
154 186
      *
155
-     * @param {Object} device - Object representing the selected device.
187
+     * @param {Device} device - Object representing the selected device.
156 188
      * @private
157
-     * @returns {void}
189
+     * @returns {Function}
158 190
      */
159
-    _onSubmit(device) {
160
-        this._hide();
161
-        AudioMode.setAudioDevice(device.type);
191
+    _onSelectDeviceFn(device: Device) {
192
+        return () => {
193
+            this._hide();
194
+            AudioMode.setAudioDevice(device.type);
195
+        };
196
+    }
197
+
198
+    /**
199
+     * Renders a single device.
200
+     *
201
+     * @param {Device} device - Object representing a single device.
202
+     * @private
203
+     * @returns {ReactElement}
204
+     */
205
+    _renderDevice(device: Device) {
206
+        const { iconName, selected, text } = device;
207
+        const selectedStyle = selected ? styles.selectedText : {};
208
+
209
+        return (
210
+            <TouchableHighlight
211
+                key = { device.type }
212
+                onPress = { this._onSelectDeviceFn(device) }
213
+                underlayColor = { UNDERLAY_COLOR } >
214
+                <View style = { styles.deviceRow } >
215
+                    <Icon
216
+                        name = { iconName }
217
+                        style = { [ styles.deviceIcon, selectedStyle ] } />
218
+                    <Text style = { [ styles.deviceText, selectedStyle ] } >
219
+                        { text }
220
+                    </Text>
221
+                </View>
222
+            </TouchableHighlight>
223
+        );
162 224
     }
163 225
 
164 226
     /**
@@ -175,10 +237,9 @@ class AudioRoutePickerDialog extends Component<Props, State> {
175 237
         }
176 238
 
177 239
         return (
178
-            <SimpleBottomSheet
179
-                onCancel = { this._onCancel }
180
-                onSubmit = { this._onSubmit }
181
-                options = { devices } />
240
+            <BottomSheet onCancel = { this._onCancel }>
241
+                { this.state.devices.map(this._renderDevice, this) }
242
+            </BottomSheet>
182 243
         );
183 244
     }
184 245
 }

+ 50
- 0
react/features/mobile/audio-mode/components/styles.js View File

@@ -0,0 +1,50 @@
1
+// @flow
2
+
3
+import { ColorPalette, createStyleSheet } from '../../../base/styles';
4
+
5
+/**
6
+ * Underlay color for the buttons on the sheet.
7
+ *
8
+ * @type {string}
9
+ */
10
+export const UNDERLAY_COLOR = '#eee';
11
+
12
+/**
13
+ * The React {@code Component} styles of {@code AudioRoutePickerDialog}.
14
+ *
15
+ * It uses a {@code BottomSheet} and these have been implemented as per the
16
+ * Material Design guidelines:
17
+ * {@link https://material.io/guidelines/components/bottom-sheets.html}.
18
+ */
19
+export default createStyleSheet({
20
+    /**
21
+     * Base style for each row.
22
+     */
23
+    deviceRow: {
24
+        alignItems: 'center',
25
+        flexDirection: 'row',
26
+        height: 48
27
+    },
28
+
29
+    /**
30
+     * Style for the {@code Icon} element in a row.
31
+     */
32
+    deviceIcon: {
33
+        fontSize: 24
34
+    },
35
+
36
+    /**
37
+     * Style for the {@code Text} element in a row.
38
+     */
39
+    deviceText: {
40
+        fontSize: 16,
41
+        marginLeft: 32
42
+    },
43
+
44
+    /**
45
+     * Style for a row which is marked as selected.
46
+     */
47
+    selectedText: {
48
+        color: ColorPalette.blue
49
+    }
50
+});

Loading…
Cancel
Save