1. 程式人生 > 實用技巧 >從零開始的野路子React/Node(6)關於模態框的二三事

從零開始的野路子React/Node(6)關於模態框的二三事

前一陣遇到過一個需求,要求在App中點選某個按鈕會彈出一個對話方塊(即模態框Modal)。第一件事自然是看看公司內部的元件庫有沒有已經實現的功能,結果這一看把我看得雲裡霧裡的,這是神馬?這又是神馬?算了,還是自己寫(抄)一個吧。

在網上翻找了許久,始終沒有特別滿意的實現,直到我找到了這篇:

https://blog.bitsrc.io/build-a-full-featured-modal-dialog-form-with-react-651dcef6c571

實現很簡潔,卻又非常好用。稍加改動,啊,真香~

這個模態框一共由3部分組成:

其中ModalContent負責實現模態框內部的內容,你現在框裡顯示資訊也好,表單也好,加幾個按鈕,都在這裡體現;

TriggerButton則負責在父頁面上實現一個按鈕,用來觸發模態框的彈出,點了它就會彈出模態框;

ModalContainer就是個容器,負責將ModalContent和TriggerButton融合起來,以及模態框顯示/隱藏的一些邏輯。

1、ModalContent

import React from 'react';
import ReactDOM from 'react-dom';
import FocusTrap from 'focus-trap-react';
import styled from 'styled-components';

export default function
ModalContent(props) { const { modalRef, buttonRef, onKeyDown, closeModal, zindex } = props; return ReactDOM.createPortal( <FocusTrap> <aside tag="aside" role="dialog" tabIndex
="-1" aria-modal="true" onKeyDown={onKeyDown}> <StyledOverlay zindex={zindex}/> <StyledWrapper zindex={zindex}> <StyledModal ref={modalRef}> <div> { props.children } <button ref={buttonRef} aria-label="Close Modal" aria-labelledby="close-modal" onClick={closeModal}>退下吧</button> </div> </StyledModal> </StyledWrapper> </aside> </FocusTrap>, document.body ); }; const StyledOverlay = styled.div` position: fixed; top: 0; left: 0; z-index: ${props => props.zindex + 1000}; width: 100vw; height: 100vh; background-color: #000; opacity: 0.5; ` //用於Modal彈出後遮蔽其他原內容 const StyledWrapper = styled.div` position: fixed; top: 0; left: 0; z-index: ${props => props.zindex + 1010}; width: 100%; height: 100%; overflow-x: hidden; overflow-y: auto; outline: 0; ` const StyledModal = styled.div` z-index: 100; background: white; position: relative; top: 80px; margin: 1.75rem auto; border-radius: 3px; max-width: 1000px; padding: 2rem; `

看上去還挺複雜的,其實真正模態框裡的內容只有<div>和</div>之間的部分,其他的部分可以看做是框體和框外的實現。

其中createPortal負責建立模態框。FocusTrap負責把Tab限制在框內部的元素上,在FocusTrap存在的情況下,你隨便怎麼按Tab鍵,高亮都只會在模態框內部的元素間跳來跳去。如果沒有FocusTrap的話,可能你按幾下Tab,高亮就跳到模態框背後的內容上了。

此外,StyledOverlay負責一個遮罩效果,遮住模態框背後的內容,它的z-index一定要高於父頁面;

StyledWrapper類似於一個模態框的外部容器,它的z-index一定要高於StyledOverlay;

StyledModal則是負責模態框的本體長什麼樣。

<div>和</div>之間的內容包含了兩部分,一部分是{ props.children },這樣一來我們可以接受任意子元件作為模態框中的內容,更加靈活。另一部分是個button,用來關閉模態框。

另外,這裡用styled-components來替代了css,而且props中的屬性可以傳入其中,我們用這一方法來控制z-index,從而方便我們之後“框中框”中的使用。

2、TriggerButton

import React from 'react';

export default function TriggerButton(props) {
    const {
        triggerText, 
        buttonRef, 
        showModal
    } = props

    return (
        <button 
            ref={buttonRef} 
            onClick={showModal}>{ triggerText }</button>
    );
};

這裡的內容很簡單,主要就是點選時呼叫父元件的showModal函式從而開啟模態框。

3、ModalContainer

這一部分是相對而言最複雜的(試圖轉成函式式元件,但貌似沒法使用ref,放棄……):

import React, { Component } from 'react';
import ModalContent from "./ModalContent";
import TriggerButton from "./TriggerButton";

export default class ModalContainer extends Component {
    state = {isShown: false};
    
    showModal = () => {
        this.setState({isShown: true});
        this.toggleScrollLock();
    };

    closeModal = () => {
        this.setState({isShown: false});
        this.toggleScrollLock();
    };

    onKeyDown = (event) => {
        if (event.keyCode === 27) {
            this.closeModal();
        }; //按下ESC
    };

    toggleScrollLock = () => {
        document.querySelector("html").classList.toggle("scroll-lock");
    };

    render () {
        return (
            <React.Fragment>
                <TriggerButton
                    showModal={this.showModal}
                    buttonRef={(n) => {this.TriggerButton=n}}
                    triggerText={this.props.buttonText}/>
                {this.state.isShown ? 
                <ModalContent
                    title={this.props.title}
                    modalRef={(n) => {this.modal=n}}
                    buttonRef={(n) => {this.closeButton=n}}
                    closeModal={this.closeModal}
                    onKeyDonw={this.onKeyDown}
                    zindex={this.props.zindex || 0}
                    children={this.props.children}/> : 
                null}  
            </React.Fragment>
        );
    }
};

isShown負責記錄模態框處於顯示還是隱藏的狀態;

showModal和closeModal分別負責開啟和關閉模態框;

onKeyDown負責在按下esc的時候關閉模態框;

toggleScrollLock用於鎖定/解鎖滾動(作用我沒明白……試過去掉,好像沒什麼影響)。

最後的部分就是一個TriggerButton和一個條件渲染的ModalContent,isShown為true的情況下顯示ModalContent,否則隱藏之,從而實現開啟/關閉模態框的效果。

ModalContent的zindex在未指定的情況下為0,對應正常的模態框,如果我們傳入一個值,還可以實現“框中框的效果”。

4、齊活了

現在我們來試試不同的模態框組合效果,把容器元件加入App中即可:

第1個框(張龍)沒有任何子元件,因此只有一個關閉按鈕:

第2個框(趙虎)有一個子元件,因此框中會顯示該子元件以及關閉按鈕:

第3個框(王朝)包含了一個標題元件以及另一個模態框作為子元件,此處我們設定了zindex,以便後來的模態框覆蓋先前的模態框:

點選“呼叫馬漢”,新的模態框會彈出:

此即最近對模態框的一些體會。

程式碼見:

https://github.com/SilenceGTX/play_with_modal