import type { DModalFooterPrivateProps } from './ModalFooter';
import type { DModalHeaderPrivateProps } from './ModalHeader';
import { isNumber, isString, isUndefined } from 'lodash';
import React, { useRef } from 'react';
import ReactDOM from 'react-dom';
import { useId, useRefExtra } from '@react-devui/hooks';
import { CheckCircleOutlined, CloseCircleOutlined, ExclamationCircleOutlined, WarningOutlined } from '@react-devui/icons';
import { checkNodeExist, getClassName } from '@react-devui/utils';
import { useMaxIndex, useDValue, useLockScroll } from '../../hooks';
import { registerComponentMate, handleModalKeyDown, TTANSITION_DURING_BASE, checkNoExpandedEl } from '../../utils';
import { DMask } from '../_mask';
import { DTransition } from '../_transition';
import { ROOT_DATA, useComponentConfig, usePrefixConfig } from '../root';
import { DModalFooter } from './ModalFooter';
import { DModalHeader } from './ModalHeader';
export interface DModalProps extends React.HTMLAttributes<HTMLDivElement> {
dVisible: boolean;
dInitialVisible?: boolean;
dWidth?: number | string;
dTop?: number | string;
dZIndex?: number | string;
dMask?: boolean;
dMaskClosable?: boolean;
dEscClosable?: boolean;
dSkipFirstTransition?: boolean;
dDestroyAfterClose?: boolean;
dType?: {
type: 'success' | 'warning' | 'error' | 'info';
title?: React.ReactNode;
description?: React.ReactNode;
icon?: React.ReactNode;
};
dHeader?: React.ReactElement | string;
dFooter?: React.ReactElement;
onClose?: () => void;
afterVisibleChange?: (visible: boolean) => void;
}
const { COMPONENT_NAME } = registerComponentMate({ COMPONENT_NAME: 'DModal' as const });
export const DModal: {
(props: DModalProps): JSX.Element | null;
Header: typeof DModalHeader;
Footer: typeof DModalFooter;
} = (props) => {
const {
children,
dVisible,
dInitialVisible = false,
dWidth = 520,
dTop = 100,
dZIndex,
dMask = true,
dMaskClosable = true,
dEscClosable = true,
dSkipFirstTransition = true,
dDestroyAfterClose = true,
dType,
dHeader,
dFooter,
onClose,
afterVisibleChange,
...restProps
} = useComponentConfig(COMPONENT_NAME, props);
const dPrefix = usePrefixConfig();
const modalRef = useRef<HTMLDivElement>(null);
const modalContentRef = useRef<HTMLDivElement>(null);
const containerRef = useRefExtra(() => {
let el = document.getElementById(`${dPrefix}modal-root`);
if (!el) {
el = document.createElement('div');
el.id = `${dPrefix}modal-root`;
document.body.appendChild(el);
}
return el;
}, true);
const dataRef = useRef<{
transformOrigin?: string;
prevActiveEl: HTMLElement | null;
}>({
prevActiveEl: null,
});
const uniqueId = useId();
const titleId = `${dPrefix}modal-title-${uniqueId}`;
const bodyId = `${dPrefix}modal-content-${uniqueId}`;
const topStyle = dTop + (isNumber(dTop) ? 'px' : '');
const [visible, changeVisible] = useDValue<boolean>(dInitialVisible, dVisible, onClose);
const maxZIndex = useMaxIndex(visible);
const zIndex = (() => {
if (!isUndefined(dZIndex)) {
return dZIndex;
}
return maxZIndex;
})();
useLockScroll(visible);
const headerNode = (() => {
if (dHeader) {
const node = isString(dHeader) ? <DModalHeader>{dHeader}</DModalHeader> : dHeader;
return React.cloneElement<DModalHeaderPrivateProps>(node, {
__id: titleId,
__onClose: () => {
changeVisible(false);
},
});
}
})();
return (
containerRef.current &&
ReactDOM.createPortal(
<DTransition
dIn={visible}
dDuring={TTANSITION_DURING_BASE}
dSkipFirstTransition={dSkipFirstTransition}
dDestroyWhenLeaved={dDestroyAfterClose}
onEnter={() => {
if (isUndefined(ROOT_DATA.clickEvent) || performance.now() - ROOT_DATA.clickEvent.time > 100) {
dataRef.current.transformOrigin = undefined;
} else if (modalContentRef.current) {
const left = `${(ROOT_DATA.pageSize.width - modalContentRef.current.offsetWidth) / 2}px`;
const top = dTop === 'center' ? `${(ROOT_DATA.pageSize.height - modalContentRef.current.offsetHeight) / 2}px` : topStyle;
dataRef.current.transformOrigin = `calc(${ROOT_DATA.clickEvent.x}px - ${left}) calc(${ROOT_DATA.clickEvent.y}px - ${top})`;
}
}}
afterEnter={() => {
afterVisibleChange?.(true);
dataRef.current.prevActiveEl = document.activeElement as HTMLElement | null;
if (modalRef.current) {
modalRef.current.focus({ preventScroll: true });
}
}}
afterLeave={() => {
afterVisibleChange?.(false);
if (dataRef.current.prevActiveEl) {
dataRef.current.prevActiveEl.focus({ preventScroll: true });
}
}}
>
{(state) => {
let transitionStyle: React.CSSProperties = {};
switch (state) {
case 'enter':
transitionStyle = { transform: 'scale(0.3)', opacity: 0 };
break;
case 'entering':
transitionStyle = {
transition: ['transform', 'opacity'].map((attr) => `${attr} ${TTANSITION_DURING_BASE}ms ease-out`).join(', '),
transformOrigin: dataRef.current.transformOrigin,
};
break;
case 'leaving':
transitionStyle = {
transform: 'scale(0.3)',
opacity: 0,
transition: ['transform', 'opacity'].map((attr) => `${attr} ${TTANSITION_DURING_BASE}ms ease-in`).join(', '),
transformOrigin: dataRef.current.transformOrigin,
};
break;
default:
break;
}
return (
<div
{...restProps}
ref={modalRef}
className={getClassName(restProps.className, `${dPrefix}modal`, {
[`${dPrefix}modal--center`]: dTop === 'center',
[`${dPrefix}modal--type`]: dType,
})}
style={{
...restProps.style,
display: state === 'leaved' ? 'none' : undefined,
zIndex,
}}
tabIndex={-1}
role="dialog"
aria-modal
aria-labelledby={headerNode ? titleId : undefined}
aria-describedby={bodyId}
onKeyDown={(e) => {
restProps.onKeyDown?.(e);
if (dEscClosable && checkNoExpandedEl(e.currentTarget) && e.code === 'Escape') {
changeVisible(false);
}
handleModalKeyDown(e);
}}
>
{dMask && (
<DMask
dVisible={visible}
onClose={() => {
if (dMaskClosable) {
changeVisible(false);
}
}}
/>
)}
<div
ref={modalContentRef}
className={`${dPrefix}modal__content`}
style={{
width: dWidth,
top: dTop === 'center' ? undefined : dTop,
maxHeight: dTop === 'center' ? undefined : `calc(100% - ${topStyle} - 20px)`,
...transitionStyle,
}}
>
{headerNode}
<div id={bodyId} className={`${dPrefix}modal__body`}>
{dType ? (
<>
<div className={`${dPrefix}modal__icon`}>
{checkNodeExist(dType.icon) ? (
dType.icon
) : dType.type === 'success' ? (
<CheckCircleOutlined dTheme="success" />
) : dType.type === 'warning' ? (
<WarningOutlined dTheme="warning" />
) : dType.type === 'error' ? (
<CloseCircleOutlined dTheme="danger" />
) : (
<ExclamationCircleOutlined dTheme="primary" />
)}
</div>
<div className={`${dPrefix}modal__type-wrapper`}>
{checkNodeExist(dType.title) && <div className={`${dPrefix}modal__title`}>{dType.title}</div>}
{checkNodeExist(dType.description) && <div className={`${dPrefix}modal__description`}>{dType.description}</div>}
</div>
</>
) : (
children
)}
</div>
{dFooter &&
React.cloneElement<DModalFooterPrivateProps>(dFooter, {
__onClose: () => {
changeVisible(false);
},
})}
</div>
</div>
);
}}
</DTransition>,
containerRef.current
)
);
};
DModal.Header = DModalHeader;
DModal.Footer = DModalFooter;