react封装Modal弹出框

3,057 阅读4分钟

1.需求导向

1.1.需求导向

最近的项目中频繁的使用modal弹出框,虽然已经有很多库可用于在 React 中创建响应式、可访问的模态框。
但是,有时在设计ui中存在这些库无法完全满足,因此需要自定义modal是非常有必要的。

1.2.实现功能

  • 1.可以底部自定义按钮的样式,文字
  • 2.可以自定义内容
  • 3.使用Esc键可以关闭模态框
  • 4.使用shift可以自动聚焦关闭按钮,使用Enter实现关闭
  • 5.弹出modal,页面禁用
  • 6.按下Shift + Tab将焦点移动到上一个可选项卡元素

2.封装思路

  • 将该model组件分成两个部分,头部以及主体部分,头部包含取消,标题部分,主体部分包含取消确定按钮,中间内容部分,并且可以使用键盘Esc操作

3.安装依赖

yarn add react-dom 
yarn add styled-components
yarn add react-focus-lock

4.开始封装

4.1使用createPortal封装外部基础modal

createPortal是ReactDOMAPI 的一部分,它允许我们在父组件之外渲染 React 组件。我们通常在
根div 元素中渲染 React 应用程序,但是通过使用门户,我们也可以在根 div 之外渲染一个组件
//Model.jsx
import {Fragment} from 'react'
import {createPortal}from 'react-dom';
import {
  Wrapper,
  Header,
  StyledModal,
  HeaderText,
  CloseButton,
  Content,
  Backdrop,
} from './style';
 const Modal = ({
  isShown,
  hide,
  modalContent,
  headerText,
  closeIcon
}) => {
  const modal = (
    <Fragment>
      <Backdrop />
      <Wrapper>
        <StyledModal>
          <Header>
            <HeaderText>{headerText}</HeaderText>
            <CloseButton onClick={hide}>{closeIcon}</CloseButton>
          </Header>
          <Content>{modalContent}</Content>
        </StyledModal>
      </Wrapper>
    </Fragment>
  );

  return isShown ? createPortal(modal, document.body) : null;
};
export default Modal
//style.js
import styled from "styled-components";
export const Wrapper = styled.div`
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 700;
  width: inherit;
  outline: 0;
`;

export const Backdrop = styled.div`
  position: fixed;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  background: rgb(74, 74, 75);
  z-index: 500;
`;

export const StyledModal = styled.div`
  z-index: 100;
  background: white;
  position: relative;
  margin: auto;
  border-radius: 8px;
`;

export const Header = styled.div`
  border-radius: 8px 8px 0 0;
  display: flex;
  justify-content: space-between;
  padding: 0.3rem;
`;

export const HeaderText = styled.div`
  text-align: center;
  align-self: center;
  color: lightgray;
`;

export const CloseButton = styled.button`
  font-size: 0.8rem;
  border: none;
  border-radius: 3px;
  margin-left: 0.5rem;
  background-color:red;
  color:#fff;
  cursor: pointer;
  :hover {
    cursor: pointer;
  }
`;

export const Content = styled.div`
  padding: 10px;
  max-height: 30rem;
  overflow-x: hidden;
  overflow-y: auto;
`;


4.2 使用自定义hook

为了使用的模态框,这里我创建一个自定义的 React hook来管理模态的状态。可以在任何要渲染模态的组件中使用自定义钩子。

import { useState } from 'react';
export const useModal = () => {
  const [isShown, setIsShown] = useState(false);
  const toggle = () => setIsShown(!isShown);
  return {
    isShown,
    toggle,
  };
};

4.3定义主体部分

模态框的主体包含两个部分,按钮和内容,封装的组件外部可以修改按钮的样式以及文字,并且可以 自定义内容。

import {Fragment} from 'react';
import { ConfirmationButtons, Message, YesButton, NoButton } from './style';
export const ConfirmationModal= (props) => {
   const {message,onConfirm,onCancel,sureColor,sureHoverColor,
         cancelColor,cancelHoverColor,cancelText,sureText}=props
  return (
    <Fragment>
      <Message>{message}</Message>
      <ConfirmationButtons>
        <YesButton onClick={onConfirm} 
         sureColor={sureColor}
         sureHoverColor={ sureHoverColor}
         >{sureText}</YesButton>
        <NoButton 
        onClick={onCancel}
        cancelColor={cancelColor}
        cancelHoverColor={cancelHoverColor}
        >{cancelText}</NoButton>
      </ConfirmationButtons>
    </Fragment>
  );
};
//style.js
import styled from "styled-components";
export const ConfirmationButtons = styled.div`
  display: flex;
  width: 100%;
  justify-content: space-around;
  height: 20px;
`;

export const Message = styled.div`
  font-size: 0.9rem;
  margin-bottom: 10px;
  text-align: center;
`;

export const YesButton = styled.button.attrs({
    backgroundColor:props=>props.sureHoverColor || props.sureColor
})`
  flex: 1;
  background-color:  ${props=>props.sureColor};
  border: none;
  border-top-left-radius: 6px;
  border-bottom-left-radius: 6px;
  cursor: pointer;
  :hover {
    background-color: ${props=>props.sureHoverColor};
    color: #fff;
    cursor: pointer;
  }
`;

export const NoButton = styled.button.attrs({
    backgroundColor:props=>props.cancelColor || props.cancelHoverColor
})`
  flex: 1;
  background-color:  ${props=>props.cancelColor};
  border: none;
  border-top-right-radius: 6px;
  border-bottom-right-radius: 6px;
  cursor: pointer;
  :hover {
    background-color:${props=>props.cancelHoverColor};
    color: #fff;
    cursor: pointer;
  }
`;

4.4.添加键盘交互

为了允许用户在ESC按键时关闭模态,我们需要向我们的模态框添加一个事件键侦听器。当ESC按键被按下并显示模态时,隐藏模态框的功能将被执行。这里使用useEffect钩子来实现这一点。

  const onKeyDown = (event) => {
  if (event.keyCode === 27
     && isShown) {
    hide();
  }
};

useEffect(() => {
//禁用滚动
  isShown ? (document.body.style.overflow = 'hidden') : 
  (document.body.style.overflow = 'unset');
  //键盘操作事件
  document.addEventListener('keydown', onKeyDown, false);
  return () => {
    document.removeEventListener('keydown', onKeyDown, false);
  };
}, [isShown, onKeyDown]);
  • Esc 键关闭模态
  • 按下Shift将焦点移动到模态内的下一个可选项卡元素
  • 按下Shift + Tab将焦点移动到上一个可选项卡元素
  • 页面禁止滚动

4.5焦点陷阱

  我们清单中的最后一项是将焦点捕获在模态内。我们可以通过单击Shift或来遍历模态内的元
素Shift + Tab。当我们到达最后一个模态框内的元素时,如果我们按下 Shift,焦点将移
动到模态之外的元素。
  但这不是我想要的。我想要的是当焦点到达最后一个模态框内的元素并继续使用 Shift 键遍
历时,焦点将转到第一个模态框内的元素。它就像一个循环。一旦我们到达循环的末尾,我们
就从头开始。
  我是通过在模态框中获取所有可聚焦元素来实现此功能,然后循环遍历它们以捕获焦点,这里
借助了react-focus-lock.
import {Fragment,useEffect} from 'react'
import {createPortal}from 'react-dom';
import FocusLock from 'react-focus-lock';
import {
  Wrapper,
  Header,
  StyledModal,
  HeaderText,
  CloseButton,
  Content,
  Backdrop,
} from './style';
 const Modal = ({
  isShown,
  hide,
  modalContent,
  headerText,
  closeIcon
}) => {
  // eslint-disable-next-line react-hooks/exhaustive-deps
  const onKeyDown = (event) => {
  if (event.keyCode === 27
     && isShown) {
    hide();
  }
};

useEffect(() => {
  isShown ? (document.body.style.overflow = 'hidden') : (document.body.style.overflow = 'unset');
  document.addEventListener('keydown', onKeyDown, false);
  return () => {
    document.removeEventListener('keydown', onKeyDown, false);
  };
}, [isShown, onKeyDown]);

  const modal = (
    <Fragment>
      <Backdrop onClick={hide} />
      //这里使用FocusLock包裹
      <FocusLock>
      <Wrapper aria-modal aria-labelledby={headerText} tabIndex={-1} role="dialog">
        <StyledModal >
          <Header>
            <HeaderText>{headerText}</HeaderText>
            <CloseButton  type="button" data-dismiss="modal" aria-label="Close" onClick={hide}>{closeIcon}</CloseButton>
          </Header>
          <Content>{modalContent}</Content>
        </StyledModal>
      </Wrapper>
      </FocusLock>
    </Fragment>
  );

  return isShown ? createPortal(modal, document.body) : null;
};
export default Modal

4.6 使用

import React, {Fragment } from 'react';
import  Modal from './components/Modal';
import { useModal } from './until/useModal';
import { ConfirmationModal } from './components/ConfirmModal';


const App= () => {
  const { isShown, toggle } = useModal();
  const onConfirm = () => {
    toggle()
  };
  const onCancel = () => toggle();
  return (
    <Fragment>
      <button onClick={toggle} disabled={isShown}>点击弹出modal</button>
      <Modal
        isShown={isShown}
        hide={toggle}
        headerText="这是一个模态框"
        closeIcon={
          'x'
        }
        modalContent={
          <ConfirmationModal
            onConfirm={onConfirm}
            onCancel={onCancel}
            message="这里可以输入一段提示语言"
            sureText='确定'
            cancelText='取消'
            sureColor='yellow'
            sureHoverColor='red'
            cancelColor='lightgrey'
            cancelHoverColor='grey'
          />
        }
      />
    </Fragment>
  );
};

export default App