import React, { useCallback, useRef } from "react"; import classNames from "classnames"; import { Overlay, OverlayLayerProps } from "../overlay"; import { useDelayVisible } from "./useDelayVisible"; import { useParentScroll } from "../_util/use-parent-scroll"; import { buildinTriggers, Trigger, TriggerWithProps } from "./trigger"; import { mergeStyle } from "../_util/merge-style"; import { DomRef } from "../domref"; import { mergeRefs } from "../_util/merge-refs"; import { attachProps } from "../_util/attach-props"; import { isChildOfType } from "../_util/is-child-of-type"; import { Bubble } from "../bubble"; import { PopConfirm } from "../popconfirm"; import { Tooltip } from "../tooltip"; export interface PopoverProps extends Pick< OverlayLayerProps, | "escapeWithReference" | "flipped" | "positionFixed" | "onExited" | "popupContainer" | "destroyOnClose" | "transitionTimeout" | "referenceElement" | "animationScaleFrom" | "updateOnDimensionChange" > { /** * 触发弹出层显示的事件 * * - `hover`:用户鼠标进入渲染内容时,显示浮层;离开渲染内容时,隐藏浮层。 * 使用该交互请保证 `children` 可以接收 `onMouseEnter` 和 `onMouseLeave` 事件 * * - `click`:用户在渲染内容上点击鼠标时,显示浮层;在渲染内容以及浮层外点击时,隐藏浮层。 * 使用该交互请保证 `children` 可以接收 `onClick` 事件 * * - 传入自定义组件以实现自定义弹出逻辑 * * @default "hover" */ trigger?: keyof typeof buildinTriggers | Trigger | TriggerWithProps; /** * 弹出层的内容是否默认打开 * @default false */ defaultVisible?: boolean; /** * 传入 `visible` 则表示使用受控模式来控制弹出层的显示,请处理 `onVisibleChange` 方法 */ visible?: boolean; /** * `visible` 变化时回调 */ onVisibleChange?: (visible: boolean) => void; /** * 触发弹出层的内容,会直接渲染出来 * * 内容触发弹出层的方式,由 `trigger` 属性指定 * * 如果传入的不是 `ReactElement`(如文本、数字等),则渲染时会套一层 `span` */ children?: React.ReactNode; /** * 弹出层内容 * @docType React.ReactNode | ((props: OverlayContentProps) => JSX.Element) */ overlay?: OverlayLayerProps["content"]; /** * 打开浮层前的延时 */ openDelay?: number; /** * 关闭浮层前的延时 */ closeDelay?: number; /** * 是否在容器滚动发生关闭 * @default false */ closeOnScroll?: boolean; /** * 气泡弹出的位置 * @default "top" */ placement?: OverlayLayerProps["placement"]; /** * 弹出位置偏离参考位置的位移 * * 如: `10`、`"50%"`、`"10 + 10%"`、`[10, 10]` * * @default 10 */ placementOffset?: OverlayLayerProps["placementOffset"]; /** * 覆盖层自定义类名 */ overlayClassName?: string; /** * 覆盖层自定义样式 */ overlayStyle?: React.CSSProperties; } export type PopoverRenderFunction = (props: PopupRenderProps) => JSX.Element; export interface PopupRenderProps { ref: React.Ref; open: () => void; close: () => void; } export type PopupTrigger = "click" | "hover"; export function Popover(props: PopoverProps) { const { overlay, trigger = "hover", placement = "top", placementOffset = 10, escapeWithReference, popupContainer, children, closeOnScroll, overlayClassName, overlayStyle, openDelay, closeDelay, updateOnDimensionChange, animationScaleFrom, transitionTimeout, destroyOnClose, positionFixed, flipped, onExited, } = props; const overlayRef = useRef(null); const childrenRef = useRef(null); const { visible, setVisible } = useDelayVisible(props); // 设置了 closeOnScroll,则使用 useParentScroll( childrenRef, visible && closeOnScroll && (() => setVisible(false, closeDelay)) ); let Trigger: Trigger; let triggerExtraProps: any = {}; // 使用指定的触发交互组件 if (typeof trigger === "string") { Trigger = buildinTriggers[trigger]; } // 支持附加 props 的触发器 else if (Array.isArray(trigger)) { [Trigger, triggerExtraProps] = trigger; } // 本身就是 Trigger 组件 else { Trigger = trigger; } // fallback Trigger = Trigger || buildinTriggers.empty; const getChildren = useCallback( (childrenProps: any) => { const hasOneValidElement = c => React.Children.count(c) === 1 && React.isValidElement(React.Children.toArray(c)[0]); // 支持元素被多层浮层组件包裹 const traverse = (children: React.ReactNode) => { let child; if (hasOneValidElement(children)) { [child] = React.Children.toArray(children); if ( isChildOfType(child, Bubble) || isChildOfType(child, PopConfirm) || isChildOfType(child, Popover) || isChildOfType(child, Tooltip) ) { return React.cloneElement( child as any, {}, traverse(child.props.children) ); } return React.cloneElement( child, attachProps(child.props, childrenProps) ); } child = {children}; return child; }; return traverse(children); }, [children] ); return ( ( { overlayRef.current = overlayElement; }} overlayProps={{ ...overlayProps, className: classNames(overlayProps.className, overlayClassName), style: mergeStyle( { // 已经隐藏之后,动画过程不响应事件 pointerEvents: visible ? null : "none", }, overlayProps.style, overlayStyle ), }} updateOnDimensionChange={updateOnDimensionChange} transitionTimeout={transitionTimeout} escapeWithReference={escapeWithReference} popupContainer={popupContainer} onExited={onExited} positionFixed={positionFixed} flipped={flipped} />, ]} > {ref => ( {getChildren(childrenProps)} )} )} /> ); } Popover.displayName = "Popover";