import { Component, createRef } from 'react'; import * as Position from './placement'; import PopoverContent from './Content'; import Trigger from './trigger'; import PopoverContext, { IPopoverContentImperativeHandle, IPopoverContext, } from './Context'; import { IPositionFunction } from './position-function'; import withPopover from './withPopover'; import { IPortalImperativeHandlers } from '../portal'; import Anchor from './Anchor'; export interface IPopoverBeforeHook { (continuation?: () => void, escape?: () => void): Promise | void; } export interface IPopoverProps { position: IPositionFunction; cushion?: number; onShow?: () => void; onClose?: () => void; onBeforeShow?: IPopoverBeforeHook; onBeforeClose?: IPopoverBeforeHook; containerSelector?: string; visible?: boolean; onVisibleChange?: (visible: boolean) => void; onPositionUpdated?: () => void; onPositionReady?: () => void; className?: string; style?: React.CSSProperties; } export interface IPopoverState { visible: boolean; } export class Popover extends Component { static contextType = PopoverContext; declare context: IPopoverContext | null; static defaultProps = { cushion: 0, containerSelector: 'body', }; static Anchor = Anchor; static Content = PopoverContent; static Trigger = Trigger; static Position = Position; static withPopover = withPopover; static Context = PopoverContext; /** * 使用内置的Trigger或者直接用Anchor的情况下由Anchor设置 * 使用自定义的Trigger也可以直接用Anchor,不用Anchor需要手动设置 */ getAnchor: (() => Element | Text | null) | null = null; private isUnmounted = false; private pendingOnBeforeHook = false; private didMountHooks: Array<() => () => void> = []; private didMountCleanup: Array<() => void> = []; readonly portalRef = createRef(); isPositionReady = false; readonly contentRef = createRef(); state = { visible: false, }; private escape = () => { this.pendingOnBeforeHook = false; }; private didMount = (cb: () => () => void) => { this.didMountHooks.push(cb); }; setVisible(visible: boolean) { const { onBeforeClose, onBeforeShow, onVisibleChange } = this.props; const hook = visible ? onBeforeShow : onBeforeClose; if (this.pendingOnBeforeHook || visible === this.state.visible) { return; } if (onVisibleChange) { return onVisibleChange(visible); } if (!hook) { return this.safeSetState({ visible }); } this.pendingOnBeforeHook = true; const continuation = () => { this.safeSetState({ visible, }); this.pendingOnBeforeHook = false; }; if (hook.length >= 1) { return hook(continuation, this.escape); } Promise.resolve(hook()).then(continuation, this.escape); } adjustPosition() { this.contentRef.current?.adjustPosition(); } open = () => { this.setVisible(true); }; close = () => { this.setVisible(false); }; /** @internal */ positionUpdated() { const { onPositionReady, onPositionUpdated } = this.props; onPositionUpdated?.(); if (!this.isPositionReady) { this.isPositionReady = true; onPositionReady?.(); } } safeSetState(nextState: IPopoverState, callback?: () => void) { if (!this.isUnmounted) { return this.setState(nextState, callback); } } static getDerivedStateFromProps( props: IPopoverProps ): Partial | null { if (typeof props.visible === 'boolean') { return { visible: props.visible, }; } return null; } componentDidMount() { const { onShow } = this.props; if (this.state.visible) { onShow && onShow(); } this.didMountCleanup = this.didMountHooks.map(it => it()); } componentDidUpdate(prevProps: IPopoverProps, prevState: IPopoverState) { if (prevState.visible !== this.state.visible) { const { onShow, onClose } = this.props; if (this.state.visible) { this.isPositionReady = false; onShow && onShow(); } else { onClose && onClose(); } } this.adjustPosition(); } componentWillUnmount() { this.isUnmounted = true; this.didMountCleanup.forEach(it => it()); } render() { const { containerSelector, position, cushion, className, children, style } = this.props; const { visible } = this.state; return ( {children} ); } } export default Popover;