All files / hooks/use-click-any-where/src useClickAnyWhere.ts

100% Statements 16/16
100% Branches 9/9
100% Functions 4/4
100% Lines 16/16

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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124                                                                                                                                                                    39x     39x     39x   39x   37x 1x       36x 7x       29x 16x           29x 29x 4x       29x     29x 29x        
import { useEffect, useRef } from "react";
import { isDocumentAvailable } from "./utils";
 
/**
 * Options for useClickAnyWhere hook
 */
export interface UseClickAnyWhereOptions {
  /**
   * Whether the event listener is enabled
   * @default true
   */
  enabled?: boolean;
  /**
   * Whether to use event capture phase
   * @default false
   */
  capture?: boolean;
  /**
   * Whether to use a passive event listener.
   *
   * A passive listener cannot call `event.preventDefault()`. This offers no
   * performance benefit for `click` events, so it is left unset by default and
   * the browser default (non-passive) applies. Only pass this if you
   * specifically need passive semantics.
   *
   * @default undefined (browser default — non-passive)
   */
  passive?: boolean;
}
 
/**
 * Handler type for click events
 */
export type ClickAnyWhereHandler = (event: MouseEvent) => void;
 
/**
 * Detects document-wide click events and calls the provided handler.
 * Useful for closing dropdowns, modals, or any component when clicking outside.
 *
 * @param handler - Callback function called when a click is detected anywhere on the document
 * @param options - Configuration options for the event listener
 *
 * @example
 * ```tsx
 * function ClickTracker() {
 *   const [lastClick, setLastClick] = useState({ x: 0, y: 0 });
 *
 *   useClickAnyWhere((event) => {
 *     setLastClick({ x: event.clientX, y: event.clientY });
 *   });
 *
 *   return (
 *     <div>
 *       Last click: ({lastClick.x}, {lastClick.y})
 *     </div>
 *   );
 * }
 * ```
 *
 * @example
 * ```tsx
 * // Conditional activation
 * function Dropdown({ isOpen, onClose }) {
 *   useClickAnyWhere(
 *     () => onClose(),
 *     { enabled: isOpen }
 *   );
 *
 *   return isOpen ? <div>Dropdown content</div> : null;
 * }
 * ```
 *
 * @example
 * ```tsx
 * // With capture phase
 * useClickAnyWhere(handleClick, { capture: true });
 * ```
 */
export function useClickAnyWhere(
  handler: ClickAnyWhereHandler,
  options: UseClickAnyWhereOptions = {}
): void {
  const { enabled = true, capture = false, passive } = options;
 
  // Store handler in ref to avoid re-registering event listener
  const handlerRef = useRef<ClickAnyWhereHandler>(handler);
 
  // Update ref when handler changes
  handlerRef.current = handler;
 
  useEffect(() => {
    // SSR check — skip attaching listeners when there is no document
    if (!isDocumentAvailable()) {
      return;
    }
 
    // Don't add listener if disabled
    if (!enabled) {
      return;
    }
 
    // Internal handler that calls the latest handler ref
    const internalHandler = (event: MouseEvent) => {
      handlerRef.current(event);
    };
 
    // Build event listener options. Only set `passive` when explicitly
    // provided so the browser default (non-passive) applies otherwise —
    // forcing passive would silently break event.preventDefault().
    const eventOptions: AddEventListenerOptions = { capture };
    if (passive !== undefined) {
      eventOptions.passive = passive;
    }
 
    // Add event listener
    document.addEventListener("click", internalHandler, eventOptions);
 
    // Cleanup
    return () => {
      document.removeEventListener("click", internalHandler, { capture });
    };
  }, [enabled, capture, passive]);
}