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