import React from 'react' ; import { PaddingProps , LayoutProps , ResponsiveValue } from 'styled-system' ; import { PortalProps } from '../Portal' ; import { Box } from '../Box' ; import PopoverOverlay from './PopoverOverlay' ; import PopoverContent from './PopoverContent' ; import { onKey , onKeys } from '../../helpers/keyEvents' ; import useWindowEvent from '../../hooks/useWindowEvent' ; import { findFocusableChild } from '../../helpers/focus' ; import type * as Polymorphic from '../../helpers/types' ; export type PopoverProps = PaddingProps & LayoutProps & { id ? : string ; /**
     * A React component to will trigger the popover
     * Click event is handled for you if this component is uncontrolled.
     */ trigger ? : React . ReactNode ; deprecated sectioned ? : boolean ; /**
     * Opens the popover.
     * By default, open state is handled automatically. Passing this value in will turn this into a controlled component.
     */ open ? : boolean ; deprecated left ? : boolean ; deprecated right ? : boolean ; deprecated top ? : boolean ; deprecated bottom ? : boolean ; position ? : ResponsiveValue < 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight' > ; /**
     * Callback function that is called when clicking outside the popover, or hitting escape.
     */ onClose ? : ( e : MouseEvent | KeyboardEvent ) => void ; /**
     * Popover Content
     */ children ? : React . ReactNode ; as ? : 'div' | 'span' ; deprecated wrapper ? : 'div' | 'span' ; portalId ? : PortalProps [ 'containerId' ] ; /**
     * Closes the popover when tabbing out of the popover. On by default.
     */ closeOnTab ? : boolean ; /**
     * Closes the popover when clicking inside the popover. Off by default.
     */ closeOnInsideClick ? : boolean ; } ; type PolymorphicPopover = Polymorphic . ForwardRefComponent < 'span' , PopoverProps > ; const Popover = React . forwardRef < HTMLSpanElement , PopoverProps > ( function Popover ( props , ref ) { const { as , id , open : controlledOpen , onClose , children , trigger , wrapper , portalId , closeOnTab = true , closeOnInsideClick = false , ... rest } = props ; const [ open , setOpen ] = React . useState ( null ) ; const popoverRef = React . useRef < HTMLDivElement > ( ) ; const activatorRef = React . useRef < HTMLDivElement > ( ) ; const shouldBeOpen = controlledOpen || open ; const Wrapper = as || wrapper || 'span' ; useWindowEvent ( 'click' , handleClick ) ; useWindowEvent ( 'keydown' , handleEsc ) ; React . useEffect ( ( ) => { if ( controlledOpen === undefined ) { setOpen ( false ) ; } } , [ ] ) ; React . useEffect ( ( ) => { if ( shouldBeOpen && popoverRef && popoverRef . current ) { const contentToFocus = findFocusableChild ( popoverRef . current ) || popoverRef . current ; setTimeout ( ( ) => { contentToFocus . focus ( ) ; } , 10 ) ; } } , [ shouldBeOpen ] ) ; React . useEffect ( ( ) => { if ( activatorRef && activatorRef . current ) { const activatorElem = findFocusableChild ( activatorRef . current ) || activatorRef . current ; activatorElem . setAttribute ( 'aria-haspopup' , 'true' ) ; activatorElem . setAttribute ( 'aria-expanded' , Boolean ( shouldBeOpen ) . toString ( ) ) ; } } , [ trigger , activatorRef , open , controlledOpen ] ) ; function handleUncontrolledToggle ( ) { setOpen ( ! open ) ; } function focusOnActivator ( ) { if ( activatorRef && activatorRef . current ) { const activatorToFocus = findFocusableChild ( activatorRef . current ) || activatorRef . current ; activatorToFocus . focus ( ) ; } } function handleClick ( e ) { const isInside = popoverRef . current && popoverRef . current . contains ( e . target as Node ) ; const isOutside = popoverRef . current && ! popoverRef . current . contains ( e . target as Node ) && activatorRef . current && ! activatorRef . current . contains ( e . target as Node ) ; if ( ( isOutside && shouldBeOpen ) || ( isInside && closeOnInsideClick && shouldBeOpen ) ) { if ( onClose ) { onClose ( e ) ; } if ( open ) { handleUncontrolledToggle ( ) ; } } } function handleEsc ( e ) { if ( onClose && shouldBeOpen ) { onKey ( 'escape' , ( ) => { onClose ( e ) ; focusOnActivator ( ) ; } ) ( e ) ; } if ( onClose && shouldBeOpen && closeOnTab ) { onKey ( 'tab' , ( ) => { onClose ( e ) ; focusOnActivator ( ) ; } ) ( e ) ; } if ( open ) { onKey ( 'escape' , ( ) => { handleUncontrolledToggle ( ) ; focusOnActivator ( ) ; } ) ( e ) ; } if ( closeOnTab && open ) { onKey ( 'tab' , ( ) => { handleUncontrolledToggle ( ) ; focusOnActivator ( ) ; } ) ( e ) ; } } function handleTrigger ( ) { if ( open !== null ) { handleUncontrolledToggle ( ) ; } } function handleActivatorKey ( e ) { if ( open === false ) { onKeys ( [ 'arrowUp' , 'arrowDown' ] , ( ) => { e . preventDefault ( ) ; handleUncontrolledToggle ( ) ; } ) ( e ) ; } } function renderPopover ( ) { function assignRefs ( node ) { if ( ref ) { ( ref as React . MutableRefObject < HTMLDivElement > ) . current = node ; } popoverRef . current = node ; } return ( < PopoverContent open = { shouldBeOpen } ref = { assignRefs } { ... rest } > { children } < /PopoverContent>); } function renderActivator ( { activatorRef : forwardedRef } ) { function assignRefs ( node ) { forwardedRef ( node ) ; activatorRef . current = node ; } return ( < Box as = { Wrapper } display = { Wrapper === 'span' ? 'inline-block' : null } position = "relative" onClick = { handleTrigger } onKeyDown = { handleActivatorKey } ref = { assignRefs } > { trigger } < /Box>); } return ( < PopoverOverlay as = { Wrapper } id = { id } open = { shouldBeOpen } renderActivator = { renderActivator } renderPopover = { renderPopover } portalId = { portalId } / > ) ; } ) as PolymorphicPopover & { PopoverContent : typeof PopoverContent ; PopoverOverlay : typeof PopoverOverlay ; } ; Popover . displayName = 'Popover' ; export default Popover ;