fix(ui): use CSS group-hover for reliable volume panel display

This commit is contained in:
2026-04-20 18:08:54 +02:00
parent 92a69fdb35
commit d767e73815
2 changed files with 26 additions and 50 deletions

View File

@@ -31,6 +31,7 @@ const initPlayer = (): void => {
const volumeRange = container.querySelector('[data-volume-range]') as HTMLInputElement const volumeRange = container.querySelector('[data-volume-range]') as HTMLInputElement
const iconVolume = container.querySelector('[data-icon-volume]') as SVGElement const iconVolume = container.querySelector('[data-icon-volume]') as SVGElement
const iconMuted = container.querySelector('[data-icon-muted]') as SVGElement const iconMuted = container.querySelector('[data-icon-muted]') as SVGElement
const volumeUnderline = container.querySelector('[data-volume-underline]') as HTMLElement
const timeDisplay = container.querySelector('[data-time]') as HTMLElement const timeDisplay = container.querySelector('[data-time]') as HTMLElement
const progressWrap = container.querySelector('[data-progress-wrap]') as HTMLElement const progressWrap = container.querySelector('[data-progress-wrap]') as HTMLElement
const progress = container.querySelector('[data-progress]') as HTMLElement const progress = container.querySelector('[data-progress]') as HTMLElement
@@ -84,7 +85,6 @@ const initPlayer = (): void => {
} }
let controlsTimeout: number | undefined let controlsTimeout: number | undefined
let isScrubbing = false let isScrubbing = false
let isHoveringVolume = false
let lastKnownVolume = 1 let lastKnownVolume = 1
let activeSubtitles: Array<{ start: number, end: number, text: string }> = [] let activeSubtitles: Array<{ start: number, end: number, text: string }> = []
let currentSubtitleTracks: Array<{ lang: string, label: string, url: string }> = [] let currentSubtitleTracks: Array<{ lang: string, label: string, url: string }> = []
@@ -627,7 +627,7 @@ const initPlayer = (): void => {
container.classList.add('show-controls') container.classList.add('show-controls')
window.clearTimeout(controlsTimeout) window.clearTimeout(controlsTimeout)
controlsTimeout = window.setTimeout(() => { controlsTimeout = window.setTimeout(() => {
if (!isScrubbing && !isHoveringVolume && !video.paused) { if (!isScrubbing && !video.paused) {
container.classList.remove('show-controls') container.classList.remove('show-controls')
} }
}, 2000) }, 2000)
@@ -852,35 +852,12 @@ const initPlayer = (): void => {
showControls() showControls()
}) })
const setVolumePanelOpen = (isOpen: boolean): void => { volumeRange?.addEventListener('pointerdown', () => {
if (volumePanel) { volumePanel?.classList.add('is-dragging')
volumePanel.classList.toggle('volume-panel-visible', isOpen) })
}
volumeWrap?.classList.toggle('is-volume-open', isOpen)
isHoveringVolume = isOpen
if (isOpen) showControls()
}
const openVolumePanel = (): void => { window.addEventListener('pointerup', () => {
setVolumePanelOpen(true) volumePanel?.classList.remove('is-dragging')
}
const closeVolumePanel = (): void => {
setVolumePanelOpen(false)
}
closeVolumePanel()
muteBtn?.addEventListener('mouseenter', openVolumePanel)
volumeWrap?.addEventListener('mouseleave', closeVolumePanel)
volumeWrap?.addEventListener('focusin', openVolumePanel)
volumeWrap?.addEventListener('focusout', (event: FocusEvent) => {
const nextTarget = event.relatedTarget
if (nextTarget instanceof Node && volumeWrap.contains(nextTarget)) return
closeVolumePanel()
}) })
backwardBtn?.addEventListener('click', () => seekBy(-10)) backwardBtn?.addEventListener('click', () => seekBy(-10))

View File

@@ -114,16 +114,13 @@ templ VideoPlayer(data shared.WatchPageData) {
</button> </button>
<div <div
data-volume-wrap data-volume-wrap
class="relative flex h-9 w-9 items-center justify-center overflow-visible sm:h-10 sm:w-10" class="group/volume relative flex h-9 w-9 items-center justify-center overflow-visible sm:h-10 sm:w-10"
> >
<span
data-volume-bridge
class="volume-bridge-off absolute inset-x-0 bottom-full h-[calc(100%+26px)]"
></span>
<div <div
data-volume-panel data-volume-panel
class="volume-panel pointer-events-none absolute bottom-[calc(100%+26px)] left-1/2 z-30 -translate-x-1/2 opacity-0 invisible transition-opacity" class="pointer-events-none absolute bottom-full left-1/2 z-30 -translate-x-1/2 pb-3 opacity-0 invisible transition-all duration-200 group-hover/volume:pointer-events-auto group-hover/volume:opacity-100 group-hover/volume:visible focus-within:pointer-events-auto focus-within:opacity-100 focus-within:visible [&.is-dragging]:pointer-events-auto [&.is-dragging]:opacity-100 [&.is-dragging]:visible"
> >
<div class="flex items-center justify-center rounded-md bg-[#2B2B2B] px-3 py-3 shadow-xl">
<input <input
data-volume-range data-volume-range
type="range" type="range"
@@ -131,10 +128,11 @@ templ VideoPlayer(data shared.WatchPageData) {
max="100" max="100"
step="1" step="1"
value="100" value="100"
class="h-24 w-4 cursor-pointer appearance-none bg-transparent accent-white [writing-mode:vertical-lr] [direction:rtl]" class="h-24 w-1.5 cursor-pointer rounded-full bg-white/20 accent-[#e50914] outline-none [writing-mode:vertical-lr] [direction:rtl]"
aria-label="Volume" aria-label="Volume"
/> />
</div> </div>
</div>
<button <button
data-mute data-mute
class="relative flex h-9 w-9 items-center justify-center pb-1 text-white sm:h-10 sm:w-10" class="relative flex h-9 w-9 items-center justify-center pb-1 text-white sm:h-10 sm:w-10"
@@ -162,6 +160,7 @@ templ VideoPlayer(data shared.WatchPageData) {
</svg> </svg>
</button> </button>
<span <span
data-volume-underline
class="volume-underline pointer-events-none absolute bottom-0 left-1/2 h-0.5 w-6 -translate-x-1/2 bg-white opacity-0 transition-opacity" class="volume-underline pointer-events-none absolute bottom-0 left-1/2 h-0.5 w-6 -translate-x-1/2 bg-white opacity-0 transition-opacity"
></span> ></span>
</div> </div>