import cx from 'classnames' ; import React from 'react' ; import ReactDOM from 'react-dom' ; import { Keyframes } from 'styled-components' ; import { DOCUMENT_BODY , OverlayBehaviorContext , OverlayBehaviorContextType } from '../../providers' ; import { mergeRefs } from '../../utils' ; import { domUtils } from '../virtual-list/dom-utils' ; import { ANIMATE_ANIMATED , ANIMATE_PREFIX , Disposable , startAnimate } from './overlay-utils/animate-utils' ; import { animations } from './overlay-utils/animations' ; import { batchedUpdates } from './overlay-utils/batchUpdate' ; import { OverlayManager } from './overlay-utils/OverlayManager' ; export interface IOverlayCloseActions { category canCloseByEsc ? : boolean ; category canCloseByOutSideClick ? : boolean ; } export interface IOverlayLifecycles { category beforeOpen ? ( state ? ) : Promise < IOverlayAnimationProps > ; category onOpen ? ( ) : void ; category afterOpen ? ( ) : void ; category beforeClose ? ( ) : IOverlayAnimationProps ; category onClose ? ( ) : void ; category afterClose ? ( ) : void ; } export interface IOverlayBackdropProps { category hasBackdrop ? : boolean ; category backdropStyle ? : React . CSSProperties ; category backdropClassName ? : string ; } export interface IOverlayAnimationProps { category default animationDuration ? : string ; category animation ? : false | { in : string | Keyframes ; out : string | Keyframes ; } ; } export interface IOverlayPortalProps { category usePortal ? : boolean ; category portalContainer ? : HTMLElement | typeof DOCUMENT_BODY ; category disableScroll ? : boolean | 'force' ; } export interface OverlayProps extends IOverlayCloseActions , IOverlayLifecycles , IOverlayBackdropProps , IOverlayAnimationProps , IOverlayPortalProps { overlayKey ? : string ; /** 是否显示弹层 */ visible : boolean ; /** 弹层请求关闭时触发事件的回调函数 */ onRequestClose ? ( reason ) : void ; /** 弹层的根节点的样式类 */ className ? : string ; /** 弹层的根节点的内联样式 */ style ? : React . CSSProperties ; /** 弹层内容 */ children ? : React . ReactNode ; /** 使用 render prop 的形式指定弹层内容，用于精确控制 DOM 结构 */ renderChildren ( pass ) : React . ReactNode ; wrapperRef ? : React . Ref < HTMLDivElement > ; safeNodes ? : ( Node | ( ( ) => Node ) ) [ ] ; /**
     * 是否关联到 portalContainer 上的 overlay manager.
     * 该属性一般保持默认即可。
     */ attachOverlayManager ? : boolean ; } interface OverlayState { prevVisible : boolean ; exiting : boolean ; } const defaultRenderChildren = ( { ref , children } ) => ( < div ref = { ref as React . RefObject < HTMLDivElement > } > { children } < /div>); /** Overlay 注释文档 */ export class Overlay extends React . Component < OverlayProps , OverlayState > { static isCustomPortalContainer ( portalContainer ? ) { return portalContainer != null && portalContainer !== 'DOCUMENT_BODY' && ! domUtils . isBody ( portalContainer ) ; } static readonly contextType = OverlayBehaviorContext ; context : OverlayBehaviorContextType ; static readonly defaultRenderChildren = defaultRenderChildren ; static readonly animations = animations ; static readonly DOCUMENT_BODY = DOCUMENT_BODY ; static defaultProps = { usePortal : true , safeNodes : [ ] as OverlayProps [ 'safeNodes' ] , canCloseByEsc : true , canCloseByOutSideClick : true , animation : { in : animations . fadeIn , out : animations . fadeOut } , renderChildren : Overlay . defaultRenderChildren , disableScroll : false , attachOverlayManager : true , animationDuration : '200ms' , } ; static getDerivedStateFromProps ( props , state ) { let nextExiting = state . exiting ; if ( state . prevVisible && ! props . visible ) { nextExiting = true ; } else if ( ! state . prevVisible && props . visible ) { nextExiting = false ; } return { prevVisible : props . visible , exiting : nextExiting , } ; } private static managerMap = new Map < Element , OverlayManager > ( ) ; static getManager ( portalContainer ) { if ( portalContainer === DOCUMENT_BODY ) { portalContainer = typeof document === 'undefined' ? null : document . body ; } if ( portalContainer == null ) { console . warn ( '[@rexd/core] Overlay.getManager(portalContainer) 调用参数 portalContainer 为 null' ) ; return ; } if ( ! Overlay . managerMap . has ( portalContainer ) ) { Overlay . managerMap . set ( portalContainer , new OverlayManager ( portalContainer ) ) ; } return Overlay . managerMap . get ( portalContainer ) ; } private resolvePortalContainer ( ) { let result = this . props . portalContainer ?? this . context . portalContainer ; if ( result === DOCUMENT_BODY ) { result = typeof document !== 'undefined' ? document . body : null ; } return result ; } constructor ( props ) { super ( props ) ; this . state = { prevVisible : props . visible , exiting : false , } ; } private _wrapperRef = React . createRef < HTMLDivElement > ( ) ; private innerRef = React . createRef < Element > ( ) ; private overlayAnimateInst : Disposable = null ; componentDidMount ( ) { const { visible } = this . props ; if ( visible ) { this . doOpenOverlay ( ) ; } } private resolveEnterAnimation ( instruction ) { const { animation : animationProp } = this . props ; if ( animationProp === false || instruction ?. animation === false ) { return null ; } return instruction ?. animation ?. in ?? animationProp ?. in ?? null ; } private async doOpenOverlay ( ) { const { beforeOpen , onOpen , afterOpen , attachOverlayManager } = this . props ; if ( attachOverlayManager ) { Overlay . getManager ( this . resolvePortalContainer ( ) ) . add ( this ) ; } const inner = this . innerRef . current ; if ( inner == null ) { return ; } inner . classList . add ( 'rex-overlay-inner' ) ; const overlayOpenInstruction = await beforeOpen ?. ( ) ; this . overlayAnimateInst ?. dispose ( ) ; this . overlayAnimateInst = startAnimate ( inner , this . resolveEnterAnimation ( overlayOpenInstruction ) , ( ) => { afterOpen ?. ( ) ; this . overlayAnimateInst = null ; } ) ; onOpen ?. ( ) ; } private resolveExitAnimation ( instruction ) { const { animation : animationProp } = this . props ; if ( animationProp === false || instruction ?. animation === false ) { return null ; } return instruction ?. animation ?. out ?? animationProp ?. out ?? null ; } private async doCloseOverlay ( force = false ) { const { beforeClose , onClose , afterClose , attachOverlayManager } = this . props ; if ( attachOverlayManager ) { Overlay . getManager ( this . resolvePortalContainer ( ) ) . delete ( this ) ; } const inner = this . innerRef . current ; if ( inner == null ) { return ; } const overlayCloseInstruction = beforeClose ?. ( ) ; this . overlayAnimateInst ?. dispose ( ) ; if ( force ) { this . overlayAnimateInst = null ; onClose ?. ( ) ; } else { this . overlayAnimateInst = startAnimate ( inner , this . resolveExitAnimation ( overlayCloseInstruction ) , ( ) => { batchedUpdates ( ( ) => { this . setState ( { exiting : false } ) ; this . overlayAnimateInst = null ; afterClose ?. ( ) ; } ) ; } ) ; onClose ?. ( ) ; } } componentDidUpdate ( prevProps , prevState ) { if ( prevProps . disableScroll !== this . props . disableScroll ) { console . warn ( '[@rexd/core] 浮层组件 props.disableScroll 不支持动态变化' ) ; } if ( prevProps . attachOverlayManager !== this . props . attachOverlayManager ) { console . warn ( '[@rexd/core] 浮层组件 props.attachOverlayManager 不支持动态变化' ) ; } if ( prevProps . visible && ! this . props . visible ) { this . doCloseOverlay ( ) ; } if ( ! prevProps . visible && this . props . visible ) { this . doOpenOverlay ( ) ; } } componentWillUnmount ( ) { if ( this . props . visible ) { this . doCloseOverlay ( true ) ; } this . overlayAnimateInst ?. dispose ( ) ; } private composedWrapperRef = mergeRefs ( this . _wrapperRef , this . props . wrapperRef ) ; public isInsideClick = ( clickNode ) => { if ( ( clickNode as Element ) . classList ?. contains ?. ( 'rex-overlay-backdrop' ) ) { return false ; } const { safeNodes } = this . props ; const wrapper = this . _wrapperRef . current ; return ( wrapper ?. contains ( clickNode ) || safeNodes . some ( ( safe ) => { if ( typeof safe === 'function' ) { safe = safe ( ) ; } return safe ?. contains ( clickNode ) ; } ) ) ; } ; render ( ) { const { usePortal , children , renderChildren , animationDuration , className , style , hasBackdrop , visible , overlayKey , attachOverlayManager , } = this . props ; const portalContainer = this . resolvePortalContainer ( ) ; if ( portalContainer == null ) { return null ; } const { exiting } = this . state ; if ( ! visible && ! exiting ) { return null ; } let content = ( < div className = { cx ( 'rex-overlay-wrapper' , { exiting , detached : ! attachOverlayManager } , className ) } style = { { '--animate-duration' : animationDuration , ... style } as any } ref = { this . composedWrapperRef } > { hasBackdrop && ( < div className = { cx ( 'rex-overlay-backdrop' , this . props . backdropClassName , ANIMATE_ANIMATED , exiting ? `${ ANIMATE_PREFIX }${ animations . fadeOut . getName ( ) }` : `${ ANIMATE_PREFIX }${ animations . fadeIn . getName ( ) }` ) } style = { { position : Overlay . isCustomPortalContainer ( portalContainer ) ? 'absolute' : undefined , ... this . props . backdropStyle , } } />)} { renderChildren ( { ref : this . innerRef , children } ) } < /div>); if ( usePortal ) { content = ReactDOM . createPortal ( content , portalContainer , overlayKey ) ; } return content ; } }