Fix the bottom bar's auto-hide never firing on mobile
All checks were successful
Publier l'image Docker / test (push) Successful in 20s
Publier l'image Docker / publish (push) Successful in 19s

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 <noreply@anthropic.com>
This commit is contained in:
Nicolas 2026-09-15 11:48:13 +02:00
parent d3e2811bd0
commit e61b302f6e
2 changed files with 37 additions and 6 deletions

View file

@ -525,20 +525,30 @@ function renderVideoPlayer(root: HTMLElement, partyId: string, party: PartyPubli
scheduleControlsHide(); 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; isPointerOverBottomBar = true;
showControls(); showControls();
} }
function onBottomBarPointerLeave() { function onBottomBarPointerLeave(e: PointerEvent) {
if (e.pointerType !== 'mouse') return;
isPointerOverBottomBar = false; isPointerOverBottomBar = false;
scheduleControlsHide(); scheduleControlsHide();
} }
playerWrap.addEventListener('pointermove', showControls); playerWrap.addEventListener('pointermove', showControls);
playerWrap.addEventListener('pointerdown', showControls); playerWrap.addEventListener('pointerdown', showControls);
bottomBar.addEventListener('mouseenter', onBottomBarPointerEnter); bottomBar.addEventListener('pointerenter', onBottomBarPointerEnter);
bottomBar.addEventListener('mouseleave', onBottomBarPointerLeave); bottomBar.addEventListener('pointerleave', onBottomBarPointerLeave);
scheduleControlsHide(); scheduleControlsHide();
/** /**
@ -1108,8 +1118,8 @@ function renderVideoPlayer(root: HTMLElement, partyId: string, party: PartyPubli
document.removeEventListener('fullscreenchange', onFullscreenChange); document.removeEventListener('fullscreenchange', onFullscreenChange);
playerWrap.removeEventListener('pointermove', showControls); playerWrap.removeEventListener('pointermove', showControls);
playerWrap.removeEventListener('pointerdown', showControls); playerWrap.removeEventListener('pointerdown', showControls);
bottomBar.removeEventListener('mouseenter', onBottomBarPointerEnter); bottomBar.removeEventListener('pointerenter', onBottomBarPointerEnter);
bottomBar.removeEventListener('mouseleave', onBottomBarPointerLeave); bottomBar.removeEventListener('pointerleave', onBottomBarPointerLeave);
unsubscribeCastAvailability(); unsubscribeCastAvailability();
unsubscribeCastSessionEnded(); unsubscribeCastSessionEnded();
// Deliberately NOT stopping an active cast here - leaving this page // Deliberately NOT stopping an active cast here - leaving this page

View file

@ -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 real wall-clock waits here - the CSS transition and class-toggling
logic were checked directly instead. 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 ## Known limitations
- Playback always goes through `hls.js` against whatever Emby's - Playback always goes through `hls.js` against whatever Emby's