All files / src/components/PageProgress PageProgress.vue

100% Statements 55/55
93.1% Branches 27/29
100% Functions 13/13
100% Lines 45/45

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                          14x                             14x 33x 14x 14x 14x     14x     14x 13x   14x           7x 7x 7x 7x   14x 13x     14x 14x 14x         12x 12x 12x 12x 12x 12x 37x       14x 16x     14x 18x   13x 13x   5x 4x 4x 4x 4x 3x 3x 3x       14x 1x 1x           70x 69x                                                                  
<script setup lang="ts">
import { computed, inject, onBeforeUnmount, onMounted, onScopeDispose, ref, watch } from "vue";
import ProgressBar from "../ProgressBar/ProgressBar.vue";
import { useLoadingConfig } from "../../config";
import { useDelayedLoading } from "../../utils/useDelayedLoading";
import {
  PAGE_PROGRESS,
  bindRouter,
  createPageProgress,
  type PageProgress,
  type RouterLike,
} from "../../utils/pageProgress";
 
const props = defineProps<{
  color?: string;
  /** Bar height in px. */
  thickness?: number;
  /** ms a task must last before the bar appears. */
  delay?: number;
  /** Announced to screen readers while the bar is visible. */
  label?: string;
  /** Follow this router's navigations (instead of passing it to the plugin). */
  router?: RouterLike;
  /** Drive the bar from a specific controller (defaults to the app's). */
  progress?: PageProgress;
}>();
 
// explicit prop > global config > built-in default
const config = useLoadingConfig();
const conf = () => config.pageProgress ?? {};
const color = computed(() => props.color ?? conf().color);
const thickness = computed(() => props.thickness ?? conf().thickness ?? 3);
const label = computed(() => props.label ?? conf().label);
 
// The app's controller (from the plugin), or a private one.
const progress = props.progress ?? inject(PAGE_PROGRESS, null) ?? createPageProgress();
 
let unbindRouter: (() => void) | undefined;
onMounted(() => {
  if (props.router) unbindRouter = bindRouter(progress, props.router);
});
onBeforeUnmount(() => unbindRouter?.());
 
/* Visuals. The real duration is unknown, so the bar starts at 10% and
   eases toward 90% (each step covers a tenth of the remaining distance);
   done() fills it to 100% and it fades out. A task shorter than `delay`
   never shows it at all. */
const START = 10;
const CEILING = 90;
const TRICKLE_MS = 200;
const FINISH_MS = 400;
 
const shown = useDelayedLoading(progress.loading, {
  delay: () => props.delay ?? conf().delay ?? 200,
  minDuration: 0,
});
const visible = ref(false);
const finishing = ref(false);
const value = ref(0);
let trickle: ReturnType<typeof setInterval> | undefined;
let finishTimer: ReturnType<typeof setTimeout> | undefined;
 
function begin() {
  clearTimeout(finishTimer);
  clearInterval(trickle);
  visible.value = true;
  finishing.value = false;
  value.value = START;
  trickle = setInterval(() => {
    value.value += (CEILING - value.value) * 0.1;
  }, TRICKLE_MS);
}
 
watch(shown, (isShown) => {
  if (isShown && !visible.value) begin();
});
 
watch(progress.loading, (loading) => {
  if (loading) {
    // a new task while the bar was fading out: pick up again, no flicker
    if (finishing.value) begin();
    return;
  }
  if (!visible.value) return; // finished within the delay: never shown
  clearInterval(trickle);
  value.value = 100;
  finishing.value = true;
  finishTimer = setTimeout(() => {
    visible.value = false;
    finishing.value = false;
    value.value = 0;
  }, FINISH_MS);
});
 
onScopeDispose(() => {
  clearInterval(trickle);
  clearTimeout(finishTimer);
});
</script>
 
<template>
  <div
    v-if="Ivisible"
    class="vslk-page-progress"
    :class="{ 'vslk-page-progress--done': finishing }"
  >
    <ProgressBar :value="value" :color="color" :thickness="thickness" :track="false" :label="label" />
  </div>
</template>
 
<style scoped>
.vslk-page-progress {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 9999;
  pointer-events: none;
}
 
/* after the bar has run to 100% (ProgressBar's own 0.3s width transition) */
.vslk-page-progress--done {
  opacity: 0;
  transition: opacity 0.2s ease 0.2s;
}
 
.vslk-page-progress :deep(.vslk-progress) {
  border-radius: 0;
}
 
@media (prefers-reduced-motion: reduce) {
  .vslk-page-progress--done {
    transition: none;
  }
}
</style>