瀏覽代碼

[RN] Refactor SimpleBottomSheet

Make it more generic by accepting any content except of just rows with text and
icons.

In addition, rework its structure so the animation is smoother, by putting the
background overlay outside of the Modal. This way, the animation doesn't affect
the background, which won't slide down.
master
Saúl Ibarra Corretgé 7 年之前
父節點
當前提交
4fdd71d1bd

+ 84
- 0
react/features/base/dialog/components/BottomSheet.native.js 查看文件

@@ -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 查看文件


+ 0
- 206
react/features/base/dialog/components/SimpleBottomSheet.native.js 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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…
取消
儲存