wix-style-react
Version:
232 lines (218 loc) • 7.7 kB
JavaScript
import React from 'react';
import PropTypes from 'prop-types';
import ReactModal from 'react-modal';
import X from 'wix-ui-icons-common/X';
import defaultTo from 'lodash/defaultTo';
import { st, classes } from './Modal.st.css';
import { flexPositions } from './constants';
import { ZIndex } from '../ZIndex';
import { FontUpgradeContext } from '../FontUpgrade/context';
import FontUpgrade from '../FontUpgrade';
import { ThemeProviderConsumerBackwardCompatible } from '../ThemeProvider/ThemeProviderConsumerBackwardCompatible';
import uniqueId from 'lodash/uniqueId';
class Modal extends React.PureComponent {
constructor(props) {
super(props);
this.CHILDREN_WRAPPER_DIV_ID = uniqueId('wsr-modal');
}
static propTypes = {
/** Applied as data-hook HTML attribute that can be used to create driver in testing */
dataHook: PropTypes.string,
/** Controls if modal is open or closed */
isOpen: PropTypes.bool.isRequired,
/** Border radius of modal */
borderRadius: PropTypes.number,
/** a11y: The value of contentLabel is set as an aria-label on the modal element. This helps assistive technology, like screen readers, to add a label to an element that would otherwise be anonymous */
contentLabel: PropTypes.string,
/** Renders modal content */
children: PropTypes.any,
/** Controls z-index of the modal overlay */
zIndex: PropTypes.number,
/** Enables to close modal when mouse clicked on overlay area */
shouldCloseOnOverlayClick: PropTypes.bool,
/** Displays a close button on the top right corner of the overlay */
shouldDisplayCloseButton: PropTypes.bool,
/** Callback that will be executed when the modal is requested to be closed, prior to actually closing */
onRequestClose: PropTypes.func,
/** Callback that will be executed after the modal has been opened */
onAfterOpen: PropTypes.func,
/** Horizontal position of the modal */
horizontalPosition: PropTypes.oneOf(['start', 'center', 'end']),
/** Vertical position of the modal */
verticalPosition: PropTypes.oneOf(['start', 'center', 'end']),
/** Number indicating the milliseconds to wait before closing the modal */
closeTimeoutMS: PropTypes.number,
/** Specifies if modal portal supports scroll */
scrollable: PropTypes.bool,
/** Specifies if modal content should become scrollable when modal size will fit the window */
scrollableContent: PropTypes.bool,
/** Sets the maximum height for a scrollable content */
maxHeight: PropTypes.string,
/** Sets the height for modal's content container */
height: PropTypes.string,
/** css position of the modal overlay */
overlayPosition: PropTypes.oneOf([
'static',
'relative',
'absolute',
'fixed',
'sticky',
]),
/** A function that returns a DOM element on which the modal should be appended to */
parentSelector: PropTypes.func,
/** Selector specifying where to apply the aria-hidden attribute */
appElement: PropTypes.string,
/** Specifies minimum spacing between full viewport and modal content */
screen: PropTypes.oneOf(['full', 'desktop', 'mobile']),
};
static defaultProps = {
borderRadius: 0,
shouldCloseOnOverlayClick: false,
shouldDisplayCloseButton: false,
horizontalPosition: 'center',
verticalPosition: 'center',
closeTimeoutMS: 500,
scrollable: true,
scrollableContent: false,
height: '100%',
maxHeight: 'auto',
overlayPosition: 'fixed',
screen: 'full',
};
render() {
const {
dataHook,
horizontalPosition,
verticalPosition,
height,
scrollableContent,
borderRadius,
zIndex,
scrollable,
isOpen,
shouldCloseOnOverlayClick,
shouldDisplayCloseButton,
onRequestClose,
onAfterOpen,
contentLabel,
closeTimeoutMS,
children,
appElement,
overlayPosition,
parentSelector,
screen,
} = this.props;
let { maxHeight } = this.props;
const justifyContent = flexPositions[horizontalPosition];
const alignItems = flexPositions[verticalPosition];
maxHeight = scrollableContent && maxHeight === 'auto' ? '100vh' : maxHeight;
const modalStyles = {
overlay: {
// Overriding defaults
position: overlayPosition,
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: defaultTo(zIndex, ZIndex('Modal')),
backgroundColor: null, // null disables the property, use css instead
// Overriding defaults - END
display: 'flex',
justifyContent,
alignItems,
overflowY: scrollable ? 'auto' : 'hidden',
},
content: {
// Overriding defaults
border: 'none',
overflowY: scrollableContent ? 'auto' : 'initial',
overflowX: scrollableContent ? 'hidden' : 'initial',
height,
maxHeight,
width: '100%',
WebkitOverflowScrolling: 'touch',
outline: 'none',
borderRadius,
padding: '0px',
// Overriding defaults - END
backgroundColor: 'transparent',
marginBottom: '0px',
position: 'relative',
},
};
if (appElement) {
ReactModal.setAppElement(appElement);
} else {
ReactModal.setAppElement('body');
}
return (
<div data-hook={dataHook}>
<FontUpgradeContext.Consumer>
{({ active }) => {
return (
<ThemeProviderConsumerBackwardCompatible>
{({ className: themeClassName }) => (
<ReactModal
portalClassName={st(
classes.root,
{ scrollable },
`portal portal-${dataHook}`,
themeClassName,
)}
isOpen={isOpen}
shouldCloseOnOverlayClick={shouldCloseOnOverlayClick}
onRequestClose={onRequestClose}
onAfterOpen={onAfterOpen}
style={modalStyles}
className={classes.modal}
contentLabel={contentLabel}
closeTimeoutMS={closeTimeoutMS}
parentSelector={parentSelector}
>
<FontUpgrade active={!!active}>
{isOpen &&
shouldDisplayCloseButton &&
this.renderCloseButton()}
<div
data-scrollable={scrollable || null}
id={this.CHILDREN_WRAPPER_DIV_ID}
className={st(classes.childrenContainer, {
screen,
})}
onClick={this.handleOverlayClick}
>
{children}
</div>
</FontUpgrade>
</ReactModal>
)}
</ThemeProviderConsumerBackwardCompatible>
);
}}
</FontUpgradeContext.Consumer>
</div>
);
}
handleOverlayClick = event => {
const { shouldCloseOnOverlayClick, onRequestClose } = this.props;
if (
shouldCloseOnOverlayClick &&
event.target.id === this.CHILDREN_WRAPPER_DIV_ID &&
onRequestClose
) {
onRequestClose();
}
};
renderCloseButton = () => {
return (
<div
onClick={this.props.onRequestClose}
className={classes.closeButton}
data-hook="modal-close-button"
>
<X size="18px" />
</div>
);
};
}
export default Modal;