400 lines
11 KiB
JavaScript
400 lines
11 KiB
JavaScript
import _extends from 'babel-runtime/helpers/extends';
|
|
import _objectWithoutProperties from 'babel-runtime/helpers/objectWithoutProperties';
|
|
// @inheritedComponent Portal
|
|
|
|
import React from 'react';
|
|
import ReactDOM from 'react-dom';
|
|
import PropTypes from 'prop-types';
|
|
import classNames from 'classnames';
|
|
import warning from 'warning';
|
|
import keycode from 'keycode';
|
|
import activeElement from 'dom-helpers/activeElement';
|
|
import contains from 'dom-helpers/query/contains';
|
|
import inDOM from 'dom-helpers/util/inDOM';
|
|
import ownerDocument from 'dom-helpers/ownerDocument';
|
|
import RefHolder from '../internal/RefHolder';
|
|
import Portal from '../Portal';
|
|
import addEventListener from '../utils/addEventListener';
|
|
import { createChainedFunction } from '../utils/helpers';
|
|
import withStyles from '../styles/withStyles';
|
|
import ModalManager from './ModalManager';
|
|
import Backdrop from './Backdrop';
|
|
|
|
function getContainer(container, defaultContainer) {
|
|
container = typeof container === 'function' ? container() : container;
|
|
return ReactDOM.findDOMNode(container) || defaultContainer;
|
|
}
|
|
|
|
function getHasTransition(props) {
|
|
return props.children ? props.children.props.hasOwnProperty('in') : false;
|
|
}
|
|
|
|
export const styles = theme => ({
|
|
root: {
|
|
display: 'flex',
|
|
width: '100%',
|
|
height: '100%',
|
|
position: 'fixed',
|
|
zIndex: theme.zIndex.modal,
|
|
top: 0,
|
|
left: 0
|
|
},
|
|
hidden: {
|
|
visibility: 'hidden'
|
|
}
|
|
});
|
|
|
|
class Modal extends React.Component {
|
|
constructor(props, context) {
|
|
super(props, context);
|
|
this.dialog = null;
|
|
this.mounted = false;
|
|
this.mountNode = null;
|
|
|
|
this.handleRendered = () => {
|
|
this.autoFocus();
|
|
|
|
if (this.props.onRendered) {
|
|
this.props.onRendered();
|
|
}
|
|
};
|
|
|
|
this.handleOpen = () => {
|
|
const doc = ownerDocument(this.mountNode);
|
|
const container = getContainer(this.props.container, doc.body);
|
|
|
|
this.props.manager.add(this, container);
|
|
this.onDocumentKeydownListener = addEventListener(doc, 'keydown', this.handleDocumentKeyDown);
|
|
this.onFocusinListener = addEventListener(doc, 'focus', this.enforceFocus, true);
|
|
};
|
|
|
|
this.handleClose = () => {
|
|
this.props.manager.remove(this);
|
|
this.onDocumentKeydownListener.remove();
|
|
this.onFocusinListener.remove();
|
|
this.restoreLastFocus();
|
|
};
|
|
|
|
this.handleExited = () => {
|
|
this.setState({ exited: true });
|
|
this.handleClose();
|
|
};
|
|
|
|
this.handleBackdropClick = event => {
|
|
if (event.target !== event.currentTarget) {
|
|
return;
|
|
}
|
|
|
|
if (this.props.onBackdropClick) {
|
|
this.props.onBackdropClick(event);
|
|
}
|
|
|
|
if (!this.props.disableBackdropClick && this.props.onClose) {
|
|
this.props.onClose(event, 'backdropClick');
|
|
}
|
|
};
|
|
|
|
this.handleDocumentKeyDown = event => {
|
|
if (!this.isTopModal() || keycode(event) !== 'esc') {
|
|
return;
|
|
}
|
|
|
|
if (this.props.onEscapeKeyDown) {
|
|
this.props.onEscapeKeyDown(event);
|
|
}
|
|
|
|
if (!this.props.disableEscapeKeyDown && this.props.onClose) {
|
|
this.props.onClose(event, 'escapeKeyDown');
|
|
}
|
|
};
|
|
|
|
this.checkForFocus = () => {
|
|
if (inDOM) {
|
|
this.lastFocus = activeElement();
|
|
}
|
|
};
|
|
|
|
this.enforceFocus = () => {
|
|
if (this.props.disableEnforceFocus || !this.mounted || !this.isTopModal()) {
|
|
return;
|
|
}
|
|
|
|
const dialogElement = this.getDialogElement();
|
|
const currentActiveElement = activeElement(ownerDocument(this.mountNode));
|
|
|
|
if (dialogElement && !contains(dialogElement, currentActiveElement)) {
|
|
dialogElement.focus();
|
|
}
|
|
};
|
|
|
|
this.state = {
|
|
exited: !this.props.open
|
|
};
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.mounted = true;
|
|
if (this.props.open) {
|
|
this.handleOpen();
|
|
}
|
|
}
|
|
|
|
componentWillReceiveProps(nextProps) {
|
|
if (nextProps.open) {
|
|
this.setState({ exited: false });
|
|
} else if (!getHasTransition(nextProps)) {
|
|
// Otherwise let handleExited take care of marking exited.
|
|
this.setState({ exited: true });
|
|
}
|
|
}
|
|
|
|
componentWillUpdate(nextProps) {
|
|
if (!this.props.open && nextProps.open) {
|
|
this.checkForFocus();
|
|
}
|
|
}
|
|
|
|
componentDidUpdate(prevProps) {
|
|
if (prevProps.open && !this.props.open && !getHasTransition(this.props)) {
|
|
// Otherwise handleExited will call this.
|
|
this.handleClose();
|
|
} else if (!prevProps.open && this.props.open) {
|
|
this.handleOpen();
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
this.mounted = false;
|
|
|
|
if (this.props.open || getHasTransition(this.props) && !this.state.exited) {
|
|
this.handleClose();
|
|
}
|
|
}
|
|
|
|
getDialogElement() {
|
|
return ReactDOM.findDOMNode(this.dialog);
|
|
}
|
|
|
|
autoFocus() {
|
|
if (this.props.disableAutoFocus) {
|
|
return;
|
|
}
|
|
|
|
const dialogElement = this.getDialogElement();
|
|
const currentActiveElement = activeElement(ownerDocument(this.mountNode));
|
|
|
|
if (dialogElement && !contains(dialogElement, currentActiveElement)) {
|
|
this.lastFocus = currentActiveElement;
|
|
|
|
if (!dialogElement.hasAttribute('tabIndex')) {
|
|
process.env.NODE_ENV !== "production" ? warning(false, ['Material-UI: the modal content node does not accept focus.', 'For the benefit of assistive technologies, ' + 'the tabIndex of the node is being set to "-1".'].join('\n')) : void 0;
|
|
dialogElement.setAttribute('tabIndex', -1);
|
|
}
|
|
|
|
dialogElement.focus();
|
|
}
|
|
}
|
|
|
|
restoreLastFocus() {
|
|
if (this.props.disableRestoreFocus) {
|
|
return;
|
|
}
|
|
|
|
if (this.lastFocus) {
|
|
this.lastFocus.focus();
|
|
this.lastFocus = null;
|
|
}
|
|
}
|
|
|
|
isTopModal() {
|
|
return this.props.manager.isTopModal(this);
|
|
}
|
|
|
|
render() {
|
|
const _props = this.props,
|
|
{
|
|
BackdropComponent,
|
|
BackdropProps,
|
|
children,
|
|
classes,
|
|
className,
|
|
container,
|
|
disableAutoFocus,
|
|
disableBackdropClick,
|
|
disableEnforceFocus,
|
|
disableEscapeKeyDown,
|
|
disableRestoreFocus,
|
|
hideBackdrop,
|
|
keepMounted,
|
|
onBackdropClick,
|
|
onClose,
|
|
onEscapeKeyDown,
|
|
onRendered,
|
|
open,
|
|
manager
|
|
} = _props,
|
|
other = _objectWithoutProperties(_props, ['BackdropComponent', 'BackdropProps', 'children', 'classes', 'className', 'container', 'disableAutoFocus', 'disableBackdropClick', 'disableEnforceFocus', 'disableEscapeKeyDown', 'disableRestoreFocus', 'hideBackdrop', 'keepMounted', 'onBackdropClick', 'onClose', 'onEscapeKeyDown', 'onRendered', 'open', 'manager']);
|
|
const { exited } = this.state;
|
|
const hasTransition = getHasTransition(this.props);
|
|
const childProps = {};
|
|
|
|
if (!keepMounted && !open && (!hasTransition || exited)) {
|
|
return null;
|
|
}
|
|
|
|
// It's a Transition like component
|
|
if (hasTransition) {
|
|
childProps.onExited = createChainedFunction(this.handleExited, children.props.onExited);
|
|
}
|
|
|
|
if (children.props.role === undefined) {
|
|
childProps.role = children.props.role || 'document';
|
|
}
|
|
|
|
if (children.props.tabIndex === undefined) {
|
|
childProps.tabIndex = children.props.tabIndex || '-1';
|
|
}
|
|
|
|
return React.createElement(
|
|
Portal,
|
|
{
|
|
ref: node => {
|
|
this.mountNode = node ? node.getMountNode() : node;
|
|
},
|
|
container: container,
|
|
onRendered: this.handleRendered
|
|
},
|
|
React.createElement(
|
|
'div',
|
|
_extends({
|
|
className: classNames(classes.root, className, {
|
|
[classes.hidden]: exited
|
|
})
|
|
}, other),
|
|
hideBackdrop ? null : React.createElement(BackdropComponent, _extends({ open: open, onClick: this.handleBackdropClick }, BackdropProps)),
|
|
React.createElement(
|
|
RefHolder,
|
|
{
|
|
ref: node => {
|
|
this.dialog = node;
|
|
}
|
|
},
|
|
React.cloneElement(children, childProps)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
Modal.propTypes = process.env.NODE_ENV !== "production" ? {
|
|
/**
|
|
* A backdrop component. Useful for custom backdrop rendering.
|
|
*/
|
|
BackdropComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
|
/**
|
|
* Properties applied to the `Backdrop` element.
|
|
*/
|
|
BackdropProps: PropTypes.object,
|
|
/**
|
|
* A single child content element.
|
|
*/
|
|
children: PropTypes.element,
|
|
/**
|
|
* Useful to extend the style applied to components.
|
|
*/
|
|
classes: PropTypes.object.isRequired,
|
|
/**
|
|
* @ignore
|
|
*/
|
|
className: PropTypes.string,
|
|
/**
|
|
* A node, component instance, or function that returns either.
|
|
* The `container` will have the portal children appended to it.
|
|
*/
|
|
container: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
|
|
/**
|
|
* If `true`, the modal will not automatically shift focus to itself when it opens, and
|
|
* replace it to the last focused element when it closes.
|
|
* This also works correctly with any modal children that have the `disableAutoFocus` prop.
|
|
*
|
|
* Generally this should never be set to `true` as it makes the modal less
|
|
* accessible to assistive technologies, like screen readers.
|
|
*/
|
|
disableAutoFocus: PropTypes.bool,
|
|
/**
|
|
* If `true`, clicking the backdrop will not fire any callback.
|
|
*/
|
|
disableBackdropClick: PropTypes.bool,
|
|
/**
|
|
* If `true`, the modal will not prevent focus from leaving the modal while open.
|
|
*
|
|
* Generally this should never be set to `true` as it makes the modal less
|
|
* accessible to assistive technologies, like screen readers.
|
|
*/
|
|
disableEnforceFocus: PropTypes.bool,
|
|
/**
|
|
* If `true`, hitting escape will not fire any callback.
|
|
*/
|
|
disableEscapeKeyDown: PropTypes.bool,
|
|
/**
|
|
* If `true`, the modal will not restore focus to previously focused element once
|
|
* modal is hidden.
|
|
*/
|
|
disableRestoreFocus: PropTypes.bool,
|
|
/**
|
|
* If `true`, the backdrop is not rendered.
|
|
*/
|
|
hideBackdrop: PropTypes.bool,
|
|
/**
|
|
* Always keep the children in the DOM.
|
|
* This property can be useful in SEO situation or
|
|
* when you want to maximize the responsiveness of the Modal.
|
|
*/
|
|
keepMounted: PropTypes.bool,
|
|
/**
|
|
* A modal manager used to track and manage the state of open
|
|
* Modals. Useful when customizing how modals interact within a container.
|
|
*/
|
|
manager: PropTypes.object,
|
|
/**
|
|
* Callback fired when the backdrop is clicked.
|
|
*/
|
|
onBackdropClick: PropTypes.func,
|
|
/**
|
|
* Callback fired when the component requests to be closed.
|
|
* The `reason` parameter can optionally be used to control the response to `onClose`.
|
|
*
|
|
* @param {object} event The event source of the callback
|
|
* @param {string} reason Can be:`"escapeKeyDown"`, `"backdropClick"`
|
|
*/
|
|
onClose: PropTypes.func,
|
|
/**
|
|
* Callback fired when the escape key is pressed,
|
|
* `disableEscapeKeyDown` is false and the modal is in focus.
|
|
*/
|
|
onEscapeKeyDown: PropTypes.func,
|
|
/**
|
|
* Callback fired once the children has been mounted into the `container`.
|
|
* It signals that the `open={true}` property took effect.
|
|
*/
|
|
onRendered: PropTypes.func,
|
|
/**
|
|
* If `true`, the modal is open.
|
|
*/
|
|
open: PropTypes.bool.isRequired
|
|
} : {};
|
|
|
|
Modal.defaultProps = {
|
|
disableAutoFocus: false,
|
|
disableBackdropClick: false,
|
|
disableEnforceFocus: false,
|
|
disableEscapeKeyDown: false,
|
|
disableRestoreFocus: false,
|
|
hideBackdrop: false,
|
|
keepMounted: false,
|
|
// Modals don't open on the server so this won't conflict with concurrent requests.
|
|
manager: new ModalManager(),
|
|
BackdropComponent: Backdrop
|
|
};
|
|
|
|
export default withStyles(styles, { flip: false, name: 'MuiModal' })(Modal); |