All files / use-intersection-observer/src utils.ts

96.55% Statements 28/29
86.95% Branches 20/23
88.88% Functions 8/9
96.42% Lines 27/28

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 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169              254x                               35x                                               10x 2x       8x                         8x                   10x                                           30x 5x   25x 11x   14x                               13x 2x       11x 1x       10x 10x   10x 1x     9x 12x 2x       7x               7x                             20x 20x    
import type { IntersectionEntry } from "./types";
 
/**
 * Check if IntersectionObserver API is supported in the current environment
 * Returns false in SSR environments or browsers without support
 */
export function isIntersectionObserverSupported(): boolean {
  return (
    typeof window !== "undefined" &&
    "IntersectionObserver" in window
  );
}
 
/**
 * Convert a native IntersectionObserverEntry to our IntersectionEntry type
 * Provides a consistent interface with additional convenience properties
 *
 * @param nativeEntry - The native IntersectionObserverEntry from the browser
 * @returns IntersectionEntry with all properties
 */
export function toIntersectionEntry(
  nativeEntry: IntersectionObserverEntry
): IntersectionEntry {
  return {
    entry: nativeEntry,
    isIntersecting: nativeEntry.isIntersecting,
    intersectionRatio: nativeEntry.intersectionRatio,
    target: nativeEntry.target,
    boundingClientRect: nativeEntry.boundingClientRect,
    intersectionRect: nativeEntry.intersectionRect,
    rootBounds: nativeEntry.rootBounds,
    time: nativeEntry.time,
  };
}
 
/**
 * Create an initial IntersectionEntry for SSR or before first observation
 * Used when initialIsIntersecting is true
 *
 * @param isIntersecting - Whether to set initial state as intersecting
 * @param target - Optional target element (null for SSR)
 * @returns A mock IntersectionEntry
 */
export function createInitialEntry(
  isIntersecting: boolean,
  target: Element | null = null
): IntersectionEntry | null {
  if (!isIntersecting) {
    return null;
  }
 
  // Create a placeholder DOMRect for SSR
  const emptyRect: DOMRectReadOnly = {
    x: 0,
    y: 0,
    width: 0,
    height: 0,
    top: 0,
    right: 0,
    bottom: 0,
    left: 0,
    toJSON: () => ({}),
  };
 
  // Create a mock native entry
  const mockNativeEntry = {
    target: target as Element,
    isIntersecting,
    intersectionRatio: isIntersecting ? 1 : 0,
    boundingClientRect: emptyRect,
    intersectionRect: emptyRect,
    rootBounds: null,
    time: typeof performance !== "undefined" ? performance.now() : Date.now(),
  } as IntersectionObserverEntry;
 
  return {
    entry: mockNativeEntry,
    isIntersecting,
    intersectionRatio: isIntersecting ? 1 : 0,
    target: target as Element,
    boundingClientRect: emptyRect,
    intersectionRect: emptyRect,
    rootBounds: null,
    time: mockNativeEntry.time,
  };
}
 
/**
 * Normalize threshold to always be an array
 * Handles both single number and array inputs
 *
 * @param threshold - Single threshold or array of thresholds
 * @returns Array of threshold values
 */
export function normalizeThreshold(
  threshold: number | number[] | undefined
): number[] {
  if (threshold === undefined) {
    return [0];
  }
  if (Array.isArray(threshold)) {
    return threshold;
  }
  return [threshold];
}
 
/**
 * Deep compare two IntersectionObserverInit options objects
 * Used to determine if observer needs to be recreated
 *
 * @param a - First options object
 * @param b - Second options object
 * @returns true if options are equal
 */
export function areOptionsEqual(
  a: IntersectionObserverInit,
  b: IntersectionObserverInit
): boolean {
  // Compare root
  if (a.root !== b.root) {
    return false;
  }
 
  // Compare rootMargin
  if (a.rootMargin !== b.rootMargin) {
    return false;
  }
 
  // Compare threshold (normalize to arrays for comparison)
  const thresholdA = normalizeThreshold(a.threshold);
  const thresholdB = normalizeThreshold(b.threshold);
 
  if (thresholdA.length !== thresholdB.length) {
    return false;
  }
 
  for (let i = 0; i < thresholdA.length; i++) {
    if (thresholdA[i] !== thresholdB[i]) {
      return false;
    }
  }
 
  return true;
}
 
/**
 * Create a no-op ref callback for SSR environments
 * Returns a function that does nothing when called
 */
export function createNoopRef(): (node: Element | null) => void {
  return () => {
    // No-op for SSR
  };
}
 
/**
 * Validate rootMargin string format
 * rootMargin follows CSS margin syntax: "10px", "10px 20px", "10px 20px 30px 40px"
 *
 * @param rootMargin - The rootMargin string to validate
 * @returns true if valid format
 */
export function isValidRootMargin(rootMargin: string): boolean {
  // Basic validation - rootMargin should contain px or %
  // Browser will handle more detailed validation
  const pattern = /^(-?\d+(\.\d+)?(px|%)?\s*){1,4}$/;
  return pattern.test(rootMargin.trim());
}