mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-03 06:47:33 +00:00
fix: nodes themselves to have focus and indication, made the image node keyboard acessible and have better aria labels, added simply cache for loading so we only show loading state for non cached images
This commit is contained in:
@@ -1,21 +1,20 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="imageUrls.length > 0"
|
||||
class="image-preview outline-none group relative flex size-full min-h-16 min-w-16 flex-col px-2 justify-center"
|
||||
tabindex="0"
|
||||
role="region"
|
||||
:aria-label="$t('g.imagePreview')"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
@keydown="handleKeyDown"
|
||||
class="image-preview group relative flex size-full min-h-16 min-w-16 flex-col px-2 justify-center"
|
||||
>
|
||||
<!-- Image Wrapper -->
|
||||
<div
|
||||
class="h-full w-full overflow-hidden rounded-[5px] bg-node-component-surface relative"
|
||||
tabindex="0"
|
||||
role="img"
|
||||
:aria-label="$t('g.imagePreview')"
|
||||
:aria-busy="isLoading"
|
||||
class="h-full w-full overflow-hidden rounded-[5px] bg-muted-background relative"
|
||||
>
|
||||
<!-- Error State -->
|
||||
<div
|
||||
v-if="imageError"
|
||||
role="alert"
|
||||
class="flex size-full flex-col items-center justify-center bg-muted-background text-center text-base-foreground py-8"
|
||||
>
|
||||
<i
|
||||
@@ -48,11 +47,12 @@
|
||||
)
|
||||
"
|
||||
@load="handleImageLoad"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
|
||||
<!-- Floating Action Buttons (appear on hover) -->
|
||||
<div v-if="isHovered" class="actions absolute top-2 right-2 flex gap-2.5">
|
||||
<!-- Floating Action Buttons (appear on hover or focus) -->
|
||||
<div
|
||||
class="actions absolute top-2 right-2 flex gap-2.5 opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity"
|
||||
>
|
||||
<!-- Mask/Edit Button -->
|
||||
<button
|
||||
v-if="!hasMultipleImages"
|
||||
@@ -104,6 +104,7 @@
|
||||
v-for="(_, index) in imageUrls"
|
||||
:key="index"
|
||||
:class="getNavigationDotClass(index)"
|
||||
:aria-current="index === currentIndex ? 'true' : undefined"
|
||||
:aria-label="
|
||||
$t('g.viewImageOfTotal', {
|
||||
index: index + 1,
|
||||
@@ -117,9 +118,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useImage } from '@vueuse/core'
|
||||
import { useToast } from 'primevue'
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import type { ShallowRef } from 'vue'
|
||||
import { computed, inject, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { downloadFile } from '@/base/common/downloadUtil'
|
||||
@@ -146,18 +149,34 @@ const actionButtonClass =
|
||||
|
||||
// Component state
|
||||
const currentIndex = ref(0)
|
||||
const isHovered = ref(false)
|
||||
const actualDimensions = ref<string | null>(null)
|
||||
const imageError = ref(false)
|
||||
const isLoading = ref(false)
|
||||
|
||||
const currentImageEl = ref<HTMLImageElement>()
|
||||
const loadedUrls = new Set<string>()
|
||||
|
||||
// Computed values
|
||||
const currentImageUrl = computed(() => props.imageUrls[currentIndex.value])
|
||||
const hasMultipleImages = computed(() => props.imageUrls.length > 1)
|
||||
const imageAltText = computed(() => `Node output ${currentIndex.value + 1}`)
|
||||
|
||||
const { isLoading: imageIsLoading, error: imageError } = useImage(
|
||||
computed(() => ({ src: currentImageUrl.value }))
|
||||
)
|
||||
|
||||
// Only show loading if image is loading AND not already loaded in this batch
|
||||
const isLoading = computed(
|
||||
() => imageIsLoading.value && !loadedUrls.has(currentImageUrl.value)
|
||||
)
|
||||
|
||||
// Listen for keydown events from parent node
|
||||
const keyEvent = inject<ShallowRef<KeyboardEvent | null>>('keyEvent')
|
||||
|
||||
if (keyEvent) {
|
||||
watch(keyEvent, (e) => {
|
||||
if (!e) return
|
||||
handleKeyDown(e)
|
||||
})
|
||||
}
|
||||
|
||||
// Watch for URL changes and reset state
|
||||
watch(
|
||||
() => props.imageUrls,
|
||||
@@ -166,11 +185,8 @@ watch(
|
||||
if (currentIndex.value >= newUrls.length) {
|
||||
currentIndex.value = 0
|
||||
}
|
||||
|
||||
// Reset loading and error states when URLs change
|
||||
actualDimensions.value = null
|
||||
imageError.value = false
|
||||
isLoading.value = newUrls.length > 0
|
||||
loadedUrls.clear()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
@@ -179,19 +195,12 @@ watch(
|
||||
const handleImageLoad = (event: Event) => {
|
||||
if (!event.target || !(event.target instanceof HTMLImageElement)) return
|
||||
const img = event.target
|
||||
isLoading.value = false
|
||||
imageError.value = false
|
||||
loadedUrls.add(currentImageUrl.value)
|
||||
if (img.naturalWidth && img.naturalHeight) {
|
||||
actualDimensions.value = `${img.naturalWidth} x ${img.naturalHeight}`
|
||||
}
|
||||
}
|
||||
|
||||
const handleImageError = () => {
|
||||
isLoading.value = false
|
||||
imageError.value = true
|
||||
actualDimensions.value = null
|
||||
}
|
||||
|
||||
// In vueNodes mode, we need to set them manually before opening the mask editor.
|
||||
const setupNodeForMaskEditor = () => {
|
||||
if (!props.nodeId || !currentImageEl.value) return
|
||||
@@ -230,20 +239,9 @@ 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
|
||||
imageError.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
isHovered.value = true
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
isHovered.value = false
|
||||
}
|
||||
|
||||
const getNavigationDotClass = (index: number) => {
|
||||
return [
|
||||
'w-2 h-2 rounded-full transition-all duration-200 border-0 cursor-pointer p-0',
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<div
|
||||
v-else
|
||||
ref="nodeContainerRef"
|
||||
tabindex="0"
|
||||
:data-node-id="nodeData.id"
|
||||
:class="
|
||||
cn(
|
||||
@@ -16,7 +17,7 @@
|
||||
// hover (only when node should handle events)
|
||||
shouldHandleNodePointerEvents &&
|
||||
'hover:ring-7 ring-node-component-ring',
|
||||
'outline-transparent outline-2',
|
||||
'outline-transparent outline-2 focus-visible:outline-node-component-outline',
|
||||
borderClass,
|
||||
outlineClass,
|
||||
cursorClass,
|
||||
@@ -48,6 +49,7 @@
|
||||
@dragover.prevent="handleDragOver"
|
||||
@dragleave="handleDragLeave"
|
||||
@drop.stop.prevent="handleDrop"
|
||||
@keydown="handleNodeKeydown"
|
||||
>
|
||||
<div class="flex flex-col justify-center items-center relative">
|
||||
<template v-if="isCollapsed">
|
||||
@@ -130,7 +132,16 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, nextTick, onErrorCaptured, onMounted, ref, watch } from 'vue'
|
||||
import {
|
||||
computed,
|
||||
nextTick,
|
||||
onErrorCaptured,
|
||||
onMounted,
|
||||
provide,
|
||||
ref,
|
||||
shallowRef,
|
||||
watch
|
||||
} from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
@@ -186,6 +197,14 @@ const { nodeData, error = null } = defineProps<LGraphNodeProps>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// Provide keydown events to child components (ImagePreview, VideoPreview, etc.)
|
||||
const keyEvent = shallowRef<KeyboardEvent | null>(null)
|
||||
provide('keyEvent', keyEvent)
|
||||
|
||||
const handleNodeKeydown = (event: KeyboardEvent) => {
|
||||
keyEvent.value = event
|
||||
}
|
||||
|
||||
const { handleNodeCollapse, handleNodeTitleUpdate, handleNodeRightClick } =
|
||||
useNodeEventHandlers()
|
||||
const { bringNodeToFront } = useNodeZIndex()
|
||||
@@ -265,6 +284,8 @@ const { onPointerdown, ...remainingPointerHandlers } = pointerHandlers
|
||||
const { startDrag } = useNodeDrag()
|
||||
|
||||
async function nodeOnPointerdown(event: PointerEvent) {
|
||||
nodeContainerRef.value?.focus()
|
||||
|
||||
if (event.altKey && lgraphNode.value) {
|
||||
const result = LGraphCanvas.cloneNodes([lgraphNode.value])
|
||||
if (result?.created?.length) {
|
||||
|
||||
Reference in New Issue
Block a user