首先先使用dumi来创建我们的组件库
可以参考这篇文件,这边就不再多做赘述
这是我们项目的目录,我们在src下创建一个Modal文件夹,并创建三个文件。
index.md
# Modal
```jsx
import { Button,Modal } from 'mavs-ui-lib';
import {useState} from 'react'
export default () => {
const [state,setstate] = useState(false)
return<div>
<Modal onCancel={()=>setstate(false)} open={state}></Modal>
<button onClick={()=>setstate(true)}>按钮</button>
</div>
};
index.tsx
import React from 'react';
import './index.less';
export interface ModalProps {
open?: boolean;
onCancel?: () => void;
}
const Modal: React.FC<ModalProps> = (props) => {
console.log(props);
return <div></div>;
};
export default Modal;
我们在项目中使用classnames库,方便我们对于组件类目的管理
npm install classnames --save
在antd的modal组件我们使用一个布尔值来控制模态框的显示隐藏。
const Modal: React.FC<ModalProps> = (props) => {
const { open } = props;
if (open) return <div></div>;
};
export default Modal;
查看antdmodal的dom结构,我们发现这个节点被挂载在body节点下。通常我们引入某个组件并使用,这个组件会存在与父组件的dom结构中。react-dom提供了一个api,createPortal允许我们把节点挂载在其他节点上。
import { createPortal } from 'react-dom';
...
if (open)
return createPortal(
<div>
<div
className={classnames({
'mavs-modal-root': true,
})}
></div>
</div>,
document.body,
);
};
当我们切换open时,我们发现节点被挂在了body上
接下来补充完接其他模态框结构
import classnames from 'classnames';
import React from 'react';
import { createPortal } from 'react-dom';
import './index.less';
export interface ModalProps {
open?: boolean;
onCancel?: () => void;
children?: React.ReactNode;
}
const Modal: React.FC<ModalProps> = (props) => {
const { open, children, onCancel } = props;
if (open)
return createPortal(
<div>
<div
className={classnames({
'mavs-modal-root': true,
})}
>
<div
onClick={onCancel && onCancel}
className={classnames({
'mavs-modal-mask': true,
})}
></div>
<div
className={classnames({
'mavs-modal-wrapper': true,
})}
>
<div
className={classnames({
'mavs-modal': true,
})}
>
<div
className={classnames({
'mavs-modal-content': true,
})}
>
{children}
</div>
</div>
</div>
</div>
</div>,
document.body,
);
};
export default Modal;
.mavs-modal{
width: 520px;
display: block;
background-color: #fff;
transition: all 0.5s;
border-radius: 10px;
margin: 0 auto;
position: relative;
top: 100px;
&-root{
}
&-mask{
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.3);
position: fixed;
left: 0;
top: 0;
transition: all 0.5s;
z-index: 1000;
}
&-wrapper{
position: fixed;
width: 100vw;
overflow: hidden;
height: 100vh;
z-index: 1001;
pointer-events: none;
left: 0;
top: 0;
}
&-content{
padding: 20px 24px;
}
}
查看一下,好像没有问题,本文结束
..
..
..
..
..
开个玩笑。目前只是把模态框显示出来了,但是缺少了动画效果。
antd-modal的弹窗有一个从按钮点击位置到视口中间的一个过渡效果,从a-b的过渡需要指定transform-orign属性。
查看antd-modal的元素,给元素内联样式添加了transform-orign属性。属性值应该就是触发弹窗开启的位置。
所以我们通过监听window的点击事件获取鼠标在屏幕中点击的位置。
const HandleClick = (e:any) =>{
const x = e.clientX
const y = e.clientY
}
useEffect(()=>{
if(open){
window.addEventListener('click',HandleClick)
}
return ()=>{
//不要忘记清除监听器
window.removeEventListener('click',HandleClick)
}
},[open])
只有打开模态框时候我们才需要获取鼠标点击位置。
transform-origin是转换起点是元素变形的起点,默认值为center,可以设置偏移量改变元素变形的起点。
我们对我们的modal简单画个图
transform-origin的的偏移量是相对本身的值。所以我们需要得到modal的到左边视口和顶部视口距离,我们可以使用ref来获取。
const [transformorign, settransformorign] = useState('');
const HandleClick = (e: any) => {
const rect = ref.current.getBoundingClientRect() as any;
const { left, top } = rect;
const x = e.clientX - left;
const y = e.clientY - top;
settransformorign(`${x}px ${y}px`);
};
这样我们就可以拿到元素的变形位置,接下来就可以写过渡的动画了。react写动画可以借助一些第三方库。我这里使用react-transition-group
npm install react-transition-group --save
import { Transition } from 'react-transition-group';
我们使用Transition组件,Transition有两个主要参数in和timeout。Transition传入一个回调函数,返回值为ReactNode,in为模态框显隐的布尔值。timeout为毫秒
in变化的时候,回调函数的参数会发生变化。参数一共有四种
exited,exiting,entering,entered,当in变为true,从exited变为entering,经过timeout毫秒变为entered。当in变为false,从entered变为exiting,经过timeout毫秒变为exited。
词穷了。。。直接上代码吧
index.tsx
import classnames from 'classnames';
import React, { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Transition } from 'react-transition-group';
import './index.less';
export interface ModalProps {
open?: boolean;
onCancel?: () => void;
children?: React.ReactNode;
}
const Modal: React.FC<ModalProps> = (props) => {
const { open, children, onCancel } = props;
const [transformorign, settransformorign] = useState('');
const ref = useRef<any>(null);
const HandleClick = (e: any) => {
const rect = ref.current.getBoundingClientRect() as any;
const { left, top } = rect;
const x = e.clientX - left;
const y = e.clientY - top;
console.log(x, y);
settransformorign(`${x}px ${y}px`);
};
useEffect(() => {
if (open) {
window.addEventListener('click', HandleClick);
}
return () => {
//不要忘记清除监听器
window.removeEventListener('click', HandleClick);
};
}, [open]);
return createPortal(
<div>
<Transition in={open} timeout={200}>
{(state) => {
return (
<div
className={classnames({
'mavs-modal-root': true,
})}
>
<div
onClick={onCancel && onCancel}
className={classnames({
'mavs-modal-mask': true,
[`mavs-modal-mask-${state}`]: true,
})}
></div>
<div
className={classnames({
'mavs-modal-wrapper': true,
})}
>
//这个元素和需要.mavs-modal位置和大小相同,用来获取top,left,不做展示。
<div
ref={ref}
className={classnames({
'mavs-modal-hidden': true,
})}
></div>
<div
style={{ transformOrigin: transformorign }}
className={classnames({
'mavs-modal': true,
[`mavs-modal-${state}`]: true,
})}
>
<div
className={classnames({
'mavs-modal-content': true,
})}
>
{children}
</div>
</div>
</div>
</div>
);
}}
</Transition>
</div>,
document.body,
);
};
export default Modal;
index.md
import { Button,Modal } from 'mavs-ui-lib';
import {useState} from 'react'
export default () => {
const [state,setstate] = useState(false)
return<div>
<Modal onCancel={()=>setstate(false)} open={state}>
<h1>tips...</h1>
<h1>tips...</h1>
<h1>tips...</h1>
</Modal>
<button style={{float:'right'}} onClick={()=>setstate(true)}>按钮</button>
<button style={{float:'left'}} onClick={()=>setstate(true)}>按钮</button>
</div>
};
index.less
.mavs-modal {
width: 520px;
display: block;
background-color: #fff;
transition: all 0.3s;
border-radius: 10px;
opacity: 1;
margin: 0 auto;
position: relative;
top: 100px;
&-entering {
visibility: visible;
transform: scale(0);
}
&-entered {
transform: scale(1);
}
&-exiting {
}
&-exited {
transform: scale(0);
}
&-hidden{
width: 520px;
display: block;
background-color: #fff;
transition: all 0.5s;
border-radius: 10px;
margin: 0 auto;
position: relative;
top: 100px;
}
&-root {
}
&-mask {
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.3);
position: fixed;
left: 0;
top: 0;
transition: all 0.4s;
z-index: 1000;
&-entered {
display: block;
}
&-exited {
opacity: 0;
pointer-events: none;
}
}
&-wrapper {
position: fixed;
width: 100vw;
overflow: hidden;
height: 100vh;
z-index: 1001;
pointer-events: none;
left: 0;
top: 0;
}
&-content {
padding: 20px 24px;
}
}
最后看一下效果