react-cm-ui
Version:
React UI for Healthy Church
513 lines (441 loc) • 16 kB
JSX
import { Portal } from 'react-portal';
import {
isNumber,
isString,
isUndefined,
kebabCase,
} from 'lodash';
import ClassNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import ReactDOM from 'react-dom';
import ScrollBar from 'react-custom-scrollbars';
import domUtils from '../../utils/domUtils';
import ModalHeader from './modalDeprecatedHeader';
import withStyles from '../../styles/withStyles';
const propTypes = {
autoHeight: PropTypes.bool,
/**
* Add a `data-testid` attribute to the ModalDeprecated's child container element.
* Used for DOM Testing.
* See https://testing-library.com/docs/queries/bytestid/.
*/
childContainerDataTestId: PropTypes.string,
/**
* Override or extend the styles applied to Modal.
*/
classes: PropTypes.shape({
root: PropTypes.string,
}),
className: PropTypes.string,
closeButton: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.shape({}),
PropTypes.string,
]),
/**
* Add a `data-testid` attribute to the ModalDeprecated's Close Button.
* Used for DOM Testing.
* See https://testing-library.com/docs/queries/bytestid/.
*/
closeButtonDataTestId: PropTypes.string,
/**
* Add a `data-testid` attribute to the ModalDeprecated's root element.
* Used for DOM Testing.
* See https://testing-library.com/docs/queries/bytestid/.
*/
dataTestId: PropTypes.string,
fluidContent: PropTypes.bool,
header: PropTypes.bool,
headerStyle: PropTypes.shape({}),
height: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string,
]),
inverse: PropTypes.bool,
isOpen: PropTypes.bool.isRequired,
maxHeight: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string,
]),
maxWidth: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string,
]),
minHeight: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string,
]),
minWidth: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string,
]),
onClickOutside: PropTypes.bool,
onClose: PropTypes.func.isRequired,
style: PropTypes.shape({}),
theme: PropTypes.shape({
zIndex: PropTypes.shape({
modal: PropTypes.number,
}),
}),
title: PropTypes.string,
titleTruncate: PropTypes.bool,
width: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string,
]),
};
const defaultProps = {
childContainerDataTestId: undefined,
classes: null,
closeButtonDataTestId: undefined,
dataTestId: undefined,
headerStyle: undefined,
theme: null,
};
const styles = (theme) => ({
root: {
alignItems: 'center',
backfaceVisibility: 'hidden',
display: 'flex',
height: '100%',
justifyContent: 'center',
left: 0,
minWidth: 320,
position: 'fixed',
top: 0,
width: '100%',
zIndex: theme.zIndex.modal,
},
});
class ModalDeprecated extends React.Component {
constructor(props) {
super(props);
this._defaultDimensions = {
autoHeightMax: null,
height: '100%',
maxHeight: '100%',
maxWidth: 'none',
minHeight: 'auto',
minWidth: '320px',
width: '100%',
};
this.state = {
height: this._defaultDimensions.height,
isOpen: props.isOpen,
isScrolled: false,
maxHeight: this._defaultDimensions.maxHeight,
maxWidth: this._defaultDimensions.maxWidth,
minHeight: this._defaultDimensions.minHeight,
minWidth: this._defaultDimensions.minWidth,
width: this._defaultDimensions.width,
};
this._mounted = false;
this.modalContainer = null;
this._onBeforeClose = this._onBeforeClose.bind(this);
this._onClickOutside = this._onClickOutside.bind(this);
this._onCloseAnimationComplete = this._onCloseAnimationComplete.bind(this);
this._onClose = this._onClose.bind(this);
this.onOpen = this.onOpen.bind(this);
this.onOpenAnimationComplete = this.onOpenAnimationComplete.bind(this);
this._onResize = this._onResize.bind(this);
this._onScrollStart = this._onScrollStart.bind(this);
this._onScrollStop = this._onScrollStop.bind(this);
}
componentDidMount() {
const { isOpen } = this.props;
this._mounted = true;
window.addEventListener('resize', this._onResize);
this._onResize();
if (isOpen) {
this.setState({
isOpen,
}, () => {
this.onOpen();
});
}
}
componentDidUpdate(prevProps) {
const {
isOpen,
} = this.props;
if (!prevProps.isOpen && isOpen) {
this.setState({
isOpen,
}, () => {
this.onOpen();
});
}
if (prevProps.isOpen && !isOpen) {
this._onBeforeClose();
}
}
componentWillUnmount() {
this._mounted = false;
window.removeEventListener('resize', this._onResize);
if (document.body.classList.contains('modal-open')) {
document.body.classList.remove('modal-open');
}
if (document.body.classList.contains('modal-dimmer')) {
document.body.classList.remove('modal-dimmer');
}
if (document.body.classList.contains('modal-open-layered')) {
document.body.classList.remove('modal-open-layered');
}
if (document.body.classList.contains('modal-animate-out')) {
document.body.classList.remove('modal-animate-out');
}
}
animationProps(el) {
let a;
const animations = {
animation: 'animationend',
OAnimation: 'oAnimationEnd',
MozAnimation: 'animationend',
WebkitAnimation: 'webkitAnimationEnd',
};
for (a in animations) {
if (el.style[a] !== undefined) {
return animations[a];
}
}
}
_onBeforeClose() {
const nodePortal = ReactDOM.findDOMNode(this);
const modalContainer = nodePortal.querySelector('.modal-container');
const animationEvent = this.animationProps(modalContainer);
document.body.classList.add('modal-animate-out');
nodePortal.classList.add('modal-animate-out');
this.modalContainer.addEventListener(animationEvent, this._onCloseAnimationComplete);
}
_onClickOutside(event) {
if (this.modalContainerRef.contains(event.target) || !this.props.onClickOutside) {
return;
}
this._onClose();
}
_onClose() {
const { onClickOutside, onClose } = this.props;
if (onClickOutside) {
document.removeEventListener('click', this._onClickOutside);
}
onClose();
}
_onCloseAnimationComplete() {
const { body } = document;
const animationEvent = this.animationProps(this.modalContainerRef);
this.modalContainerRef.removeEventListener(animationEvent, this._onCloseAnimationComplete);
const element = document.body;
const modalLength = document.querySelectorAll('.ui.modal').length;
if (modalLength <= 2) {
body.classList.remove('modal-open-layered');
}
if (modalLength <= 1) {
const scrollPosition = parseInt(element.style.top, 10);
body.classList.remove('modal-open');
window.scroll(0, Math.abs(scrollPosition));
body.style.top = null;
}
body.classList.remove('modal-animate-out');
this.setState({
isOpen: false,
});
}
onOpen() {
const {
autoHeight,
onClickOutside,
maxWidth,
theme,
} = this.props;
const { body } = document;
const nodePortal = ReactDOM.findDOMNode(this);
const scrollPosition = window.pageYOffset;
const modalLength = document.querySelectorAll('.ui.modal').length;
this.modalContainer = nodePortal.querySelector('.modal-container');
const modalDimmer = nodePortal.querySelector('.modal-dimmer');
const animationEvent = this.animationProps(this.modalContainer);
let newZIndex = theme.zIndex.modal + 2; // adding 2 accounts for the frist .modal and .modal-dimmers- z-indexes
this.modalContainer.addEventListener(animationEvent, this.onOpenAnimationComplete);
if (onClickOutside) {
document.addEventListener('click', this._onClickOutside);
}
if (domUtils.hasClassName(body, 'modal-open')) {
newZIndex += modalLength;
domUtils.addClassName(body, 'modal-open-layered');
nodePortal.style.zIndex = newZIndex;
this.modalContainer.style.zIndex = newZIndex;
modalDimmer.style.display = 'none';
} else {
body.style.top = `-${scrollPosition}px`;
domUtils.addClassName(body, 'modal-open');
nodePortal.style.zIndex = newZIndex - 1;
this.modalContainer.style.zIndex = newZIndex + modalLength;
}
if (!isUndefined(maxWidth)) {
this.modalContainer.style.maxWidth = isNumber(maxWidth) ?
`${maxWidth}px` :
isString(maxWidth) ?
maxWidth :
null;
} else {
this.modalContainer.style.maxWidth = '768px';
}
if (autoHeight) {
const modalPaddingBottom = parseInt(getComputedStyle(nodePortal).paddingBottom);
const modalPaddingTop = parseInt(getComputedStyle(nodePortal).paddingTop);
const modalHeight = nodePortal.offsetHeight;
const newAutoHeightMax = modalHeight - modalPaddingBottom - modalPaddingTop;
this.setState({ autoHeightMax: newAutoHeightMax });
}
}
onOpenAnimationComplete() {
const animationEvent = this.animationProps(this.modalContainerRef);
this.modalContainer.removeEventListener(animationEvent, this.onOpenAnimationComplete);
const { onOpenComplete } = this.props;
if (typeof onOpenComplete === 'function') {
onOpenComplete();
}
}
_onResize() {
if (this._mounted) {
let dimensions = {};
if (window.matchMedia('(min-width: 768px)').matches === true) {
dimensions = {
height: this.props.height || '500px',
maxHeight: this.props.maxHeight || this._defaultDimensions.maxHeight,
maxWidth: this.props.maxWidth || this._defaultDimensions.maxWidth,
minHeight: this.props.minHeight || '305px',
minWidth: this.props.minWidth || this._defaultDimensions.minWidth,
width: this.props.width || '640px',
};
} else {
dimensions = {
height: this._defaultDimensions.height,
maxHeight: this._defaultDimensions.maxHeight,
maxWidth: this._defaultDimensions.maxWidth,
minHeight: this._defaultDimensions.minHeight,
minWidth: this._defaultDimensions.minWidth,
width: this._defaultDimensions.width,
};
}
this.setState(dimensions);
}
}
_onScrollStart() {
const scrollContainerPos = this.modalContainerInner.parentNode.scrollTop;
this.setState({ isScrolled: scrollContainerPos > 0 });
}
_onScrollStop() {
const scrollContainerPos = this.modalContainerInner.parentNode.scrollTop;
this.setState({ isScrolled: scrollContainerPos > 0 });
}
render() {
const {
autoHeight,
children,
childContainerDataTestId,
classes,
className,
closeButton,
closeButtonDataTestId,
dataTestId,
fluidContent,
headerStyle,
inverse,
title,
titleTruncate,
style,
} = this.props;
const {
autoHeightMax,
height,
isOpen,
isScrolled,
maxHeight,
maxWidth,
minHeight,
minWidth,
width,
} = this.state;
if (!isOpen) {
return false;
}
const rootClasses = ClassNames(
'ui',
'modal',
classes.root,
className,
);
const containerInnerClasses = ClassNames('modal-container', {
'modal-container-inverse': inverse,
'modal-container-is-scrolled': isScrolled,
});
const containerInnerStyles = {
height,
maxHeight,
maxWidth,
minHeight,
minWidth,
width,
...style,
};
const containerInnerScrollStyles = {
...(fluidContent && {
height: '100%',
}),
...((!title && !closeButton) && {
paddingTop: 33,
}),
};
return (
<Portal>
<div className={rootClasses} data-testid={dataTestId}>
<div
className={containerInnerClasses}
ref={(ref) => this.modalContainerRef = ref}
style={containerInnerStyles}
>
<ScrollBar
autoHeight={autoHeight}
autoHeightMax={autoHeightMax}
autoHide
onScrollStart={this._onScrollStart}
onScrollStop={this._onScrollStop}
>
<div
className="modal-container-inner"
ref={(el) => this.modalContainerInner = el}
style={containerInnerScrollStyles}
>
{(title || closeButton) && (
<ModalHeader
closeButton={closeButton}
closeButtonDataTestId={closeButtonDataTestId}
inverse={inverse}
key={`modal-header-${kebabCase(title)}`}
onClose={this._onClose}
title={title}
titleTruncate={titleTruncate}
style={headerStyle}
/>
)}
<div
className="modal-children"
data-testid={childContainerDataTestId}
key={`modal-children-${kebabCase(title)}`}
>
{children}
</div>
</div>
</ScrollBar>
</div>
<div className="modal-dimmer" />
</div>
</Portal>
);
}
}
ModalDeprecated.propTypes = propTypes;
ModalDeprecated.defaultProps = defaultProps;
export default withStyles(styles, { withTheme: true })(ModalDeprecated);