import _extends from "@babel/runtime/helpers/esm/extends";
import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
/* eslint-disable @typescript-eslint/no-use-before-define, react/prop-types */
import activeElement from 'dom-helpers/activeElement';
import contains from 'dom-helpers/contains';
import canUseDOM from 'dom-helpers/canUseDOM';
import listen from 'dom-helpers/listen';
import PropTypes from 'prop-types';
import React, { useState, useRef, useCallback, useImperativeHandle, forwardRef, useEffect } from 'react';
import ReactDOM from 'react-dom';
import useMounted from '@restart/hooks/useMounted';
import useWillUnmount from '@restart/hooks/useWillUnmount';
import usePrevious from '@restart/hooks/usePrevious';
import useEventCallback from '@restart/hooks/useEventCallback';
import ModalManager from './ModalManager';
import useWaitForDOMRef from './useWaitForDOMRef';
var manager;
function getManager() {
if (!manager) manager = new ModalManager();
return manager;
}
function useModalManager(provided) {
var modalManager = provided || getManager();
var modal = useRef({
dialog: null,
backdrop: null
});
return Object.assign(modal.current, {
add: function add(container, className) {
return modalManager.add(modal.current, container, className);
},
remove: function remove() {
return modalManager.remove(modal.current);
},
isTopModal: function isTopModal() {
return modalManager.isTopModal(modal.current);
},
setDialogRef: useCallback(function (ref) {
modal.current.dialog = ref;
}, []),
setBackdropRef: useCallback(function (ref) {
modal.current.backdrop = ref;
}, [])
});
}
var Modal = /*#__PURE__*/forwardRef(function (_ref, ref) {
var _ref$show = _ref.show,
show = _ref$show === void 0 ? false : _ref$show,
_ref$role = _ref.role,
role = _ref$role === void 0 ? 'dialog' : _ref$role,
className = _ref.className,
style = _ref.style,
children = _ref.children,
_ref$backdrop = _ref.backdrop,
backdrop = _ref$backdrop === void 0 ? true : _ref$backdrop,
_ref$keyboard = _ref.keyboard,
keyboard = _ref$keyboard === void 0 ? true : _ref$keyboard,
onBackdropClick = _ref.onBackdropClick,
onEscapeKeyDown = _ref.onEscapeKeyDown,
transition = _ref.transition,
backdropTransition = _ref.backdropTransition,
_ref$autoFocus = _ref.autoFocus,
autoFocus = _ref$autoFocus === void 0 ? true : _ref$autoFocus,
_ref$enforceFocus = _ref.enforceFocus,
enforceFocus = _ref$enforceFocus === void 0 ? true : _ref$enforceFocus,
_ref$restoreFocus = _ref.restoreFocus,
restoreFocus = _ref$restoreFocus === void 0 ? true : _ref$restoreFocus,
restoreFocusOptions = _ref.restoreFocusOptions,
renderDialog = _ref.renderDialog,
_ref$renderBackdrop = _ref.renderBackdrop,
renderBackdrop = _ref$renderBackdrop === void 0 ? function (props) {
return /*#__PURE__*/React.createElement("div", props);
} : _ref$renderBackdrop,
providedManager = _ref.manager,
containerRef = _ref.container,
containerClassName = _ref.containerClassName,
onShow = _ref.onShow,
_ref$onHide = _ref.onHide,
onHide = _ref$onHide === void 0 ? function () {} : _ref$onHide,
onExit = _ref.onExit,
onExited = _ref.onExited,
onExiting = _ref.onExiting,
onEnter = _ref.onEnter,
onEntering = _ref.onEntering,
onEntered = _ref.onEntered,
rest = _objectWithoutPropertiesLoose(_ref, ["show", "role", "className", "style", "children", "backdrop", "keyboard", "onBackdropClick", "onEscapeKeyDown", "transition", "backdropTransition", "autoFocus", "enforceFocus", "restoreFocus", "restoreFocusOptions", "renderDialog", "renderBackdrop", "manager", "container", "containerClassName", "onShow", "onHide", "onExit", "onExited", "onExiting", "onEnter", "onEntering", "onEntered"]);
var container = useWaitForDOMRef(containerRef);
var modal = useModalManager(providedManager);
var isMounted = useMounted();
var prevShow = usePrevious(show);
var _useState = useState(!show),
exited = _useState[0],
setExited = _useState[1];
var lastFocusRef = useRef(null);
useImperativeHandle(ref, function () {
return modal;
}, [modal]);
if (canUseDOM && !prevShow && show) {
lastFocusRef.current = activeElement();
}
if (!transition && !show && !exited) {
setExited(true);
} else if (show && exited) {
setExited(false);
}
var handleShow = useEventCallback(function () {
modal.add(container, containerClassName);
removeKeydownListenerRef.current = listen(document, 'keydown', handleDocumentKeyDown);
removeFocusListenerRef.current = listen(document, 'focus', // the timeout is necessary b/c this will run before the new modal is mounted
// and so steals focus from it
function () {
return setTimeout(handleEnforceFocus);
}, true);
if (onShow) {
onShow();
} // autofocus after onShow to not trigger a focus event for previous
// modals before this one is shown.
if (autoFocus) {
var currentActiveElement = activeElement(document);
if (modal.dialog && currentActiveElement && !contains(modal.dialog, currentActiveElement)) {
lastFocusRef.current = currentActiveElement;
modal.dialog.focus();
}
}
});
var handleHide = useEventCallback(function () {
modal.remove();
removeKeydownListenerRef.current == null ? void 0 : removeKeydownListenerRef.current();
removeFocusListenerRef.current == null ? void 0 : removeFocusListenerRef.current();
if (restoreFocus) {
var _lastFocusRef$current;
// Support: <=IE11 doesn't support `focus()` on svg elements (RB: #917)
(_lastFocusRef$current = lastFocusRef.current) == null ? void 0 : _lastFocusRef$current.focus == null ? void 0 : _lastFocusRef$current.focus(restoreFocusOptions);
lastFocusRef.current = null;
}
}); // TODO: try and combine these effects: https://github.com/react-bootstrap/react-overlays/pull/794#discussion_r409954120
// Show logic when:
// - show is `true` _and_ `container` has resolved
useEffect(function () {
if (!show || !container) return;
handleShow();
}, [show, container,
/* should never change: */
handleShow]); // Hide cleanup logic when:
// - `exited` switches to true
// - component unmounts;
useEffect(function () {
if (!exited) return;
handleHide();
}, [exited, handleHide]);
useWillUnmount(function () {
handleHide();
}); // --------------------------------
var handleEnforceFocus = useEventCallback(function () {
if (!enforceFocus || !isMounted() || !modal.isTopModal()) {
return;
}
var currentActiveElement = activeElement();
if (modal.dialog && currentActiveElement && !contains(modal.dialog, currentActiveElement)) {
modal.dialog.focus();
}
});
var handleBackdropClick = useEventCallback(function (e) {
if (e.target !== e.currentTarget) {
return;
}
onBackdropClick == null ? void 0 : onBackdropClick(e);
if (backdrop === true) {
onHide();
}
});
var handleDocumentKeyDown = useEventCallback(function (e) {
if (keyboard && e.keyCode === 27 && modal.isTopModal()) {
onEscapeKeyDown == null ? void 0 : onEscapeKeyDown(e);
if (!e.defaultPrevented) {
onHide();
}
}
});
var removeFocusListenerRef = useRef();
var removeKeydownListenerRef = useRef();
var handleHidden = function handleHidden() {
setExited(true);
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
onExited == null ? void 0 : onExited.apply(void 0, args);
};
var Transition = transition;
if (!container || !(show || Transition && !exited)) {
return null;
}
var dialogProps = _extends({
role: role,
ref: modal.setDialogRef,
// apparently only works on the dialog role element
'aria-modal': role === 'dialog' ? true : undefined
}, rest, {
style: style,
className: className,
tabIndex: -1
});
var dialog = renderDialog ? renderDialog(dialogProps) : /*#__PURE__*/React.createElement("div", dialogProps, /*#__PURE__*/React.cloneElement(children, {
role: 'document'
}));
if (Transition) {
dialog = /*#__PURE__*/React.createElement(Transition, {
appear: true,
unmountOnExit: true,
"in": !!show,
onExit: onExit,
onExiting: onExiting,
onExited: handleHidden,
onEnter: onEnter,
onEntering: onEntering,
onEntered: onEntered
}, dialog);
}
var backdropElement = null;
if (backdrop) {
var BackdropTransition = backdropTransition;
backdropElement = renderBackdrop({
ref: modal.setBackdropRef,
onClick: handleBackdropClick
});
if (BackdropTransition) {
backdropElement = /*#__PURE__*/React.createElement(BackdropTransition, {
appear: true,
"in": !!show
}, backdropElement);
}
}
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/ReactDOM.createPortal( /*#__PURE__*/React.createElement(React.Fragment, null, backdropElement, dialog), container));
});
var propTypes = {
/**
* Set the visibility of the Modal
*/
show: PropTypes.bool,
/**
* A DOM element, a `ref` to an element, or function that returns either. The Modal is appended to it's `container` element.
*
* For the sake of assistive technologies, the container should usually be the document body, so that the rest of the
* page content can be placed behind a virtual backdrop as well as a visual one.
*/
container: PropTypes.any,
/**
* A callback fired when the Modal is opening.
*/
onShow: PropTypes.func,
/**
* A callback fired when either the backdrop is clicked, or the escape key is pressed.
*
* The `onHide` callback only signals intent from the Modal,
* you must actually set the `show` prop to `false` for the Modal to close.
*/
onHide: PropTypes.func,
/**
* Include a backdrop component.
*/
backdrop: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf(['static'])]),
/**
* A function that returns the dialog component. Useful for custom
* rendering. **Note:** the component should make sure to apply the provided ref.
*
* ```js static
* renderDialog={props => }
* ```
*/
renderDialog: PropTypes.func,
/**
* A function that returns a backdrop component. Useful for custom
* backdrop rendering.
*
* ```js
* renderBackdrop={props => }
* ```
*/
renderBackdrop: PropTypes.func,
/**
* A callback fired when the escape key, if specified in `keyboard`, is pressed.
*
* If preventDefault() is called on the keyboard event, closing the modal will be cancelled.
*/
onEscapeKeyDown: PropTypes.func,
/**
* A callback fired when the backdrop, if specified, is clicked.
*/
onBackdropClick: PropTypes.func,
/**
* A css class or set of classes applied to the modal container when the modal is open,
* and removed when it is closed.
*/
containerClassName: PropTypes.string,
/**
* Close the modal when escape key is pressed
*/
keyboard: PropTypes.bool,
/**
* A `react-transition-group@2.0.0` `` component used
* to control animations for the dialog component.
*/
transition: PropTypes.elementType,
/**
* A `react-transition-group@2.0.0` `` component used
* to control animations for the backdrop components.
*/
backdropTransition: PropTypes.elementType,
/**
* When `true` The modal will 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 `autoFocus` prop.
*
* Generally this should never be set to `false` as it makes the Modal less
* accessible to assistive technologies, like screen readers.
*/
autoFocus: PropTypes.bool,
/**
* When `true` The modal will prevent focus from leaving the Modal while open.
*
* Generally this should never be set to `false` as it makes the Modal less
* accessible to assistive technologies, like screen readers.
*/
enforceFocus: PropTypes.bool,
/**
* When `true` The modal will restore focus to previously focused element once
* modal is hidden
*/
restoreFocus: PropTypes.bool,
/**
* Options passed to focus function when `restoreFocus` is set to `true`
*
* @link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#Parameters
*/
restoreFocusOptions: PropTypes.shape({
preventScroll: PropTypes.bool
}),
/**
* Callback fired before the Modal transitions in
*/
onEnter: PropTypes.func,
/**
* Callback fired as the Modal begins to transition in
*/
onEntering: PropTypes.func,
/**
* Callback fired after the Modal finishes transitioning in
*/
onEntered: PropTypes.func,
/**
* Callback fired right before the Modal transitions out
*/
onExit: PropTypes.func,
/**
* Callback fired as the Modal begins to transition out
*/
onExiting: PropTypes.func,
/**
* Callback fired after the Modal finishes transitioning out
*/
onExited: PropTypes.func,
/**
* A ModalManager instance used to track and manage the state of open
* Modals. Useful when customizing how modals interact within a container
*/
manager: PropTypes.instanceOf(ModalManager)
};
Modal.displayName = 'Modal';
Modal.propTypes = propTypes;
export default Object.assign(Modal, {
Manager: ModalManager
});