All files / src/hooks useClickOutside.ts

100% Statements 41/41
90% Branches 18/20
100% Functions 2/2
100% Lines 41/41

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 631x     1x                         1x 51x 51x 51x 51x 51x 51x   51x 51x   14x 3x   3x 2x 2x 3x 1x 1x 1x 1x 14x   14x 14x 14x 14x   14x 13x 13x 13x 2x 2x 13x   14x   14x 14x 14x 14x 14x 51x 51x  
import { useEffect, useRef } from 'react';
 
// Constants
import { SELECT_CLOSE_DELAY } from '@shared/constants';
 
interface UseClickOutsideProps {
  wrapperRef: React.RefObject<HTMLDivElement | null>;
  open: boolean;
  onClose: () => void;
}
 
/**
 * Detects clicks outside the wrapper,
 * intelligently ignores Radix Select dropdowns
 * and adds a small delay to avoid race conditions when switching dropdowns.
 */
export const useClickOutside = ({
  wrapperRef,
  open,
  onClose,
}: UseClickOutsideProps) => {
  const isSelectOpen = useRef(false);
  const closeTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
 
  useEffect(() => {
    if (!open || !wrapperRef.current) return;
 
    const observer = new MutationObserver(() => {
      const hasSelect = !!document.querySelector('[data-radix-select-content]');
 
      if (hasSelect) {
        if (closeTimeout.current) clearTimeout(closeTimeout.current);
        isSelectOpen.current = true;
      } else {
        closeTimeout.current = setTimeout(() => {
          isSelectOpen.current = false;
        }, SELECT_CLOSE_DELAY);
      }
    });
 
    observer.observe(document.body, {
      childList: true,
      subtree: true,
    });
 
    const handleClickOutside = (event: MouseEvent) => {
      const target = event.target as HTMLElement;
      if (isSelectOpen.current) return;
      if (wrapperRef.current?.contains(target)) return;
      if (target.closest('[data-radix-select-content]')) return;
      onClose();
    };
 
    document.addEventListener('mousedown', handleClickOutside);
 
    return () => {
      observer.disconnect();
      document.removeEventListener('mousedown', handleClickOutside);
      if (closeTimeout.current) clearTimeout(closeTimeout.current);
    };
  }, [wrapperRef, open, onClose]);
};