import { useState , useRef , useEffect , useCallback , useMemo , ReactNode , DOMAttributes , SyntheticEvent , MutableRefObject , MouseEvent } from 'react' ; import { createPortal , findDOMNode } from 'react-dom' ; import useSSR from 'use-ssr' ; type HTMLElRef = MutableRefObject < HTMLElement > ; type CustomEvent = { event ? : SyntheticEvent < any , Event > ; portal : HTMLElRef ; targetEl : HTMLElRef ; } & SyntheticEvent < any , Event > ; type CustomEventHandler = ( customEvent : CustomEvent ) => void ; type CustomEventHandlers = { [ K in keyof DOMAttributes < K > ] ? : CustomEventHandler ; } ; type EventListenerMap = { [ K in keyof DOMAttributes < K > ] : keyof GlobalEventHandlersEventMap ; } ; type EventListenersRef = MutableRefObject < { [ K in keyof DOMAttributes < K > ] ? : ( event : SyntheticEvent < any , Event > ) => void ; } > ; export type UsePortalOptions = { closeOnOutsideClick ? : boolean ; closeOnEsc ? : boolean ; bindTo ? : HTMLElement ; isOpen ? : boolean ; onOpen ? : CustomEventHandler ; onClose ? : CustomEventHandler ; onPortalClick ? : CustomEventHandler ; } & CustomEventHandlers ; type UsePortalObjectReturn = { } ; type UsePortalArrayReturn = [ ] ; export const errorMessage1 = 'You must either add a `ref` to the element you are interacting with or pass an `event` to openPortal(e) or togglePortal(e).' ; export default function usePortal ( { closeOnOutsideClick = true , closeOnEsc = true , bindTo , isOpen : defaultIsOpen = false , onOpen , onClose , onPortalClick , ... eventHandlers } = { } ) { const { isServer , isBrowser } = useSSR ( ) ; const [ isOpen , makeOpen ] = useState ( defaultIsOpen ) ; const open = useRef ( isOpen ) ; const setOpen = useCallback ( ( v ) => { open . current = v ; makeOpen ( v ) ; } , [ ] ) ; const targetEl = useRef ( ) as HTMLElRef ; const portal = useRef ( isBrowser ? document . createElement ( 'div' ) : null ) as HTMLElRef ; useEffect ( ( ) => { if ( isBrowser && ! portal . current ) portal . current = document . createElement ( 'div' ) ; } , [ isBrowser , portal ] ) ; const elToMountTo = useMemo ( ( ) => { if ( isServer ) return ; return ( bindTo && findDOMNode ( bindTo ) ) || document . body ; } , [ isServer , bindTo ] ) ; const createCustomEvent = ( e ) => { if ( ! e ) return { portal , targetEl , event : e } ; const event = e || { } ; if ( event . persist ) event . persist ( ) ; event . portal = portal ; event . targetEl = targetEl ; event . event = e ; const { currentTarget } = e ; if ( ! targetEl . current && currentTarget && currentTarget !== document ) targetEl . current = event . currentTarget ; return event ; } ; const customEventHandlers = Object . entries ( eventHandlers ) . reduce < any > ( ( acc , [ handlerName , eventHandler ] ) => { acc [ handlerName ] = ( event ? ) => { if ( isServer ) return ; eventHandler ( createCustomEvent ( event ) ) ; } ; return acc ; } , { } ) ; const openPortal = useCallback ( ( e ) => { if ( isServer ) return ; const customEvent = createCustomEvent ( e ) ; if ( targetEl . current == null ) { setTimeout ( ( ) => setOpen ( true ) , 0 ) ; throw Error ( errorMessage1 ) ; } if ( onOpen ) onOpen ( customEvent ) ; setOpen ( true ) ; } , [ isServer , portal , setOpen , targetEl , onOpen ] ) ; const closePortal = useCallback ( ( e ) => { if ( isServer ) return ; const customEvent = createCustomEvent ( e ) ; if ( onClose && open . current ) onClose ( customEvent ) ; if ( open . current ) setOpen ( false ) ; } , [ isServer , onClose , setOpen ] ) ; const togglePortal = useCallback ( ( e ) : void => open . current ? closePortal ( e ) : openPortal ( e ) , [ closePortal , openPortal ] ) ; const handleKeydown = useCallback ( ( e ) : void => ( e . key === 'Escape' && closeOnEsc ) ? closePortal ( e ) : undefined , [ closeOnEsc , closePortal ] ) ; const handleOutsideMouseClick = useCallback ( ( e ) : void => { const containsTarget = ( target ) => target . current . contains ( e . target as HTMLElement ) ; if ( containsTarget ( portal ) || ( e as any ) . button !== 0 || ! open . current || containsTarget ( targetEl ) ) return ; if ( closeOnOutsideClick ) closePortal ( e ) ; } , [ isServer , closePortal , closeOnOutsideClick , portal ] ) ; const handleMouseDown = useCallback ( ( e ) : void => { if ( isServer || ! ( portal . current instanceof HTMLElement ) ) return ; const customEvent = createCustomEvent ( e ) ; if ( portal . current . contains ( customEvent . target as HTMLElement ) && onPortalClick ) onPortalClick ( customEvent ) ; handleOutsideMouseClick ( e ) ; } , [ handleOutsideMouseClick ] ) ; const eventListeners = useRef ( { } ) as EventListenersRef ; useEffect ( ( ) => { if ( isServer ) return ; if ( ! ( elToMountTo instanceof HTMLElement ) || ! ( portal . current instanceof HTMLElement ) ) return ; const eventHandlerMap = { onScroll : 'scroll' , onWheel : 'wheel' , } ; const node = portal . current ; elToMountTo . appendChild ( portal . current ) ; Object . entries ( eventHandlerMap ) . forEach ( ( [ handlerName , eventListenerName ] ) => { if ( ! eventHandlers [ handlerName as keyof EventListenerMap ] ) return ; eventListeners . current [ handlerName as keyof EventListenerMap ] = ( e ) => ( eventHandlers [ handlerName as keyof EventListenerMap ] as any ) ( createCustomEvent ( e ) ) ; document . addEventListener ( eventListenerName as keyof GlobalEventHandlersEventMap , eventListeners . current [ handlerName as keyof EventListenerMap ] as any ) ; } ) ; document . addEventListener ( 'keydown' , handleKeydown ) ; document . addEventListener ( 'mousedown' , handleMouseDown as any ) ; return ( ) => { Object . entries ( eventHandlerMap ) . forEach ( ( [ handlerName , eventListenerName ] ) => { if ( ! eventHandlers [ handlerName as keyof EventListenerMap ] ) return ; document . removeEventListener ( eventListenerName as keyof GlobalEventHandlersEventMap , eventListeners . current [ handlerName as keyof EventListenerMap ] as any ) ; delete eventListeners . current [ handlerName as keyof EventListenerMap ] ; } ) ; document . removeEventListener ( 'keydown' , handleKeydown ) ; document . removeEventListener ( 'mousedown' , handleMouseDown as any ) ; elToMountTo . removeChild ( node ) ; } ; } , [ isServer , handleOutsideMouseClick , handleKeydown , elToMountTo , portal ] ) ; const Portal = useCallback ( ( { children } ) => { if ( portal . current != null ) return createPortal ( children , portal . current ) ; return null ; } , [ portal ] ) ; return Object . assign ( [ openPortal , closePortal , open . current , Portal , togglePortal , targetEl , portal ] , { isOpen : open . current , openPortal , ref : targetEl , closePortal , togglePortal , Portal , portalRef : portal , ... customEventHandlers , bind : { ref : targetEl , ... customEventHandlers } } ) ; }