You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

276 lines
8.2 KiB

import classNames from 'classnames';
import addEventListener from 'dom-helpers/addEventListener';
import canUseDOM from 'dom-helpers/canUseDOM';
import ownerDocument from 'dom-helpers/ownerDocument';
import removeEventListener from 'dom-helpers/removeEventListener';
import getScrollbarSize from 'dom-helpers/scrollbarSize';
import useCallbackRef from '@restart/hooks/useCallbackRef';
import useEventCallback from '@restart/hooks/useEventCallback';
import useMergedRefs from '@restart/hooks/useMergedRefs';
import useWillUnmount from '@restart/hooks/useWillUnmount';
import transitionEnd from 'dom-helpers/transitionEnd';
import * as React from 'react';
import { useCallback, useMemo, useRef, useState } from 'react';
import BaseModal from '@restart/ui/Modal';
import { getSharedManager } from './BootstrapModalManager';
import Fade from './Fade';
import ModalBody from './ModalBody';
import ModalContext from './ModalContext';
import ModalDialog from './ModalDialog';
import ModalFooter from './ModalFooter';
import ModalHeader from './ModalHeader';
import ModalTitle from './ModalTitle';
import { useBootstrapPrefix, useIsRTL } from './ThemeProvider';
import { jsx as _jsx } from "react/jsx-runtime";
const defaultProps = {
show: false,
backdrop: true,
keyboard: true,
autoFocus: true,
enforceFocus: true,
restoreFocus: true,
animation: true,
dialogAs: ModalDialog
};
/* eslint-disable no-use-before-define, react/no-multi-comp */
function DialogTransition(props) {
return /*#__PURE__*/_jsx(Fade, { ...props,
timeout: null
});
}
function BackdropTransition(props) {
return /*#__PURE__*/_jsx(Fade, { ...props,
timeout: null
});
}
/* eslint-enable no-use-before-define */
const Modal = /*#__PURE__*/React.forwardRef(({
bsPrefix,
className,
style,
dialogClassName,
contentClassName,
children,
dialogAs: Dialog,
'aria-labelledby': ariaLabelledby,
'aria-describedby': ariaDescribedby,
'aria-label': ariaLabel,
/* BaseModal props */
show,
animation,
backdrop,
keyboard,
onEscapeKeyDown,
onShow,
onHide,
container,
autoFocus,
enforceFocus,
restoreFocus,
restoreFocusOptions,
onEntered,
onExit,
onExiting,
onEnter,
onEntering,
onExited,
backdropClassName,
manager: propsManager,
...props
}, ref) => {
const [modalStyle, setStyle] = useState({});
const [animateStaticModal, setAnimateStaticModal] = useState(false);
const waitingForMouseUpRef = useRef(false);
const ignoreBackdropClickRef = useRef(false);
const removeStaticModalAnimationRef = useRef(null);
const [modal, setModalRef] = useCallbackRef();
const mergedRef = useMergedRefs(ref, setModalRef);
const handleHide = useEventCallback(onHide);
const isRTL = useIsRTL();
bsPrefix = useBootstrapPrefix(bsPrefix, 'modal');
const modalContext = useMemo(() => ({
onHide: handleHide
}), [handleHide]);
function getModalManager() {
if (propsManager) return propsManager;
return getSharedManager({
isRTL
});
}
function updateDialogStyle(node) {
if (!canUseDOM) return;
const containerIsOverflowing = getModalManager().getScrollbarWidth() > 0;
const modalIsOverflowing = node.scrollHeight > ownerDocument(node).documentElement.clientHeight;
setStyle({
paddingRight: containerIsOverflowing && !modalIsOverflowing ? getScrollbarSize() : undefined,
paddingLeft: !containerIsOverflowing && modalIsOverflowing ? getScrollbarSize() : undefined
});
}
const handleWindowResize = useEventCallback(() => {
if (modal) {
updateDialogStyle(modal.dialog);
}
});
useWillUnmount(() => {
removeEventListener(window, 'resize', handleWindowResize);
removeStaticModalAnimationRef.current == null ? void 0 : removeStaticModalAnimationRef.current();
}); // We prevent the modal from closing during a drag by detecting where the
// the click originates from. If it starts in the modal and then ends outside
// don't close.
const handleDialogMouseDown = () => {
waitingForMouseUpRef.current = true;
};
const handleMouseUp = e => {
if (waitingForMouseUpRef.current && modal && e.target === modal.dialog) {
ignoreBackdropClickRef.current = true;
}
waitingForMouseUpRef.current = false;
};
const handleStaticModalAnimation = () => {
setAnimateStaticModal(true);
removeStaticModalAnimationRef.current = transitionEnd(modal.dialog, () => {
setAnimateStaticModal(false);
});
};
const handleStaticBackdropClick = e => {
if (e.target !== e.currentTarget) {
return;
}
handleStaticModalAnimation();
};
const handleClick = e => {
if (backdrop === 'static') {
handleStaticBackdropClick(e);
return;
}
if (ignoreBackdropClickRef.current || e.target !== e.currentTarget) {
ignoreBackdropClickRef.current = false;
return;
}
onHide == null ? void 0 : onHide();
};
const handleEscapeKeyDown = e => {
if (!keyboard && backdrop === 'static') {
// Call preventDefault to stop modal from closing in restart ui,
// then play our animation.
e.preventDefault();
handleStaticModalAnimation();
} else if (keyboard && onEscapeKeyDown) {
onEscapeKeyDown(e);
}
};
const handleEnter = (node, isAppearing) => {
if (node) {
updateDialogStyle(node);
}
onEnter == null ? void 0 : onEnter(node, isAppearing);
};
const handleExit = node => {
removeStaticModalAnimationRef.current == null ? void 0 : removeStaticModalAnimationRef.current();
onExit == null ? void 0 : onExit(node);
};
const handleEntering = (node, isAppearing) => {
onEntering == null ? void 0 : onEntering(node, isAppearing); // FIXME: This should work even when animation is disabled.
addEventListener(window, 'resize', handleWindowResize);
};
const handleExited = node => {
if (node) node.style.display = ''; // RHL removes it sometimes
onExited == null ? void 0 : onExited(node); // FIXME: This should work even when animation is disabled.
removeEventListener(window, 'resize', handleWindowResize);
};
const renderBackdrop = useCallback(backdropProps => /*#__PURE__*/_jsx("div", { ...backdropProps,
className: classNames(`${bsPrefix}-backdrop`, backdropClassName, !animation && 'show')
}), [animation, backdropClassName, bsPrefix]);
const baseModalStyle = { ...style,
...modalStyle
}; // If `display` is not set to block, autoFocus inside the modal fails
// https://github.com/react-bootstrap/react-bootstrap/issues/5102
baseModalStyle.display = 'block';
const renderDialog = dialogProps => /*#__PURE__*/_jsx("div", {
role: "dialog",
...dialogProps,
style: baseModalStyle,
className: classNames(className, bsPrefix, animateStaticModal && `${bsPrefix}-static`),
onClick: backdrop ? handleClick : undefined,
onMouseUp: handleMouseUp,
"aria-label": ariaLabel,
"aria-labelledby": ariaLabelledby,
"aria-describedby": ariaDescribedby,
children: /*#__PURE__*/_jsx(Dialog, { ...props,
onMouseDown: handleDialogMouseDown,
className: dialogClassName,
contentClassName: contentClassName,
children: children
})
});
return /*#__PURE__*/_jsx(ModalContext.Provider, {
value: modalContext,
children: /*#__PURE__*/_jsx(BaseModal, {
show: show,
ref: mergedRef,
backdrop: backdrop,
container: container,
keyboard: true // Always set true - see handleEscapeKeyDown
,
autoFocus: autoFocus,
enforceFocus: enforceFocus,
restoreFocus: restoreFocus,
restoreFocusOptions: restoreFocusOptions,
onEscapeKeyDown: handleEscapeKeyDown,
onShow: onShow,
onHide: onHide,
onEnter: handleEnter,
onEntering: handleEntering,
onEntered: onEntered,
onExit: handleExit,
onExiting: onExiting,
onExited: handleExited,
manager: getModalManager(),
transition: animation ? DialogTransition : undefined,
backdropTransition: animation ? BackdropTransition : undefined,
renderBackdrop: renderBackdrop,
renderDialog: renderDialog
})
});
});
Modal.displayName = 'Modal';
Modal.defaultProps = defaultProps;
export default Object.assign(Modal, {
Body: ModalBody,
Header: ModalHeader,
Title: ModalTitle,
Footer: ModalFooter,
Dialog: ModalDialog,
TRANSITION_DURATION: 300,
BACKDROP_TRANSITION_DURATION: 150
});