diff --git a/docs/3.api/1.components/5.nuxt-loading-indicator.md b/docs/3.api/1.components/5.nuxt-loading-indicator.md index 19ee62fe09..016070181c 100644 --- a/docs/3.api/1.components/5.nuxt-loading-indicator.md +++ b/docs/3.api/1.components/5.nuxt-loading-indicator.md @@ -33,6 +33,7 @@ You can pass custom HTML or components through the loading indicator's default s - `height`: Height of the loading bar, in pixels (default `3`). - `duration`: Duration of the loading bar, in milliseconds (default `2000`). - `throttle`: Throttle the appearing and hiding, in milliseconds (default `200`). +- `estimatedProgress`: By default Nuxt will back off as it approaches 100%. You can provide a custom function to customize the progress estimation, which is a function that receives the duration of the loading bar (above) and the elapsed time. It should return a value between 0 and 100. ::callout This component is optional. :br @@ -42,3 +43,7 @@ To achieve full customization, you can implement your own one based on [its sour ::callout You can hook into the underlying indicator instance using [the `useLoadingIndicator` composable](/docs/api/composables/use-loading-indicator), which will allow you to trigger start/finish events yourself. :: + +::callout +The loading indicator's speed gradually decreases after reaching a specific point controlled by `estimatedProgress`. This adjustment provides a more accurate reflection of longer page loading times and prevents the indicator from prematurely showing 100% completion. +:: diff --git a/docs/3.api/2.composables/use-loading-indicator.md b/docs/3.api/2.composables/use-loading-indicator.md index e468c8a5d0..dba5679738 100644 --- a/docs/3.api/2.composables/use-loading-indicator.md +++ b/docs/3.api/2.composables/use-loading-indicator.md @@ -13,6 +13,12 @@ links: A composable which returns the loading state of the page. Used by [``](/docs/api/components/nuxt-loading-indicator) and controllable. It hooks into [`page:loading:start`](/docs/api/advanced/hooks#app-hooks-runtime) and [`page:loading:end`](/docs/api/advanced/hooks#app-hooks-runtime) to change its state. +## Parameters + +- `duration`: Duration of the loading bar, in milliseconds (default `2000`). +- `throttle`: Throttle the appearing and hiding, in milliseconds (default `200`). +- `estimatedProgress`: By default Nuxt will back off as it approaches 100%. You can provide a custom function to customize the progress estimation, which is a function that receives the duration of the loading bar (above) and the elapsed time. It should return a value between 0 and 100. + ## Properties ### `isLoading` @@ -38,3 +44,16 @@ Set the `progress` value to `100`, stop all timers and intervals then reset the ### `clear()` Used by `finish()`. Clear all timers and intervals used by the composable. + +## Example + +```ts + +``` diff --git a/packages/nuxt/src/app/components/nuxt-loading-indicator.ts b/packages/nuxt/src/app/components/nuxt-loading-indicator.ts index 926f063559..1020be8164 100644 --- a/packages/nuxt/src/app/components/nuxt-loading-indicator.ts +++ b/packages/nuxt/src/app/components/nuxt-loading-indicator.ts @@ -19,12 +19,17 @@ export default defineComponent({ color: { type: [String, Boolean], default: 'repeating-linear-gradient(to right,#00dc82 0%,#34cdfe 50%,#0047e1 100%)' - } + }, + estimatedProgress: { + type: Function as unknown as () => (duration: number, elapsed: number) => number, + required: false + }, }, setup (props, { slots, expose }) { const { progress, isLoading, start, finish, clear } = useLoadingIndicator({ duration: props.duration, - throttle: props.throttle + throttle: props.throttle, + estimatedProgress: props.estimatedProgress, }) expose({ diff --git a/packages/nuxt/src/app/composables/loading-indicator.ts b/packages/nuxt/src/app/composables/loading-indicator.ts index 4a882c1c5d..10f4226c6e 100644 --- a/packages/nuxt/src/app/composables/loading-indicator.ts +++ b/packages/nuxt/src/app/composables/loading-indicator.ts @@ -7,10 +7,12 @@ export type LoadingIndicatorOpts = { duration: number /** @default 200 */ throttle: number -} - -function _increase (progress: Ref, num: number) { - progress.value = Math.min(100, progress.value + num) + /** + * You can provide a custom function to customize the progress estimation, + * which is a function that receives the duration of the loading bar (above) + * and the elapsed time. It should return a value between 0 and 100. + */ + estimatedProgress?: (duration: number, elapsed: number) => number } function _hide (isLoading: Ref, progress: Ref) { @@ -32,14 +34,20 @@ export type LoadingIndicator = { clear: () => void } +function defaultEstimatedProgress (duration: number, elapsed: number): number { + const completionPercentage = elapsed / duration * 100 + return (2 / Math.PI * 100) * Math.atan(completionPercentage / 50) +} + function createLoadingIndicator (opts: Partial = {}) { const { duration = 2000, throttle = 200 } = opts + const getProgress = opts.estimatedProgress || defaultEstimatedProgress const nuxtApp = useNuxtApp() const progress = ref(0) const isLoading = ref(false) - const step = computed(() => 10000 / duration) + let done = false + let rafId: number - let _timer: any = null let _throttle: any = null const start = () => set(0) @@ -54,30 +62,42 @@ function createLoadingIndicator (opts: Partial = {}) { if (throttle && import.meta.client) { _throttle = setTimeout(() => { isLoading.value = true - _startTimer() + _startProgress() }, throttle) } else { isLoading.value = true - _startTimer() + _startProgress() } } function finish () { progress.value = 100 + done = true clear() _hide(isLoading, progress) } function clear () { - clearInterval(_timer) clearTimeout(_throttle) - _timer = null + cancelAnimationFrame(rafId) _throttle = null } - function _startTimer () { + function _startProgress () { + done = false + let startTimeStamp: number + + function step (timeStamp: number): void { + if (done) { return } + + startTimeStamp ??= timeStamp + const elapsed = timeStamp - startTimeStamp + progress.value = Math.max(0, Math.min(100, getProgress(duration, elapsed))) + rafId = requestAnimationFrame(step) + } + if (import.meta.client) { - _timer = setInterval(() => { _increase(progress, step.value) }, 100) + rafId = requestAnimationFrame(step) } }