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/
我们目标实现了!
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;
复制代码
查看浏览器:
点击按钮后:
我们的目标成功了!
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);
}
}
复制代码
查看浏览器:
点击按钮后:
我们实现了弹窗效果
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
修改@/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;
复制代码
浏览器看效果
我们成功了
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
- 修改button 的z-index 为10
- 修改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;
复制代码
查看浏览器, 点击按钮:
问题: 按钮居然是悬浮在mask上面
我们尝试再去修改 mask 的 z-index 为999
发现: 按钮居然还是是悬浮在mask上面
原因: 按钮的z-index 大于mask父类div的z-index, 根据层叠上下文规则, 按钮永远是悬浮在mask上面
解决办法:
将dialog放到body上, 那么就无法被body内部元素所遮挡
修改@/lib/Dialog/Dialog.tsx
查看浏览器: 修改mask的z-index 为11, 就能发现mask不会被按钮遮挡
我们的目标成功了
7. 目标七: 创建简单调用的Alert 的 api
现阶段调用dialog很麻烦, 需要创建一个useState, 引入dialog, 传入参数.
我的目标:我希望一句话调用 Alert的dialog:
import {alert} from './dialog';
<button onClick={() => alert('1')}>alert</button>
复制代码
点击按钮: 就会出现:
- 先修改代码:
@/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;
};
复制代码
- 修改:
@/lib/Dialog/Dialog.example.tsx
import {alert} from './dialog';
<button onClick={() => alert('1')}>alert</button>
复制代码
- 查看浏览器: 点击alert按钮:
成功弹出dialog
我们成功了
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>
复制代码
实现:
- 修改
@/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;
};
复制代码
- 修改
@/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;
复制代码
查看浏览器:
点击confirm按钮:
成功实现目标!