Skip to content

Commit ca0eeff

Browse files
dante01yoonclaude
andcommitted
fix: scope hover/focus controls to image container only
Move mouseenter/mouseleave and focusin/focusout from root div to the image container (single mode) and grid container (grid mode). This prevents nav button clicks from triggering the dimmed hover state via focusin. Add cursor-pointer to image areas in both DisplayCarousel and ImagePreview. Add thumbnail carousel edge fade effect. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 000add0 commit ca0eeff

File tree

3 files changed

+65
-27
lines changed

3 files changed

+65
-27
lines changed

src/renderer/extensions/vueNodes/components/ImagePreview.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<!-- Image Wrapper -->
88
<div
99
ref="imageWrapperEl"
10-
class="relative flex min-h-0 w-full flex-1 overflow-hidden rounded-[5px] bg-transparent"
10+
class="relative flex min-h-0 w-full flex-1 cursor-pointer overflow-hidden rounded-[5px] bg-transparent"
1111
tabindex="0"
1212
role="img"
1313
:aria-label="$t('g.imagePreview')"

src/renderer/extensions/vueNodes/widgets/components/DisplayCarousel.test.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ function findThumbnailButtons(wrapper: ReturnType<typeof mount>) {
100100
return wrapper.findAll('button').filter((btn) => btn.find('img').exists())
101101
}
102102

103+
function findImageContainer(wrapper: ReturnType<typeof mount>) {
104+
return wrapper.find('[tabindex="0"]')
105+
}
106+
103107
describe('DisplayCarousel Single Mode', () => {
104108
describe('Component Rendering', () => {
105109
it('renders main image', () => {
@@ -276,7 +280,7 @@ describe('DisplayCarousel Accessibility', () => {
276280
it('shows controls on focusin for keyboard users', async () => {
277281
const wrapper = createGalleriaWrapper([...TEST_IMAGES_SMALL])
278282

279-
await wrapper.find('div').trigger('focusin')
283+
await findImageContainer(wrapper).trigger('focusin')
280284
await nextTick()
281285

282286
expect(wrapper.find('[aria-label="Switch to grid view"]').exists()).toBe(
@@ -290,11 +294,13 @@ describe('DisplayCarousel Accessibility', () => {
290294
it('hides controls on focusout when focus leaves component', async () => {
291295
const wrapper = createGalleriaWrapper([...TEST_IMAGES_SMALL])
292296

293-
await wrapper.find('div').trigger('focusin')
297+
await findImageContainer(wrapper).trigger('focusin')
294298
await nextTick()
295299

296-
// Focus leaves the component entirely
297-
await wrapper.find('div').trigger('focusout', { relatedTarget: null })
300+
// Focus leaves the image container entirely
301+
await findImageContainer(wrapper).trigger('focusout', {
302+
relatedTarget: null
303+
})
298304
await nextTick()
299305

300306
expect(wrapper.find('[aria-label="Switch to grid view"]').exists()).toBe(
@@ -307,8 +313,8 @@ describe('DisplayCarousel Grid Mode', () => {
307313
it('switches to grid mode via toggle button on hover', async () => {
308314
const wrapper = createGalleriaWrapper([...TEST_IMAGES_SMALL])
309315

310-
// Trigger hover to reveal toggle button
311-
await wrapper.find('div').trigger('mouseenter')
316+
// Trigger focus on image container to reveal toggle button
317+
await findImageContainer(wrapper).trigger('focusin')
312318
await nextTick()
313319

314320
const toggleBtn = wrapper.find('[aria-label="Switch to grid view"]')
@@ -325,7 +331,7 @@ describe('DisplayCarousel Grid Mode', () => {
325331
it('does not show grid toggle for single image', async () => {
326332
const wrapper = createGalleriaWrapper([...TEST_IMAGES_SINGLE])
327333

328-
await wrapper.find('div').trigger('mouseenter')
334+
await findImageContainer(wrapper).trigger('focusin')
329335
await nextTick()
330336

331337
expect(wrapper.find('[aria-label="Switch to grid view"]').exists()).toBe(
@@ -336,12 +342,16 @@ describe('DisplayCarousel Grid Mode', () => {
336342
it('switches back to single mode via toggle button', async () => {
337343
const wrapper = createGalleriaWrapper([...TEST_IMAGES_SMALL])
338344

339-
// Switch to grid
340-
await wrapper.find('div').trigger('mouseenter')
345+
// Switch to grid via focus on image container
346+
await findImageContainer(wrapper).trigger('focusin')
341347
await nextTick()
342348
await wrapper.find('[aria-label="Switch to grid view"]').trigger('click')
343349
await nextTick()
344350

351+
// Focus the grid container to reveal toggle
352+
await findImageContainer(wrapper).trigger('focusin')
353+
await nextTick()
354+
345355
// Switch back to single
346356
const singleToggle = wrapper.find('[aria-label="Switch to single view"]')
347357
expect(singleToggle.exists()).toBe(true)
@@ -356,8 +366,8 @@ describe('DisplayCarousel Grid Mode', () => {
356366
it('clicking grid image switches to single mode focused on that image', async () => {
357367
const wrapper = createGalleriaWrapper([...TEST_IMAGES_SMALL])
358368

359-
// Switch to grid
360-
await wrapper.find('div').trigger('mouseenter')
369+
// Switch to grid via focus on image container
370+
await findImageContainer(wrapper).trigger('focusin')
361371
await nextTick()
362372
await wrapper.find('[aria-label="Switch to grid view"]').trigger('click')
363373
await nextTick()
@@ -382,8 +392,8 @@ describe('DisplayCarousel Grid Mode', () => {
382392
props: { widget, modelValue: images.value }
383393
})
384394

385-
// Switch to grid
386-
await wrapper.find('div').trigger('mouseenter')
395+
// Switch to grid via focus on image container
396+
await findImageContainer(wrapper).trigger('focusin')
387397
await nextTick()
388398
await wrapper.find('[aria-label="Switch to grid view"]').trigger('click')
389399
await nextTick()

src/renderer/extensions/vueNodes/widgets/components/DisplayCarousel.vue

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
<template>
22
<div
3-
ref="rootEl"
43
class="flex max-w-full flex-col rounded-lg bg-component-node-widget-background"
5-
@mouseenter="isHovered = true"
6-
@mouseleave="isHovered = false"
7-
@focusin="isFocused = true"
8-
@focusout="handleFocusOut"
94
>
105
<!-- Single Mode -->
116
<template v-if="displayMode === 'single'">
127
<div class="flex flex-col gap-2 p-4">
138
<!-- Main Image Container -->
14-
<div class="relative flex items-center justify-center">
9+
<div
10+
ref="imageContainerEl"
11+
class="relative flex cursor-pointer items-center justify-center"
12+
tabindex="0"
13+
@mouseenter="isHovered = true"
14+
@mouseleave="isHovered = false"
15+
@focusin="isFocused = true"
16+
@focusout="handleFocusOut"
17+
>
1518
<img
1619
v-if="activeItem"
1720
:src="getItemSrc(activeItem)"
@@ -31,7 +34,10 @@
3134
:class="toggleButtonClass"
3235
class="absolute top-2 left-2"
3336
:aria-label="t('g.switchToGridView')"
34-
@click="displayMode = 'grid'"
37+
@click="
38+
isHovered = false
39+
displayMode = 'grid'
40+
"
3541
>
3642
<i class="icon-[lucide--layout-grid] size-4" />
3743
</button>
@@ -95,15 +101,24 @@
95101
<!-- Thumbnails -->
96102
<div
97103
v-if="showThumbnails"
98-
class="flex items-center gap-2 overflow-x-hidden scroll-smooth"
104+
class="flex items-center gap-1 overflow-x-hidden scroll-smooth"
105+
style="
106+
mask-image: linear-gradient(
107+
to right,
108+
transparent,
109+
black 20px,
110+
black calc(100% - 20px),
111+
transparent
112+
);
113+
"
99114
>
100115
<button
101116
v-for="(item, index) in galleryImages"
102117
:key="getItemSrc(item)"
103118
:ref="(el) => setThumbnailRef(el as HTMLElement | null, index)"
104119
:class="
105120
cn(
106-
'size-10 shrink-0 cursor-pointer overflow-hidden rounded-sm border-2 border-transparent transition-colors',
121+
'shrink-0 cursor-pointer overflow-hidden rounded-lg border-2 border-transparent p-1 transition-colors',
107122
index === activeIndex && 'border-base-foreground'
108123
)
109124
"
@@ -113,7 +128,7 @@
113128
<img
114129
:src="getItemThumbnail(item)"
115130
:alt="getItemAlt(item, index)"
116-
class="size-full object-cover"
131+
class="size-10 rounded-sm object-cover"
117132
/>
118133
</button>
119134
</div>
@@ -137,14 +152,22 @@
137152
<div
138153
ref="gridContainerEl"
139154
class="relative h-[296px] overflow-clip rounded-sm bg-component-node-background"
155+
tabindex="0"
156+
@mouseenter="isHovered = true"
157+
@mouseleave="isHovered = false"
158+
@focusin="isFocused = true"
159+
@focusout="handleFocusOut"
140160
>
141161
<!-- Toggle to Single (hover, top-left) -->
142162
<button
143163
v-if="showControls"
144164
:class="toggleButtonClass"
145165
class="absolute top-2 left-2 z-10"
146166
:aria-label="t('g.switchToSingleView')"
147-
@click="displayMode = 'single'"
167+
@click="
168+
isHovered = false
169+
displayMode = 'single'
170+
"
148171
>
149172
<i class="icon-[lucide--square] size-4" />
150173
</button>
@@ -226,7 +249,7 @@ const isFocused = ref(false)
226249
const hoveredGridIndex = ref(-1)
227250
const imageDimensions = ref<string | null>(null)
228251
const thumbnailRefs = ref<(HTMLElement | null)[]>([])
229-
const rootEl = ref<HTMLDivElement>()
252+
const imageContainerEl = ref<HTMLDivElement>()
230253
const gridContainerEl = ref<HTMLDivElement>()
231254
232255
const showControls = computed(() => isHovered.value || isFocused.value)
@@ -325,7 +348,11 @@ function getItemAlt(item: GalleryImage, index: number): string {
325348
}
326349
327350
function handleFocusOut(event: FocusEvent) {
328-
if (!rootEl.value?.contains(event.relatedTarget as Node)) {
351+
const container =
352+
displayMode.value === 'single'
353+
? imageContainerEl.value
354+
: gridContainerEl.value
355+
if (!container?.contains(event.relatedTarget as Node)) {
329356
isFocused.value = false
330357
}
331358
}
@@ -382,6 +409,7 @@ function goToNext() {
382409
function selectFromGrid(index: number) {
383410
activeIndex.value = index
384411
imageDimensions.value = null
412+
isHovered.value = false
385413
displayMode.value = 'single'
386414
}
387415

0 commit comments

Comments
 (0)