fix: make hover-based buttons accessible on touch devices (#7872)

## Summary
- Define `touch:` Tailwind variant using `@media (hover: none)` to
target touch devices
- Add `touch:opacity-100` to `TreeExplorerTreeNode` for node action
buttons
- Add `useMediaQuery('(hover: none)')` to `MediaAssetCard` for action
overlay visibility

## Problem
On touch devices, sidebar buttons that appear on hover are inaccessible
because:
1. The `touch:` Tailwind variant was used but never defined (classes
silently ignored)
2. `TreeExplorerTreeNode` had no touch support for action buttons
3. `MediaAssetCard` used JS-based `useElementHover` which doesn't work
on touch

## Screenshots (Touch Device Emulation)

### Before (main branch)
- No "Generated"/"Imported" tabs visible in header
- Only duration chips shown on cards, no action buttons (zoom, menu)

![Before - Touch Device](https://i.imgur.com/V0qcr2D.png)

### After (with fix)
- "Generated"/"Imported" tabs visible in header
- Action buttons (zoom, menu) visible on left of cards
- Duration chips moved to right side

![After - Touch Device](https://i.imgur.com/vQ3dUBc.png)

## Test plan
- [ ] On touch device: verify Media Assets sidebar
"Imported"/"Generated" tabs are visible
- [ ] On touch device: verify Node Library filter buttons are visible
- [ ] On touch device: verify tree node action buttons (bookmark, help)
are visible
- [ ] On touch device: verify media asset card zoom/menu buttons are
visible
- [ ] On desktop with mouse: verify hover behavior still works as
expected
This commit is contained in:
Johnpaul Chiwetelu
2026-01-07 07:29:39 +01:00
committed by GitHub
parent dcf0886d89
commit 11f8cdb9bd
3 changed files with 26 additions and 7 deletions

View File

@@ -9,6 +9,8 @@
@config '../../tailwind.config.ts';
@custom-variant touch (@media (hover: none));
@theme {
--text-xxs: 0.625rem;
--text-xxs--line-height: calc(1 / 0.625);

View File

@@ -28,7 +28,7 @@
/>
</div>
<div
class="node-actions motion-safe:opacity-0 motion-safe:group-hover/tree-node:opacity-100"
class="node-actions touch:opacity-100 motion-safe:opacity-0 motion-safe:group-hover/tree-node:opacity-100"
>
<slot name="actions" :node="props.node" />
</div>

View File

@@ -68,9 +68,10 @@
</IconGroup>
</template>
<!-- Output count (top-right) -->
<template v-if="showOutputCount" #top-right>
<!-- Output count or duration chip (top-right) -->
<template v-if="showOutputCount || showTouchDurationChip" #top-right>
<Button
v-if="showOutputCount"
v-tooltip.top.pt:pointer-events-none="
$t('mediaAsset.actions.seeMoreOutputs')
"
@@ -81,6 +82,12 @@
<i class="icon-[lucide--layers] size-4" />
<span>{{ outputCount }}</span>
</Button>
<!-- Duration chip on touch devices (far right) -->
<SquareChip
v-else-if="showTouchDurationChip"
variant="gray"
:label="formattedDuration"
/>
</template>
</CardTop>
</template>
@@ -124,7 +131,7 @@
</template>
<script setup lang="ts">
import { useElementHover, whenever } from '@vueuse/core'
import { useElementHover, useMediaQuery, whenever } from '@vueuse/core'
import { computed, defineAsyncComponent, provide, ref, toRef } from 'vue'
import IconGroup from '@/components/button/IconGroup.vue'
@@ -202,6 +209,7 @@ const showVideoControls = ref(false)
const imageDimensions = ref<{ width: number; height: number } | undefined>()
const isHovered = useElementHover(cardContainerRef)
const isTouch = useMediaQuery('(hover: none)')
const actions = useMediaAssetActions()
@@ -272,19 +280,28 @@ const durationChipClasses = computed(() => {
return ''
})
// Show static chips when NOT hovered and NOT playing (normal state)
// Show static chips when NOT hovered and NOT playing (normal state on non-touch)
const showStaticChips = computed(
() =>
!loading &&
!!asset &&
!isHovered.value &&
!isVideoPlaying.value &&
!isTouch.value &&
formattedDuration.value
)
// Show action overlay when hovered OR playing
// Show duration chip in top-right on touch devices
const showTouchDurationChip = computed(
() => !loading && !!asset && isTouch.value && formattedDuration.value
)
// Show action overlay when hovered, playing, or on touch device
const showActionsOverlay = computed(
() => !loading && !!asset && (isHovered.value || isVideoPlaying.value)
() =>
!loading &&
!!asset &&
(isHovered.value || isVideoPlaying.value || isTouch.value)
)
const handleZoomClick = () => {