UNPKG

@protonapp/react-native-material-ui

Version:
498 lines (444 loc) 13.3 kB
/* eslint-disable import/no-unresolved, import/extensions */ import Color from 'color'; import React, { PureComponent } from 'react'; import { View, Text, LayoutAnimation, StyleSheet, TouchableWithoutFeedback, Animated, } from 'react-native'; import PropTypes from 'prop-types'; import { ViewPropTypes } from '../utils'; /* eslint-enable import/no-unresolved, import/extensions */ import Icon from '../Icon'; import IconToggle from '../IconToggle'; import RippleFeedback from '../RippleFeedback'; /* eslint-disable import/no-unresolved, import/extensions */ import getPlatformElevation from '../styles/getPlatformElevation'; /* eslint-enable import/no-unresolved, import/extensions */ import withTheme from '../styles/withTheme'; function darkenOrLighten(color, ratio = 0.5) { const c = Color(color); return c.luminosity() > 0.5 ? c.darken(ratio) : c.lighten(ratio); } const propTypes = { /** * Array of names of icons (or elements) that will be shown after the main button is pressed * Remember, you should specify key for each element, if you use array of elements */ actions: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.element), PropTypes.arrayOf( PropTypes.shape({ icon: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), label: PropTypes.string, name: PropTypes.string, }), ), ]), /** * Called when button is pressed. Text is passed as param */ onPress: PropTypes.func, /** * Called when button is long pressed. Text is passed as param */ onLongPress: PropTypes.func, /** * Set true if you want to hide action button */ hidden: PropTypes.bool, /** * If specified it'll be shown before text */ icon: PropTypes.oneOfType([ PropTypes.element, PropTypes.string, PropTypes.object, ]), /** * Leave it empty if you don't want any transition after press. Otherwise, it will be trnasform * to another view - depends on transition value. */ transition: PropTypes.oneOf(['toolbar', 'speedDial']), /** * Set ripple color */ rippleColor: PropTypes.string, /** * You can override any style for this button */ style: PropTypes.shape({ container: ViewPropTypes.style, icon: Text.propTypes.style, // eslint-disable-line }), }; const defaultProps = { actions: null, onPress: null, onLongPress: null, transition: null, icon: 'add', style: {}, hidden: false, rippleColor: null, }; function getStyles(props, state) { const { size, theme } = props; const { actionButton } = theme; const local = { container: {}, }; if (size) { local.container = { height: size, width: size, borderRadius: size / 2, }; } local.container = { ...local.container, ...getPlatformElevation(state.elevation), }; return { positionContainer: [ actionButton.positionContainer, local.positionContainer, props.style.positionContainer, ], toolbarPositionContainer: [ actionButton.toolbarPositionContainer, local.toolbarPositionContainer, props.style.toolbarPositionContainer, ], container: [actionButton.container, local.container, props.style.container], overlayContainer: [ actionButton.overlayContainer, local.overlayContainer, props.style.overlayContainer, ], toolbarContainer: [ actionButton.toolbarContainer, local.toolbarContainer, props.style.toolbarContainer, ], toolbarActionContainer: [ actionButton.toolbarActionContainer, local.toolbarActionContainer, props.style.toolbarActionContainer, ], speedDialContainer: [ actionButton.speedDialContainer, local.speedDialContainer, props.style.speedDialContainer, ], speedDialActionContainer: [ actionButton.speedDialActionContainer, local.speedDialActionContainer, props.style.speedDialActionContainer, ], speedDialActionLabel: [ actionButton.speedDialActionLabel, local.speedDialActionLabel, props.style.speedDialActionLabel, ], speedDialActionLabelContainer: [ actionButton.speedDialActionLabelContainer, local.speedDialActionLabelContainer, props.style.speedDialActionLabelContainer, ], speedDialActionIconContainer: [ actionButton.speedDialActionIconContainer, local.speedDialActionIconContainer, props.style.speedDialActionIconContainer, ], speedDialActionIcon: [ actionButton.speedDialActionIcon, local.speedDialActionIcon, props.style.speedDialActionIcon, ], icon: [actionButton.icon, local.icon, props.style.icon], }; } const getRippleContainerStyle = containerStyle => { const flattenContainer = StyleSheet.flatten(containerStyle); const { height, width, borderRadius } = flattenContainer; return { container: { height, width, borderRadius } }; }; class ActionButton extends PureComponent { constructor(props) { super(props); this.state = { render: 'button', elevation: 6 }; } componentWillReceiveProps(nextProps) { const { hidden } = this.props; if (nextProps.hidden !== hidden) { if (nextProps.hidden === true) { this.hide(); } else { this.show(); } } } componentWillUpdate(nextProps, nextState) { const { render } = this.state; if (render !== nextState.render) { LayoutAnimation.easeInEaseOut(); } } onPress = action => { const { onPress } = this.props; this.toggleState(); if (onPress) { onPress(action); } }; getActionItemKey = ({ icon, name }) => { let key = icon; if (name) { key = name; } else if (React.isValidElement(icon) && icon.key) { key = icon.key; // eslint-disable-line } return key; }; getRippleColor() { const { rippleColor } = this.props; if (rippleColor) { return rippleColor; } const styles = getStyles(this.props, this.state); const { backgroundColor } = StyleSheet.flatten(styles.container); return darkenOrLighten(backgroundColor).toString(); } toggleState = () => { const { transition } = this.props; const { render } = this.state; if (render === 'button') { if (transition) { this.setState({ render: transition }); } } else { this.setState({ render: 'button' }); } }; show = () => { // Animated.timing(this.state.scaleValue, { // toValue: 1, // duration: 225, // easing: Easing.bezier(0.0, 0.0, 0.2, 1), // useNativeDriver: true, // }).start(); }; hide = () => { // Animated.timing(this.state.scaleValue, { // // TODO: why is not 0 here? // // see: https://github.com/facebook/react-native/issues/10510 // toValue: 0.01, // duration: 195, // easing: Easing.bezier(0.4, 0.0, 0.6, 1), // useNativeDriver: true, // }).start(); }; renderToolbarTransition = styles => { const { actions } = this.props; return ( <View style={styles.toolbarPositionContainer}> <View key="main-button" style={styles.toolbarContainer}> {actions.map(action => { if (typeof action === 'string') { return this.renderToolbarAction(styles, action); } if (React.isValidElement(action)) { return this.renderToolbarElementAction(styles, action); } const { icon, label, name } = action; // TODO: pass action return this.renderToolbarLabelAction(styles, icon, label, name); })} </View> </View> ); }; renderSpeedDialTransition = styles => { const { actions } = this.props; return ( <View style={[StyleSheet.absoluteFillObject, { flex: 1 }]}> <TouchableWithoutFeedback onPress={this.toggleState}> <View style={styles.overlayContainer}> <View style={[styles.positionContainer, styles.speedDialContainer]}> <View style={{ alignItems: 'flex-end', marginBottom: 16, }} > {actions.map(action => { if (typeof action === 'string') { return this.renderAction(styles, action); } if (React.isValidElement(action)) { return this.renderElementAction(styles, action); } const { icon, label, name } = action; // TODO: pass action return this.renderLabelAction(styles, icon, label, name); })} </View> {this.renderMainButton(styles)} </View> </View> </TouchableWithoutFeedback> </View> ); }; renderMainButton = styles => { const { onLongPress, icon } = this.props; const { render } = this.state; const mainIcon = render !== 'button' ? 'clear' : icon; return ( <RippleFeedback style={getRippleContainerStyle(styles.container)} color={this.getRippleColor()} onPress={() => this.onPress('main-button')} onLongPress={onLongPress} delayPressIn={20} borderless={false} > <View key="main-button" style={styles.container}> {this.renderIconButton(styles, mainIcon)} </View> </RippleFeedback> ); }; renderToolbarAction = (styles, icon, name) => { let content; const key = this.getActionItemKey({ icon, name }); if (React.isValidElement(icon)) { content = ( <RippleFeedback color={this.getRippleColor()} onPress={() => this.onPress(key)} delayPressIn={20} > {this.renderIconButton(styles, icon)} </RippleFeedback> ); } else { content = ( <IconToggle key={key} name={key} onPress={() => this.onPress(key)} style={{ icon: styles.icon }} /> ); } return ( <View key={key} style={styles.toolbarActionContainer}> {content} </View> ); }; renderToolbarElementAction = (styles, icon) => { const key = this.getActionItemKey({ icon }); return ( <View key={key} style={styles.toolbarActionContainer}> {this.renderToolbarAction(styles, icon)} </View> ); }; /** * TODO: implement labels for toolbar? */ renderToolbarLabelAction = (styles, icon, label, name) => { const key = this.getActionItemKey({ icon, name }); return ( <View key={key} style={styles.toolbarActionContainer}> {this.renderToolbarAction(styles, icon, name)} </View> ); }; renderAction = (styles, icon, name) => { const key = this.getActionItemKey({ icon, name }); return ( <View key={key} style={styles.speedDialActionIconContainer}> <View style={styles.speedDialActionIcon}> <RippleFeedback style={getRippleContainerStyle(styles.speedDialActionIcon)} color={this.getRippleColor()} onPress={() => this.onPress(key)} delayPressIn={20} > {this.renderIconButton(styles, icon)} </RippleFeedback> </View> </View> ); }; renderElementAction = (styles, icon) => { const key = this.getActionItemKey({ icon }); return ( <View key={key} style={styles.speedDialActionContainer}> {this.renderAction(styles, icon)} </View> ); }; renderLabelAction = (styles, icon, label, name) => { const key = this.getActionItemKey({ icon, name }); return ( <View key={key} style={styles.speedDialActionContainer}> <View style={styles.speedDialActionLabelContainer}> <Text style={styles.speedDialActionLabel}>{label}</Text> </View> {this.renderAction(styles, icon, name)} </View> ); }; renderIconButton = (styles, icon) => { let result; if (React.isValidElement(icon)) { result = icon; } else if (icon.name && icon.iconSet) { result = ( <Icon name={icon.name} iconSet={icon.iconSet} style={styles.icon} /> ); } else { result = <Icon name={icon} style={styles.icon} />; } return ( <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', }} pointerEvents="box-only" > {result} </View> ); }; renderButton = styles => ( <Animated.View style={styles.positionContainer}> {this.renderMainButton(styles)} </Animated.View> ); render() { const { render } = this.state; const styles = getStyles(this.props, this.state); if (render === 'toolbar') { return this.renderToolbarTransition(styles); } if (render === 'speedDial') { return this.renderSpeedDialTransition(styles); } return this.renderButton(styles); } } ActionButton.propTypes = propTypes; ActionButton.defaultProps = defaultProps; export default withTheme(ActionButton);