mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 10:59:53 +00:00
feat: improve vue node video preview loading and a11y (#7558)
## Summary Applies appropriate logic from below PRs (which affected image outputs/previews) to video previews - https://github.com/Comfy-Org/ComfyUI_frontend/pull/7268: don't port the 250ms logic for videos, as it is not as relevant - videos typically take longer than 250ms and most browsers have a built-in loading state that will be displayed between component mount and video onloaded that we don't want to flash. Use the native video loaded event instead - https://github.com/Comfy-Org/ComfyUI_frontend/pull/7252: apply to videos 1-for-1 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7558-feat-improve-vue-node-video-preview-loading-and-a11y-2cb6d73d365081eab4dcfeb1e62c553b) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -2,16 +2,20 @@
|
||||
<div
|
||||
v-if="imageUrls.length > 0"
|
||||
class="video-preview group relative flex size-full min-h-16 min-w-16 flex-col px-2"
|
||||
tabindex="0"
|
||||
role="region"
|
||||
:aria-label="$t('g.videoPreview')"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
@keydown="handleKeyDown"
|
||||
>
|
||||
<!-- Video Wrapper -->
|
||||
<div
|
||||
ref="videoWrapperEl"
|
||||
class="relative h-full w-full grow overflow-hidden rounded-[5px] bg-node-component-surface"
|
||||
tabindex="0"
|
||||
role="region"
|
||||
:aria-label="$t('g.videoPreview')"
|
||||
:aria-busy="showLoader"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
@focusin="handleFocusIn"
|
||||
@focusout="handleFocusOut"
|
||||
>
|
||||
<!-- Error State -->
|
||||
<div
|
||||
@@ -27,18 +31,18 @@
|
||||
|
||||
<!-- Loading State -->
|
||||
<Skeleton
|
||||
v-if="isLoading && !videoError"
|
||||
v-if="showLoader && !videoError"
|
||||
class="absolute inset-0 size-full"
|
||||
border-radius="5px"
|
||||
width="16rem"
|
||||
height="16rem"
|
||||
width="100%"
|
||||
height="100%"
|
||||
/>
|
||||
|
||||
<!-- Main Video -->
|
||||
<video
|
||||
v-if="!videoError"
|
||||
:src="currentVideoUrl"
|
||||
:class="cn('block size-full object-contain', isLoading && 'invisible')"
|
||||
:class="cn('block size-full object-contain', showLoader && 'invisible')"
|
||||
controls
|
||||
loop
|
||||
playsinline
|
||||
@@ -47,10 +51,13 @@
|
||||
/>
|
||||
|
||||
<!-- Floating Action Buttons (appear on hover) -->
|
||||
<div v-if="isHovered" class="actions absolute top-2 right-2 flex gap-1">
|
||||
<div
|
||||
v-if="isHovered || isFocused"
|
||||
class="actions absolute top-2 right-2 flex gap-2.5"
|
||||
>
|
||||
<!-- Download Button -->
|
||||
<button
|
||||
class="action-btn cursor-pointer rounded-lg border-0 bg-white p-2 text-black shadow-sm transition-all duration-200 hover:bg-smoke-100"
|
||||
:class="actionButtonClass"
|
||||
:title="$t('g.downloadVideo')"
|
||||
:aria-label="$t('g.downloadVideo')"
|
||||
@click="handleDownload"
|
||||
@@ -60,7 +67,7 @@
|
||||
|
||||
<!-- Close Button -->
|
||||
<button
|
||||
class="action-btn cursor-pointer rounded-lg border-0 bg-white p-2 text-black shadow-sm transition-all duration-200 hover:bg-smoke-100"
|
||||
:class="actionButtonClass"
|
||||
:title="$t('g.removeVideo')"
|
||||
:aria-label="$t('g.removeVideo')"
|
||||
@click="handleRemove"
|
||||
@@ -94,7 +101,7 @@
|
||||
<span v-if="videoError" class="text-red-400">
|
||||
{{ $t('g.errorLoadingVideo') }}
|
||||
</span>
|
||||
<span v-else-if="isLoading" class="text-smoke-400">
|
||||
<span v-else-if="showLoader" class="text-smoke-400">
|
||||
{{ $t('g.loading') }}...
|
||||
</span>
|
||||
<span v-else>
|
||||
@@ -126,12 +133,18 @@ const props = defineProps<VideoPreviewProps>()
|
||||
const { t } = useI18n()
|
||||
const nodeOutputStore = useNodeOutputStore()
|
||||
|
||||
const actionButtonClass =
|
||||
'flex h-8 min-h-8 items-center justify-center gap-2.5 rounded-lg border-0 bg-button-surface px-2 py-2 text-button-surface-contrast shadow-sm transition-colors duration-200 hover:bg-button-hover-surface focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-button-surface-contrast focus-visible:ring-offset-2 focus-visible:ring-offset-transparent cursor-pointer'
|
||||
|
||||
// Component state
|
||||
const currentIndex = ref(0)
|
||||
const isHovered = ref(false)
|
||||
const isFocused = ref(false)
|
||||
const actualDimensions = ref<string | null>(null)
|
||||
const videoError = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const showLoader = ref(false)
|
||||
|
||||
const videoWrapperEl = ref<HTMLDivElement>()
|
||||
|
||||
// Computed values
|
||||
const currentVideoUrl = computed(() => props.imageUrls[currentIndex.value])
|
||||
@@ -149,16 +162,16 @@ watch(
|
||||
// Reset loading and error states when URLs change
|
||||
actualDimensions.value = null
|
||||
videoError.value = false
|
||||
isLoading.value = newUrls.length > 0
|
||||
showLoader.value = newUrls.length > 0
|
||||
},
|
||||
{ deep: true }
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
// Event handlers
|
||||
const handleVideoLoad = (event: Event) => {
|
||||
if (!event.target || !(event.target instanceof HTMLVideoElement)) return
|
||||
const video = event.target
|
||||
isLoading.value = false
|
||||
showLoader.value = false
|
||||
videoError.value = false
|
||||
if (video.videoWidth && video.videoHeight) {
|
||||
actualDimensions.value = `${video.videoWidth} x ${video.videoHeight}`
|
||||
@@ -166,7 +179,7 @@ const handleVideoLoad = (event: Event) => {
|
||||
}
|
||||
|
||||
const handleVideoError = () => {
|
||||
isLoading.value = false
|
||||
showLoader.value = false
|
||||
videoError.value = true
|
||||
actualDimensions.value = null
|
||||
}
|
||||
@@ -194,7 +207,7 @@ const setCurrentIndex = (index: number) => {
|
||||
if (index >= 0 && index < props.imageUrls.length) {
|
||||
currentIndex.value = index
|
||||
actualDimensions.value = null
|
||||
isLoading.value = true
|
||||
showLoader.value = true
|
||||
videoError.value = false
|
||||
}
|
||||
}
|
||||
@@ -207,6 +220,16 @@ const handleMouseLeave = () => {
|
||||
isHovered.value = false
|
||||
}
|
||||
|
||||
const handleFocusIn = () => {
|
||||
isFocused.value = true
|
||||
}
|
||||
|
||||
const handleFocusOut = (event: FocusEvent) => {
|
||||
if (!videoWrapperEl.value?.contains(event.relatedTarget as Node)) {
|
||||
isFocused.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getNavigationDotClass = (index: number) => {
|
||||
return [
|
||||
'w-2 h-2 rounded-full transition-all duration-200 border-0 cursor-pointer',
|
||||
|
||||
Reference in New Issue
Block a user