import { createPopper , Instance , Placement , State } from '@popperjs/core' ; import { CSSProperties , Dispatch , Ref , SetStateAction , useCallback , useEffect , useMemo , useRef , useState , } from 'react' ; import { isUA } from 'reakit-utils/dom' ; import { shallowEqual } from 'reakit-utils/shallowEqual' ; import useLayoutEffect from 'use-isomorphic-layout-effect' ; const isSafari = isUA ( 'Mac' ) && ! isUA ( 'Chrome' ) && isUA ( 'Safari' ) ; export interface PopperState { /**
     * The reference element.
     */ referenceRef : Ref < HTMLElement | undefined > ; private popperRef : Ref < HTMLElement | undefined > ; private arrowRef : Ref < HTMLElement | undefined > ; private popoverStyles : CSSProperties ; private arrowStyles : CSSProperties ; private originalPlacement : Placement ; private update : ( ) => boolean ; /**
     * Actual `placement`.
     */ placement : Placement ; } export interface PopperActions { /**
     * Change the `placement` state.
     */ place : Dispatch < SetStateAction < Placement > > ; } export interface UsePopperProps extends Partial < Pick < PopperState , 'placement' > > { /**
     * Whether or not the popover should have `position` set to `fixed`.
     */ fixed ? : boolean ; /**
     * Flip the popover's placement when it starts to overlap its reference
     * element.
     */ flip ? : boolean ; /**
     * Offset between the reference and the popover: [main axis, alt axis]. Should not be combined with `gutter`.
     */ offset ? : [ number | string , number | string ] ; /**
     * Offset between the reference and the popover on the main axis. Should not be combined with `offset`.
     */ gutter ? : number ; /**
     * Prevents popover from being positioned outside the boundary.
     */ preventOverflow ? : boolean ; /**
     * Whether the popper should be visible.
     */ visible ? : boolean ; } export interface PopperStateReturn extends PopperState , PopperActions { } function applyStyles ( styles ? ) { return ( prevStyles ) => { if ( styles && ! shallowEqual ( prevStyles , styles ) ) { return styles as CSSProperties ; } return prevStyles ; } ; } export function usePopper ( props = { } ) { const { gutter = 12 , placement : sealedPlacement = 'bottom' , flip : flip = true , offset : sealedOffset , preventOverflow : preventOverflow = true , fixed = false , visible = false , } = props ; const popper = useRef < Instance | null > ( null ) ; const referenceRef = useRef < HTMLElement > ( ) ; const popperRef = useRef < HTMLElement > ( ) ; const arrowRef = useRef < HTMLElement > ( ) ; const [ originalPlacement , place ] = useState ( sealedPlacement ) ; const [ placement , setPlacement ] = useState ( sealedPlacement ) ; const [ offset ] = useState ( sealedOffset || [ 0 , gutter ] ) ; const [ popoverStyles , setPopoverStyles ] = useState < CSSProperties > ( { position : fixed ? 'fixed' : 'absolute' , left : '-9999999px' , top : '-9999999px' , } ) ; const [ arrowStyles , setArrowStyles ] = useState < CSSProperties > ( { } ) ; const update = useCallback ( ( ) => { if ( popper . current ) { popper . current . forceUpdate ( ) ; return true ; } return false ; } , [ ] ) ; const updateState = useCallback ( ( state ) => { if ( state . placement ) { setPlacement ( state . placement ) ; } if ( state . styles ) { setPopoverStyles ( applyStyles ( state . styles . popper ) ) ; if ( arrowRef . current ) { setArrowStyles ( applyStyles ( state . styles . arrow ) ) ; } } } , [ ] ) ; useLayoutEffect ( ( ) => { if ( referenceRef . current && popperRef . current ) { popper . current = createPopper ( referenceRef . current , popperRef . current , { placement : originalPlacement , strategy : fixed ? 'fixed' : 'absolute' , onFirstUpdate : isSafari ? updateState : undefined , modifiers : [ { name : 'eventListeners' , enabled : visible , } , { name : 'applyStyles' , enabled : false , } , { name : 'flip' , enabled : flip , options : { padding : 8 } , } , { name : 'offset' , options : { offset } , } , { name : 'preventOverflow' , enabled : preventOverflow , options : { tetherOffset : ( ) => arrowRef . current ?. clientWidth || 0 , } , } , { name : 'arrow' , enabled : ! ! arrowRef . current , options : { element : arrowRef . current } , } , { name : 'updateState' , phase : 'write' , requires : [ 'computeStyles' ] , enabled : visible , fn : ( { state } ) => updateState ( state ) , } , ] , } ) ; } return ( ) => { if ( popper . current ) { popper . current . destroy ( ) ; popper . current = null ; } } ; } , [ originalPlacement , fixed , visible , flip , offset , preventOverflow , updateState ] ) ; useEffect ( ( ) => { if ( ! visible ) { return ; } const id = window . requestAnimationFrame ( ( ) => { popper . current ?. forceUpdate ( ) ; } ) ; return ( ) => { window . cancelAnimationFrame ( id ) ; } ; } , [ visible ] ) ; return useMemo ( ( ) => ( { referenceRef , popperRef , arrowRef , popoverStyles , arrowStyles , update , originalPlacement , placement , place , } ) , [ arrowStyles , originalPlacement , placement , popoverStyles , update ] ) ; }