import { State as PopperState , PositioningStrategy } from "@popperjs/core" ; import classNames from "classnames" ; import * as React from "react" ; import { Manager , Popper , PopperChildrenProps , Reference , ReferenceChildrenProps , StrictModifier } from "react-popper" ; import { AbstractPureComponent2 , Classes as CoreClasses , DISPLAYNAME_PREFIX , HTMLDivProps , Keys , refHandler , mergeRefs , Overlay , Utils , IRef , } from "@blueprintjs/core" ; import * as Classes from "./classes" ; import * as Errors from "./errors" ; import { POPOVER_ARROW_SVG_SIZE , Popover2Arrow } from "./popover2Arrow" ; import { positionToPlacement } from "./popover2PlacementUtils" ; import { Popover2SharedProps } from "./popover2SharedProps" ; import { ResizeSensor2 } from "./resizeSensor2" ; import { Tooltip2 } from "./tooltip2" ; import { getBasePlacement , getTransformOrigin } from "./utils" ; export const Popover2InteractionKind = { CLICK : "click" as "click" , CLICK_TARGET_ONLY : "click-target" as "click-target" , HOVER : "hover" as "hover" , HOVER_TARGET_ONLY : "hover-target" as "hover-target" , } ; export type Popover2InteractionKind = typeof Popover2InteractionKind [ keyof typeof Popover2InteractionKind ] ; export type Popover2Props < TProps = React . HTMLProps < HTMLElement > > = IPopover2Props < TProps > ; deprecated export interface IPopover2Props < TProps = React . HTMLProps < HTMLElement > > extends Popover2SharedProps < TProps > { default autoFocus ? : boolean ; /** HTML props for the backdrop element. Can be combined with `backdropClassName`. */ backdropProps ? : React . HTMLProps < HTMLDivElement > ; /**
     * The content displayed inside the popover.
     */ content ? : string | JSX . Element ; default interactionKind ? : Popover2InteractionKind ; default hasBackdrop ? : boolean ; default shouldReturnFocusOnClose ? : boolean ; /**
     * Ref supplied to the `Classes.POPOVER` element.
     */ popoverRef ? : IRef < HTMLElement > ; see https default positioningStrategy ? : PositioningStrategy ; } export interface IPopover2State { isOpen : boolean ; hasDarkParent : boolean ; } template T export class Popover2 < T > extends AbstractPureComponent2 < Popover2Props < T > , IPopover2State > { public static displayName = `${ DISPLAYNAME_PREFIX }.Popover2` ; public static defaultProps : Popover2Props = { boundary : "clippingParents" , captureDismiss : false , defaultIsOpen : false , disabled : false , fill : false , hasBackdrop : false , hoverCloseDelay : 300 , hoverOpenDelay : 150 , inheritDarkTheme : true , interactionKind : Popover2InteractionKind . CLICK , minimal : false , openOnTargetFocus : true , positioningStrategy : "absolute" , renderTarget : undefined as any , shouldReturnFocusOnClose : false , targetTagName : "span" , transitionDuration : 300 , usePortal : true , } ; public state : IPopover2State = { hasDarkParent : false , isOpen : this . getIsOpen ( this . props ) , } ; /**
     * DOM element that contains the popover.
     * When `usePortal={true}`, this element will be portaled outside the usual DOM flow,
     * so this reference can be very useful for testing.
     */ public popoverElement : HTMLElement | null = null ; /** DOM element that contains the target. */ public targetElement : HTMLElement | null = null ; /** Popover ref handler */ private popoverRef : React . Ref < HTMLDivElement > = refHandler ( this , "popoverElement" , this . props . popoverRef ) ; /** Target ref handler */ private targetRef : React . Ref < HTMLElement > = el => ( this . targetElement = el ) ; private cancelOpenTimeout ? : ( ) => void ; private isMouseInTargetOrPopover = false ; private lostFocusOnSamePage = true ; private popperScheduleUpdate ? : ( ) => Promise < Partial < PopperState > | null > ; private isControlled = ( ) => this . props . isOpen !== undefined ; private isArrowEnabled = ( ) => ! this . props . minimal && this . props . modifiers ?. arrow ?. enabled !== false ; private isHoverInteractionKind = ( ) => { return ( this . props . interactionKind === Popover2InteractionKind . HOVER || this . props . interactionKind === Popover2InteractionKind . HOVER_TARGET_ONLY ) ; } ; private getPopoverElement ( ) { return this . popoverElement ?. querySelector ( `.${ Classes . POPOVER2 }` ) ; } private getIsOpen ( props ) { if ( props . disabled ) { return false ; } else { return props . isOpen ?? props . defaultIsOpen ! ; } } public render ( ) { const { disabled , content , placement , position = "auto" , positioningStrategy } = this . props ; const { isOpen } = this . state ; const isContentEmpty = content == null || ( typeof content === "string" && content . trim ( ) === "" ) ; if ( isContentEmpty ) { if ( ! disabled && isOpen !== false && ! Utils . isNodeEnv ( "production" ) ) { console . warn ( Errors . POPOVER2_WARN_EMPTY_CONTENT ) ; } return this . renderTarget ( { ref : noop } ) ; } return ( < Manager > < Reference > { this . renderTarget } < /Reference> < Popper innerRef = { this . popoverRef } placement = { placement ?? positionToPlacement ( position ) } strategy = { positioningStrategy } modifiers = { this . getPopperModifiers ( ) } > { this . renderPopover } < /Popper> < /Manager>); } public componentDidMount ( ) { this . updateDarkParent ( ) ; } public componentDidUpdate ( props , state ) { super . componentDidUpdate ( props , state ) ; this . updateDarkParent ( ) ; const nextIsOpen = this . getIsOpen ( this . props ) ; if ( this . props . isOpen != null && nextIsOpen !== this . state . isOpen ) { this . setOpenState ( nextIsOpen ) ; this . setState ( { isOpen : nextIsOpen } ) ; } else if ( this . props . disabled && this . state . isOpen && this . props . isOpen == null ) { this . setOpenState ( false ) ; } } protected validateProps ( props ) { if ( props . isOpen == null && props . onInteraction != null ) { console . warn ( Errors . POPOVER2_WARN_UNCONTROLLED_ONINTERACTION ) ; } if ( props . hasBackdrop && ! props . usePortal ) { console . warn ( Errors . POPOVER2_WARN_HAS_BACKDROP_INLINE ) ; } if ( props . hasBackdrop && props . interactionKind !== Popover2InteractionKind . CLICK ) { console . warn ( Errors . POPOVER2_HAS_BACKDROP_INTERACTION ) ; } if ( props . placement !== undefined && props . position !== undefined ) { console . warn ( Errors . POPOVER2_WARN_PLACEMENT_AND_POSITION_MUTEX ) ; } const childrenCount = React . Children . count ( props . children ) ; const hasRenderTargetPropp = props . renderTarget !== undefined ; if ( childrenCount === 0 && ! hasRenderTargetPropp ) { console . warn ( Errors . POPOVER2_REQUIRES_TARGET ) ; } if ( childrenCount > 1 ) { console . warn ( Errors . POPOVER2_WARN_TOO_MANY_CHILDREN ) ; } if ( childrenCount > 0 && hasRenderTargetPropp ) { console . warn ( Errors . POPOVER2_WARN_DOUBLE_TARGET ) ; } } public reposition = ( ) => this . popperScheduleUpdate ?. ( ) ; private renderTarget = ( { ref : popperChildRef } ) => { const { children , className , fill , openOnTargetFocus , renderTarget } = this . props ; const { isOpen } = this . state ; const isControlled = this . isControlled ( ) ; const isHoverInteractionKind = this . isHoverInteractionKind ( ) ; let { targetTagName } = this . props ; if ( fill ) { targetTagName = "div" ; } const ref = mergeRefs ( popperChildRef , this . targetRef ) ; const targetEventHandlers = isHoverInteractionKind ? { onBlur : this . handleTargetBlur , onContextMenu : this . handleTargetContextMenu , onFocus : this . handleTargetFocus , onMouseEnter : this . handleMouseEnter , onMouseLeave : this . handleMouseLeave , } : { onClick : this . handleTargetClick , onKeyDown : ( event ) => Keys . isKeyboardClick ( event . keyCode ) && this . handleTargetClick ( event ) , } ; const targetTabIndex = openOnTargetFocus && isHoverInteractionKind ? 0 : undefined ; const targetProps = { "aria-haspopup" : "true" , className : classNames ( className , Classes . POPOVER2_TARGET , { [ Classes . POPOVER2_OPEN ] : isOpen , [ CoreClasses . ACTIVE ] : ! isControlled && isOpen && ! isHoverInteractionKind , } ) , ref , ... ( ( targetEventHandlers as unknown ) as T ) , } ; let target ; if ( renderTarget !== undefined ) { target = renderTarget ( { ... targetProps , isOpen , tabIndex : targetTabIndex , } ) ; } else { const childTarget = Utils . ensureElement ( React . Children . toArray ( children ) [ 0 ] ) ! ; if ( childTarget === undefined ) { return null ; } const targetModifierClasses = { [ CoreClasses . ACTIVE ] : isOpen && ! isControlled && ! isHoverInteractionKind , [ CoreClasses . FILL ] : fill , } ; const clonedTarget = React . cloneElement ( childTarget , { className : classNames ( childTarget . props . className , targetModifierClasses ) , disabled : isOpen && Utils . isElementOfType ( childTarget , Tooltip2 ) ? true : childTarget . props . disabled , tabIndex : childTarget . props . tabIndex ?? targetTabIndex , } ) ; const wrappedTarget = React . createElement ( targetTagName ! , targetProps , clonedTarget ) ; target = wrappedTarget ; } return ( < ResizeSensor2 targetRef = { ref } onResize = { this . reposition } > { target } < /ResizeSensor2>); } ; private renderPopover = ( popperProps ) => { const { interactionKind , shouldReturnFocusOnClose , usePortal } = this . props ; const { isOpen } = this . state ; const transformOrigin = getTransformOrigin ( popperProps . placement , this . isArrowEnabled ( ) ? ( popperProps . arrowProps . style as any ) : undefined ) ; this . popperScheduleUpdate = popperProps . update ; const popoverHandlers = { onClick : this . handlePopoverClick , onKeyDown : event => Keys . isKeyboardClick ( event . keyCode ) && this . handlePopoverClick ( event ) , } ; if ( interactionKind === Popover2InteractionKind . HOVER || ( ! usePortal && interactionKind === Popover2InteractionKind . HOVER_TARGET_ONLY ) ) { popoverHandlers . onMouseEnter = this . handleMouseEnter ; popoverHandlers . onMouseLeave = this . handleMouseLeave ; } const basePlacement = getBasePlacement ( popperProps . placement ) ; const popoverClasses = classNames ( Classes . POPOVER2 , { [ CoreClasses . DARK ] : this . props . inheritDarkTheme && this . state . hasDarkParent , [ CoreClasses . MINIMAL ] : this . props . minimal , [ Classes . POPOVER2_CAPTURING_DISMISS ] : this . props . captureDismiss , } , `${ Classes . POPOVER2_CONTENT_PLACEMENT }-${ basePlacement }` , this . props . popoverClassName ) ; const defaultAutoFocus = this . isHoverInteractionKind ( ) ? false : undefined ; return ( < Overlay autoFocus = { this . props . autoFocus ?? defaultAutoFocus } backdropClassName = { Classes . POPOVER2_BACKDROP } backdropProps = { this . props . backdropProps } canEscapeKeyClose = { this . props . canEscapeKeyClose } canOutsideClickClose = { this . props . interactionKind === Popover2InteractionKind . CLICK } enforceFocus = { this . props . enforceFocus } hasBackdrop = { this . props . hasBackdrop } isOpen = { isOpen } onClose = { this . handleOverlayClose } onClosed = { this . props . onClosed } onClosing = { this . props . onClosing } onOpened = { this . props . onOpened } onOpening = { this . props . onOpening } transitionDuration = { this . props . transitionDuration } transitionName = { Classes . POPOVER2 } usePortal = { this . props . usePortal } portalClassName = { this . props . portalClassName } portalContainer = { this . props . portalContainer } shouldReturnFocusOnClose = { this . isHoverInteractionKind ( ) ? false : shouldReturnFocusOnClose } > < div className = { Classes . POPOVER2_TRANSITION_CONTAINER } ref = { popperProps . ref } style = { popperProps . style } > < ResizeSensor2 onResize = { this . reposition } > < div className = { popoverClasses } style = { { transformOrigin } } ref = { this . popoverRef } { ... popoverHandlers } > { this . isArrowEnabled ( ) && ( < Popover2Arrow arrowProps = { popperProps . arrowProps } placement = { popperProps . placement } / > ) } < div className = { Classes . POPOVER2_CONTENT } > { this . props . content } < /div> < /div> < /ResizeSensor2> < /div> < /Overlay>); } ; private getPopperModifiers ( ) { const { modifiers } = this . props ; return [ { enabled : this . isArrowEnabled ( ) , name : "arrow" , ... modifiers ?. arrow , } , { name : "computeStyles" , ... modifiers ?. computeStyles , options : { adaptive : true , gpuAcceleration : false , ... modifiers ?. computeStyles ?. options , } , } , { enabled : this . isArrowEnabled ( ) , name : "offset" , ... modifiers ?. offset , options : { offset : [ 0 , POPOVER_ARROW_SVG_SIZE / 2 ] , ... modifiers ?. offset ?. options , } , } , { name : "flip" , ... modifiers ?. flip , options : { boundary : this . props . boundary , rootBoundary : this . props . rootBoundary , ... modifiers ?. flip ?. options , } , } , { name : "preventOverflow" , ... modifiers ?. preventOverflow , options : { boundary : this . props . boundary , rootBoundary : this . props . rootBoundary , ... modifiers ?. preventOverflow ?. options , } , } , ] ; } private handleTargetFocus = ( e ) => { if ( this . props . openOnTargetFocus && this . isHoverInteractionKind ( ) ) { if ( e . relatedTarget == null && ! this . lostFocusOnSamePage ) { return ; } this . handleMouseEnter ( ( e as unknown ) as React . MouseEvent < HTMLElement > ) ; } } ; private handleTargetBlur = ( e ) => { if ( this . props . openOnTargetFocus && this . isHoverInteractionKind ( ) ) { if ( e . relatedTarget != null ) { if ( e . relatedTarget !== this . popoverElement && ! this . isElementInPopover ( e . relatedTarget as HTMLElement ) ) { this . handleMouseLeave ( ( e as unknown ) as React . MouseEvent < HTMLElement > ) ; } } else { this . handleMouseLeave ( ( e as unknown ) as React . MouseEvent < HTMLElement > ) ; } } this . lostFocusOnSamePage = e . relatedTarget != null ; } ; private handleTargetContextMenu = ( e ) => { if ( e . defaultPrevented ) { this . setOpenState ( false , e ) ; } } ; private handleMouseEnter = ( e ) => { this . isMouseInTargetOrPopover = true ; if ( ! this . props . usePortal && this . isElementInPopover ( e . target as Element ) && this . props . interactionKind === Popover2InteractionKind . HOVER_TARGET_ONLY && ! this . props . openOnTargetFocus ) { this . handleMouseLeave ( e ) ; } else if ( ! this . props . disabled ) { this . setOpenState ( true , e , this . props . hoverOpenDelay ) ; } } ; private handleMouseLeave = ( e ) => { this . isMouseInTargetOrPopover = false ; this . setTimeout ( ( ) => { if ( this . isMouseInTargetOrPopover ) { return ; } this . setOpenState ( false , e , this . props . hoverCloseDelay ) ; } ) ; } ; private handlePopoverClick = ( e ) => { const eventTarget = e . target as HTMLElement ; const eventPopover = eventTarget . closest ( `.${ Classes . POPOVER2 }` ) ; const eventPopoverV1 = eventTarget . closest ( `.${ CoreClasses . POPOVER }` ) ; const isEventFromSelf = ( eventPopover ?? eventPopoverV1 ) === this . getPopoverElement ( ) ; const isEventPopoverCapturing = eventPopover ?. classList . contains ( Classes . POPOVER2_CAPTURING_DISMISS ) ?? eventPopoverV1 ?. classList . contains ( CoreClasses . POPOVER_CAPTURING_DISMISS ) ?? false ; const dismissElement = eventTarget . closest ( `.${ Classes . POPOVER2_DISMISS }, .${ Classes . POPOVER2_DISMISS_OVERRIDE }` ) ; const dismissElementV1 = eventTarget . closest ( `.${ CoreClasses . POPOVER_DISMISS }, .${ CoreClasses . POPOVER_DISMISS_OVERRIDE }` ) ; const shouldDismiss = dismissElement ?. classList . contains ( Classes . POPOVER2_DISMISS ) ?? dismissElementV1 ?. classList . contains ( CoreClasses . POPOVER_DISMISS ) ?? false ; const isDisabled = eventTarget . closest ( `:disabled, .${ CoreClasses . DISABLED }` ) != null ; if ( shouldDismiss && ! isDisabled && ( ! isEventPopoverCapturing || isEventFromSelf ) ) { this . setOpenState ( false , e ) ; } } ; private handleOverlayClose = ( e ? ) => { if ( this . targetElement === null || e === undefined ) { return ; } const eventTarget = e . target as HTMLElement ; if ( ! Utils . elementIsOrContains ( this . targetElement , eventTarget ) || e . nativeEvent instanceof KeyboardEvent ) { this . setOpenState ( false , e ) ; } } ; private handleTargetClick = ( e ) => { if ( ! this . props . disabled && ! this . isElementInPopover ( e . target as HTMLElement ) ) { if ( this . props . isOpen == null ) { this . setState ( prevState => ( { isOpen : ! prevState . isOpen } ) ) ; } else { this . setOpenState ( ! this . props . isOpen , e ) ; } } } ; private setOpenState ( isOpen , e ? , timeout ? ) { this . cancelOpenTimeout ?. ( ) ; if ( timeout !== undefined && timeout > 0 ) { this . cancelOpenTimeout = this . setTimeout ( ( ) => this . setOpenState ( isOpen , e ) , timeout ) ; } else { if ( this . props . isOpen == null ) { this . setState ( { isOpen } ) ; } else { this . props . onInteraction ?. ( isOpen , e ) ; } if ( ! isOpen ) { this . props . onClose ?. ( e ! ) ; } } } private updateDarkParent ( ) { if ( this . props . usePortal && this . state . isOpen ) { const hasDarkParent = this . targetElement != null && this . targetElement . closest ( `.${ CoreClasses . DARK }` ) != null ; this . setState ( { hasDarkParent } ) ; } } private isElementInPopover ( element ) { return this . getPopoverElement ( ) ?. contains ( element ) ?? false ; } } function noop ( ) { }