手把手教你搭建 React UI 组件库 (四) 实现 dialog 组件

1. 目标一: 渲染最简单的dialog

新建文件@/lib/Dialog/Dialog.tsx

import React from 'react';

const  Dialog: React.FC = ()=>{
    return <div>'我是Dialog'</div>
}

export default Dialog;
复制代码

新建文件: @/lib/Dialog/Dialog.example.tsx

import React from 'react';
import Dialog from './Dialog';

const DialogExample = ()=>{

    return(
        <Dialog/>
    )
}

export default DialogExample;
复制代码

修改@/lib/index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import Icon from './Icon';
import DialogExample from './Dialog/Dialog.example';


const fn =(e: React.MouseEvent<SVGSVGElement>)=>{
    console.log('haha',e.currentTarget);
}

ReactDOM.render(
    <div>
        <Icon icon='wechat'
              onClick={fn}
              onMouseMove={()=>{console.log('move');}}
        />
        <Icon icon='alipay'/>
        <DialogExample/>
    </div>
    ,
    document.getElementById('root')
)
复制代码

运行: yarn start

浏览器查看:http://localhost:8080/

image.png 我们目标实现了!

2. 目标二: 实现dialog 的visible的功能

需求: 能传递visible的值, 当visible为true时, 显示dialog, 为false隐藏

修改文件: @/lib/Dialog/Dialog.example.tsx

import React, {useState} from 'react';
import Dialog from './Dialog';

const DialogExample = () => {

    const [dialogVisible, setDialogVisible] = useState<boolean>(false);

    return (
        <div>
            <button onClick={()=>setDialogVisible(!dialogVisible)}>按钮</button>
            <Dialog visible={dialogVisible}/>
        </div>
    );
};

export default DialogExample;
复制代码

修改@/lib/Dialog/Dialog.tsx

import React from 'react';


interface PropsType {
    visible: boolean
}

const Dialog: React.FC<PropsType> = ({visible}) => {
    return visible ?
        <div>'我是Dialog'</div> :
        null;
};

export default Dialog;
复制代码

查看浏览器:

image.png 点击按钮后:

扫描二维码关注公众号,回复: 13697223 查看本文章

image.png

我们的目标成功了!

3. 目标三: 搭建dialog 架构

3.1 实现弹窗效果

实现弹窗弹出dialog 修改@/lib/Dialog/Dialog.tsx

import React, { Fragment } from 'react';
import './Dialog.scss'
import Icon from '../Icon';


interface PropsType {
    visible: boolean
}

const Dialog: React.FC<PropsType> =
    ({visible, children}) => {
    return visible ?
        <Fragment>
            <div className='sweetui-dialog-mask'></div>
            <div className='sweetui-dialog'>
                <div className='sweetui-dialog-close'>
                    <Icon icon='close'/>
                </div>
                <header className='sweetui-dialog-header'>标题</header>
                <main className='sweetui-dialog-main'>
                    {children}
                </main>
                <footer className='sweetui-dialog-footer'>
                    footer
                </footer>
            </div>
        </Fragment>:
        null;
};

export default Dialog;
复制代码

修改@/lib/Dialog/Dialog.example.tsx

import React, {useState} from 'react';
import Dialog from './Dialog';

const DialogExample = () => {

    const [dialogVisible, setDialogVisible] = useState<boolean>(false);

    return (
        <div>
            <button onClick={()=>setDialogVisible(!dialogVisible)}>按钮</button>
            <Dialog visible={dialogVisible}>
                <div>hi</div>
            </Dialog>
        </div>
    );
};

export default DialogExample;
复制代码

新建样式文件:lib/Dialog/Dialog.scss:

.sweetui-dialog{
  width: 10em;
  height: 10em;
  background-color: white;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);

  &-mask{
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: fade-out(black, 0.5);
  }
}
复制代码

查看浏览器:

image.png

点击按钮后:

image.png 我们实现了弹窗效果

3.2 优化代码

我们发现:@/lib/Dialog/Dialog.tsx, className有太多重复字段 'sweetui-dialog'

import React, { Fragment } from 'react';
import './Dialog.scss'
import Icon from '../Icon';


interface PropsType {
    visible: boolean
}

const Dialog: React.FC<PropsType> =
    ({visible, children}) => {
    return visible ?
        <Fragment>
            <div className='sweetui-dialog-mask'></div>
            <div className='sweetui-dialog'>
                <div className='sweetui-dialog-close'>
                    <Icon icon='close'/>
                </div>
                <header className='sweetui-dialog-header'>标题</header>
                <main className='sweetui-dialog-main'>
                    {children}
                </main>
                <footer className='sweetui-dialog-footer'>
                    footer
                </footer>
            </div>
        </Fragment>:
        null;
};

export default Dialog;
复制代码

我们用一个函数来生成className: 修改如下:

import React, {Fragment} from 'react';
import './Dialog.scss';
import Icon from '../Icon';


interface PropsType {
    visible: boolean
}

const scopedClass = (name?: string) => {
    return ['sweetui-dialog', name].filter(Boolean).join('-');
};
const sc = scopedClass;

const Dialog: React.FC<PropsType> =
    ({visible, children}) => {
        return visible ?
            <Fragment>
                <div className={sc('mask')}></div>
                <div className={sc()}>
                    <div className={sc('close')}>
                        <Icon icon='close'/>
                    </div>
                    <header className={sc('header')}>标题</header>
                    <main className={sc('main')}>
                        {children}
                    </main>
                    <footer className={sc('footer')}>
                        footer
                    </footer>
                </div>
            </Fragment> :
            null;
    };

export default Dialog;
复制代码

继续优化: 观察函数 scopedClass

const scopedClass = (name?: string) => {
    return ['sweetui-dialog', name].filter(Boolean).join('-');
};
复制代码

如果我想修改 里面的字符串sweetui-dialog, 很困难. 那么我们可以: 新建文件@/lib/utils.ts:

export const scopedClassMaker = (prefix: string) => {
    return (name?: string) => {
        return [prefix, name].filter(Boolean).join('-');
    };
};
复制代码

修改文件:@/lib/Dialog/Dialog.tsx

import React, {Fragment} from 'react';
import './Dialog.scss';
import Icon from '../Icon';
import {scopedClassMaker} from '../utils';


interface PropsType {
    visible: boolean
}

const scopedClass = scopedClassMaker('sweetui-dialog')
const sc = scopedClass;

const Dialog: React.FC<PropsType> =
    ({visible, children}) => {
        return visible ?
            <Fragment>
                <div className={sc('mask')}></div>
                <div className={sc()}>
                    <div className={sc('close')}>
                        <Icon icon='close'/>
                    </div>
                    <header className={sc('header')}>标题</header>
                    <main className={sc('main')}>
                        {children}
                    </main>
                    <footer className={sc('footer')}>
                        footer
                    </footer>
                </div>
            </Fragment> :
            null;
    };

export default Dialog;
复制代码

我们实现了我们的目标

4. 目标四: 给dialog 添加css

image.png

修改@/lib/Dialog/Dialog.scss

@import "../helper";

.sweetui-dialog {
  position: fixed;
  background: white;
  min-width: 20em;
  z-index: 2;
  border-radius: 4px;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);

  &-mask {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: fade_out(black, 0.5);
    z-index: 1;
  }

  &-header {
    font-size: 22px;
    padding: 8px 16px;
    border-bottom: 1px solid grey;
  }

  &-main {
    padding: 8px 16px;
    min-height: 6em;
  }

  &-footer {
    padding: 8px 16px;
    border-top: 1px solid grey;
    display: flex;
    justify-content: flex-end;
  }

  &-close {
    position: absolute;
    bottom: 100%;
    left: 100%;
    background: $main-color;
    width: 2em;
    height: 2em;
    border-radius: 50%;
    transform: translate(-50%, 50%);
    display: flex;
    justify-content: center;
    align-items: center;
    color: white;
  }
}

复制代码

新建: @/lib/_helper.scss

$main-color: #1890ff;
复制代码

新建: @/lib/index.scss

[class^=sweetui-] {
  box-sizing: border-box;
  &::after,
  &::before {
    box-sizing: border-box;
  }
}
button {
  box-sizing: border-box;
  height: 32px;
  margin: 0 4px;
  border-radius: 4px;
  &:first-child {
    margin-left: 0;
  }
  &:last-child {
    margin-right: 0;
  }
}
复制代码

修改@/lib/Dialog/Dialog.tsx


import React, {Fragment, ReactElement} from 'react';
import './Dialog.scss';
import Icon from '../Icon';
import {scopedClassMaker} from '../utils';


interface PropsType {
    visible: boolean;
    buttons?: Array<ReactElement>;
}

const scopedClass = scopedClassMaker('sweetui-dialog')
const sc = scopedClass;

const Dialog: React.FC<PropsType> =
    ({visible, children,buttons}) => {
        return visible ?
            <Fragment>
                <div className={sc('mask')}></div>
                <div className={sc()}>
                    <div className={sc('close')}>
                        <Icon icon='close'/>
                    </div>
                    <header className={sc('header')}>标题</header>
                    <main className={sc('main')}>
                        {children}
                    </main>
                    <footer className={sc('footer')}>
                        {buttons && buttons.map((button, index) =>
                            React.cloneElement(button, {key: index})
                        )}
                    </footer>
                </div>
            </Fragment> :
            null;
    };

export default Dialog;
复制代码

修改:@/lib/Dialog/Dialog.example.tsx

import React, {useState} from 'react';
import Dialog from './Dialog';

const DialogExample = () => {

    const [dialogVisible, setDialogVisible] = useState<boolean>(false);

    return (
        <div>
            <button onClick={()=>setDialogVisible(!dialogVisible)}>按钮</button>
            <Dialog
                visible={dialogVisible}
                buttons={[
                    <button onClick={()=>setDialogVisible(false)}>ok</button>,
                    <button  onClick={()=>setDialogVisible(false)}>cancle</button>
                ]}

            >
                <div>hi</div>
            </Dialog>
        </div>
    );
};

export default DialogExample;
复制代码

浏览器看效果

image.png

我们成功了

5. 目标五: 给mask和 closeIcon 添加关闭事件

修改@/lib/Dialog/Dialog.example.tsx

import React, {useState} from 'react';
import Dialog from './Dialog';

const DialogExample = () => {

    const [dialogVisible, setDialogVisible] = useState<boolean>(false);

    const closeDialog = ()=>setDialogVisible(false)
    return (
        <div>
            <button onClick={()=>setDialogVisible(!dialogVisible)}>按钮</button>
            <Dialog
                visible={dialogVisible}
                buttons={[
                    <button onClick={closeDialog}>ok</button>,
                    <button  onClick={closeDialog}>cancle</button>
                ]}
                onClose={closeDialog}
                closeOnClickMask={true}
            >
                <div>hi</div>
            </Dialog>
        </div>
    );
};

export default DialogExample;
复制代码

修改@/lib/Dialog/Dialog.tsx

import React, {Fragment, MouseEventHandler, ReactElement} from 'react';
import './Dialog.scss';
import Icon from '../Icon';
import {scopedClassMaker} from '../utils';


interface PropsType {
    visible: boolean;
    onClose: MouseEventHandler;
    buttons?: Array<ReactElement>;
    closeOnClickMask?: boolean;
}

const scopedClass = scopedClassMaker('sweetui-dialog')
const sc = scopedClass;

const Dialog: React.FC<PropsType> =
    ({
         visible,
         children,
         buttons,
        onClose,
        closeOnClickMask,
    }) => {
        const closeOnMask: MouseEventHandler = (e)=>{
            if(closeOnClickMask){
                onClose(e)
            }
        }

        return visible ?
            <Fragment>
                <div className={sc('mask')} onClick={closeOnMask}> </div>
                <div className={sc()}>
                    <div className={sc('close')} onClick={onClose}>
                        <Icon icon='close' />
                    </div>
                    <header className={sc('header')}>标题</header>
                    <main className={sc('main')}>
                        {children}
                    </main>
                    <footer className={sc('footer')}>
                        {buttons && buttons.map((button, index) =>
                            React.cloneElement(button, {key: index})
                        )}
                    </footer>
                </div>
            </Fragment> :
            null;
    };

export default Dialog;
复制代码

我们的目标成功了

6. 目标六: 解决 mask 容易被遮挡的问题

发现问题: 我们尝试修改@/lib/Dialog/Dialog.example.tsx

  1. 修改button 的z-index 为10
  2. 修改Dialog 的父类z-index 为9

import React, {useState, Fragment} from 'react';
import Dialog from './Dialog';

const DialogExample = () => {

    const [dialogVisible, setDialogVisible] = useState<boolean>(false);

    const closeDialog = ()=>setDialogVisible(false)
    return (
        <Fragment>

            <button
                onClick={()=>setDialogVisible(!dialogVisible)}
                style={{position:'relative', zIndex:10}} 
            >按钮</button>
            
            <div style={{position:'relative', zIndex:9}}>
                <Dialog
                    visible={dialogVisible}
                    buttons={[
                        <button onClick={closeDialog}>ok</button>,
                        <button  onClick={closeDialog}>cancle</button>
                    ]}
                    onClose={closeDialog}
                    closeOnClickMask={true}
                >
                    <div>hi</div>
                </Dialog>
            </div>
        </Fragment>
    );
};

export default DialogExample;
复制代码

查看浏览器, 点击按钮:

image.png

问题: 按钮居然是悬浮在mask上面

我们尝试再去修改 mask 的 z-index 为999

image.png

发现: 按钮居然还是是悬浮在mask上面

原因: 按钮的z-index 大于mask父类div的z-index, 根据层叠上下文规则, 按钮永远是悬浮在mask上面

解决办法: 将dialog放到body上, 那么就无法被body内部元素所遮挡

修改@/lib/Dialog/Dialog.tsx

image.png

image.png

查看浏览器: 修改mask的z-index 为11, 就能发现mask不会被按钮遮挡

image.png

我们的目标成功了

7. 目标七: 创建简单调用的Alert 的 api

现阶段调用dialog很麻烦, 需要创建一个useState, 引入dialog, 传入参数.

我的目标:我希望一句话调用 Alert的dialog:

import {alert} from './dialog';

<button onClick={() => alert('1')}>alert</button>
复制代码

点击按钮: 就会出现:

image.png

  1. 先修改代码:@/lib/Dialog/Dialog.tsx
export const alert = (content: ReactNode)  => {
    const close = () => {
        ReactDOM.render(React.cloneElement(component, {visible: false}), div);
        ReactDOM.unmountComponentAtNode(div);
        div.remove();
    };
    const component =
        <Dialog
            visible={true}
            onClose={() => {
                close();
            }}>
            {content}
        </Dialog>;
    const div = document.createElement('div');
    document.body.append(div);
    ReactDOM.render(component, div);
    return close;
};
复制代码

image.png

  1. 修改: @/lib/Dialog/Dialog.example.tsx
import {alert} from './dialog';


<button onClick={() => alert('1')}>alert</button>
复制代码

image.png

  1. 查看浏览器: 点击alert按钮:

成功弹出dialog

image.png

我们成功了

8. 目标八: 实现易用的confirm 的api

8.1 实现简易confirm api

目标: 想实现下面的简单调用, 不用创建 useState,就能简单使用的confirm

import Dialog, {alert, confirm} from './Dialog';


<button onClick={() => {
                const closeConfirm = confirm(
                'content',
                [
                    <button onClick={()=>closeConfirm()}>ok</button>,
                    <button onClick={()=>closeConfirm()}>cancle</button>
                ],
            )}}>
                confirm
</button>

复制代码

实现:

  1. 修改@/lib/Dialog/Dialog.tsx
export const confirm = (content: ReactNode, buttons: Array<ReactElement>)  => {
    const close = () => {
        ReactDOM.render(React.cloneElement(component, {visible: false}), div);
        ReactDOM.unmountComponentAtNode(div);
        div.remove();
    };
    const component =
        <Dialog
            visible={true}
            onClose={() => {
                close();
            }}
            buttons={buttons}
        >
            {content}
        </Dialog>;
    const div = document.createElement('div');
    document.body.append(div);
    ReactDOM.render(component, div);
    return close;
};
复制代码

image.png

  1. 修改@/lib/Dialog/Dialog.example.tsx
import React, {useState, Fragment} from 'react';
import Dialog, {alert, confirm} from './Dialog';

const DialogExample = () => {

    const [dialogVisible, setDialogVisible] = useState<boolean>(false);

    const closeDialog = () => setDialogVisible(false);
    return (
        <Fragment>
            <button onClick={() => alert('1')}>alert</button>
            <div>-------</div>
            <button onClick={() => {
                const closeConfirm = confirm(
                'content',
                [
                    <button onClick={()=>closeConfirm()}>ok</button>,
                    <button onClick={()=>closeConfirm()}>cancle</button>
                ],
            )}}>
                confirm
            </button>
            <div>-------</div>
            <button
                onClick={() => setDialogVisible(!dialogVisible)}
            >按钮
            </button>
            <Dialog
                visible={dialogVisible}
                buttons={[
                    <button onClick={closeDialog}>ok</button>,
                    <button onClick={closeDialog}>cancle</button>
                ]}
                onClose={closeDialog}
                closeOnClickMask={true}
            >
                <div>hi</div>
            </Dialog>

        </Fragment>
    );
};

export default DialogExample;
复制代码

查看浏览器:

image.png

点击confirm按钮:

image.png 成功实现目标!

猜你喜欢

转载自juejin.im/post/7074862644548599838