/* eslint-disable jsx-a11y/no-noninteractive-tabindex */ import React, { useEffect, ReactNode, useRef } from "react"; import { createPortal } from "react-dom"; import classNames from "classnames"; import { StyledProps } from "../_type"; import { Button } from "../button"; import { FadeTransition, SlideTransition } from "../transition"; import { createRocket } from "../_util/create-rocket"; import { AttachContainer, getOverlayRoot } from "../_util/get-overlay-root"; import { useVisibleTransition } from "../_util/use-visible-transition"; import { callBoth } from "../_util/call-both"; import { ModalMessage } from "./ModalMessage"; import { useOutsideClick } from "../_util/use-outside-click"; import { useConfig } from "../_util/config-context"; import { uuid } from "../_util/uuid"; import { zStack } from "../_util/z-stack"; import { BackDrop } from "../backdrop"; import { useOverlayFocus } from "../_util/use-overlay-focus"; import { KeyMap } from "../_util/key-map"; import { useTranslation } from "../i18n"; import { mergeRefs } from "../util"; import { forwardRefWithStatics } from "../_util/forward-ref-with-statics"; import { modalEventEmitter } from "./event"; /** * 对话框组件配置 */ export interface ModalProps extends StyledProps { /** * 对话框内容 */ children?: ReactNode; /** * 对话框是否可见 */ visible?: boolean; /** * 对话框的标题 * 如果禁用关闭图标的同时,没有传入对话框标题,则不渲染标题 */ caption?: string | JSX.Element; /** * 对话框尺寸,决定对话框的宽度 * - `"s"` 小尺寸对话框,宽度 480px * - `"m"` 默认尺寸对话框,宽度 550px * - `"l"` 大尺寸对话框,宽度 800px * - `"xl"` 超大尺寸对话框,宽度 950px * - `"auto"` 自动适应宽度 * - 传入数字/百分比可以自定义宽度 */ size?: "s" | "m" | "l" | "xl" | "auto" | number | string; /** * 对话框关闭时回调 */ onClose?: () => void; /** * 对话框关闭动画结束时回调 */ onExited?: () => void; /** * 默认 ESC 键会触发 `onClose` 的调用,设置为 `true` 禁止该行为 * @default false */ disableEscape?: boolean; /** * 是否禁用关闭图标 * 如果禁用关闭图标的同时,没有传入对话框标题,则不渲染标题 * @default false */ disableCloseIcon?: boolean; /** * 是否启用点击遮罩关闭 * @default false */ maskClosable?: boolean; /** * 关闭时是否销毁 Modal 中元素 * @default true * @since 2.3.0 */ destroyOnClose?: boolean; /** * 挂载组件的节点 * @default document.body * @since 2.5.0 */ popupContainer?: AttachContainer; /** * 遮罩样式 * @since 2.5.4 */ maskStyle?: React.CSSProperties; } /** * 全局配置 */ export interface ModelConfigOptions extends Pick< ModalProps, | "size" | "disableEscape" | "disableCloseIcon" | "maskClosable" | "destroyOnClose" | "popupContainer" > {} const modelConfigOptions: ModelConfigOptions = {}; // 容器们 const ModalHeader = createRocket("ModalHeader", "div.@{prefix}-dialog__header"); const ModalBody = createRocket("ModalBody", "div.@{prefix}-dialog__body"); const ModalFooter = createRocket( "ModalFooter", "div.@{prefix}-dialog__footer", "div.@{prefix}-dialog__btnwrap" ); /** * 模态对话框组件 */ export const Modal = forwardRefWithStatics( function Modal(props: ModalProps, fRef: React.Ref) { const { visible, caption, size, onClose, onExited, disableEscape, disableCloseIcon, maskClosable, destroyOnClose = true, className, style, children, popupContainer, maskStyle, } = Object.assign({}, modelConfigOptions, props); const { classPrefix, popupContainer: globalPopupContainer } = useConfig(); const t = useTranslation(); const ref = useRef(null); const focusRef = useRef(null); const idRef = useRef(uuid()); const firstRender = useRef(!!visible); const { listen, ignoreProps } = useOutsideClick(ref); listen(() => { if (maskClosable && visible && zStack.top === idRef.current && onClose) { onClose(); } }); if (visible && !firstRender.current) { firstRender.current = true; } // 监听 ESC 键 useEffect(() => { if (!visible) { return () => null; } const handleKeydown = (evt: KeyboardEvent) => { if ( evt.key === KeyMap.Esc && !disableEscape && zStack.top === idRef.current && onClose ) { onClose(); } }; window.addEventListener("keydown", handleKeydown); return () => window.removeEventListener("keydown", handleKeydown); }, [visible, disableEscape, onClose]); useEffect(() => { if (firstRender.current) { modalEventEmitter.emit(visible ? "open" : "close"); } }, [visible]); const { contentIn, shouldContentEnter, shouldContentRender, onContentExit, } = useVisibleTransition(visible); useEffect(() => { if (contentIn) { const id = idRef.current; zStack.push(id); return () => { zStack.remove(id); }; } return () => {}; }, [contentIn]); useOverlayFocus(visible, focusRef); if ( !shouldContentRender && (destroyOnClose || (!destroyOnClose && !firstRender.current)) ) { return null; } // 有标题,或者有图标,就需要渲染 header const hasHeader = Boolean(caption) || !disableCloseIcon; // 内置尺寸名称 let sizeClassName: string = null; if ( typeof size === "string" && ["s", "l", "xl", "auto"].indexOf(size) > -1 ) { sizeClassName = `size-${size}`; } const dialog = (