All files / virtual-keyboard/src/hooks useKeyRepeat.ts

96.77% Statements 30/31
88.88% Branches 8/9
100% Functions 7/7
100% Lines 29/29

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                                                                                            1468x   1468x 1468x 1468x 1468x 1468x 1468x 1468x 1468x   1468x 1468x   1468x 1398x 2x 2x   1398x 4x 4x       1468x 6x 6x 6x 4x 4x 4x 6x           1468x     1468x 1435x                        
import { useCallback, useEffect, useMemo, useRef } from "react";
import {
  KEY_REPEAT_DELAY_MS,
  KEY_REPEAT_INTERVAL_MS,
} from "../constants";
 
/** Pointer/touch handlers returned by {@link useKeyRepeat}, to spread on a key. */
export interface KeyRepeatHandlers {
  onMouseDown: () => void;
  onMouseUp: () => void;
  onMouseLeave: () => void;
  onTouchStart: () => void;
  onTouchEnd: () => void;
  /** Touch was cancelled (system gesture / scroll takeover) — must stop repeat. */
  onTouchCancel: () => void;
  onBlur: () => void;
}
 
export interface UseKeyRepeatOptions {
  /** Delay before repeating begins (ms). */
  delay?: number;
  /** Interval between repeats once started (ms). */
  interval?: number;
}
 
/**
 * Auto-repeat a key action while it is held. Returns hold handlers that, once
 * `enabled`, wait `delay` ms then fire `onRepeat` every `interval` ms until the
 * pointer is released / leaves / the touch is cancelled / the key blurs. The
 * consumer's own `onClick` still fires the initial single press (so a short tap
 * = one action, a long hold = the release click + N repeats).
 *
 * SSR-safe (touches no timers at import), and **leak-free**: every timer is
 * cleared on release and on unmount, so it is safe under StrictMode.
 *
 * @param enabled - When false the handlers are inert (no repeat).
 * @param onRepeat - Fired on each repeat tick. Read from a ref, so its identity
 *   may change freely without restarting anything.
 * @param options - `delay` / `interval` overrides.
 */
export function useKeyRepeat(
  enabled: boolean,
  onRepeat: () => void,
  options: UseKeyRepeatOptions = {}
): KeyRepeatHandlers {
  const { delay = KEY_REPEAT_DELAY_MS, interval = KEY_REPEAT_INTERVAL_MS } =
    options;
 
  const onRepeatRef = useRef(onRepeat);
  onRepeatRef.current = onRepeat;
  const enabledRef = useRef(enabled);
  enabledRef.current = enabled;
  const delayRef = useRef(delay);
  delayRef.current = delay;
  const intervalRef = useRef(interval);
  intervalRef.current = interval;
 
  const timeoutId = useRef<ReturnType<typeof setTimeout> | null>(null);
  const intervalId = useRef<ReturnType<typeof setInterval> | null>(null);
 
  const stop = useCallback(() => {
    if (timeoutId.current !== null) {
      clearTimeout(timeoutId.current);
      timeoutId.current = null;
    }
    if (intervalId.current !== null) {
      clearInterval(intervalId.current);
      intervalId.current = null;
    }
  }, []);
 
  const start = useCallback(() => {
    Iif (!enabledRef.current) return;
    stop();
    timeoutId.current = setTimeout(() => {
      timeoutId.current = null;
      onRepeatRef.current();
      intervalId.current = setInterval(() => {
        onRepeatRef.current();
      }, intervalRef.current);
    }, delayRef.current);
  }, [stop]);
 
  // Clear any pending timers on unmount (leak-free / StrictMode-safe).
  useEffect(() => stop, [stop]);
 
  // Stable handlers object (matches useLongPress's memoized `bind`).
  return useMemo<KeyRepeatHandlers>(
    () => ({
      onMouseDown: start,
      onMouseUp: stop,
      onMouseLeave: stop,
      onTouchStart: start,
      onTouchEnd: stop,
      onTouchCancel: stop,
      onBlur: stop,
    }),
    [start, stop]
  );
}