import React, { HTMLProps, useRef } from "react"; import ReactDOM from "react-dom"; import classNames from "classnames"; import { TransitionProps } from "react-transition-group/Transition"; import { Placement, Modifiers } from "popper.js"; import { Popper, PopperChildrenProps, PopperProps } from "react-popper"; import { Omit } from "../_type"; import { AttachContainer, getOverlayRoot } from "../_util/get-overlay-root"; import { mergeStyle } from "../_util/merge-style"; import { callBoth } from "../_util/call-both"; import { ScaleTransition } from "../transition"; import { useVisibleTransition } from "../_util/use-visible-transition"; import { injectValue } from "../_util/inject-value"; import { useConfig } from "../_util/config-context"; import { noop } from "../_util/noop"; import { mergeRefs } from "../util"; export interface OverlayLayerProps { /** * 覆盖层内容 * @docType React.ReactNode | ((props: OverlayContentProps) => React.ReactNode) */ content: React.ReactNode | ((props: OverlayContentProps) => React.ReactNode); /** * 参考的定位内容 * * 如果提供了 referenceElement,则不通过 `children` 定位,可以实现自定义定位参考 * @see https://github.com/FezVrasta/react-popper#usage-without-a-reference-htmlelement */ referenceElement?: PopperProps["referenceElement"]; /** * 挂载组件的结点 * @default document.body * @since 2.5.0 */ popupContainer?: AttachContainer; /** * 用于获取覆盖层 DOM Ref */ overlayRef?: React.Ref; /** * 覆盖层自定义属性,会附加到覆盖层的 div 上 * * 要使用 `ref`,请传入 `overlayRef` */ overlayProps?: HTMLProps; /** * 覆盖层相对于定位元素的位置 * @default "bottom-start" */ placement?: Placement; /** * 覆盖层偏离定位元素的距离 * * 如: `10`、`"50%"`、`"10 + 10%"`、`[10, 10]` * @default 5 */ placementOffset?: number | string | [number | string, number | string]; /** * 覆盖层是否可见 */ visible?: boolean; /** * 关闭时是否销毁内容元素 * @default true * @since 2.6.0 */ destroyOnClose?: boolean; /** * 出现时渐变动画时长 * @default { enter: 50, exit: 300 } */ transitionTimeout?: TransitionProps["timeout"]; /** * 是否在 `resize` 和 `scroll` 事件发生的时候更新位置 * @default true */ updateOnDimensionChange?: boolean; /** * 出现动画滑动距离 * @default 2 */ animationScaleFrom?: number; /** * 是否可以跟随参考元素离开可视范围 * @default false */ escapeWithReference?: boolean; /** * 距离可视范围空间不足时是否翻转 `placement` * @default true * @since 2.7.0 */ flipped?: boolean; /** * 浮层是否使用 `fixed` 定位 * @default true * @since 2.7.0 */ positionFixed?: PopperProps["positionFixed"]; /** * 关闭动画结束时回调 * @since 2.6.0 */ onExited?: () => void; /** * https://popper.js.org/popper-documentation.html#modifiers */ modifiers?: Modifiers; } export interface OverlayContentProps extends Omit { visible: boolean; } const scaleOriginForPlacement = (originMap => (placement: Placement) => { const basePlacement = placement.split("-").shift(); return originMap[basePlacement]; })({ top: "bottom", bottom: "top", left: "right", right: "left", }); /** * 为定位元素创建一个覆盖层 * * @example * ```js const [visible, setVisible] = useState(false); const open = () => setVisible(true); const close = () => setVisible(false); 我是浮层内容,关闭} children={ref => 点击弹出浮层} /> ``` */ export const OverlayLayer = React.forwardRef(function OverlayLayer( { content, overlayRef, overlayProps = {}, placement = "bottom-start", visible, placementOffset = 5, transitionTimeout = { enter: 50, exit: 300 }, destroyOnClose = true, updateOnDimensionChange, referenceElement, animationScaleFrom = 0.94, // 为什么?因为此时效果比较好 escapeWithReference, popupContainer, modifiers = {}, flipped = true, positionFixed = true, onExited = noop, }: OverlayLayerProps, ref: React.Ref ) { const { classPrefix, popupContainer: globalPopupContainer } = useConfig(); const firstRender = useRef(!!visible); if (visible && !firstRender.current) { firstRender.current = true; } // visible 启动时,才开始渲染内容,进行动画 const { contentIn, shouldContentRender, shouldContentEnter, onContentExit, } = useVisibleTransition(visible); if ( !shouldContentRender && (destroyOnClose || (!destroyOnClose && !firstRender.current)) ) { return null; } // 渲染定位组件 return ReactDOM.createPortal( {popper => { const overlayContent = injectValue(content)({ ...popper, visible }); return ( popper.scheduleUpdate()} onExited={callBoth(onContentExit, onExited)} unmountOnExit={destroyOnClose} >
{React.isValidElement(overlayContent) ? ( overlayContent ) : ( {overlayContent} )}
); }}
, getOverlayRoot(popupContainer || globalPopupContainer) ); }); OverlayLayer.displayName = "OverlayLayer";