Skip to content

fix(core): defer pointerdown tab activation under the HTML5 drag backend (DV-37)#1453

Open
mathuo wants to merge 2 commits into
v8-branchfrom
claude/linear-bugs-dv37-firefox-drag
Open

fix(core): defer pointerdown tab activation under the HTML5 drag backend (DV-37)#1453
mathuo wants to merge 2 commits into
v8-branchfrom
claude/linear-bugs-dv37-firefox-drag

Conversation

@mathuo

@mathuo mathuo commented Jul 13, 2026

Copy link
Copy Markdown
Owner

⚠️ Needs manual Firefox verification before merge

This fixes a Firefox-only, native-HTML5-drag bug that cannot be reproduced in an automated test — see "Why no e2e test" below. Please drag an inactive tab in Firefox to confirm the drag now starts.

Problem (DV-37 / #932)

With the HTML5 drag backend (the default for mouse on desktop), a tab is natively draggable and a native drag arms from the same pointerdown+move gesture. On pointerdown of an inactive tab, tabs.ts synchronously calls openPanel(panel) — moving focus and relaying out the tab strip. Firefox aborts a pending native drag when focus/layout changes mid-gesture, so a drag started on an inactive tab never begins (it just activates and snaps back). Chrome is more lenient, so it works there.

Fix

Defer the pointerdown activation one frame (requestAnimationFrame) only when the HTML5 backend is live, so dragstart arms first. A plain click still activates on the next frame (imperceptible). The pointer backend has no native drag to pre-empt and stays fully synchronous. The deferred activation is cancelled on dispose and skipped if the tab is removed within the frame.

Scope is deliberately minimal to respect the "bug fixes must not change expected behaviour" rule: the only change is a one-frame deferral on the mouse/HTML5 activation path; timing of the end state is preserved, and every other activation route (pointer backend, edge groups, shift-to-float, keyboard, programmatic) is untouched.

Why no e2e test (I checked empirically)

I installed Firefox and tested whether Playwright could reproduce this. Playwright cannot initiate a native HTML5 dragstart in any browser heremouse.down/move/up and the dedicated locator.dragTo() both produce dragstart=0 in both Chromium and Firefox against the native backend. Since a green Chromium control is impossible, the harness can't distinguish the Firefox bug from its own inability to start a native drag, and synthetic DragEvent dispatch would bypass the exact native-initiation timing that is the bug. So this genuinely requires a human dragging in Firefox.

Test

tabActivationPointerDown.spec.ts asserts the observable contract of the fix in jsdom:

  • html5 backend — pointerdown on an inactive tab does not activate synchronously; activation lands on the next frame. (Fails on v8-branch, passes here — verified.)
  • pointer backend — activation stays synchronous (unchanged).
  • dispose — a pending deferred activation is cancelled cleanly, no late openPanel.

Full dockview-core suite: 1227 passed.

Fixes DV-37

🤖 Generated with Claude Code


Generated by Claude Code

mathuo and others added 2 commits July 13, 2026 19:03
With the HTML5 drag backend a tab is natively `draggable`, and a native drag
arms from the same pointerdown+move gesture. Activating an inactive tab on
pointerdown synchronously moves focus and relayouts the tab strip, and Firefox
aborts a pending native drag when that happens — so a drag started on an
inactive tab never begins (#932).

Defer the pointerdown activation one frame when the HTML5 backend is live, so
`dragstart` arms first; a plain click still activates on the next frame, which
is imperceptible. The pointer backend has no native drag to pre-empt and stays
synchronous. The deferred activation is cancelled on dispose and skipped if the
tab is removed within the frame.

jsdom cannot drive a native drag, so the added test asserts the observable
contract (deferred-then-applied under html5, synchronous under pointer);
real Firefox drag behaviour requires manual verification.

Fixes DV-37

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011JE1SmW4mWp6AdoNVbHprb
`tsc --build` rejects assigning `undefined` to the `MutableDisposable.value`
setter (typed `IDisposable`). The clear was unnecessary — a rAF handle that has
already fired cancels to a no-op, and the next pointerdown (or dispose) replaces
the stored disposable — so remove it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011JE1SmW4mWp6AdoNVbHprb
@sonarqubecloud

Copy link
Copy Markdown

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant