React hook 实现弹窗

·  阅读 1008

平时项目使用弹窗,用 npm 仓库的就可以。
或者antd design 的也不错。
我尝试用react hook 实现一个弹窗。参数配置参考蚂蚁文档的部分。

支持: <Modal {...props}/> 方式调用
支持: Modal.sucess('...'); 方式调用 支持:Modal.success({...porps}); 方式调用

image.png

代码示例

export default function Main() {
    const [isVisible, setIsVisible] = useState(false);
    const showModal = () => {
        setIsVisible(true);
    };
    const handleOk = () => {
        setIsVisible(false);
    };
    const handleCancel = () => {
        setIsVisible(false);
    };

    //函数式引用
    const showInstance = () => {
        Modal.success("ok");
    };
    return (
        <div>
            <button className={styles.btn} onClick={showModal}>
                open Modal
            </button>
            <button className={styles.btn} onClick={showInstance}>
                实例弹窗
            </button>
            <Modal
                title="标题"
                visible={isVisible}
                onOk={handleOk}
                onCancel={handleCancel}
                style={{ borderRadius: "4px", background: "#202229" }} // 设置浮层的样式
                width={520} //设置浮层的宽度
                centered={true} //垂直居中显示
                closable={true} //是否显示右上角关闭按钮 默认显示true
                closeIcon = {} //自定义关闭按钮
                mask={true} //是否显示遮罩 默认显示true
                maskCloseable={false} //点击遮罩是否关闭 默认关闭true
                maskStyle={{ background: "rgba(0, 0, 0, 0.85)" }} //遮罩样式
                okText="确认"
                cancelText="取消"
                footer={
                    <div>
                       <button onClick={handleOk}>测试取消</button>
                    </div>
                }
            >
                <div style={{ color: "#fff" }}>这里是自定义的内容</div>
            </Modal>
        </div>
    );
}
复制代码

代码结构

    Modal
        |--- / Dialog  //核心弹窗组件
        |--- / RootNode // 动态节点 移除挂载部分
        |--- index.js // 核心弹窗静态方法,函数引用部分,导出
复制代码

image.png

代码入口

import React from "react";
import ReactDOM from "react-dom";
import Dialog from "./Dialog/index";
import Modal from "./RootNode/index";

const show = (props) => {
    let timer = null;

    const div_wrap = document.createElement("div");
    document.body.appendChild(div_wrap);

    // 移除节点
    function removeNode(fn) {
        ReactDOM.unmountComponentAtNode(div_wrap);
        div_wrap && document.body.removeChild(div_wrap);
        if (typeof fn === "function") {
            fn();
        }
    }

    const _onCancel = () => {
        timer = setTimeout(() => {
            removeNode(props.onCancel);
            clearTimeout(timer);
        }, 350);
    };
    const _onOk = () => {
        timer = setTimeout(() => {
            removeNode(props.onOk);
            clearTimeout(timer);
        }, 350);
    };

    const _props = {
        visible: true,
        onOk: _onOk,
        onCancel: _onCancel,
        mask: true,
        maskCloseable: false,
    };

    ReactDOM.render(
        <Dialog {..._props}  >
            {props.content}
        </Dialog>,
        div_wrap
    );
    return null;
};

Modal.success = function success(content) {
    common(content, "success");
};

Modal.warning = function warning(content) {
    common(content, "warning");
};

Modal.info = function info(content) {
    common(content, "info");
};

// 合并参数为对象
function common(content, type) {
    let obj = {};
    if (typeof content === "object") {
        obj = {
            ...content,
        };
    } else {
        obj = {
            content,
            _type: type,
        };
    }
    return show(obj);
}

export default Modal;

复制代码

根节点挂载和移除

import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
import styles from './index.less';
import Dialog from '../Dialog/index';

function Modal(props) {
    const { visible } = props;
    const [rootNode, setRootNode] = useState(null);

    // 监听动画结束的时间,关闭弹窗时,移除dom节点
    function aniEnd(endTime) {
        return endTime * 1000;
    }

    // 监听visible,和 根节点rootNode 渲染弹窗
    useEffect(() => {
        const get_rootNode = document.querySelector('#modal_wrap');

        if (visible) {
            if (!get_rootNode) {
                const modal_wrap = document.createElement('div');
                modal_wrap.setAttribute('id', 'modal_wrap');
                modal_wrap.setAttribute('class', `${styles.modal_wrap}`);
                document.body.appendChild(modal_wrap);
                setRootNode(modal_wrap);
            } else {
                setRootNode(get_rootNode);
            }
        } else {
            // 弹窗消失,等消失动画完成,将根节点清除
            if (rootNode) {
                const timer = setTimeout(() => {
                    setRootNode(null);
                    clearTimeout(timer);
                }, 500);
            } else {
                // 根节点清除后,移除dom
                const get_rootNode = document.querySelector('#modal_wrap');
                get_rootNode && document.body.removeChild(get_rootNode);
            }
        }
    }, [visible, rootNode]);

    return rootNode ? ReactDOM.createPortal(<Dialog {...props} aniEnd={aniEnd} />, rootNode) : null;
}

export default React.memo(Modal);

复制代码
.modal_wrap {
    position: absolute;
    width: 100vw;
    height: 100vh;
    left: 0;
    top: 0;
}

复制代码

核心弹窗

import React, { useState, useEffect, forwardRef } from 'react';
import classNames from 'classnames';
import styles from './index.less';

function Dialog(props, ref) {
    const {
        visible,
        onOk,
        onCancel,
        width,
        style,
        centered,
        title,
        closable, 
        closeIcon, 
        mask, 
        maskStyle,
        maskCloseable, 
        okText,
        // okStyle, 
        cancelText,
        footer, 
        // _type, // 消息类型
        // aniEnd,
    } = props;

    const modalStyle = {
        width: `${width}px`,
        ...style,
    };

    const [isOpen, setIsOpen] = useState(false);

    const modalClass = classNames({
        [`${styles.modal}`]: true,
        [`${styles.modalCenter_ani}`]: centered, // 垂直居中
        [`${styles.modalShow_ani}`]: centered ? false : true, // 非垂直居中
        [`${styles.modalHidden_ani}`]: !isOpen && !centered, // 非垂直居中 消失
        [`${styles.modalHidden_center_ani}`]: !isOpen && centered, // 垂直居中 消失
    });

    const maskClass = classNames({
        [`${styles.mask}`]: true,
        [`${styles.mask_hidden_ani}`]: !isOpen,
    });

    const closeBtn = (
        <span className={styles.close} onClick={handle_cancel}>
            X
        </span>
    );

    const _closeIcon = closeIcon
        ? React.cloneElement(closeIcon, {
              ...closeIcon.props,
              onClick: handle_cancel,
          })
        : closeBtn;

    function handle_ok() {
        setIsOpen(false);
        onOk && onOk();
    }
    function handle_cancel() {
        setIsOpen(false);
        onCancel && onCancel();
    }

    // 监控动画结束的回调
    function watchAniEnd(e) {
        // console.log(e);
        // typeof aniEnd === "function" && aniEnd(e.elapsedTime);
    }

    useEffect(() => {
        setIsOpen(visible);
    }, [visible]);

    return (
        <>
            {mask ? (
                <div
                    onClick={() => {
                        maskCloseable && onCancel();
                    }}
                    style={maskStyle}
                    className={maskClass}
                    ref={ref}
                    onAnimationEnd={watchAniEnd}
                ></div>
            ) : null}

            <div style={modalStyle} className={modalClass}>
                {title ? <div className={styles.title}>{title}</div> : null}
                <div className={styles.closeIcon}>{closable ? _closeIcon : null}</div>
                <div className={styles.body}>{props.children}</div>
                <div className={styles.footer}>
                    {footer === null ? null : React.isValidElement(footer) ? (
                        footer
                    ) : (
                        <div className={styles.footer_btn}>
                            <button className={styles.btn_ok} onClick={handle_ok}>
                                {okText}
                            </button>
                            <button className={styles.btn_cancel} onClick={handle_cancel}>
                                {cancelText}
                            </button>
                        </div>
                    )}
                </div>
            </div>
        </>
    );
}

export default forwardRef(Dialog);

Dialog.defaultProps = {
    centered: false,
    closable: false,
    mask: true,
    maskCloseable: 'false',
    okText: '确认',
    cancelText: '取消',
};

复制代码
.mask {
    position: absolute;
    top: 0;
    left: 0;
    width: 100vw;
    height: 100vh;
    background-color: rgba(0, 0, 0, 0.8);
    z-index: 1000;
}
.mask_hidden_ani {
    animation: maskHidden 0.4s forwards;
}

.modal {
    position: absolute;
    left: 50%;
    top: -50%;
    transform: translateX(-50%);
    width: 500px;
    height: auto;
    border-radius: 4px;
    display: block;
    z-index: 1001;
    // background-color: #ccc;
    padding:24px;
    background: rgb(32, 34, 41);
    transform-origin: 0 0;
    transition: all 1s;
}

.modalShow_ani {
    transform-origin: 0 0;
    animation: boxShow 0.3s forwards;
}
.modalHidden_ani {
    animation: boxHidden 0.3s forwards;
}

.modalCenter_ani {
    top: 50%;
    transform: translate(-50%, -50%);
    animation: boxShow_center 0.3s forwards;
}
.modalHidden_center_ani{
    animation: boxHidden_center 0.3s forwards;
}

.visible {
    display: block;
}

.title {
    height: 30px;
    line-height: 30px;
    color:#fff;
    margin-top: 8px;
    font-size: 18px;
    // border-bottom: 1px solid #fff;
}
.closeIcon {
    position: absolute;
    right: 0;
    top: 0;
    cursor: pointer;
}
.close {
    position: absolute;
    top: 10px;
    right: 10px;
    width: 20px;
    height: 20px;
    line-height: 20px;
    padding: 5px;
    text-align: center;
    cursor: pointer;
    color:#fff;
}

.body{
    color:#fff;
    min-height: 84px;
    font-size: 14px;
    padding-top: 8px;
}

.footer {
    width: 100%;
    min-height: 32px;
    // padding: 0 24px 24px 24px;
    button {
        outline: none;
        border:none;
        float: right;
        cursor: pointer;
        color: #fff;
    }
}
.footer_btn {
    // border-top: 1px solid #fff;
    width: 100%;
    height: 100%;
    color:#fff;
    

    .btn_ok {
        margin-left: 16px;
        width: 96px;
        height: 32px;
        line-height: 32px;
        border-radius: 2px;
        text-align: center;
        background-color: #3e7dff;
        &:hover {
            background: #709fff;
        }
    }
    .btn_cancel {
        height: 32px;
        width: 96px;
        line-height: 32px;
        border-radius: 2px;
        text-align: center;
        background-color: #2a2d35;
        &:hover {
            background: #414554;
        }
    }
}

@keyframes boxShow {
    from {
        top: -20%;
        opacity: 1;
       
    }
    to {
        top: 20%;
        opacity: 1;
        transform: translateX(-50%);
      
    }
}
@keyframes boxShow_center {
    from {
        top: -20%;
        opacity: 0;
    }
    to {
        top: 50%;
        opacity: 1;
    }
}
@keyframes boxHidden {
    0% {
        opacity: 1;
        top:20%;
    }

    50%{
        opacity: 0;
    }

    100% {
        opacity: 0;
        top:-20%;
        // transform: scale(0.2);
    }
}
@keyframes boxHidden_center {
    0% {
        opacity: 1;
        top:50%;
        // left: 50%;
    }

    50%{
        opacity: 0;
    }

    100% {
        opacity: 0;
        top:-20%;
        // left: 50%;
        // transform: scale(0.2);
    }
}
@keyframes maskHidden {
    from {
        opacity: 1;
    }
    to {
        opacity: 0;
    }
}

复制代码

里边用了很多定时器。还有需要改进的地方。请各位指正。peace.

分类:
前端
标签: