import React, { useRef, useEffect, MouseEvent, FC, ReactElement, forwardRef, Ref, ReactNode, } from 'react' import { mergeRefs } from 'react-merge-refs' import hasParent from './has-parent' interface ClickOutsideProps { active: boolean onClick: (e?: MouseEvent) => void ref?: Ref children?: ReactNode } /** * Use forward ref to allow this component to be used with other components like * focus-trap-react, that rely on the same type of ref forwarding to direct children */ const ClickOutside: FC = forwardRef( ({ active = true, onClick, children }, forwardedRef) => { const innerRef = useRef() const child = children ? (React.Children.only(children) as any) : undefined if (!child || child.type === React.Fragment) { /** * React Fragments can't be used, as it would not be possible to pass the ref * created here to them. */ throw new Error('A valid non Fragment React Children should be provided') } if (typeof onClick != 'function') { throw new Error('onClick must be a valid function') } useEffect(() => { if (active) { document.addEventListener('mousedown', handleClick) document.addEventListener('touchstart', handleClick) } return () => { if (active) { document.removeEventListener('mousedown', handleClick) document.removeEventListener('touchstart', handleClick) } } }) const handleClick = (event: any) => { /** * Check if the clicked element is contained by the top level tag provided to the * ClickOutside component, if not, Outside clicked! Fire onClick cb */ if (!hasParent(event.target, innerRef?.current)) { onClick(event) } } /** * Preserve the child ref prop if exists and merge it with the one used here and the * proxied by the forwardRef method */ const composedRefCallback = (element: ReactElement) => { if (typeof child.ref === 'function') { child.ref(element) } else if (child.ref) { child.ref.current = element } } return React.cloneElement(child, { ref: mergeRefs([composedRefCallback, innerRef, forwardedRef]), }) } ) ClickOutside.displayName = 'ClickOutside' export default ClickOutside