Modal
元件为弹出相关元件提供了重要的基础建设,如 Dialog
、Popover
、Drawer
...等等。
各家元件库参考
在 Antd 元件当中,对於 Modal 就直接定义为 对话框
元件,其使用时机是当系统流程当中需要用户处理额外事务,但又不希望跳转页面以打断目前工作流程时,提供一个弹出互动框解决方案。
https://ant.design/components/modal-cn/
但对於 MUI 来说,他就是另一个思维,以下是他对於 Modal 的定义:
The modal component provides a solid foundation for creating dialogs, popovers, lightboxes, or whatever else.
意思就是说,Antd 的 Modal 对於 MUI 来说其实已经是一个 Dialog 元件,他是 Modal 元件的延伸应用。
换句话说,MUI 的 Modal 是一个基础建设元件,所以类似这种弹窗式互动的元件,例如 对话框(Dialog)
、弹出提示框(Popovers)
、菜单(Menu)
、抽屉(Drawer)
...等等元件,都是能够基於 Modal 来实现的。
我们再来看 Bootstrap,Bootstrap 里面的 Modal 也是跟 Antd 一样,是直接说 Modal 是一个对话视窗:
https://bootstrap5.hexschool.com/docs/5.0/components/modal/
我们简单看了上述几种说词之後,我觉得 MUI 这样的思维还是比较吸引我,毕竟这样的方式比较能够重复利用相同逻辑的程序码。
就像 MUI 所提到的,我们想想看,其实很多元件是换汤不换药,明明逻辑是一样的,为什麽就只因为样式不一样我们就要把同样的逻辑再重新做一次呢?
所以今天我们想要演示的题目,就是我们先来做一个简单的 Modal,再来,我们会利用这个 Modal 来打造我们的 Dialog。
使用方式
接下来我们来看一下各家元件库是怎麽设计元件的使用介面,首先来看 Antd:
<Button type="primary" onClick={showModal}>
Open Modal
</Button>
<Modal
title="Basic Modal"
visible={isModalVisible}
onOk={handleOk}
onCancel={handleCancel}
>
<p>Some contents...</p>
<p>Some contents...</p>
<p>Some contents...</p>
</Modal>
再来我们也看一下 MUI:
<Button onClick={handleOpen}>Open modal</Button>
<Modal
open={open}
onClose={handleClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box sx={style}>
<Typography id="modal-modal-title" variant="h6" component="h2">
Text in a modal
</Typography>
<Typography id="modal-modal-description" sx={{ mt: 2 }}>
Duis mollis, est non commodo luctus, nisi erat porttitor ligula.
</Typography>
</Box>
</Modal>
在 Antd 当中,他的介面可以成为我们 Dialog 元件的参考,MUI 的 Modal props 介面应该就足够做一个基础建设,综合以上的介面,一个最简易的 Modal 应该是可以长这样:
<Button onClick={handleOpen}>Open Modal</Button>
<Modal
isOpen={isOpen}
onClose={handleClose}
>
{children}
</Modal>
上述的介面当中,isOpen
这个 props 来控制 Modal 出现还是消失,而画面上必需要有一个地方可以触发 isOpen
变成 true
,这边的范例统一都是用一颗 Button
来触发。再来,要关闭一个 Modal 的时候,我们可以点击 children 以外的区域,或是将来变成 Dialog 的时候,可以点击 Header 的 <CloseIcon />
,所以在 Modal 里面,除了 children 以外,给他 isOpen
和 onClose
就能够满足最低限度的需要了。
Modal props
属性 | 说明 | 类型 | 默认值 |
---|---|---|---|
isOpen | 是否显示 | boolean | false |
children | 内容 | ReactNode | |
onClose | 触发关闭 | function | |
animationDuration | 定义动画完成一次周期的时间(ms) | number | 200 |
hasMask | 是否显示遮罩 | boolean | true |
Dialog props
属性 | 说明 | 类型 | 默认值 |
---|---|---|---|
isOpen | 是否显示 | boolean | false |
title | 标题内容 | function | |
children | 内容 | ReactNode | |
onClose | 触发关闭 | function | |
onSubmit | 触发确认事件 | function |
在前几篇的 Drawer 当中,其实同样的逻辑我们已经做过一次,所以请原谅我这次当个坏宝宝,做个错误示范,我就直接把他抄过来。
如果你今天想要当一个乖宝宝,那应该怎麽做呢?假设时光倒流,我们应该是需要先做出 Modal
这个基础建设元件,然後再来利用 Modal 来实现出我们的 Drawer
元件,这样就不会出现同样的逻辑重复出现在 Modal 和 Drawer 了。
但没关系,这个风风雨雨的社会,浪子有一天还是有机会回头,虽然以前做坏,但现在要做一个善良的歹囝,薰莫阁食,酒袂阁焦(写 code 写到唱起来 XDD)。
等一下我们还是会示范一下怎麽当好宝宝,我们会把 Modal
用来实现 Dialog
。
Modal
以下就是我们这次的 Modal:
const Modal = ({
isOpen,
onClose,
animationDuration,
children,
hasMask,
}) => {
const [removeDOM, setRemoveDOM] = useState(!isOpen);
useEffect(() => {
if (isOpen) {
setRemoveDOM(false);
} else {
setTimeout(() => {
setRemoveDOM(true);
}, (animationDuration + 100));
}
}, [animationDuration, isOpen]);
return !removeDOM && (
<Portal>
{hasMask && (
<Mask
$isOpen={isOpen}
$animationDuration={animationDuration}
onClick={onClose}
/>
)}
<ModalWrapper
$isOpen={isOpen}
>
{children}
</ModalWrapper>
</Portal>
);
};
因为之前在 Drawer
有详细说明过,这次就简单来复习。
我们的 Modal 一样也是全版盖在画面上,所以为了避免被盖住的问题,我们一样用 Portal
把他传送到最上层。
再来 Portal 的内容当中,会有 Mask
以及 ModalWrapper
,其中 ModalWrapper
里面就是放我们的 children,也就是对话窗的内容。
另外因为前面 MUI 有说,Popover、Menu 也是有机会可以使用 Modal 来实现,而这两个元件他是没有 Mask 遮罩的,所以我们用一个 hasMask
的 boolean 来决定是不是要使用遮罩来弱化背景。
最後我们使用 removeDOM
这个 boolean 帮助我们在关闭 Modal 之後的 100ms,也就是 Modal 消失动画结束之後,我们把这个被关闭的元件从 DOM 当中移除掉。
那这样我们就能够做出像下面这样的 Modal 了:
import Button from '../components/Button';
import Modal from '../components/Modal';
const ModalDemo = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
<Modal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
>
<div style={{ background: '#FFF' }}>Modal content</div>
</Modal>
</>
);
};
基於 Modal 实现的 Dialog
先给大家看一下我们要实现的 Dialog 长这样,这边我是以 Antd 的 Basic Modal 样式为例:
这个 Dialog 主要分成三大部分,Header
、Content
、Footer
。
Header 当中我们需要显示标题,也需要有一个叉叉按钮让我们可以关闭对话框。
const Header = ({ title, onClose }) => (
<HeaderWrapper>
{title}
<CloseButton onClick={onClose}>
<CloseIcon />
</CloseButton>
</HeaderWrapper>
);
Content 的部分就是我们对话框的内容,这边就直接显示 children。
Footer 主要是两个按钮,一个是确认按钮,一个取消按钮。
const Footer = ({ onClose, onSubmit }) => (
<FooterWrapper>
<ButtonGroup>
<Button variant="outlined" onClick={onClose}>取消</Button>
<Button onClick={onSubmit}>确认</Button>
</ButtonGroup>
</FooterWrapper>
);
所以我们基於 Modal 的 Dialog 按照上述的描述就长这样了:
import Modal from '../Modal';
import Header from './Header';
import Footer from './Footer';
const Dialog = ({
isOpen,
onClose,
onSubmit,
title,
children,
}) => (
<Modal
isOpen={isOpen}
onClose={onClose}
>
<DialogWrapper $isOpen={isOpen}>
<Header title={title} onClose={onClose} />
<Content>
{children}
</Content>
<Footer onClose={onClose} onSubmit={onSubmit} />
</DialogWrapper>
</Modal>
);
特别说明一下 DialogWrapper
是我们对话框的背景,主要是做一些样式的设定以及布局。
样式的部分例如对话框的 背景颜色
、宽度
、圆角
、阴影
以及 出现及消失的动画
:
const DialogWrapper = styled.div`
width: calc(100vw - 40px);
max-width: 520px;
border-radius: 4px;
background: #FFF;
box-shadow: 0 3px 6px -4px #0000001f, 0 6px 16px #00000014, 0 9px 28px 8px #0000000d;
animation: ${(props) => (props.$isOpen ? showDialog : hideDialog)} 200ms ease-in-out forwards;
`;
动画的部分,出现的时候我希望他可以有淡入的效果,并且让他微微从小变大,好像有从距离远的地方服出来的感觉。消失的时候就反过来,让他有淡出效果,并且让他微微缩小,有种退到後面去感觉:
import styled, { keyframes } from 'styled-components';
const hideDialog = keyframes`
0% {
transform: scale(1);
opacity: 1;
}
100% {
transform: scale(0.9);
opacity: 0;
}
`;
const showDialog = keyframes`
0% {
transform: scale(0.9);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
`;
最後来展示我们用 Modal 实现 Dialog 的成果:
const DialogDemo = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Dialog</Button>
<Dialog
isOpen={isOpen}
onClose={() => setIsOpen(false)}
title={(
<div style={{ fontWeight: 500 }}>
Title
</div>
)}
>
<div>
<div>Some contents...</div>
<div>Some contents...</div>
<div>Some contents...</div>
</div>
</Dialog>
</>
);
};
Modal 元件原始码:
Source code
Dialog 元件原始码:
Source code
Storybook:
Modal
<<: DAY26 - 网站分析工具介绍 - 质化与量化的结合 - Matomo
>>: [Day28] - Django-REST-Framework API 期末专案实作 (三)
27 - Concern 最後整理的方式再来讲到 Rails 提供功能,主要目的在把相同逻辑 cod...
这个是第四次参加铁人赛,并且也是第一次参加自我挑战组 ~ 想说这一次要放过自已一下, 就想说报个自我...
引线的练习实作 规则 为达到节省叶节点指向NULL的空间浪费 说明 1.在建立节点的同时,设置左右引...
延续上堂课的内容,本堂课新增1.产品建立成功页面。2.产品更新页面。3.产品资料呈现。(上堂课没有完...
The Abstract Factory pattern provides an interface...