import React, { useRef, useEffect } from 'react'; import { ClickAwayMouseEventHandler, ClickAwayTouchEventHandler, OutsideClickHandlerProps } from './type'; import useEventCallback from '@cleartrip/ct-design-use-event-callback'; /** * maps onClick -> click */ function mapEventPropToEvent( eventProp: ClickAwayMouseEventHandler | ClickAwayTouchEventHandler, ): 'click' | 'mousedown' | 'mouseup' | 'touchstart' | 'touchend' | 'pointerdown' | 'pointerup' { return eventProp.substring(2).toLowerCase() as | 'click' | 'mousedown' | 'mouseup' | 'touchstart' | 'touchend' | 'pointerdown' | 'pointerup'; } const OutsideClickHandler = React.forwardRef( ({ onOutsideClick, children, disabled = false, mouseEvent = 'onClick', touchEvent = 'onTouchEnd', wrapperProps }) => { // node ref to track element events const node = useRef(null); // press event ref like in mobile const pressedRef = useRef(false); // check if event tracking is activated const activatedRef = React.useRef(false); React.useEffect(() => { // Ensure that this component is not "activated" synchronously. // https://github.com/facebook/react/issues/20074 setTimeout(() => { activatedRef.current = true; }, 0); return () => { activatedRef.current = false; }; }, []); const childrenProps = { ref: node, ...wrapperProps }; const handleClick = useEventCallback((event: MouseEvent | TouchEvent) => { // Do not act if user performed touchmove if (pressedRef.current) { pressedRef.current = false; return; } let insideDOM; if (event.composedPath) { insideDOM = node.current && event.composedPath().indexOf(node.current) > -1; } else { const target = event.target as Node | null; insideDOM = !document.documentElement.contains(target) || (node.current && node.current.contains(target)); } if (!insideDOM && activatedRef.current) { onOutsideClick(event); } }); // use effect for touch event React.useEffect(() => { const mappedTouchEvent = mapEventPropToEvent(touchEvent); const handleTouchMove = () => { pressedRef.current = true; }; if (!disabled) { document.addEventListener(mappedTouchEvent, handleClick); document.addEventListener('touchmove', handleTouchMove); } return () => { document.removeEventListener(mappedTouchEvent, handleClick); document.removeEventListener('touchmove', handleTouchMove); }; }, [handleClick, touchEvent, disabled]); // use effect for mouse event useEffect(() => { const mappedMouseEvent = mapEventPropToEvent(mouseEvent); if (!disabled) { document.addEventListener(mappedMouseEvent, handleClick, false); } return () => { document.removeEventListener(mappedMouseEvent, handleClick, false); }; }, [handleClick, mouseEvent, disabled]); return React.createElement('div', childrenProps, children); }, ); // TODO add documentation OutsideClickHandler.displayName = 'OutsideClickHandler'; export default OutsideClickHandler;