import React from 'react'; import {Animated, Dimensions, Easing, StyleProp, StyleSheet, TouchableWithoutFeedback, View, ViewStyle} from 'react-native'; import Portal from '../portal'; const styles = StyleSheet.create({ wrap: { flex: 1, backgroundColor: 'rgba(0,0,0,0)', } as ViewStyle, mask: { backgroundColor: 'black', opacity: 0.5, } as ViewStyle, content: { backgroundColor: 'white', } as ViewStyle, absolute: { position: 'absolute', top: 0, bottom: 0, left: 0, right: 0, }, }); const screen = Dimensions.get('window'); export interface IModalPropTypes { wrapStyle?: StyleProp; maskStyle?: StyleProp; style?: {}; animationType: 'none' | 'fade' | 'slide-up' | 'slide-down'; animationDuration?: number; visible: boolean; maskClosable?: boolean; animateAppear?: boolean; onClose?: () => void; onAnimationEnd?: (visible: boolean) => void; } export default class RCModal extends React.Component { static defaultProps = { wrapStyle: styles.wrap, maskStyle: styles.mask, animationType: 'slide-up', animateAppear: false, animationDuration: 300, visible: false, maskClosable: true, onClose() {}, onAnimationEnd(_visible: boolean) {}, } as IModalPropTypes; animMask: any; animDialog: any; constructor(props: IModalPropTypes) { super(props); const {visible} = props; this.state = { position: new Animated.Value(this.getPosition(visible)), scale: new Animated.Value(this.getScale(visible)), opacity: new Animated.Value(this.getOpacity(visible)), modalVisible: visible, }; } UNSAFE_componentWillReceiveProps(nextProps: IModalPropTypes) { if (this.shouldComponentUpdate(nextProps, null)) { this.setState({ modalVisible: true, }); } } shouldComponentUpdate(nextProps: IModalPropTypes, nextState: any) { if (this.props.visible || this.props.visible !== nextProps.visible) { return true; } if (nextState) { if (nextState.modalVisible !== this.state.modalVisible) { return true; } } return false; } componentDidMount() { if (this.props.animateAppear && this.props.animationType !== 'none') { this.componentDidUpdate({} as IModalPropTypes); } } componentDidUpdate(prevProps: IModalPropTypes) { const {props} = this; if (prevProps.visible !== props.visible) { this.animateDialog(props.visible); } } componentWillUnmount() { this.stopDialogAnim(); } animateMask = (visible: boolean) => { this.stopMaskAnim(); this.state.opacity.setValue(this.getOpacity(!visible)); this.animMask = Animated.timing(this.state.opacity, { toValue: this.getOpacity(visible), duration: this.props.animationDuration, useNativeDriver: true, }); this.animMask.start(() => { this.animMask = null; }); }; stopMaskAnim = () => { if (this.animMask) { this.animMask.stop(); this.animMask = null; } }; stopDialogAnim = () => { if (this.animDialog) { this.animDialog.stop(); this.animDialog = null; } }; animateDialog = (visible: boolean) => { this.stopDialogAnim(); this.animateMask(visible); let {animationType, animationDuration} = this.props; animationDuration = animationDuration!; if (animationType !== 'none') { if (animationType === 'slide-up' || animationType === 'slide-down') { this.state.position.setValue(this.getPosition(!visible)); this.animDialog = Animated.timing(this.state.position, { toValue: this.getPosition(visible), duration: animationDuration, easing: (visible ? Easing.elastic(0.7) : undefined) as any, useNativeDriver: true, }); } else if (animationType === 'fade') { this.animDialog = Animated.parallel([ Animated.timing(this.state.opacity, { toValue: this.getOpacity(visible), duration: animationDuration, easing: (visible ? Easing.elastic(0.7) : undefined) as any, useNativeDriver: true, }), Animated.spring(this.state.scale, { toValue: this.getScale(visible), useNativeDriver: true, }), ]); } this.animDialog.start(() => { this.animDialog = null; if (!visible) { this.setState({ modalVisible: false, }); } if (this.props.onAnimationEnd) { this.props.onAnimationEnd(visible); } }); } else { if (!visible) { this.setState({ modalVisible: false, }); } } }; close = () => { this.animateDialog(false); }; onMaskClose = () => { if (this.props.maskClosable && this.props.onClose) { this.props.onClose(); } }; getPosition = (visible: boolean) => { if (visible) { return 0; } return this.props.animationType === 'slide-down' ? -screen.height : screen.height; }; getScale = (visible: boolean) => { return visible ? 1 : 1.05; }; getOpacity = (visible: boolean) => { return visible ? 1 : 0; }; render() { const {props} = this; if (!this.state.modalVisible) { return null as any; } const animationStyleMap = { none: {}, 'slide-up': {transform: [{translateY: this.state.position}]}, 'slide-down': {transform: [{translateY: this.state.position}]}, fade: { transform: [{scale: this.state.scale}], opacity: this.state.opacity, }, }; return ( {this.props.children} ); } }