import { RefObject , useRef , useState , useEffect , useCallback } from "react" ; import canUsePassiveEvents from "./canUsePassiveEvents" ; export const DEFAULT_IGNORE_CLASS = "ignore-onclickoutside" ; export interface Callback < T extends Event = Event > { ( event ) : void ; } type El = HTMLElement ; type Refs = RefObject < El > [ ] ; export interface Options { refs ? : Refs ; disabled ? : boolean ; eventTypes ? : string [ ] ; excludeScrollbar ? : boolean ; ignoreClass ? : string | string [ ] ; detectIFrame ? : boolean ; } interface Return { ( element ) : void ; } const checkClass = ( el , cl ) : boolean => el . classList ?. contains ( cl ) ; const hasIgnoreClass = ( e , ignoreClass ) : boolean => { let el = e . target || e ; while ( el ) { if ( Array . isArray ( ignoreClass ) ) { if ( ignoreClass . some ( ( c ) => checkClass ( el , c ) ) ) return true ; } else if ( checkClass ( el , ignoreClass ) ) { return true ; } el = el . parentElement ; } return false ; } ; const clickedOnScrollbar = ( e ) : boolean => document . documentElement . clientWidth <= e . clientX || document . documentElement . clientHeight <= e . clientY ; const getEventOptions = ( type ) : { passive : boolean ; } | boolean => type . includes ( "touch" ) && canUsePassiveEvents ( ) ? { passive : true } : false ; const useOnclickOutside = ( callback , { refs : refsOpt , disabled , eventTypes = [ "mousedown" , "touchstart" ] , excludeScrollbar , ignoreClass = DEFAULT_IGNORE_CLASS , detectIFrame = true , } = { } ) : Return => { const [ refsState , setRefsState ] = useState < Refs > ( [ ] ) ; const callbackRef = useRef ( callback ) ; callbackRef . current = callback ; const ref = useCallback ( ( el ) => setRefsState ( ( prevState ) => [ ... prevState , { current : el } ] ) , [ ] ) ; useEffect ( ( ) => { if ( ! refsOpt ?. length && ! refsState . length ) return ; const getEls = ( ) => { const els = [ ] ; ( refsOpt || refsState ) . forEach ( ( { current } ) => current && els . push ( current ) ) ; return els ; } ; const handler = ( e ) => { if ( ! hasIgnoreClass ( e , ignoreClass ) && ! ( excludeScrollbar && clickedOnScrollbar ( e ) ) && getEls ( ) . every ( ( el ) => ! el . contains ( e . target ) ) ) callbackRef . current ( e ) ; } ; const blurHandler = ( e ) => setTimeout ( ( ) => { const { activeElement } = document ; if ( activeElement ?. tagName === "IFRAME" && ! hasIgnoreClass ( activeElement , ignoreClass ) && ! getEls ( ) . includes ( activeElement as HTMLIFrameElement ) ) callbackRef . current ( e ) ; } , 0 ) ; const removeEventListener = ( ) => { eventTypes . forEach ( ( type ) => document . removeEventListener ( type , handler , getEventOptions ( type ) ) ) ; if ( detectIFrame ) window . removeEventListener ( "blur" , blurHandler ) ; } ; if ( disabled ) { removeEventListener ( ) ; return ; } eventTypes . forEach ( ( type ) => document . addEventListener ( type , handler , getEventOptions ( type ) ) ) ; if ( detectIFrame ) window . addEventListener ( "blur" , blurHandler ) ; return ( ) => removeEventListener ( ) ; } , [ refsState , ignoreClass , excludeScrollbar , disabled , detectIFrame , JSON . stringify ( eventTypes ) , ] ) ; return ref ; } ; export default useOnclickOutside ;