UNPKG

react-native-ui-lib

Version:

[![Build Status](https://travis-ci.org/wix/react-native-ui-lib.svg?branch=master)](https://travis-ci.org/wix/react-native-ui-lib) [![npm](https://img.shields.io/npm/v/react-native-ui-lib.svg)](https://www.npmjs.com/package/react-native-ui-lib) [![NPM Down

460 lines (414 loc) • 11.9 kB
import _ from 'lodash'; import PropTypes from 'prop-types'; import React from 'react'; import {StyleSheet} from 'react-native'; import * as Animatable from 'react-native-animatable'; import {BlurView} from 'react-native-blur'; import {BaseComponent} from '../../commons'; import {ThemeManager, Colors, Typography, BorderRadiuses} from '../../style'; import Assets from '../../assets'; import View from '../view'; import Button from '../button'; import Image from '../image'; const DURATION = 300; const DELAY = 100; /** * @description Toast component for showing a feedback about a user action. * @extends: Animatable.View * @extendslink: https://github.com/oblador/react-native-animatable * @gif: https://media.giphy.com/media/3oFzm1pKqGXybiDUre/giphy.gif * @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/ToastsScreen.js */ export default class Toast extends BaseComponent { static displayName = 'Toast'; static propTypes = { /** * Whether to show or hide the toast */ visible: PropTypes.bool, /** * The position of the toast. top or bottom */ position: PropTypes.oneOf(['relative', 'top', 'bottom']), /** * The height of the toast */ height: PropTypes.number, /** * The background color of the toast */ backgroundColor: PropTypes.string, /** * the toast content color (message, actions labels) */ color: PropTypes.string, /** * the toast message */ message: PropTypes.string, /** * a custom style for the toast message */ messageStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]), /** * a left icon */ icon: PropTypes.number, /** * one or two actions for the user to display in the toast */ actions: PropTypes.arrayOf(PropTypes.shape(Button.propTypes)), /** * callback for dismiss action */ onDismiss: PropTypes.func, /** * number of milliseconds to automatically invoke the onDismiss callback */ autoDismiss: PropTypes.number, /** * show dismiss action */ allowDismiss: PropTypes.bool, /** * callback for end of component animation */ onAnimationEnd: PropTypes.func, /** * render a custom toast content (better use with StyleSheet.absoluteFillObject to support safe area) */ renderContent: PropTypes.func, /** * should message be centered in the toast */ centerMessage: PropTypes.bool, /** * should the toast appear/disappear with animation */ animated: PropTypes.bool, /** * enable blur effect for Toast */ enableBlur: PropTypes.bool, /** * blur option for blur effect according to react-native-blur lib (make sure enableBlur is on) */ blurOptions: PropTypes.object, /** * custom zIndex for toast */ zIndex: PropTypes.number, }; static defaultProps = { position: 'top', color: Colors.white, animated: true, zIndex: 100, }; constructor(props) { super(props); this.state = { isVisible: false, animationConfig: this.getAnimation(true), contentAnimation: this.getContentAnimation(true), duration: DURATION, delay: DELAY, }; const {animated} = this.props; if (animated) { setupRelativeAnimation(getHeight(this.props)); } } componentWillReceiveProps(nextProps) { const {visible, animated} = nextProps; const {isVisible} = this.state; if (visible !== isVisible) { if (animated) { setupRelativeAnimation(getHeight(nextProps)); } const newState = animated ? { animationConfig: this.getAnimation(visible), contentAnimation: this.getContentAnimation(visible), } : { animationConfig: {}, contentAnimation: {}, }; this.setState(newState); } } generateStyles() { this.styles = createStyles(this.props); } getPositionStyle() { const {position} = this.props; return position === 'relative' ? {position} : getAbsolutePositionStyle(position); } getAnimation(shouldShow) { const {position, useNativeDriver} = this.props; const animationDescriptor = getAnimationDescriptor(position, this.state); const {animation, duration, delay} = shouldShow ? animationDescriptor.enter : animationDescriptor.exit; return {animation, duration, delay, useNativeDriver, onAnimationEnd: () => this.onAnimationEnd()}; } getContentAnimation(shouldShow) { const {position} = this.props; const {duration, delay} = this.state; if (position === 'relative') { return { animation: shouldShow ? 'fadeIn' : 'fadeOut', duration, delay: shouldShow ? delay : 0, onAnimationEnd: () => this.onAnimationEnd(), }; } } getBlurOptions() { const {blurOptions} = this.getThemeProps(); return { blurType: 'light', amount: 5, ...blurOptions, }; } renderContent() { const {actions, allowDismiss, renderContent, icon, color} = this.getThemeProps(); if (_.isFunction(renderContent)) { return renderContent(this.props); } const hasOneAction = _.size(actions) === 1; const height = getHeight(this.props); return ( <View row height={height} centerV spread> {icon && ( <View> <Image source={icon} resizeMode={'contain'} style={this.styles.icon} tintColor={color}/> </View> )} {this.renderMessage()} {(hasOneAction || allowDismiss) && ( <View row height="100%"> {hasOneAction && this.renderOneAction()} {this.renderDismissButton()} </View> )} </View> ); } renderMessage() { const {message, messageStyle, centerMessage, color} = this.props; const {contentAnimation} = this.state; return ( <View flex centerH={centerMessage}> <Animatable.Text style={[this.styles.message, color && {color}, messageStyle]} {...contentAnimation}> {message} </Animatable.Text> </View> ); } renderOneAction() { const action = _.first(this.props.actions); const {contentAnimation} = this.state; if (action) { return ( <Animatable.View {...contentAnimation}> <Button size="medium" style={this.styles.oneActionStyle} backgroundColor={Colors.rgba(ThemeManager.primaryColor, 0.7)} {...action} /> </Animatable.View> ); } } renderTwoActions() { const {actions} = this.props; const {contentAnimation} = this.state; return ( <Animatable.View style={this.styles.containerWithTwoActions} {...contentAnimation}> <Button size="small" {...actions[0]} /> <Button marginL-12 size="small" {...actions[1]} /> </Animatable.View> ); } renderDismissButton() { const {allowDismiss, color} = this.props; const {contentAnimation} = this.state; if (allowDismiss) { return ( <Animatable.View style={{justifyContent: 'center'}} {...contentAnimation}> <Button link iconStyle={[this.styles.dismissIconStyle, color && {tintColor: color}]} iconSource={Assets.icons.x} onPress={this.onDismiss} /> </Animatable.View> ); } } // This weird layout should support iphoneX safe are render() { const {backgroundColor, actions, enableBlur, testID, zIndex, renderContent} = this.getThemeProps(); const {animationConfig} = this.state; const hasOneAction = _.size(actions) === 1; const hasTwoActions = _.size(actions) === 2; const positionStyle = this.getPositionStyle(); const height = getHeight(this.props); const blurOptions = this.getBlurOptions(); const shouldShowToast = this.shouldShowToast(); if (!shouldShowToast) { return null; } return ( <View style={[positionStyle]} useSafeArea testID={testID}> <View height={height}/> <Animatable.View style={[ this.styles.container, !renderContent && {paddingHorizontal: 20}, backgroundColor && {backgroundColor}, hasOneAction && this.styles.containerWithOneAction, {zIndex}, ]} {...animationConfig} > {enableBlur && <BlurView style={this.styles.blurView} {...blurOptions} />} {this.renderContent()} {hasTwoActions && <View>{this.renderTwoActions()}</View>} </Animatable.View> </View> ); } shouldShowToast() { const {visible} = this.props; const {isVisible} = this.state; return isVisible || (visible && !isVisible); } onAnimationEnd() { const {visible} = this.props; this.setState({ isVisible: visible, }); this.setDismissTimer(); _.invoke(this.props, 'onAnimationEnd', visible); } setDismissTimer() { const {autoDismiss, onDismiss} = this.props; if (autoDismiss && onDismiss) { const timer = setTimeout(() => { this.onDismiss(timer); }, autoDismiss); } } onDismiss = (timer) => { if (timer) { clearTimeout(timer); } _.invoke(this.props, 'onDismiss'); } } function createStyles() { return StyleSheet.create({ container: { ...StyleSheet.absoluteFillObject, backgroundColor: Colors.rgba(ThemeManager.primaryColor, 0.8), }, containerWithOneAction: { paddingRight: 0, }, containerWithTwoActions: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', paddingBottom: 14, }, message: { color: Colors.white, ...Typography.text80, }, oneActionStyle: { borderRadius: BorderRadiuses.br0, minWidth: undefined, height: '100%', }, dismissIconStyle: { width: 12, height: 12, tintColor: Colors.white, }, blurView: { ...StyleSheet.absoluteFillObject, }, icon: { flex: 1, width: 24, height: 24, marginRight: 16, }, }); } function getAnimationDescriptor(name, {duration = DURATION, delay = DELAY}) { const defaultProps = {duration, delay: 0}; const animationDescriptorMap = { top: { enter: {...defaultProps, animation: 'slideInDown_toast'}, exit: {...defaultProps, animation: 'slideOutUp_toast'}, }, bottom: { enter: {...defaultProps, animation: 'slideInUp_toast'}, exit: {...defaultProps, animation: 'slideOutDown_toast'}, }, relative: { enter: {...defaultProps, animation: 'growUp_toast'}, exit: {...defaultProps, animation: 'growDown_toast', delay}, }, }; return animationDescriptorMap[name] || {}; } function getAbsolutePositionStyle(location) { return { position: 'absolute', left: 0, right: 0, [location]: 0, }; } function setupRelativeAnimation(height) { Animatable.initializeRegistryWithDefinitions({ // bottom slideInUp_toast: { from: {translateY: height}, to: {translateY: 0}, }, slideOutDown_toast: { from: {translateY: 0}, to: {translateY: height}, }, // top slideInDown_toast: { from: {translateY: -height}, to: {translateY: 0}, }, slideOutUp_toast: { from: {translateY: 0}, to: {translateY: -height}, }, // relative growUp_toast: { from: {height: 0}, to: {height}, }, growDown_toast: { from: {height}, to: {height: 0}, }, }); } function getHeight({height, actions}) { if (_.isUndefined(height)) { return _.size(actions) === 2 ? 92 : 48; } return height; }