All files / src/utils useDelayedLoading.ts

100% Statements 41/41
100% Branches 17/17
100% Functions 7/7
100% Lines 36/36

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                  8x 8x                           96x 96x       96x 93x   96x 4x 4x 4x     96x 65x 65x 65x         96x   96x 158x   151x 146x   76x 76x 76x   75x 75x 25x 75x       70x 70x 70x   18x 18x   5x 4x 4x             96x   96x    
import { onScopeDispose, readonly, ref, toValue, watch, type MaybeRefOrGetter, type Ref } from "vue";
 
export interface DelayedLoadingOptions {
  /** ms a load must last before the loader appears. Default 200. */
  delay?: MaybeRefOrGetter<number>;
  /** ms the loader stays up once shown, so it never just blinks. Default 500. */
  minDuration?: MaybeRefOrGetter<number>;
}
 
export const DEFAULT_DELAY = 200;
export const DEFAULT_MIN_DURATION = 500;
 
/**
 * Turns a raw `loading` flag into "should a loader be visible":
 * - a load shorter than `delay` never shows a loader (no flash);
 * - once shown, the loader stays for at least `minDuration`;
 * - a load that restarts while the loader is still up keeps it up.
 *
 * Options are read when a load starts or ends, so they may be reactive.
 */
export function useDelayedLoading(
  loading: MaybeRefOrGetter<boolean>,
  options: DelayedLoadingOptions = {}
): Readonly<Ref<boolean>> {
  const show = ref(false);
  let shownAt = 0;
  let delayTimer: ReturnType<typeof setTimeout> | undefined;
  let hideTimer: ReturnType<typeof setTimeout> | undefined;
 
  const ms = (value: MaybeRefOrGetter<number> | undefined, fallback: number) =>
    Math.max(0, toValue(value) ?? fallback);
 
  const clear = () => {
    clearTimeout(delayTimer);
    clearTimeout(hideTimer);
    delayTimer = hideTimer = undefined;
  };
 
  const reveal = () => {
    delayTimer = undefined;
    shownAt = Date.now();
    show.value = true;
  };
 
  // On the server the delay can never elapse and nothing would clear the
  // timers, so the loader simply stays hidden until the client takes over.
  const isServer = typeof window === "undefined";
 
  watch(
    () => toValue(loading),
    (isLoading) => {
      if (isServer) return;
      if (isLoading) {
        // Restarted while held up by minDuration: just stay visible.
        clearTimeout(hideTimer);
        hideTimer = undefined;
        if (show.value || delayTimer) return;
 
        const delay = ms(options.delay, DEFAULT_DELAY);
        if (delay === 0) reveal();
        else delayTimer = setTimeout(reveal, delay);
        return;
      }
 
      // Finished before the delay: the loader never appears.
      clearTimeout(delayTimer);
      delayTimer = undefined;
      if (!show.value) return;
 
      const remaining = ms(options.minDuration, DEFAULT_MIN_DURATION) - (Date.now() - shownAt);
      if (remaining <= 0) show.value = false;
      else
        hideTimer = setTimeout(() => {
          hideTimer = undefined;
          show.value = false;
        }, remaining);
    },
    // sync: react to every change, even two within one tick
    { immediate: true, flush: "sync" }
  );
 
  onScopeDispose(clear);
 
  return readonly(show);
}