fix: loading state to show loader only if it takes more than 250ms (#7268)

## Summary

To prevent the flash of "loading..." and "calculating dimensions" when
loading cached images only set loading set if longer than 250ms

## Changes

- **What**: ImagePreview.vue
- **Breaking**: <!-- Any breaking changes (if none, remove this line)
-->
- **Dependencies**: <!-- New dependencies (if none, remove this line)
-->

## Review Focus

<!-- Critical design decisions or edge cases that need attention -->

<!-- If this PR fixes an issue, uncomment and update the line below -->
<!-- Fixes #ISSUE_NUMBER -->

## Screenshots (if applicable)

The retrigger loading is because i have throttled 4g slow in the demo.
So cache takes time. Normally this doesn't happen.


https://github.com/user-attachments/assets/335ca7e4-4ce1-43dd-b7d0-9ee88e187069

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7268-fix-loading-state-to-show-loader-only-if-it-takes-more-than-150ms-2c46d73d365081a6b311f78ba3e1cffd)
by [Unito](https://www.unito.io)
This commit is contained in:
Simula_r
2025-12-09 22:17:43 -08:00
committed by GitHub
parent 356ebe538f
commit dedc77786f
2 changed files with 24 additions and 31 deletions

View File

@@ -29,24 +29,16 @@
</p>
</div>
<!-- Loading State -->
<Skeleton
v-if="isLoading && !imageError"
border-radius="5px"
width="100%"
height="100%"
/>
<div v-if="showLoader && !imageError" class="size-full">
<Skeleton border-radius="5px" width="100%" height="100%" />
</div>
<!-- Main Image -->
<img
v-if="!imageError"
ref="currentImageEl"
:src="currentImageUrl"
:alt="imageAltText"
:class="
cn(
'block size-full object-contain pointer-events-none',
isLoading && 'invisible'
)
"
class="block size-full object-contain pointer-events-none"
@load="handleImageLoad"
@error="handleImageError"
/>
@@ -91,7 +83,7 @@
<span v-if="imageError" class="text-red-400">
{{ $t('g.errorLoadingImage') }}
</span>
<span v-else-if="isLoading" class="text-base-foreground">
<span v-else-if="showLoader" class="text-base-foreground">
{{ $t('g.loading') }}...
</span>
<span v-else>
@@ -117,6 +109,7 @@
</template>
<script setup lang="ts">
import { useTimeoutFn } from '@vueuse/core'
import { useToast } from 'primevue'
import Skeleton from 'primevue/skeleton'
import { computed, ref, watch } from 'vue'
@@ -126,7 +119,6 @@ import { downloadFile } from '@/base/common/downloadUtil'
import { app } from '@/scripts/app'
import { useCommandStore } from '@/stores/commandStore'
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
import { cn } from '@/utils/tailwindUtil'
interface ImagePreviewProps {
/** Array of image URLs to display */
@@ -149,10 +141,19 @@ const currentIndex = ref(0)
const isHovered = ref(false)
const actualDimensions = ref<string | null>(null)
const imageError = ref(false)
const isLoading = ref(false)
const showLoader = ref(false)
const currentImageEl = ref<HTMLImageElement>()
const { start: startDelayedLoader, stop: stopDelayedLoader } = useTimeoutFn(
() => {
showLoader.value = true
},
250,
// Make sure it doesnt run on component mount
{ immediate: false }
)
// Computed values
const currentImageUrl = computed(() => props.imageUrls[currentIndex.value])
const hasMultipleImages = computed(() => props.imageUrls.length > 1)
@@ -169,17 +170,19 @@ watch(
// Reset loading and error states when URLs change
actualDimensions.value = null
imageError.value = false
isLoading.value = newUrls.length > 0
if (newUrls.length > 0) startDelayedLoader()
},
{ deep: true }
{ deep: true, immediate: true }
)
// Event handlers
const handleImageLoad = (event: Event) => {
if (!event.target || !(event.target instanceof HTMLImageElement)) return
const img = event.target
isLoading.value = false
stopDelayedLoader()
showLoader.value = false
imageError.value = false
if (img.naturalWidth && img.naturalHeight) {
actualDimensions.value = `${img.naturalWidth} x ${img.naturalHeight}`
@@ -187,7 +190,8 @@ const handleImageLoad = (event: Event) => {
}
const handleImageError = () => {
isLoading.value = false
stopDelayedLoader()
showLoader.value = false
imageError.value = true
actualDimensions.value = null
}
@@ -230,8 +234,7 @@ const setCurrentIndex = (index: number) => {
if (currentIndex.value === index) return
if (index >= 0 && index < props.imageUrls.length) {
currentIndex.value = index
actualDimensions.value = null
isLoading.value = true
startDelayedLoader()
imageError.value = false
}
}

View File

@@ -208,11 +208,6 @@ describe('ImagePreview', () => {
await navigationDots[1].trigger('click')
await nextTick()
// Simulate image load event to clear loading state
const component = wrapper.vm as any
component.isLoading = false
await nextTick()
// Now should show second image
const imgElement = wrapper.find('img')
expect(imgElement.exists()).toBe(true)
@@ -265,11 +260,6 @@ describe('ImagePreview', () => {
await navigationDots[1].trigger('click')
await nextTick()
// Simulate image load event to clear loading state
const component = wrapper.vm as any
component.isLoading = false
await nextTick()
// Alt text should update
const imgElement = wrapper.find('img')
expect(imgElement.exists()).toBe(true)