import classNames from "classnames" ; import { ModifierFn } from "popper.js" ; import * as React from "react" ; import { polyfill } from "react-lifecycles-compat" ; import { Manager , Popper , PopperChildrenProps , Reference , ReferenceChildrenProps } from "react-popper" ; import { AbstractPureComponent2 , Classes , IRef , refHandler , setRef } from "../../common" ; import * as Errors from "../../common/errors" ; import { DISPLAYNAME_PREFIX , HTMLDivProps } from "../../common/props" ; import * as Utils from "../../common/utils" ; import { Overlay } from "../overlay/overlay" ; import { ResizeSensor } from "../resize-sensor/resizeSensor" ; import { Tooltip } from "../tooltip/tooltip" ; import { PopoverArrow } from "./popoverArrow" ; import { positionToPlacement } from "./popoverMigrationUtils" ; import { IPopoverSharedProps , PopperModifiers } from "./popoverSharedProps" ; import { arrowOffsetModifier , getTransformOrigin } from "./popperUtils" ; export const PopoverInteractionKind = { 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 PopoverInteractionKind = typeof PopoverInteractionKind [ keyof typeof PopoverInteractionKind ] ; export interface IPopoverProps extends IPopoverSharedProps { /** HTML props for the backdrop element. Can be combined with `backdropClassName`. */ backdropProps ? : React . HTMLProps < HTMLDivElement > ; /**
     * The content displayed inside the popover. This can instead be provided as
     * the _second_ element in `children` (first is `target`).
     */ content ? : string | JSX . Element ; /**
     * Whether the wrapper and target should take up the full width of their container.
     * Note that supplying `true` for this prop will force  `targetTagName="div"` and
     * `wrapperTagName="div"`.
     */ fill ? : boolean ; default interactionKind ? : PopoverInteractionKind ; default hasBackdrop ? : boolean ; default shouldReturnFocusOnClose ? : boolean ; /**
     * Ref supplied to the `Classes.POPOVER` element.
     */ popoverRef ? : IRef < HTMLElement > ; /**
     * The target to which the popover content is attached. This can instead be
     * provided as the _first_ element in `children`.
     */ target ? : string | JSX . Element ; } export interface IPopoverState { transformOrigin : string ; isOpen : boolean ; hasDarkParent : boolean ; } deprecated @ polyfill export class Popover extends AbstractPureComponent2 < IPopoverProps , IPopoverState > { public static displayName = `${ DISPLAYNAME_PREFIX }.Popover` ; private popoverRef = Utils . createReactRef < HTMLDivElement > ( ) ; public static defaultProps : IPopoverProps = { boundary : "scrollParent" , captureDismiss : false , defaultIsOpen : false , disabled : false , fill : false , hasBackdrop : false , hoverCloseDelay : 300 , hoverOpenDelay : 150 , inheritDarkTheme : true , interactionKind : PopoverInteractionKind . CLICK , minimal : false , modifiers : { } , openOnTargetFocus : true , shouldReturnFocusOnClose : false , targetTagName : "span" , transitionDuration : 300 , usePortal : true , wrapperTagName : "span" , } ; /**
     * 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 ; public state : IPopoverState = { hasDarkParent : false , isOpen : this . getIsOpen ( this . props ) , transformOrigin : "" , } ; private cancelOpenTimeout ? : ( ) => void ; private isMouseInTargetOrPopover = false ; private lostFocusOnSamePage = true ; private popperScheduleUpdate ? : ( ) => void ; private handlePopoverRef : IRef < HTMLElement > = refHandler ( this , "popoverElement" , this . props . popoverRef ) ; private handleTargetRef = ( ref ) => ( this . targetElement = ref ) ; public render ( ) { const { className , disabled , fill , placement , position = "auto" , shouldReturnFocusOnClose } = this . props ; const { isOpen } = this . state ; let { wrapperTagName } = this . props ; if ( fill ) { wrapperTagName = "div" ; } const isContentEmpty = Utils . ensureElement ( this . understandChildren ( ) . content ) == null ; if ( isContentEmpty && ! disabled && isOpen !== false && ! Utils . isNodeEnv ( "production" ) ) { console . warn ( Errors . POPOVER_WARN_EMPTY_CONTENT ) ; } const wrapperClasses = classNames ( Classes . POPOVER_WRAPPER , className , { [ Classes . FILL ] : fill , } ) ; const defaultAutoFocus = this . isHoverInteractionKind ( ) ? false : undefined ; const wrapper = React . createElement ( wrapperTagName ! , { className : wrapperClasses } , < Reference innerRef = { this . handleTargetRef } > { this . renderTarget } < /Reference>, <Overlay autoFocus={this.props.autoFocus ?? defaultAutoFocus} backdropClassName={Classes.POPOVER_BACKDROP} backdropProps={this.props.backdropProps} canEscapeKeyClose={this.props.canEscapeKeyClose} canOutsideClickClose={this.props.interactionKind === PopoverInteractionKind.CLICK} className={this.props.portalClassName} enforceFocus={this.props.enforceFocus} hasBackdrop={this.props.hasBackdrop} isOpen={isOpen && !isContentEmpty} 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.POPOVER} usePortal={this.props.usePortal} portalContainer={this.props.portalContainer}  shouldReturnFocusOnClose = { this . isHoverInteractionKind ( ) ? false : shouldReturnFocusOnClose } > < Popper innerRef = { this . handlePopoverRef } placement = { placement ?? positionToPlacement ( position ) } modifiers = { this . getPopperModifiers ( ) } > { this . renderPopover } < /Popper> < /Overlay>); return < Manager > { wrapper } < /Manager>; } public componentDidMount ( ) { this . updateDarkParent ( ) ; } public componentDidUpdate ( prevProps , prevState ) { super . componentDidUpdate ( prevProps , prevState ) ; if ( prevProps . popoverRef !== this . props . popoverRef ) { setRef ( prevProps . popoverRef , null ) ; this . handlePopoverRef = refHandler ( this , "popoverElement" , this . props . popoverRef ) ; setRef ( this . props . popoverRef , this . popoverElement ) ; } 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 ) ; } } public reposition = ( ) => this . popperScheduleUpdate ?. ( ) ; protected validateProps ( props ) { if ( props . isOpen == null && props . onInteraction != null ) { console . warn ( Errors . POPOVER_WARN_UNCONTROLLED_ONINTERACTION ) ; } if ( props . hasBackdrop && ! props . usePortal ) { console . warn ( Errors . POPOVER_WARN_HAS_BACKDROP_INLINE ) ; } if ( props . hasBackdrop && props . interactionKind !== PopoverInteractionKind . CLICK ) { console . error ( Errors . POPOVER_HAS_BACKDROP_INTERACTION ) ; } if ( props . placement !== undefined && props . position !== undefined ) { console . warn ( Errors . POPOVER_WARN_PLACEMENT_AND_POSITION_MUTEX ) ; } const childrenCount = React . Children . count ( props . children ) ; const hasContentProp = props . content !== undefined ; const hasTargetProp = props . target !== undefined ; if ( childrenCount === 0 && ! hasTargetProp ) { console . error ( Errors . POPOVER_REQUIRES_TARGET ) ; } if ( childrenCount > 2 ) { console . warn ( Errors . POPOVER_WARN_TOO_MANY_CHILDREN ) ; } if ( childrenCount > 0 && hasTargetProp ) { console . warn ( Errors . POPOVER_WARN_DOUBLE_TARGET ) ; } if ( childrenCount === 2 && hasContentProp ) { console . warn ( Errors . POPOVER_WARN_DOUBLE_CONTENT ) ; } } private updateDarkParent ( ) { if ( this . props . usePortal && this . state . isOpen ) { const hasDarkParent = this . targetElement != null && this . targetElement . closest ( `.${ Classes . DARK }` ) != null ; this . setState ( { hasDarkParent } ) ; } } private renderPopover = ( popperProps ) => { const { interactionKind , usePortal } = this . props ; const { transformOrigin } = this . state ; this . popperScheduleUpdate = popperProps . scheduleUpdate ; const popoverHandlers = { onClick : this . handlePopoverClick , } ; if ( interactionKind === PopoverInteractionKind . HOVER || ( ! usePortal && interactionKind === PopoverInteractionKind . HOVER_TARGET_ONLY ) ) { popoverHandlers . onMouseEnter = this . handleMouseEnter ; popoverHandlers . onMouseLeave = this . handleMouseLeave ; } const popoverClasses = classNames ( Classes . POPOVER , { [ Classes . DARK ] : this . props . inheritDarkTheme && this . state . hasDarkParent , [ Classes . MINIMAL ] : this . props . minimal , [ Classes . POPOVER_CAPTURING_DISMISS ] : this . props . captureDismiss , } , this . props . popoverClassName ) ; return ( < div className = { Classes . TRANSITION_CONTAINER } ref = { popperProps . ref } style = { popperProps . style } > < ResizeSensor onResize = { this . reposition } > < div className = { popoverClasses } style = { { transformOrigin } } ref = { this . popoverRef } { ... popoverHandlers } > { this . isArrowEnabled ( ) && ( < PopoverArrow arrowProps = { popperProps . arrowProps } placement = { popperProps . placement } / > ) } < div className = { Classes . POPOVER_CONTENT } > { this . understandChildren ( ) . content } < /div> < /div> < /ResizeSensor> < /div>); } ; private renderTarget = ( referenceProps ) => { const { fill , openOnTargetFocus , targetClassName , targetProps = { } } = this . props ; const { isOpen } = this . state ; const isControlled = this . isControlled ( ) ; const isHoverInteractionKind = this . isHoverInteractionKind ( ) ; let { targetTagName } = this . props ; if ( fill ) { targetTagName = "div" ; } const finalTargetProps = isHoverInteractionKind ? { onBlur : this . handleTargetBlur , onFocus : this . handleTargetFocus , onMouseEnter : this . handleMouseEnter , onMouseLeave : this . handleMouseLeave , } : { onClick : this . handleTargetClick , } ; finalTargetProps [ "aria-haspopup" ] = "true" ; finalTargetProps . className = classNames ( Classes . POPOVER_TARGET , { [ Classes . POPOVER_OPEN ] : isOpen } , targetProps . className , targetClassName ) ; finalTargetProps . ref = referenceProps . ref ; const rawTarget = Utils . ensureElement ( this . understandChildren ( ) . target ) ; if ( rawTarget === undefined ) { return null ; } const rawTabIndex = rawTarget . props . tabIndex ; const tabIndex = rawTabIndex == null && openOnTargetFocus && isHoverInteractionKind ? 0 : rawTabIndex ; const clonedTarget = React . cloneElement ( rawTarget , { className : classNames ( rawTarget . props . className , { [ Classes . ACTIVE ] : isOpen && ! isControlled && ! isHoverInteractionKind , } ) , disabled : isOpen && Utils . isElementOfType ( rawTarget , Tooltip ) ? true : rawTarget . props . disabled , tabIndex , } ) ; const target = React . createElement ( targetTagName ! , { ... targetProps , ... finalTargetProps , } , clonedTarget ) ; return < ResizeSensor onResize = { this . reposition } > { target } < /ResizeSensor>; } ; private understandChildren ( ) { const { children , content : contentProp , target : targetProp } = this . props ; const [ targetChild , contentChild ] = React . Children . toArray ( children ) ; return { content : contentChild == null ? contentProp : contentChild , target : targetChild == null ? targetProp : targetChild , } ; } private isControlled = ( ) => this . props . isOpen !== undefined ; private getIsOpen ( props ) { if ( props . disabled ) { return false ; } else if ( props . isOpen != null ) { return props . isOpen ; } else { return props . defaultIsOpen ! ; } } private getPopperModifiers ( ) { const { boundary , modifiers } = this . props ; const { flip = { } , preventOverflow = { } } = modifiers ! ; return { ... modifiers , arrowOffset : { enabled : this . isArrowEnabled ( ) , fn : arrowOffsetModifier , order : 510 , } , flip : { boundariesElement : boundary , ... flip } , preventOverflow : { boundariesElement : boundary , ... preventOverflow } , updatePopoverState : { enabled : true , fn : this . updatePopoverState , order : 900 , } , } ; } 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 > ) ; } this . props . targetProps ?. onFocus ?. ( e ) ; } ; private handleTargetBlur = ( e ) => { if ( this . props . openOnTargetFocus && this . isHoverInteractionKind ( ) ) { if ( e . relatedTarget != null && ! this . isElementInPopover ( e . relatedTarget as HTMLElement ) ) { this . handleMouseLeave ( ( e as unknown ) as React . MouseEvent < HTMLElement > ) ; } } this . lostFocusOnSamePage = e . relatedTarget != null ; this . props . targetProps ?. onBlur ?. ( e ) ; } ; private handleMouseEnter = ( e ) => { this . isMouseInTargetOrPopover = true ; if ( ! this . props . usePortal && this . isElementInPopover ( e . target as Element ) && this . props . interactionKind === PopoverInteractionKind . HOVER_TARGET_ONLY && ! this . props . openOnTargetFocus ) { this . handleMouseLeave ( e ) ; } else if ( ! this . props . disabled ) { this . setOpenState ( true , e , this . props . hoverOpenDelay ) ; } this . props . targetProps ?. onMouseEnter ?. ( e ) ; } ; private handleMouseLeave = ( e ) => { this . isMouseInTargetOrPopover = false ; this . setTimeout ( ( ) => { if ( this . isMouseInTargetOrPopover ) { return ; } this . setOpenState ( false , e , this . props . hoverCloseDelay ) ; } ) ; this . props . targetProps ?. onMouseLeave ?. ( e ) ; } ; private handlePopoverClick = ( e ) => { const eventTarget = e . target as HTMLElement ; const eventPopover = eventTarget . closest ( `.${ Classes . POPOVER }` ) ; const isEventFromSelf = eventPopover === this . popoverRef . current ; const isEventPopoverCapturing = eventPopover ?. classList . contains ( Classes . POPOVER_CAPTURING_DISMISS ) ; const dismissElement = eventTarget . closest ( `.${ Classes . POPOVER_DISMISS }, .${ Classes . POPOVER_DISMISS_OVERRIDE }` ) ; const shouldDismiss = dismissElement != null && dismissElement . classList . contains ( Classes . POPOVER_DISMISS ) ; const isDisabled = eventTarget . closest ( `:disabled, .${ Classes . 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 ) ; } } this . props . targetProps ?. onClick ?. ( 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 isArrowEnabled ( ) { const { minimal , modifiers } = this . props ; return ! minimal && ( modifiers ?. arrow == null || modifiers . arrow . enabled ) ; } private isElementInPopover ( element ) { return this . popoverElement ?. contains ( element ) ; } private isHoverInteractionKind ( ) { return ( this . props . interactionKind === PopoverInteractionKind . HOVER || this . props . interactionKind === PopoverInteractionKind . HOVER_TARGET_ONLY ) ; } private updatePopoverState : ModifierFn = data => { this . setState ( { transformOrigin : getTransformOrigin ( data ) } ) ; return data ; } ; }