【Day28】反馈元件 - Modal

元件介绍

Modal 元件为弹出相关元件提供了重要的基础建设,如 DialogPopoverDrawer...等等。

参考设计 & 属性分析

各家元件库参考

在 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 来实现的。


https://mui.com/api/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 以外,给他 isOpenonClose 就能够满足最低限度的需要了。

介面设计

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 主要分成三大部分,HeaderContentFooter

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

27 - Concern 最後整理的方式再来讲到 Rails 提供功能,主要目的在把相同逻辑 cod...

30-1 之 前言

这个是第四次参加铁人赛,并且也是第一次参加自我挑战组 ~ 想说这一次要放过自已一下, 就想说报个自我...

【资料结构】引线的练习实作

引线的练习实作 规则 为达到节省叶节点指向NULL的空间浪费 说明 1.在建立节点的同时,设置左右引...

Python Flask 架站笔记 第4天 版面设计与新增、更新、编辑功能按钮

延续上堂课的内容,本堂课新增1.产品建立成功页面。2.产品更新页面。3.产品资料呈现。(上堂课没有完...

【C#】Creational Patterns Abstract Factory Mode

The Abstract Factory pattern provides an interface...