Context
Two defect classes measured this week both trace to the same foundation: the drag sensors are a mouse/touch-event, hit-testing-based design. (1) A pointer crossing an iframe pane starves the move stream and can swallow pointerup, leaving a session in neo-is-dragging forever — #17883, shielded in PR #17887 by making iframes hit-test-inert during a session. (2) A session whose pointerup is lost terminates never — observed surviving multiple later gestures. Both are symptoms of relying on hit-testing to keep receiving a gesture the sensor already owns.
The platform primitive that removes the class is pointer capture: setPointerCapture(pointerId) routes every subsequent pointer event to the capturing element until release — hit-testing bypassed entirely (iframes included), pointerup/pointercancel delivery guaranteed to the captor, implicit release built in.
The Architectural Reality
src/main/draggable/sensor/Mouse.mjs — claims gestures via testPathInclusion (:135), installs a document-level dragstart → preventDefault for the gesture (:162/:217), stamps body.neo-drag-active (:160/:215); secondary-click/ctrl/meta gating lives here too.
src/main/draggable/sensor/Touch.mjs — a separate sensor for the same job. Pointer Events unify mouse, touch, and pen: the migration plausibly collapses two sensors into one.
src/main/addon/DragDrop.mjs:424 — session cleanup half.
- The sensors' OUTPUT contract (
drag:start / drag:move / drag:end synthetic events into the worker world) stays untouched — consumers, DragZones, the dock stack, and the #17885 native-drag partition gate never notice.
The Fix
Replace the mouse/touch listeners with pointer listeners; on claiming a gesture, setPointerCapture on a stable capture target; translate to the existing synthetic vocabulary; release capture on end/cancel. Port the existing gating semantics verbatim (dragTargetClasses claim, primary-button/modifier checks, the native-dragstart suppression for claimed gestures, neo-drag-active stamping). Migration details that must not be lost:
touch-action: none (or pan-x/pan-y as appropriate) on drag handles — without it, browser gestures fire pointercancel mid-drag on touch devices.
pointercancel is a first-class terminal now: it must end sessions as cleanly as pointerup (this is the structural fix for the never-terminating session).
- The #17887 iframe shield stays in place as defense-in-depth; whether it can retire is a later, evidence-gated decision once capture has soaked (explicitly not part of this ticket).
- The #17885 partition contract (native-drag sources exempt from sensor claiming) applies unchanged to the new listeners.
Acceptance Criteria
Out of Scope
- Retiring the #17887 shield (separate, evidence-gated follow-up).
- Native HTML5 drag sources (#17885) — partition unchanged.
- The SharedWorkers first-mount race (#17888) — different layer.
Related
- #17883 / PR #17887 — the shielded symptom this makes structural
- #17885 — the native-drag partition that must keep holding
- #17888 — sibling robustness work in the dock stack
Live latest-open sweep: latest 20 open checked 2026-08-30 ~18:20 UTC — no equivalent. A2A claim sweep: none; the idea was routed to @neo-opus-grace via A2A ~18:17 as future-direction — this ticket banks it per operator direction; her #17887 lane is untouched and she has first claim here if she wants it.
Origin Session ID: b51135ac-ec45-4689-ac45-0e99fb073069
Retrieval Hint: "pointer capture drag sensor iframe pointerup stuck neo-drag-active" · query_recent_turns 2026-08-30 (iframe drag forensics)
Context
Two defect classes measured this week both trace to the same foundation: the drag sensors are a mouse/touch-event, hit-testing-based design. (1) A pointer crossing an iframe pane starves the move stream and can swallow
pointerup, leaving a session inneo-is-draggingforever — #17883, shielded in PR #17887 by making iframes hit-test-inert during a session. (2) A session whosepointerupis lost terminates never — observed surviving multiple later gestures. Both are symptoms of relying on hit-testing to keep receiving a gesture the sensor already owns.The platform primitive that removes the class is pointer capture:
setPointerCapture(pointerId)routes every subsequent pointer event to the capturing element until release — hit-testing bypassed entirely (iframes included),pointerup/pointercanceldelivery guaranteed to the captor, implicit release built in.The Architectural Reality
src/main/draggable/sensor/Mouse.mjs— claims gestures viatestPathInclusion(:135), installs a document-leveldragstart → preventDefaultfor the gesture (:162/:217), stampsbody.neo-drag-active(:160/:215); secondary-click/ctrl/meta gating lives here too.src/main/draggable/sensor/Touch.mjs— a separate sensor for the same job. Pointer Events unify mouse, touch, and pen: the migration plausibly collapses two sensors into one.src/main/addon/DragDrop.mjs:424— session cleanup half.drag:start/drag:move/drag:endsynthetic events into the worker world) stays untouched — consumers, DragZones, the dock stack, and the #17885 native-drag partition gate never notice.The Fix
Replace the mouse/touch listeners with pointer listeners; on claiming a gesture,
setPointerCaptureon a stable capture target; translate to the existing synthetic vocabulary; release capture on end/cancel. Port the existing gating semantics verbatim (dragTargetClasses claim, primary-button/modifier checks, the native-dragstartsuppression for claimed gestures,neo-drag-activestamping). Migration details that must not be lost:touch-action: none(orpan-x/pan-yas appropriate) on drag handles — without it, browser gestures firepointercancelmid-drag on touch devices.pointercancelis a first-class terminal now: it must end sessions as cleanly aspointerup(this is the structural fix for the never-terminating session).Acceptance Criteria
drag:*contract is byte-compatible for consumers.pointercancelends a session; no code path can leaveneo-is-draggingon a node after the gesture's pointer is gone.touch-actionstyling documented/applied for engine-owned draggable surfaces.Out of Scope
Related
Live latest-open sweep: latest 20 open checked 2026-08-30 ~18:20 UTC — no equivalent. A2A claim sweep: none; the idea was routed to @neo-opus-grace via A2A ~18:17 as future-direction — this ticket banks it per operator direction; her #17887 lane is untouched and she has first claim here if she wants it.
Origin Session ID: b51135ac-ec45-4689-ac45-0e99fb073069
Retrieval Hint: "pointer capture drag sensor iframe pointerup stuck neo-drag-active" · query_recent_turns 2026-08-30 (iframe drag forensics)