[RN] Add SimpleBottomSheet component

It emulates Android's BottomSheet in pure JavaScript. It's implemented as
another Dialog, so it can be used instead of one.

The implementation only supports text options with an associated icon, and an
optional 'selected' marker.
This commit is contained in:
Saúl Ibarra Corretgé 2017-11-03 11:10:58 +01:00 committed by Lyubo Marinov
parent fc3bc21eea
commit 8198e52b93
4 changed files with 268 additions and 0 deletions

View File

@ -0,0 +1,186 @@
// @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 { bottomSheet as styles } from './styles';
/**
* Underlay color for the buttons on the sheet.
*
* @type {string}
*/
const BUTTON_UNDERLAY_COLOR = '#eee';
/**
* {@code SimpleBottomSheet}'s React {@code Component} prop types.
*/
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
};
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) {
this.props.onSubmit && this.props.onSubmit(option);
}
_onCancel: () => void;
/**
* Cancels the dialog by calling the onCancel prop callback.
*
* @private
* @returns {void}
*/
_onCancel() {
this.props.onCancel && this.props.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 onPress = this._onButtonPress.bind(this, option);
const { iconName, selected, text } = option;
const selectedStyle = selected ? styles.rowSelectedText : {};
return (
<TouchableHighlight
key = { index }
onPress = { onPress }
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);

View File

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

View File

@ -19,3 +19,84 @@ export default createStyleSheet({
color: ColorPalette.darkGrey color: ColorPalette.darkGrey
} }
}); });
/**
* The React {@code Component} styles for {@code SimpleBottomSheet}.
* These styles have been implemented as per the Material Design guidelines:
* https://material.io/guidelines/components/bottom-sheets.html
*/
export const bottomSheet = createStyleSheet({
/**
* Style for the container of the sheet.
*/
container: {
flex: 1,
flexDirection: 'row'
},
/**
* Style for a backdrop overlay covering the screen while the
*/
overlay: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
bottom: 0,
left: 0,
position: 'absolute',
right: 0,
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',
backgroundColor: ColorPalette.white,
flex: 1
}
});