react-redux-toastr
Version:
react-redux-toastr is a React toastr message implemented with Redux
402 lines (332 loc) • 9.98 kB
JavaScript
import React, {isValidElement} from 'react'; // eslint-disable-line no-unused-vars
import PropTypes from 'prop-types';
import classnames from 'classnames';
import ProgressBar from './ProgressBar';
import Icon from './Icon';
import {onCSSTransitionEnd, _bind} from './utils';
export default class ToastrBox extends React.Component {
static displayName = 'ToastrBox';
static propTypes = {
item: PropTypes.shape({
options: PropTypes.shape({
transitionIn: PropTypes.string,
transitionOut: PropTypes.string
})
})
};
constructor(props) {
super(props);
let {
transitionIn,
transitionOut
} = props.item.options;
this.isHiding = false;
this.shouldClose = false;
this.intervalId = null;
this.ignoreIsHiding = false;
this.transitionIn = transitionIn || this.props.transitionIn;
this.transitionOut = transitionOut || this.props.transitionOut;
// an identifier to facilitate aria labelling for a11y for multiple instances of the component family in the DOM
this.id = props.item.a11yId || Math.floor(Math.random() * 9999);
this.state = {progressBar: null};
_bind(
[
'renderSubComponent',
'renderIcon',
'renderToastr',
'renderCloseButton',
'renderMessage',
'_onAnimationComplete',
'_removeToastr',
'_setTransition',
'_clearTransition',
'_setIntervalId',
'_setIsHiding',
'_setShouldClose'
],
this
);
}
componentDidMount() {
const {item} = this.props;
if (this.props.inMemory[item.id]) return;
const timeOut = this._getItemTimeOut();
if (timeOut) {
this._setIntervalId(setTimeout(this._removeToastr, timeOut));
}
if (timeOut && item.options.progressBar) {
this.setState({progressBar: {duration: this._getItemTimeOut()}});
}
this._setTransition();
onCSSTransitionEnd(this.toastrBoxElement, this._onAnimationComplete);
this.props.addToMemory(item.id);
if (this.closeButton !== undefined && !item.options.disableCloseButtonFocus) {
this.closeButton.focus();
}
}
componentWillUnmount() {
if (this.intervalId) {
clearTimeout(this.intervalId);
}
// when toast unloads the toast close button automatically focuses on the next toast control (if any)
// need to add a micro delay to allow the DOM to recycle
setTimeout(function() {
const toastrControls = document.querySelectorAll('.toastr-control:not(.disable-auto-focus)');
if (toastrControls.length) {
toastrControls[0].focus();
}
}, 50);
}
get isToastrClickable() {
const {onToastrClick, closeOnToastrClick} = this.props.item.options;
const hasOnToastrClick = !!onToastrClick;
return hasOnToastrClick || closeOnToastrClick;
}
handlePressEnterOrSpaceKeyToastr = (e) => {
if (e.key === ' ' || e.key === 'Enter') {
this.handleClickToastr(e);
}
};
handlePressEnterOrSpaceKeyCloseButton = (e) => {
if (e.key === ' ' || e.key === 'Enter') {
this.handleClickCloseButton(e);
}
}
handleClickToastr = () => {
let {onToastrClick, closeOnToastrClick} = this.props.item.options;
this.ignoreIsHiding = true;
if (onToastrClick) {
onToastrClick();
}
if (closeOnToastrClick) {
this._setShouldClose(true);
this._removeToastr();
}
};
handleClickCloseButton = (e) => {
let {onCloseButtonClick} = this.props.item.options;
e.stopPropagation();
this.ignoreIsHiding = true;
if (onCloseButtonClick) {
onCloseButtonClick();
}
this._setShouldClose(true);
this._removeToastr();
};
mouseEnter = () => {
clearTimeout(this.intervalId);
this._setIntervalId(null);
this._setIsHiding(false);
const {progressBar} = this.props.item.options;
const timeOut = this._getItemTimeOut();
if (timeOut && progressBar) {
this.setState({progressBar: null});
}
};
mouseLeave = () => {
const {removeOnHover, removeOnHoverTimeOut} = this.props.item.options;
if (!this.isHiding && (removeOnHover || this.shouldClose)) {
const interval = removeOnHover === true ? (removeOnHoverTimeOut || 1000) : removeOnHover;
this._setIntervalId(setTimeout(this._removeToastr, interval));
const {progressBar} = this.props.item.options;
const timeOut = this._getItemTimeOut();
if (timeOut && progressBar) {
this.setState({progressBar: {duration: interval}});
}
}
};
renderSubComponent() {
const {
id,
options
} = this.props.item;
const removeCurrentToastrFunc = () => this.props.remove(id);
if (isValidElement(options.component)) {
return React.cloneElement(options.component, {
remove: removeCurrentToastrFunc
});
}
return (
<options.component remove={removeCurrentToastrFunc}/>
);
}
renderIcon() {
const {
type,
options
} = this.props.item;
if (isValidElement(options.icon)) {
return React.cloneElement(options.icon);
}
const iconName = (type === 'light') ? options.icon : type;
return <Icon name={iconName} />;
}
renderCloseButton() {
let closeButtonAttributes = {
tabIndex: 0,
role: 'button',
onKeyPress: this.handlePressEnterOrSpaceKeyCloseButton
};
if (this.isToastrClickable) {
closeButtonAttributes = {};
}
return (
<div
className={classnames('close-toastr', 'toastr-control', {
'disable-auto-focus': this.props.item.options.disableCloseButtonFocus
})}
aria-label="toast"
onClick={this.handleClickCloseButton}
ref={ref => this.closeButton = ref}
{...closeButtonAttributes}
>
<span>✕</span>
</div>
);
}
renderToastr() {
const {
type,
options,
message,
title
} = this.props.item;
const ariaAttributes = {};
if (title) {
ariaAttributes['aria-labelledby'] = `dialogTitle-${this.id}`;
}
if (message) {
ariaAttributes['aria-describedby'] = `dialogDesc-${this.id}`;
}
return (
<div>
<div className="rrt-left-container">
<div className="rrt-holder">
{this.renderIcon()}
</div>
</div>
{options.status && type === 'light' && <div className={classnames('toastr-status', options.status)}/>}
<div className="rrt-middle-container" role="alertdialog" {...ariaAttributes}>
{title && <div id={`dialogTitle-${this.id}`} className="rrt-title">{title}</div>}
{message && <div id={`dialogDesc-${this.id}`} className="rrt-text">{message}</div>}
{options.component && this.renderSubComponent()}
</div>
<div className="rrt-right-container">
{options.showCloseButton && this.renderCloseButton()}
</div>
{this.state.progressBar ? <ProgressBar {...this.state.progressBar}/> : null}
</div>
);
}
renderMessage() {
const {
title,
message,
options
} = this.props.item;
return (
<div>
<div className="rrt-title">
{title}
{this.renderCloseButton()}
</div>
<div className="rrt-text">
{message}
{options.component && this.renderSubComponent()}
</div>
</div>
);
}
toastr() {
if (this.props.item.type === 'message') {
return this.renderMessage();
}
return this.renderToastr();
}
_getItemTimeOut() {
const {item} = this.props;
let {timeOut} = item.options;
if (typeof timeOut === 'undefined') {
timeOut = this.props.timeOut;
}
return timeOut;
}
_onAnimationComplete() {
const {remove, item} = this.props;
const {options, id} = item;
if (this.isHiding || this.ignoreIsHiding) {
this._setIsHiding(false);
this.ignoreIsHiding = false;
remove(id);
if (options.onHideComplete) {
options.onHideComplete();
}
} else if (!this.isHiding && options.onShowComplete) {
options.onShowComplete();
}
}
_removeToastr() {
if (!this.isHiding) {
this._setIsHiding(true);
this._setTransition(true, false);
onCSSTransitionEnd(this.toastrBoxElement, this._onAnimationComplete);
}
}
_setTransition(hide, autoRemove = true) {
const animationType = hide ? this.transitionOut : this.transitionIn;
const onEndListener = (e) => {
if (e && e.target == this.toastrBoxElement) {
this.toastrBoxElement.classList.remove(animationType);
}
};
if (this.toastrBoxElement) {
if (autoRemove) {
onCSSTransitionEnd(this.toastrBoxElement, onEndListener);
}
this.toastrBoxElement.classList.add(animationType);
}
}
_clearTransition() {
if (this.toastrBoxElement) {
this.toastrBoxElement.classList.remove(this.transitionIn, this.transitionOut);
}
}
_setIntervalId(intervalId) {
this.intervalId = intervalId;
}
_setIsHiding(val) {
this.isHiding = val;
}
_setShouldClose(val) {
this.shouldClose = val;
}
render() {
const {
options,
type
} = this.props.item;
let toastrClickAttributes = {};
if (this.isToastrClickable) {
toastrClickAttributes.role = 'button';
toastrClickAttributes.tabIndex = 0;
toastrClickAttributes.onClick = this.handleClickToastr;
toastrClickAttributes.onKeyPress = this.handlePressEnterOrSpaceKeyToastr;
}
return (
<div
ref={(ref) => this.toastrBoxElement = ref}
className={classnames(
'toastr',
'animated',
'rrt-' + type,
options.className
)}
onMouseEnter={this.mouseEnter}
onMouseLeave={this.mouseLeave}
{...toastrClickAttributes}
>
{this.toastr()}
</div>
);
}
}