From e61b302f6e494d1227256d785c2a8a4d42a5fd3e Mon Sep 17 00:00:00 2001 From: Nicolas Date: Tue, 15 Sep 2026 11:48:13 +0200 Subject: [PATCH] Fix the bottom bar's auto-hide never firing on mobile Reported right after shipping: the bar never actually collapsed on mobile. Classic mobile-web trap: mouseenter/mouseleave on the bar tracked "is the pointer resting on the controls" so hovering could hold it open, but a touch pointer has no real hover - tapping the bar fires a synthetic pointerenter with no pointerleave ever coming after it (no cursor left to lift away). The very first tap anywhere on the bar latched isPointerOverBottomBar permanently true, and the bar could never auto-hide again for the rest of that session. Fixed by checking event.pointerType === 'mouse' in both handlers - touch/pen never even attempt the "hovering holds it open" path, and fall back to the same plain activity-based wake-up (pointerdown/ pointermove on the whole player) every device already has. Verified: full typecheck/build. Reproduced the exact failure directly - dispatched a synthetic touch pointerenter with no matching pointerleave (the real-world case) against the fixed logic and confirmed it still transitions to .minimal on schedule, where the mouseenter-based version would have latched open. A real touchscreen's own event behavior isn't independently reproducible here, so this is the strongest confirmation available without one. Co-Authored-By: Claude Sonnet 5 --- client/src/views/player/Player.ts | 22 ++++++++++++++++------ docs/architecture.md | 21 +++++++++++++++++++++ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/client/src/views/player/Player.ts b/client/src/views/player/Player.ts index 283b166..3e8f939 100644 --- a/client/src/views/player/Player.ts +++ b/client/src/views/player/Player.ts @@ -525,20 +525,30 @@ function renderVideoPlayer(root: HTMLElement, partyId: string, party: PartyPubli scheduleControlsHide(); } - function onBottomBarPointerEnter() { + // Mouse only (pointerType check) - not just an optimization. A touch + // pointer has no real "hovering": tapping the bar fires a pointerenter + // with no matching pointerleave ever coming (there's no cursor left to + // lift away), which used to latch isPointerOverBottomBar permanently + // true after the very first tap on the bar - the whole thing could + // never auto-hide again from that point on, on any touch device. + // Touch's own "wake the bar up" path is already covered below by the + // plain pointerdown/pointermove listeners on the whole player. + function onBottomBarPointerEnter(e: PointerEvent) { + if (e.pointerType !== 'mouse') return; isPointerOverBottomBar = true; showControls(); } - function onBottomBarPointerLeave() { + function onBottomBarPointerLeave(e: PointerEvent) { + if (e.pointerType !== 'mouse') return; isPointerOverBottomBar = false; scheduleControlsHide(); } playerWrap.addEventListener('pointermove', showControls); playerWrap.addEventListener('pointerdown', showControls); - bottomBar.addEventListener('mouseenter', onBottomBarPointerEnter); - bottomBar.addEventListener('mouseleave', onBottomBarPointerLeave); + bottomBar.addEventListener('pointerenter', onBottomBarPointerEnter); + bottomBar.addEventListener('pointerleave', onBottomBarPointerLeave); scheduleControlsHide(); /** @@ -1108,8 +1118,8 @@ function renderVideoPlayer(root: HTMLElement, partyId: string, party: PartyPubli document.removeEventListener('fullscreenchange', onFullscreenChange); playerWrap.removeEventListener('pointermove', showControls); playerWrap.removeEventListener('pointerdown', showControls); - bottomBar.removeEventListener('mouseenter', onBottomBarPointerEnter); - bottomBar.removeEventListener('mouseleave', onBottomBarPointerLeave); + bottomBar.removeEventListener('pointerenter', onBottomBarPointerEnter); + bottomBar.removeEventListener('pointerleave', onBottomBarPointerLeave); unsubscribeCastAvailability(); unsubscribeCastSessionEnded(); // Deliberately NOT stopping an active cast here - leaving this page diff --git a/docs/architecture.md b/docs/architecture.md index ba010e6..ace0d2e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2769,6 +2769,27 @@ inactivity, does hovering genuinely prevent it) wasn't driven through real wall-clock waits here - the CSS transition and class-toggling logic were checked directly instead. +**Reported broken on mobile, right after shipping**: it never actually +auto-hid there. Root cause - a classic mobile-web trap, not a typo: +`mouseenter`/`mouseleave` on the bar tracked "is the pointer resting on +the controls" so hovering could hold them open, but a touch pointer has +no real hover - tapping the bar fires a synthetic `pointerenter` with +no `pointerleave` ever coming after it (there's no cursor left to lift +away), so the very first tap anywhere on the bar latched +`isPointerOverBottomBar` permanently `true` and the bar could never +auto-hide again for the rest of that session. Fixed by checking +`event.pointerType === 'mouse'` in both handlers - touch/pen never even +attempt the "hovering holds it open" path at all, and correctly fall +back to the same plain activity-based wake-up (`pointerdown`/ +`pointermove` on the whole player) every device already gets. Confirmed +by reproducing the exact failure directly - dispatched a synthetic +touch `pointerenter` with no matching `pointerleave` (the real-world +case) against the fixed logic and watched it still transition to +`.minimal` on schedule, where the original `mouseenter`-based version +would have latched open. A real touchscreen's own event behavior still +isn't independently reproducible here, so this is the strongest +confirmation available without one. + ## Known limitations - Playback always goes through `hls.js` against whatever Emby's