react-native-dropdownalert
Version:
A simple alert to notify users about new chat messages, something went wrong or everything is ok.
665 lines (663 loc) • 19.3 kB
JavaScript
import React, {Component} from 'react';
import {
StyleSheet,
SafeAreaView,
View,
TouchableOpacity,
Animated,
StatusBar,
PanResponder,
Image,
Text,
} from 'react-native';
import PropTypes from 'prop-types';
import {
DEFAULT_IMAGE_DIMENSIONS,
IS_ANDROID,
IS_IOS_BELOW_11,
TYPE,
ACTION,
HEIGHT,
getDefaultStatusBarStyle,
getDefaultStatusBarBackgroundColor,
} from './Utils';
import Queue from './Queue';
export default class DropdownAlert extends Component {
static propTypes = {
imageSrc: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
infoImageSrc: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.object,
]),
warnImageSrc: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.object,
]),
errorImageSrc: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.object,
]),
successImageSrc: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.object,
]),
cancelBtnImageSrc: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.object,
]),
infoColor: PropTypes.string,
warnColor: PropTypes.string,
errorColor: PropTypes.string,
successColor: PropTypes.string,
closeInterval: PropTypes.number,
startDelta: PropTypes.number,
endDelta: PropTypes.number,
wrapperStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number]),
containerStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number]),
contentContainerStyle: PropTypes.oneOfType([
PropTypes.object,
PropTypes.number,
]),
titleStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number]),
messageStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number]),
imageStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number]),
cancelBtnImageStyle: PropTypes.oneOfType([
PropTypes.object,
PropTypes.number,
]),
titleNumOfLines: PropTypes.number,
messageNumOfLines: PropTypes.number,
onClose: PropTypes.func,
onCancel: PropTypes.func,
showCancel: PropTypes.bool,
tapToCloseEnabled: PropTypes.bool,
panResponderEnabled: PropTypes.bool,
translucent: PropTypes.bool,
useNativeDriver: PropTypes.bool,
isInteraction: PropTypes.bool,
activeStatusBarStyle: PropTypes.string,
activeStatusBarBackgroundColor: PropTypes.string,
inactiveStatusBarStyle: PropTypes.string,
inactiveStatusBarBackgroundColor: PropTypes.string,
updateStatusBar: PropTypes.bool,
elevation: PropTypes.number,
zIndex: PropTypes.number,
sensitivity: PropTypes.number,
defaultContainer: PropTypes.oneOfType([PropTypes.object, PropTypes.number]),
defaultTextContainer: PropTypes.oneOfType([
PropTypes.object,
PropTypes.number,
]),
renderImage: PropTypes.func,
renderCancel: PropTypes.func,
renderTitle: PropTypes.func,
renderMessage: PropTypes.func,
testID: PropTypes.string,
accessibilityLabel: PropTypes.string,
accessible: PropTypes.bool,
titleTextProps: PropTypes.object,
messageTextProps: PropTypes.object,
onTap: PropTypes.func,
};
static defaultProps = {
onClose: () => {},
onCancel: () => {},
closeInterval: 4000,
startDelta: -100,
endDelta: 0,
titleNumOfLines: 1,
messageNumOfLines: 3,
imageSrc: null,
infoImageSrc: require('./assets/info.png'),
warnImageSrc: require('./assets/warn.png'),
errorImageSrc: require('./assets/error.png'),
successImageSrc: require('./assets/success.png'),
cancelBtnImageSrc: require('./assets/cancel.png'),
infoColor: '#2B73B6',
warnColor: '#cd853f',
errorColor: '#cc3232',
successColor: '#32A54A',
showCancel: false,
tapToCloseEnabled: true,
panResponderEnabled: true,
wrapperStyle: null,
containerStyle: {
flexDirection: 'row',
backgroundColor: '#202020',
},
contentContainerStyle: {
flex: 1,
flexDirection: 'row',
},
titleStyle: {
fontSize: 16,
textAlign: 'left',
fontWeight: 'bold',
color: 'white',
backgroundColor: 'transparent',
},
messageStyle: {
fontSize: 14,
textAlign: 'left',
fontWeight: 'normal',
color: 'white',
backgroundColor: 'transparent',
},
imageStyle: {
width: DEFAULT_IMAGE_DIMENSIONS,
height: DEFAULT_IMAGE_DIMENSIONS,
alignSelf: 'center',
},
cancelBtnImageStyle: {
width: DEFAULT_IMAGE_DIMENSIONS,
height: DEFAULT_IMAGE_DIMENSIONS,
},
cancelBtnStyle: {
alignSelf: 'center',
},
defaultContainer: {
flexDirection: 'row',
padding: 8,
},
defaultTextContainer: {
flex: 1,
padding: 8,
},
translucent: false,
activeStatusBarStyle: 'light-content',
activeStatusBarBackgroundColor: getDefaultStatusBarBackgroundColor(),
inactiveStatusBarStyle: getDefaultStatusBarStyle(),
inactiveStatusBarBackgroundColor: getDefaultStatusBarBackgroundColor(),
updateStatusBar: true,
isInteraction: true,
useNativeDriver: true,
elevation: 1,
zIndex: null,
sensitivity: 20,
renderImage: undefined,
renderCancel: undefined,
renderTitle: undefined,
renderMessage: undefined,
testID: undefined,
accessibilityLabel: undefined,
accessible: false,
titleTextProps: undefined,
messageTextProps: undefined,
onTap: () => {},
};
constructor(props) {
super(props);
this.state = {
isOpen: false,
topValue: 0,
height: 0,
};
this.alertData = {
type: '',
message: '',
title: '',
payload: {},
interval: props.closeInterval,
action: '',
};
this.panResponder = this.getPanResponder();
this.queue = new Queue();
this.animationValue = new Animated.Value(0);
}
componentWillUnmount() {
if (this.state.isOpen) {
this.closeAction(ACTION.programmatic);
}
}
getPanResponder = () => {
return PanResponder.create({
onStartShouldSetPanResponder: (event, gestureState) =>
this._onShouldStartPan(event, gestureState),
onMoveShouldSetPanResponder: (event, gestureState) =>
this._onShouldMovePan(event, gestureState),
onPanResponderMove: (event, gestureState) =>
this._onMovePan(event, gestureState),
onPanResponderRelease: (event, gestureState) =>
this._onDonePan(event, gestureState),
onPanResponderTerminate: (event, gestureState) =>
this._onDonePan(event, gestureState),
});
};
_onShouldStartPan = () => {
return this.props.panResponderEnabled;
};
_onShouldMovePan = (event, gestureState) => {
const {sensitivity, panResponderEnabled} = this.props;
const dx = Math.abs(gestureState.dx);
const dy = Math.abs(gestureState.dy);
const isDxSensitivity = dx < sensitivity;
const isDySensitivity = dy >= sensitivity;
return isDxSensitivity && isDySensitivity && panResponderEnabled;
};
_onMovePan = (event, gestureState) => {
if (gestureState.dy < 0) {
this.setState({topValue: gestureState.dy});
}
};
_onDonePan = (event, gestureState) => {
const start = this.getStartDelta(this.state.height, this.props.startDelta);
const delta = start / 5;
if (gestureState.dy < delta) {
this.closeAction(ACTION.pan);
}
};
getStringValue = (value) => {
try {
if (typeof value !== 'string') {
if (Array.isArray(value)) {
return value.join(' ');
}
if (typeof value === 'object') {
return `${JSON.stringify(value)}`;
}
return `${value}`;
}
return value;
} catch (error) {
return error.toString();
}
};
alertWithType = async (
type = '',
title = '',
message = '',
payload = {},
interval,
) => {
// type is not validated so unexpected types will render alert with default styles.
// these default styles can be overridden with style props. (for example, containerStyle)
const {closeInterval} = this.props;
// title and message are converted to strings
const data = {
type,
title: this.getStringValue(title),
message: this.getStringValue(message),
payload,
interval: closeInterval,
};
// closeInterval prop is overridden if interval is provided
if (interval && typeof interval === 'number') {
data.interval = interval;
}
this.queue.enqueue(data);
// start processing queue when it has at least one
if (this.getQueueSize() === 1) {
this._processQueue();
}
};
clearQueue = () => {
this.queue.clear();
};
getQueueSize = () => {
return this.queue.size;
};
_processQueue = () => {
const data = this.queue.firstItem;
if (data) {
this.open(data);
}
};
open = (data = {}) => {
this.alertData = data;
this.setState({isOpen: true});
this.animate(1, 450, () => {
if (data.interval > 0) {
this.closeAutomatic(data.interval);
}
});
};
closeAction = (action = ACTION.programmatic, onDone = () => {}) => {
// action is how the alert was closed.
// alert currently closes itself by:
// tap, pan, cancel, programmatic or automatic
if (this.state.isOpen) {
this.clearCloseTimeoutID();
this.close(action, onDone);
}
};
closeAutomatic = (interval) => {
this.clearCloseTimeoutID();
this.closeTimeoutID = setTimeout(() => {
this.close(ACTION.automatic);
}, interval);
};
close = (action, onDone = () => {}) => {
this.animate(0, 250, () => {
const {onClose, updateStatusBar, onCancel, onTap} = this.props;
this.alertData.action = action;
this.queue.dequeue();
if (action === ACTION.cancel) {
onCancel(this.alertData);
} else {
if (action === ACTION.tap) {
onTap(this.alertData);
}
onClose(this.alertData);
}
this.setState({isOpen: false, topValue: 0, height: 0});
this.updateStatusBar(updateStatusBar, false);
this._processQueue();
onDone();
});
};
updateStatusBar = (shouldUpdate = true, active = false) => {
if (shouldUpdate) {
if (IS_ANDROID) {
const {
inactiveStatusBarBackgroundColor,
activeStatusBarBackgroundColor,
translucent,
} = this.props;
if (active) {
let backgroundColor = activeStatusBarBackgroundColor;
const type = this.alertData.type;
if (type !== TYPE.custom) {
backgroundColor = this.getBackgroundColorForType(type);
}
StatusBar.setBackgroundColor(backgroundColor, true);
StatusBar.setTranslucent(translucent);
} else {
StatusBar.setBackgroundColor(inactiveStatusBarBackgroundColor, true);
}
}
const {inactiveStatusBarStyle, activeStatusBarStyle} = this.props;
if (active) {
StatusBar.setBarStyle(activeStatusBarStyle, true);
} else {
StatusBar.setBarStyle(inactiveStatusBarStyle, true);
}
}
};
clearCloseTimeoutID = () => {
if (this.closeTimeoutID) {
clearTimeout(this.closeTimeoutID);
}
};
animate = (toValue, duration = 450, onComplete = () => {}) => {
const {useNativeDriver, isInteraction} = this.props;
Animated.spring(this.animationValue, {
toValue: toValue,
duration: duration,
friction: 9,
useNativeDriver,
isInteraction,
}).start(onComplete);
};
getStartDelta = (height, start) => {
const windowHeight = HEIGHT;
const startMin = 0 - height;
const startMax = windowHeight + height;
if (start < 0 && start !== startMin) {
return startMin;
} else if (start > startMax) {
return startMax;
}
return start;
};
getEndDelta = (height, end) => {
const windowHeight = HEIGHT;
const endMin = 0;
const endMax = windowHeight;
if (end < endMin) {
return endMin;
} else if (end >= endMax) {
return endMax - height;
}
return end;
};
getOutputRange = (height, startDelta, endDelta) => {
if (!height) {
return [startDelta, endDelta];
}
const start = this.getStartDelta(height, startDelta);
const end = this.getEndDelta(height, endDelta);
return [start, end];
};
getStyleForType = (type) => {
const {defaultContainer} = this.props;
switch (type) {
case TYPE.info:
return [
StyleSheet.flatten(defaultContainer),
{backgroundColor: this.props.infoColor},
];
case TYPE.warn:
return [
StyleSheet.flatten(defaultContainer),
{backgroundColor: this.props.warnColor},
];
case TYPE.error:
return [
StyleSheet.flatten(defaultContainer),
{backgroundColor: this.props.errorColor},
];
case TYPE.success:
return [
StyleSheet.flatten(defaultContainer),
{backgroundColor: this.props.successColor},
];
default:
return [
StyleSheet.flatten(defaultContainer),
StyleSheet.flatten(this.props.containerStyle),
];
}
};
getSourceForType = (type) => {
switch (type) {
case TYPE.info:
return this.props.infoImageSrc;
case TYPE.warn:
return this.props.warnImageSrc;
case TYPE.error:
return this.props.errorImageSrc;
case TYPE.success:
return this.props.successImageSrc;
default:
return this.props.imageSrc;
}
};
getBackgroundColorForType = (type) => {
switch (type) {
case TYPE.info:
return this.props.infoColor;
case TYPE.warn:
return this.props.warnColor;
case TYPE.error:
return this.props.errorColor;
case TYPE.success:
return this.props.successColor;
default:
return this.props.containerStyle.backgroundColor;
}
};
_onLayoutEvent = (event) => {
const {height} = event.nativeEvent.layout;
if (height > this.state.height) {
const {startDelta, endDelta} = this.props;
const start = this.getStartDelta(height, startDelta);
const end = this.getEndDelta(height, endDelta);
if (startDelta !== start || endDelta !== end) {
this.setState({height});
}
}
};
_renderImage = (source, imageStyle) => {
const {renderImage} = this.props;
if (renderImage) {
return renderImage(this.props, this.alertData);
}
if (!source) {
return null;
}
let style = imageStyle;
if (!style.width) {
style.width = DEFAULT_IMAGE_DIMENSIONS;
}
if (!style.height) {
style.height = DEFAULT_IMAGE_DIMENSIONS;
}
const isRemote = typeof source === 'string';
const src = isRemote ? {uri: source} : source;
return <Image style={style} source={src} />;
};
_renderTitle = (title) => {
if (this.props.renderTitle) {
return this.props.renderTitle(this.props, this.alertData);
}
if (!title || title.length === 0) {
return null;
}
const {titleTextProps, titleStyle, titleNumOfLines} = this.props;
return (
<Text
{...titleTextProps}
style={titleStyle}
numberOfLines={titleNumOfLines}>
{title}
</Text>
);
};
_renderMessage = (message) => {
if (this.props.renderMessage) {
return this.props.renderMessage(this.props, this.alertData);
}
if (!message || message.length === 0) {
return null;
}
const {messageTextProps, messageStyle, messageNumOfLines} = this.props;
return (
<Text
{...messageTextProps}
style={messageStyle}
numberOfLines={messageNumOfLines}>
{message}
</Text>
);
};
_renderCancel = (show = false) => {
if (!show) {
return null;
}
const {
renderCancel,
cancelBtnStyle,
cancelBtnImageSrc,
cancelBtnImageStyle,
} = this.props;
if (renderCancel) {
return renderCancel(this.props, this.alertData);
}
return (
<TouchableOpacity
style={cancelBtnStyle}
onPress={() => this.closeAction(ACTION.cancel)}>
{this._renderImage(cancelBtnImageSrc, cancelBtnImageStyle)}
</TouchableOpacity>
);
};
render() {
const {isOpen} = this.state;
if (!isOpen) {
return null;
}
const {
elevation,
zIndex,
wrapperStyle,
tapToCloseEnabled,
accessibilityLabel,
testID,
accessible,
contentContainerStyle,
defaultTextContainer,
startDelta,
endDelta,
translucent,
updateStatusBar,
showCancel,
imageStyle,
} = this.props;
const {topValue, height} = this.state;
const {type, payload, title, message} = this.alertData;
let style = this.getStyleForType(type);
let imageSrc = this.getSourceForType(type);
// imageSrc is overridden when payload has source property
// other than it existing and not an object there is no validation to ensure it is image source expected by Image
if (
payload &&
payload.hasOwnProperty('source') &&
payload.source &&
typeof payload.source !== 'object'
) {
imageSrc = payload.source;
}
if (IS_ANDROID && translucent) {
style = [style, {paddingTop: StatusBar.currentHeight}];
}
this.updateStatusBar(updateStatusBar, true);
const outputRange = this.getOutputRange(height, startDelta, endDelta);
let wrapperAnimStyle = {
transform: [
{
translateY: this.animationValue.interpolate({
inputRange: [0, 1],
outputRange,
}),
},
],
position: 'absolute',
top: topValue,
left: 0,
right: 0,
elevation: elevation,
};
if (zIndex != null) {
wrapperAnimStyle.zIndex = zIndex;
}
let ContentView = SafeAreaView;
if (IS_IOS_BELOW_11 || IS_ANDROID) {
ContentView = View;
}
const activeOpacity = !tapToCloseEnabled || showCancel ? 1 : 0.95;
const onPress = !tapToCloseEnabled
? null
: () => this.closeAction(ACTION.tap);
return (
<Animated.View
ref={(ref) => (this.mainView = ref)}
{...this.panResponder.panHandlers}
style={[wrapperAnimStyle, wrapperStyle]}>
<TouchableOpacity
activeOpacity={activeOpacity}
onPress={onPress}
disabled={!tapToCloseEnabled}
onLayout={(event) => this._onLayoutEvent(event)}
testID={testID}
accessibilityLabel={accessibilityLabel}
accessible={accessible}>
<View style={style}>
<ContentView style={StyleSheet.flatten(contentContainerStyle)}>
{this._renderImage(imageSrc, imageStyle)}
<View style={StyleSheet.flatten(defaultTextContainer)}>
{this._renderTitle(title)}
{this._renderMessage(message)}
</View>
{this._renderCancel(showCancel)}
</ContentView>
</View>
</TouchableOpacity>
</Animated.View>
);
}
}