box-ui-elements-mlh
Version:
177 lines (155 loc) • 5.76 kB
Flow
// @flow
import * as React from 'react';
import classNames from 'classnames';
import tabbable from 'tabbable';
import omit from 'lodash/omit';
import FocusTrap from '../focus-trap';
import LoadingIndicator from '../loading-indicator';
import Portal from '../portal';
import ModalDialog from './ModalDialog';
import './Modal.scss';
type Props = {
children: React.Node,
/** Additional CSS classname of the `.modal` element */
className?: string,
focusElementSelector?: string,
isLoading?: boolean,
isOpen?: boolean,
onBackdropClick?: Function,
/** `(event) => {}`. `event` can be of type `SyntheticKeyboardEvent|SyntheticMouseEvent`. */
onRequestClose?: Function,
/** modal usually use Portal as Portal stays if parent is removed, but it cannot retain DOM tree's resin props */
shouldNotUsePortal?: boolean,
style: {
backdrop?: Object,
dialog?: Object,
},
title?: React.Node,
};
class Modal extends React.Component<Props> {
static defaultProps = {
style: {
backdrop: {},
dialog: {},
},
};
componentDidMount() {
const { isOpen } = this.props;
if (isOpen) {
this.onModalOpen();
}
}
componentDidUpdate(prevProps: Props) {
const { isLoading, isOpen } = this.props;
// Set focus if modal is transitioning from closed -> open and/or loading -> not loading
if ((!prevProps.isOpen || prevProps.isLoading) && isOpen && !isLoading) {
this.onModalOpen();
}
}
/**
* Call props.onRequestClose when escape is pressed
* @param {SyntheticKeyboardEvent} event
*/
onKeyDown = (event: SyntheticKeyboardEvent<>) => {
const { isOpen, onRequestClose } = this.props;
if (isOpen && onRequestClose && event.key === 'Escape') {
event.stopPropagation();
onRequestClose(event);
}
};
/**
* Call props.onRequestClose when backdrop is clicked
* @param {SyntheticMouseEvent} event
*/
onBackdropClick = (event: SyntheticMouseEvent<HTMLDivElement>) => {
const { onRequestClose, onBackdropClick } = this.props;
if (onBackdropClick) {
onBackdropClick(event);
} else if (onRequestClose) {
onRequestClose(event);
}
};
/**
* Focuses on the correct element in the popup when it opens
*/
onModalOpen = () => {
setTimeout(() => {
const { focusElementSelector } = this.props;
const focusElementSelectorTrimmed = focusElementSelector && focusElementSelector.trim();
if (focusElementSelectorTrimmed) {
this.focusElement(focusElementSelectorTrimmed);
} else {
this.focusFirstUsefulElement();
}
}, 0);
};
dialog: ?HTMLElement;
/**
* Focus the first useful element in the modal (i.e. not the close button, unless it's the only thing)
*/
focusFirstUsefulElement = () => {
if (!this.dialog) {
return;
}
const tabbableEls = tabbable(this.dialog);
if (tabbableEls.length > 1) {
tabbableEls[1].focus();
} else if (tabbableEls.length > 0) {
tabbableEls[0].focus();
}
};
/**
* Focus the element that matches the selector in the modal
* @throws {Error} When the elementSelector does not match any element
*/
focusElement = (elementSelector: string) => {
if (!this.dialog) {
return;
}
const el = this.dialog.querySelector(elementSelector);
if (el) {
el.focus();
} else {
throw new Error(`Could not find element matching selector ${elementSelector} to focus on.`);
}
};
render() {
const { className, isLoading, isOpen, onRequestClose, shouldNotUsePortal, style, ...rest } = this.props;
if (!isOpen) {
return null;
}
const bodyOverrideStyle = `
body {
overflow:hidden;
}
`;
// used `omit` here to prevent certain key/value pairs from going into the spread on `ModalDialog`
const modalProps = omit(rest, ['onBackdropClick', 'focusElementSelector']);
const WrapperComponent = shouldNotUsePortal ? 'div' : Portal;
// Render a style tag to prevent body from scrolling as long as the Modal is open
return (
<WrapperComponent className={classNames('modal', className)} onKeyDown={this.onKeyDown} tabIndex="-1">
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */}
<div className="modal-backdrop" onClick={this.onBackdropClick} style={style.backdrop} />
<FocusTrap className="modal-dialog-container">
{isLoading ? (
<LoadingIndicator size="large" />
) : (
<ModalDialog
modalRef={modalEl => {
// This callback gets passed through as a regular prop since
// ModalDialog is wrapped in a HOC
this.dialog = modalEl;
}}
onRequestClose={onRequestClose}
style={style.dialog}
{...modalProps}
/>
)}
</FocusTrap>
<style type="text/css">{bodyOverrideStyle}</style>
</WrapperComponent>
);
}
}
export default Modal;