使用react模仿ant design写一个modal组件

774 阅读4分钟

首先先使用dumi来创建我们的组件库

可以参考这篇文件,这边就不再多做赘述

image.png

这是我们项目的目录,我们在src下创建一个Modal文件夹,并创建三个文件。

image.png

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;

image.png

查看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上

image.png

接下来补充完接其他模态框结构

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;
    }
}

Kapture 2024-05-10 at 10.45.00.gif 查看一下,好像没有问题,本文结束 .. ..

..

..

..

开个玩笑。目前只是把模态框显示出来了,但是缺少了动画效果。

Kapture 2024-05-10 at 15.35.36.gif antd-modal的弹窗有一个从按钮点击位置到视口中间的一个过渡效果,从a-b的过渡需要指定transform-orign属性。

image.png

查看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,可以设置偏移量改变元素变形的起点。

image.png

我们对我们的modal简单画个图

image.png 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有两个主要参数intimeoutTransition传入一个回调函数,返回值为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;
  }
}

最后看一下效果

Kapture 2024-05-10 at 18.12.39.gif