创建一个简单的modal组件,可通过组件方式和函数方式调用
核心
为什么要放在body下?
- 浮层组件(悬浮在其他组件上的,例如modal、message等反馈组件),如果其祖先元素有overflow:hidden ,就容易被遮盖;或者z-index层级不够大,就容易被覆盖(z-index和父元素的index也有关),所以,通常放在document.body下。
- 解决方法:使用createPortal(要被包裹的节点,实际在dom上包裹的节点)
- ”传送门“:一般来讲,虚拟dom(react纤维)与dom节点的数据结构是对应的,但是悬浮就要求虚拟dom与真实dom无法一一对应:如果两个组件是父子对应关系,那么它们的dom节点也是对应的父子关系。而我们要打打破这种关系,就有了portal:将一个组件,传送到另一个节点中去(只是在dom节点中保持这种关系,但是在虚拟dom中,父子关系还是原来的父子关系)
import React, { useLayoutEffect, useMemo } from 'react';
import { createPortal } from 'react-dom';
import { IPortalProps } from './interface';
export const Portal: React.FC<IPortalProps> = (props) => {
const { open, children } = props;
const container = useMemo(() => {
return document.createElement('div');
}, []);
function appendDiv() {
if (!container.parentElement) {
document.body.appendChild(container);
}
}
function removeDiv() {
if (container.parentElement) {
document.body.removeChild(container);
}
}
useLayoutEffect(() => {
if (open) {
appendDiv();
} else {
removeDiv();
}
return removeDiv;
}, [open]);
return createPortal(children, container);//这样就有一个Portal组件,将子组件传送到container中,但在虚拟dom中还是处于其原父组件
};
-
使用createRoot 在浏览器的 DOM 节点中创建根节点以显示 React 组件,创建一个react的渲染节点,这个createRoot(div)返回一个节点对象,上面有两个方法,render将组件渲染到该节点上, unmounted将该节点从dom树上卸载,这个方法主要是将组件渲染到指定的dom里面(这个dom是一个外部的dom)
-
好处:弹出层不会受到祖先元素的样式的影响
下面是一个modal组件: index.tsx
import { info } from './info';
import { default as MyModal } from './modal';
const Modal: typeof MyModal & { info: typeof info } = MyModal as any;
Modal.info = info;
export default Modal;
info.tsx
import React from 'react';
import { createRoot, Root } from 'react-dom/client';
import { IModalProps } from './interface';
import Modal from './modal';
interface Iprops extends Partial<IModalProps> {
afterClose?: () => void;
}
const MARK = 'react-render-root';
function reactRender(
node: React.ReactNode,
container: {
docu: DocumentFragment;
[MARK]: Root | null;
},
) {
if (!container[MARK]) {
container[MARK] = createRoot(container.docu);
}
container[MARK]?.render(node);
}
export function info(config: Iprops) {
const container: { docu: DocumentFragment; [MARK]: null | Root } = {
docu: document.createDocumentFragment() as DocumentFragment,
[MARK]: null,
};
//代码片段中
let timeoutId: ReturnType<typeof setTimeout>;
function render(props: any) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
const { close, children, ...rest } = props;
reactRender(
rest.open ? (
<Modal
onCancel={() => {
close();
}}
onOk={() => {
close();
}}
cancelText={null}
{...rest}
>
{children}
</Modal>
) : null,
container,
);
});
}
let currentConfig = { ...config, open: true } as any;
function destroy() {
config.onCancel?.();
container[MARK]?.unmount();
}
function close() {
currentConfig = {
...currentConfig,
open: false,
close: () => {
destroy();
},
};
render(currentConfig);
}
currentConfig.close = close;
render(currentConfig);
return {
close,
};
}
modal.tsx
import React, { FC, useEffect, useMemo, useRef } from 'react';
import { IModalProps } from './interface';
import { Portal } from './portal';
import './index.scss';
const Modal: FC<Partial<IModalProps>> = (props) => {
const {
open = true,
mask = true,
title = '提示',
cancelText = '取消',
okText = '确定',
closable = true,
footer,
onOk = () => {},
onCancel = () => {},
maskClosable = true,
children,
width,
classNames,
} = props;
const dom = useRef(null);
const myFooter = useMemo(() => {
if (footer === null) return null;
if (footer === undefined)
return (
<div className="modal_footer">
{cancelText === null ? null : typeof cancelText === 'string' ? (
<div className="modal_cancel modal_button_bx" onClick={onCancel}>
{cancelText}
</div>
) : (
cancelText
)}
{okText === null ? null : typeof okText === 'string' ? (
<div className="modal_ok modal_button_bx" onClick={onOk}>
{okText}
</div>
) : (
okText
)}
</div>
);
return footer;
}, [footer, cancelText, okText]);
const myTitle = useMemo(() => {
if (title === null) return null;
if (typeof title === 'string') {
return (
<div className="modal_header">
<div className="modal_title">
<span>{title}</span>
<span className=" close"></span>
</div>
{closable && (
<div className="modal_close close" onClick={onCancel}></div>
)}
</div>
);
}
return title;
}, [title, closable, mask, maskClosable]);
const maskOnClick = useMemo(() => {
return mask && maskClosable
? (e: any) => {
const isClickWrapper = dom.current === e?.target;
if (isClickWrapper) {
onCancel(e);
}
}
: () => {};
}, [mask, mask, onCancel]);
const wrapperClassName = (mask ? 'mask' : '') + ' zh_modal ' + (classNames||'');
const w =
width !== undefined
? isNaN(Number(width))
? width
: width + 'px'
: undefined;
return (
<Portal open={open}>
<div className={wrapperClassName} onClick={maskOnClick} ref={dom}>
<div className="modal_wrapper" style={{ width: w }}>
{/* 头部 */}
{myTitle}
<div className="modal_content">{children}</div>
{myFooter}
</div>
</div>
</Portal>
);
};
export default Modal;
portal.tsx
import React, { useLayoutEffect, useMemo } from 'react';
import { createPortal } from 'react-dom';
import { IPortalProps } from './interface';
export const Portal: React.FC<IPortalProps> = (props) => {
const { open, children } = props;
const container = useMemo(() => {
return document.createElement('div');
}, []);
function appendDiv() {
if (!container.parentElement) {
document.body.appendChild(container);
}
}
function removeDiv() {
if (container.parentElement) {
document.body.removeChild(container);
}
}
useLayoutEffect(() => {
if (open) {
appendDiv();
} else {
removeDiv();
}
return removeDiv;
}, [open]);
return createPortal(children, container);
};
index.scss
.zh_modal {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
font-size: 16px;
font-family: PingFang SC, PingFang SC-Regular;
text-align: center;
color: #333b56;
z-index: 9999;
&.mask {
background-color: rgba(0, 0, 0, 0.45);
}
.modal_wrapper {
border-radius: 8px;
position: absolute;
background-color: white;
top: 25%;
left: 50%;
transform: translate(-50%,-50%);
width: 500px;
box-shadow: 0px 6px 18px 0px rgba(0,0,0,0.13);
.modal_header {
width: 100%;
background: #f7f8fa;
border-radius: 8px 0px 0px 8px;
display: flex;
padding: 17px 0;
position: relative;
.modal_title {
font-weight: Medium;
flex-grow: 1;
height: 22px;
line-height: 1;
display: flex;
justify-content: center;
align-items: center;
}
.modal_close {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
width: 11px;
height: 11px;
overflow: hidden;
cursor: pointer;
&:before,
&::after {
content: '';
position: absolute;
right: 0;
top: 0;
right: 0;
left: calc(50% - 1px);
height: 100%;
width: 1px;
background: #333b56;
}
&:before {
transform: rotate(-45deg);
}
&:after {
transform: rotate(45deg);
}
}
.close {
width: 11px;
margin-right: 27px;
}
}
.modal_content {
padding-top: 28px;
}
.modal_footer {
display: flex;
padding: 20px 0 30px 0;
justify-content: center;
.modal_cancel {
background: transparent;
border: 1px solid #ebebeb;
color: #333b56;
cursor: pointer;
}
.modal_ok {
background: #d1a366;
color: #ffffff;
cursor: pointer;
}
.modal_button_bx {
padding: 9px 66px;
border-radius: 100px;
&:nth-child(2) {
margin-left: 20px;
}
}
}
}
}