Skip to content

fix(core): prevent mobile controls flash on first tap after auto-hide#1556

Open
R-Delfino95 wants to merge 3 commits into
videojs:mainfrom
R-Delfino95:fix/mobile-controls-flash
Open

fix(core): prevent mobile controls flash on first tap after auto-hide#1556
R-Delfino95 wants to merge 3 commits into
videojs:mainfrom
R-Delfino95:fix/mobile-controls-flash

Conversation

@R-Delfino95
Copy link
Copy Markdown
Collaborator

@R-Delfino95 R-Delfino95 commented May 18, 2026

Closes #1446

Summary

On Android Chrome, after controls auto-hide, the first tap on the video area made the controls flash briefly and immediately disappear. The second tap behaved correctly. The bug reoccurred every cycle (interact with any button → wait for auto-hide → tap → flash).

Root cause

Three things interact:

  1. The skin registers a tap gesture with action="toggleControls" (touch) alongside doubletap gestures (seekStep ×2, toggleFullscreen). Because doubletap bindings exist, TapRecognizer.handleUp defers the tap action by DOUBLETAP_WINDOW = 200 ms to disambiguate, and the deferred callback re-reads live state at fire time.

  2. <media-container>'s own pointerup listener calls this.focus({ preventScroll: true }) so keyboard events reach the hotkey coordinator. That call fires focusin synchronously.

  3. The container's pointerup listener is registered in connectedCallback, before controlsFeature.attach registers its own. So on every pointerup, the container's listener runs first.

The result, on the first tap after auto-hide (controlsVisible=false):

  • pointerdown (touch) → records pointerDownTime.
  • pointerup (touch) fires. Container's listener runs first → this.focus()focusin fires synchronously.
  • The focusin handler in controlsFeature calls setActive() → flips controlsVisible to true.
  • The controls feature's own pointerup listener runs next (records the touch timestamp), then the gesture coordinator defers the tap action by 200 ms.
  • 200 ms later the deferred callback fires toggleControls(), reads controlsVisible=true, calls setInactive() → controls hide. That's the flash.

The second tap worked because focus was already inside the container, so this.focus() was skipped and no synchronous focusin fired.

A secondary issue: Android Chrome dispatches a synthetic mouseleave after touchend, which independently called setInactive().

Fix

In packages/core/src/dom/store/features/controls.ts:

  • Track touch timestamps on pointerdown as well as pointerup (renamed lastTouchUpAtlastTouchAt). Recording on pointerdown is essential: the container's pointerup listener runs before ours, so without an earlier timestamp the guard below would always see lastTouchAt=0 and not short-circuit.
  • Guard focusin against synthetic focus within 500 ms of any touch event — mirrors the existing mouseleave guard.
  • On touch pointermove, only refresh the idle timer when already active; don't flip visibility. Prevents the same race if a pointermove fires mid-tap on devices that drift slightly during touch.
  • Guard mouseleave against the synthetic event Android Chrome dispatches after touchend.

Keyboard-driven focus still activates controls — lastTouchAt is only set on touch events, so non-touch focusin (e.g. tabbing into the container) is unaffected.

Test plan

  • pnpm -F @videojs/core test src/dom/store/features/tests/controls.test.ts — 34 tests pass, including new coverage for: focusin fired between touch pointerdown and pointerup (the actual race) is ignored, focusin after the guard window still activates, synthetic mouseleave within 500 ms of touch pointerup is ignored, touch pointermove does not flip visibility mid-gesture.
  • pnpm typecheck
  • pnpm -F @videojs/core build
  • pnpm lint:fix:file on both touched files
  • Manual: Android Chrome — play → wait for auto-hide → tap blank area → controls show and remain for the full idle timeout. Verified across multiple cycles.
  • Manual: desktop Chrome — keyboard focus on the container still shows controls (focusin guard only suppresses inside the 500 ms touch window).
  • Manual: iOS Safari — first tap after auto-hide still shows controls without flash.

Note

Medium Risk
Changes core controls visibility heuristics for touch events (pointermove, focusin, mouseleave) and adds timing-based guards, which could affect control activation/idle behavior across devices and input types.

Overview
Prevents Android Chrome’s first-tap-after-idle controls flash by adding touch-aware guards in controlsFeature that ignore synthetic focusin and mouseleave events shortly after touch interactions.

Updates pointermove handling so touch movement only refreshes the idle timer (without reactivating/showing controls mid-gesture), and records lastTouchAt on both touch pointerdown and pointerup to cover the focus race window. Adds targeted tests covering these touch/synthetic-event scenarios.

Reviewed by Cursor Bugbot for commit e74dd65. Bugbot is set up for automated code reviews on this repo. Configure here.

R-Delfino95 and others added 2 commits May 14, 2026 17:25
…ntrols

On touch devices, pointermove between pointerdown and pointerup was
calling setActive() and flipping controlsVisible to true. The gesture
recognizer's 200 ms deferred tap then read the already-true value and
toggled controls off — causing a visible flash on every first tap after
auto-hide.

Fix: touch pointermove only reschedules the idle timer when already
active; it no longer forces visibility on. Also guard mouseleave against
the synthetic event Android Chrome fires within 500 ms of touchend.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@netlify
Copy link
Copy Markdown

netlify Bot commented May 18, 2026

👷 Deploy request for vjs10-site pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit e74dd65

@vercel
Copy link
Copy Markdown

vercel Bot commented May 18, 2026

@R-Delfino95 is attempting to deploy a commit to the Mux Team on Vercel.

A member of the Team first needs to authorize it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Bug: mobile controls flash and immediately hide on first tap after auto-hide

1 participant