All files / src/hooks useAutoFocus.ts

100% Statements 52/52
87.5% Branches 21/24
100% Functions 2/2
100% Lines 52/52

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 751x             1x 88x 88x   88x 55x 55x   54x 54x 1x 53x     55x 55x   55x 55x 55x   55x 40x   40x 40x     40x 10x 9x 9x 9x 10x 10x   30x 30x   30x 29x   1x 1x   29x 26x 26x 26x 30x 36x 1x 1x 40x     55x 55x 55x   55x 54x 54x 54x 54x 88x 88x  
import { useEffect, useRef, type RefObject } from 'react';
 
/**
 * Auto-focus a ref element with retry logic to handle animation timing.
 * Keeps retrying until focus succeeds or max attempts reached.
 * Works on both mobile and desktop by detecting when element is actually interactive.
 */
export const useAutoFocus = (ref: RefObject<HTMLElement | null>) => {
  const attemptCountRef = useRef(0);
  const maxAttemptsRef = useRef(0);
 
  useEffect(() => {
    const element = ref.current;
    if (!element) return;
 
    const isMobile =
      typeof window !== 'undefined' && window.matchMedia
        ? window.matchMedia('(max-width: 767px)').matches
        : false;
 
    // Mobile animations take longer (1000ms+ with delays), desktop is faster
    const maxAttempts = isMobile ? 20 : 5;
    const delayMs = isMobile ? 100 : 50;
 
    let timeoutId: NodeJS.Timeout | null = null;
    let frameId: number | null = null;
    let cancelled = false;
 
    const attemptFocus = () => {
      if (cancelled) return;
 
      const element = ref.current;
      if (!element) return;
 
      // Only focus if element is in the DOM and visible
      if (element.offsetParent === null) {
        if (attemptCountRef.current < maxAttemptsRef.current) {
          attemptCountRef.current++;
          timeoutId = setTimeout(attemptFocus, delayMs);
        }
        return;
      }
 
      try {
        element.focus();
        // Check if focus actually succeeded on next frame
        frameId = requestAnimationFrame(() => {
          if (document.activeElement === element) {
            // Focus succeeded - stop trying
            return;
          }
          // Focus didn't stick, retry
          if (attemptCountRef.current < maxAttemptsRef.current) {
            attemptCountRef.current++;
            timeoutId = setTimeout(attemptFocus, delayMs);
          }
        });
      } catch (error) {
        console.warn('[useAutoFocus] Failed to focus element:', error);
      }
    };
 
    // Start focus attempts
    maxAttemptsRef.current = maxAttempts;
    attemptCountRef.current = 0;
    timeoutId = setTimeout(attemptFocus, 50);
 
    return () => {
      cancelled = true;
      if (timeoutId) clearTimeout(timeoutId);
      if (frameId) cancelAnimationFrame(frameId);
    };
  }, [ref]);
};