Merge pull request #2932 from saghul/refactor-bottomsheet
[RN] Refactor SimpleBottomSheet
This commit is contained in:
commit
ab7e572162
|
@ -0,0 +1,84 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component, type Node } from 'react';
|
||||
import { Modal, TouchableWithoutFeedback, View } from 'react-native';
|
||||
|
||||
import { bottomSheetStyles as styles } from './styles';
|
||||
|
||||
/**
|
||||
* The type of {@code BottomSheet}'s React {@code Component} prop types.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The children to be displayed within this component.
|
||||
*/
|
||||
children: Node,
|
||||
|
||||
/**
|
||||
* Handler for the cancel event, which happens when the user dismisses
|
||||
* the sheet.
|
||||
*/
|
||||
onCancel: ?Function
|
||||
};
|
||||
|
||||
/**
|
||||
* A component emulating Android's BottomSheet. For all intents and purposes,
|
||||
* this component has been designed to work and behave as a {@code Dialog}.
|
||||
*/
|
||||
export default class BottomSheet extends Component<Props> {
|
||||
/**
|
||||
* Initializes a new {@code BottomSheet} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return [
|
||||
<View
|
||||
key = 'overlay'
|
||||
style = { styles.overlay } />,
|
||||
<Modal
|
||||
animationType = { 'slide' }
|
||||
key = 'modal'
|
||||
onRequestClose = { this._onCancel }
|
||||
transparent = { true }
|
||||
visible = { true }>
|
||||
<View style = { styles.container }>
|
||||
<TouchableWithoutFeedback
|
||||
onPress = { this._onCancel } >
|
||||
<View style = { styles.backdrop } />
|
||||
</TouchableWithoutFeedback>
|
||||
<View style = { styles.sheet }>
|
||||
{ this.props.children }
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
];
|
||||
}
|
||||
|
||||
_onCancel: () => void;
|
||||
|
||||
/**
|
||||
* Cancels the dialog by calling the onCancel prop callback.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onCancel() {
|
||||
const { onCancel } = this.props;
|
||||
|
||||
onCancel && onCancel();
|
||||
}
|
||||
}
|
|
@ -1,206 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Text,
|
||||
TouchableHighlight,
|
||||
TouchableWithoutFeedback,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Icon } from '../../font-icons';
|
||||
|
||||
import { simpleBottomSheet as styles } from './styles';
|
||||
|
||||
/**
|
||||
* Underlay color for the buttons on the sheet.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
const BUTTON_UNDERLAY_COLOR = '#eee';
|
||||
|
||||
type Option = {
|
||||
|
||||
/**
|
||||
* Name of the icon which will be rendered on the right.
|
||||
*/
|
||||
iconName: string,
|
||||
|
||||
/**
|
||||
* True if the element is selected (will be highlighted in blue),
|
||||
* false otherwise.
|
||||
*/
|
||||
selected: boolean,
|
||||
|
||||
/**
|
||||
* Text which will be rendered in the row.
|
||||
*/
|
||||
text: string
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* The type of {@code SimpleBottomSheet}'s React {@code Component} prop types.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Handler for the cancel event, which happens when the user dismisses
|
||||
* the sheet.
|
||||
*/
|
||||
onCancel: Function,
|
||||
|
||||
/**
|
||||
* Handler for the event when an option has been selected in the sheet.
|
||||
*/
|
||||
onSubmit: Function,
|
||||
|
||||
/**
|
||||
* Array of options which will be rendered as rows.
|
||||
*/
|
||||
options: Array<Option>
|
||||
};
|
||||
|
||||
/**
|
||||
* A component emulating Android's BottomSheet, in a simplified form.
|
||||
* It supports text options with an icon, which the user can tap. The style has
|
||||
* been implemented following the Material Design guidelines for bottom
|
||||
* sheets: https://material.io/guidelines/components/bottom-sheets.html
|
||||
*
|
||||
* For all intents and purposes, this component has been designed to work and
|
||||
* behave as a {@code Dialog}.
|
||||
*/
|
||||
class SimpleBottomSheet extends Component<Props> {
|
||||
/**
|
||||
* Initializes a new {@code SimpleBottomSheet} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only React {@code Component} props with
|
||||
* which the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._onButtonPress = this._onButtonPress.bind(this);
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<Modal
|
||||
animationType = { 'slide' }
|
||||
onRequestClose = { this._onCancel }
|
||||
transparent = { true }
|
||||
visible = { true }>
|
||||
<View style = { styles.container }>
|
||||
<TouchableWithoutFeedback
|
||||
onPress = { this._onCancel } >
|
||||
<View style = { styles.overlay } />
|
||||
</TouchableWithoutFeedback>
|
||||
<View style = { styles.sheet }>
|
||||
<View style = { styles.rowsWrapper }>
|
||||
{ this._renderOptions() }
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
_onButtonPress: (?Object) => void;
|
||||
|
||||
/**
|
||||
* Handle pressing of one of the options. The sheet will be hidden and the
|
||||
* onSubmit prop will be called with the selected option.
|
||||
*
|
||||
* @param {Object} option - The option which the user selected.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onButtonPress(option) {
|
||||
const { onSubmit } = this.props;
|
||||
|
||||
onSubmit && onSubmit(option);
|
||||
}
|
||||
|
||||
_onCancel: () => void;
|
||||
|
||||
/**
|
||||
* Cancels the dialog by calling the onCancel prop callback.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onCancel() {
|
||||
const { onCancel } = this.props;
|
||||
|
||||
onCancel && onCancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders sheet rows based on the options prop.
|
||||
*
|
||||
* @private
|
||||
* @returns {Array} - Array of rows to be rendered in the sheet.
|
||||
*/
|
||||
_renderOptions() {
|
||||
return this.props.options.map(
|
||||
(option, index) => this._renderRow(option, index));
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a single row of the sheet.
|
||||
*
|
||||
* @param {Object} option - Single option which needs to be rendered.
|
||||
* @param {int} index - Option index, used as a key for React.
|
||||
* @private
|
||||
* @returns {ReactElement} - A row element with an icon and text.
|
||||
*/
|
||||
_renderRow(option, index) {
|
||||
const { iconName, selected, text } = option;
|
||||
const selectedStyle = selected ? styles.rowSelectedText : {};
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
key = { index }
|
||||
|
||||
// TODO The following disables an eslint error alerting about a
|
||||
// known potential/theoretical performance pernalty:
|
||||
//
|
||||
// A bind call or arrow function in a JSX prop will create a
|
||||
// brand new function on every single render. This is bad for
|
||||
// performance, as it will result in the garbage collector being
|
||||
// invoked way more than is necessary. It may also cause
|
||||
// unnecessary re-renders if a brand new function is passed as a
|
||||
// prop to a component that uses reference equality check on the
|
||||
// prop to determine if it should update.
|
||||
//
|
||||
// I'm not addressing the potential/theoretical performance
|
||||
// penalty at the time of this writing because it doesn't seem
|
||||
// to me that it's a practical performance penalty in the case.
|
||||
//
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onPress = { this._onButtonPress.bind(this, option) }
|
||||
underlayColor = { BUTTON_UNDERLAY_COLOR } >
|
||||
<View style = { styles.row } >
|
||||
<Icon
|
||||
name = { iconName }
|
||||
style = { [ styles.rowIcon, selectedStyle ] } />
|
||||
<View style = { styles.rowPadding } />
|
||||
<Text style = { [ styles.rowText, selectedStyle ] } >
|
||||
{ text }
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect()(SimpleBottomSheet);
|
|
@ -1,4 +1,4 @@
|
|||
export { default as BottomSheet } from './BottomSheet';
|
||||
export { default as DialogContainer } from './DialogContainer';
|
||||
export { default as Dialog } from './Dialog';
|
||||
export { default as SimpleBottomSheet } from './SimpleBottomSheet';
|
||||
export { default as StatelessDialog } from './StatelessDialog';
|
||||
|
|
|
@ -21,21 +21,36 @@ export const dialog = createStyleSheet({
|
|||
});
|
||||
|
||||
/**
|
||||
* The React {@code Component} styles of {@code SimpleBottomSheet}. These have
|
||||
* The React {@code Component} styles of {@code BottomSheet}. These have
|
||||
* been implemented as per the Material Design guidelines:
|
||||
* {@link https://material.io/guidelines/components/bottom-sheets.html}.
|
||||
*/
|
||||
export const simpleBottomSheet = createStyleSheet({
|
||||
export const bottomSheetStyles = createStyleSheet({
|
||||
/**
|
||||
* Style for a backdrop which dims the view in the background. This view
|
||||
* will also be clickable. The backgroundColor is applied to the overlay
|
||||
* view instead, so the modal animation doesn't affect the backdrop.
|
||||
*/
|
||||
backdrop: {
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Style for the container of the sheet.
|
||||
*/
|
||||
container: {
|
||||
alignItems: 'flex-end',
|
||||
flex: 1,
|
||||
flexDirection: 'row'
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
/**
|
||||
* Style for a backdrop overlay covering the screen while the
|
||||
* Style for an overlay on top of which the sheet will be displayed.
|
||||
*/
|
||||
overlay: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
|
@ -46,57 +61,13 @@ export const simpleBottomSheet = createStyleSheet({
|
|||
top: 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Base style for each row.
|
||||
*/
|
||||
row: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
height: 48
|
||||
},
|
||||
|
||||
/**
|
||||
* Style for the {@code Icon} element in a row.
|
||||
*/
|
||||
rowIcon: {
|
||||
fontSize: 24
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper for adding some padding between the icon and text in a row.
|
||||
*/
|
||||
rowPadding: {
|
||||
width: 32
|
||||
},
|
||||
|
||||
/**
|
||||
* Style for a row which is marked as selected.
|
||||
*/
|
||||
rowSelectedText: {
|
||||
color: ColorPalette.blue
|
||||
},
|
||||
|
||||
/**
|
||||
* Style for the {@code Text} element in a row.
|
||||
*/
|
||||
rowText: {
|
||||
fontSize: 16
|
||||
},
|
||||
|
||||
/**
|
||||
* Wrapper for all rows, it adds a margin to the sheet container.
|
||||
*/
|
||||
rowsWrapper: {
|
||||
marginHorizontal: 16,
|
||||
marginVertical: 8
|
||||
},
|
||||
|
||||
/**
|
||||
* Bottom sheet's base style.
|
||||
*/
|
||||
sheet: {
|
||||
alignSelf: 'flex-end',
|
||||
flex: 1,
|
||||
backgroundColor: ColorPalette.white,
|
||||
flex: 1
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8
|
||||
}
|
||||
});
|
||||
|
|
|
@ -2,15 +2,45 @@
|
|||
|
||||
import _ from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
import { NativeModules } from 'react-native';
|
||||
import { NativeModules, Text, TouchableHighlight, View } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { hideDialog, SimpleBottomSheet } from '../../../base/dialog';
|
||||
import { hideDialog, BottomSheet } from '../../../base/dialog';
|
||||
import { translate } from '../../../base/i18n';
|
||||
|
||||
import { Icon } from '../../../base/font-icons';
|
||||
|
||||
import styles, { UNDERLAY_COLOR } from './styles';
|
||||
|
||||
/**
|
||||
* {@code PasswordRequiredPrompt}'s React {@code Component} prop types.
|
||||
* Type definition for a single entry in the device list.
|
||||
*/
|
||||
type Device = {
|
||||
|
||||
/**
|
||||
* Name of the icon which will be rendered on the right.
|
||||
*/
|
||||
iconName: string,
|
||||
|
||||
/**
|
||||
* True if the element is selected (will be highlighted in blue),
|
||||
* false otherwise.
|
||||
*/
|
||||
selected: boolean,
|
||||
|
||||
/**
|
||||
* Text which will be rendered in the row.
|
||||
*/
|
||||
text: string,
|
||||
|
||||
/**
|
||||
* Device type.
|
||||
*/
|
||||
type: string
|
||||
};
|
||||
|
||||
/**
|
||||
* {@code AudioRoutePickerDialog}'s React {@code Component} prop types.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
|
@ -25,12 +55,15 @@ type Props = {
|
|||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* {@code AudioRoutePickerDialog}'s React {@code Component} state types.
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* Array of available devices.
|
||||
*/
|
||||
devices: Array<string>
|
||||
devices: Array<Device>
|
||||
};
|
||||
|
||||
const { AudioMode } = NativeModules;
|
||||
|
@ -87,12 +120,11 @@ class AudioRoutePickerDialog extends Component<Props, State> {
|
|||
* @param {Props} props - The read-only React {@code Component} props with
|
||||
* which the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -146,19 +178,49 @@ class AudioRoutePickerDialog extends Component<Props, State> {
|
|||
this._hide();
|
||||
}
|
||||
|
||||
_onSubmit: (?Object) => void;
|
||||
_onSelectDeviceFn: (Device) => Function;
|
||||
|
||||
/**
|
||||
* Handles the selection of a device on the sheet. The selected device will
|
||||
* be used by {@code AudioMode}.
|
||||
* Builds and returns a function which handles the selection of a device
|
||||
* on the sheet. The selected device will be used by {@code AudioMode}.
|
||||
*
|
||||
* @param {Object} device - Object representing the selected device.
|
||||
* @param {Device} device - Object representing the selected device.
|
||||
* @private
|
||||
* @returns {void}
|
||||
* @returns {Function}
|
||||
*/
|
||||
_onSubmit(device) {
|
||||
this._hide();
|
||||
AudioMode.setAudioDevice(device.type);
|
||||
_onSelectDeviceFn(device: Device) {
|
||||
return () => {
|
||||
this._hide();
|
||||
AudioMode.setAudioDevice(device.type);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a single device.
|
||||
*
|
||||
* @param {Device} device - Object representing a single device.
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderDevice(device: Device) {
|
||||
const { iconName, selected, text } = device;
|
||||
const selectedStyle = selected ? styles.selectedText : {};
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
key = { device.type }
|
||||
onPress = { this._onSelectDeviceFn(device) }
|
||||
underlayColor = { UNDERLAY_COLOR } >
|
||||
<View style = { styles.deviceRow } >
|
||||
<Icon
|
||||
name = { iconName }
|
||||
style = { [ styles.deviceIcon, selectedStyle ] } />
|
||||
<Text style = { [ styles.deviceText, selectedStyle ] } >
|
||||
{ text }
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -175,10 +237,9 @@ class AudioRoutePickerDialog extends Component<Props, State> {
|
|||
}
|
||||
|
||||
return (
|
||||
<SimpleBottomSheet
|
||||
onCancel = { this._onCancel }
|
||||
onSubmit = { this._onSubmit }
|
||||
options = { devices } />
|
||||
<BottomSheet onCancel = { this._onCancel }>
|
||||
{ this.state.devices.map(this._renderDevice, this) }
|
||||
</BottomSheet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
// @flow
|
||||
|
||||
import { ColorPalette, createStyleSheet } from '../../../base/styles';
|
||||
|
||||
/**
|
||||
* Underlay color for the buttons on the sheet.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const UNDERLAY_COLOR = '#eee';
|
||||
|
||||
/**
|
||||
* The React {@code Component} styles of {@code AudioRoutePickerDialog}.
|
||||
*
|
||||
* It uses a {@code BottomSheet} and these have been implemented as per the
|
||||
* Material Design guidelines:
|
||||
* {@link https://material.io/guidelines/components/bottom-sheets.html}.
|
||||
*/
|
||||
export default createStyleSheet({
|
||||
/**
|
||||
* Base style for each row.
|
||||
*/
|
||||
deviceRow: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
height: 48
|
||||
},
|
||||
|
||||
/**
|
||||
* Style for the {@code Icon} element in a row.
|
||||
*/
|
||||
deviceIcon: {
|
||||
fontSize: 24
|
||||
},
|
||||
|
||||
/**
|
||||
* Style for the {@code Text} element in a row.
|
||||
*/
|
||||
deviceText: {
|
||||
fontSize: 16,
|
||||
marginLeft: 32
|
||||
},
|
||||
|
||||
/**
|
||||
* Style for a row which is marked as selected.
|
||||
*/
|
||||
selectedText: {
|
||||
color: ColorPalette.blue
|
||||
}
|
||||
});
|
Loading…
Reference in New Issue