LearnNewsExamplesServices
Frontmatter
id18099
titleGridDragScroll's monitor outlives a native drag from the grid body
stateClosed
labels
bugaicoregrid
assigneesneo-opus-vega
createdAtSep 2, 2026, 4:32 PM
updatedAtSep 2, 2026, 4:54 PM
githubUrlhttps://github.com/neomjs/neo/issues/18099
authorneo-opus-vega
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 2, 2026, 4:54 PM

GridDragScroll's monitor outlives a native drag from the grid body

neo-opus-vega
neo-opus-vega commented on Sep 2, 2026, 4:32 PM

Context

A consumer grid declares a nativeDragZone (Neo.component.Base#nativeDragZone_, filled by Neo.main.addon.NativeDragSource) so a row can be dragged as an HTML5 drag into content the synthetic pipeline cannot reach — here an editor iframe. The same grid keeps the default drag-to-scroll (Neo.grid.ScrollManager#dragScroll → Neo.main.addon.GridDragScroll). The operator reported (2026-09-02): drag a row onto the editor, drop, move the pointer back over the grid — the grid still shows the grabbing cursor and scrolls under a plain mouse move, no button pressed.

Reproduced in real Chromium against that grid, read off Neo.main.addon.GridDragScroll and a document-level event witness:

moment monitorDrag activeDrag body cursor events seen
after the press + first moves true false '' dragstart on the row cell
after the drop true false '' dragend — no mouseup
pointer moved back over the grid, no button false true grabbing —

The Problem

GridDragScroll#onDragStart (its mousedown handler, src/main/addon/GridDragScroll.mjs:257-283) arms a monitor on every left press over a registered body: monitorDrag, a delay timer, and document-level mousemove / mouseup listeners that wait for minDistance to start a drag-scroll. NativeDragSource arms the row draggable on the same press and the browser starts a native drag on the next moves. From dragstart on, the browser delivers no mousemove and no mouseup to the page — only drag* events — so the monitor never sees its release. dragend fires; the addon does not listen to it. The monitor survives the whole drag and the drop.

The next ordinary pointer movement over the document then reaches the stale monitor listener: elapsed time and distance from the original press both qualify, onMonitorMove calls startDrag, and a drag-scroll runs with no button held — document.body.style.cursor = grabbing !important, document mousemove/mouseup listeners attached, the view scrolling under the pointer until some later mouseup arrives. The neo-draggable exclusion at :239-243 does not help: NativeDragSource arms draggable lazily on the first move, so at press time the row is neither neo-draggable nor draggable.

The Architectural Reality

  • src/main/addon/GridDragScroll.mjs — construct binds onDragStart/onMonitorEnd/onMonitorMove (:80-83); onDragStart arms the monitor (:257-283); onMonitorMove starts the drag when delay + distance are met (:289-300); onMonitorEnd clears the timer, the monitor and its listeners (:307-316); onDragEnd retires an active drag, restores the cursor and starts the kinetic tail (:127-…); unregister already ends an active drag by calling onDragEnd({type: 'mouseup'}) (:410) — the precedent for ending without a real event. The addon has no dragstart / dragend awareness at all.
  • src/main/addon/NativeDragSource.mjs — document-level capture listeners for mousedown (arm), dragstart (fill DataTransfer), dragend + mouseup (disarm) (:110-118); its class doc states the browser suppresses mouseup during a native drag (:174). Since PR #18091 it stamps neo-native-drag-active on body for the drag's lifetime.
  • src/grid/ScrollManager.mjs — registers the view with the addon on mount (updateDragScrollAddon); dragScroll_ defaults to true, so every grid has the monitor.
  • Component fixture with a real native drag: test/playwright/component/apps/empty-viewport lists NativeDragSource in mainThreadAddons; test/playwright/component/component/NativeDragZone.spec.mjs drives dragstart; test/playwright/component/apps/grid-focus (the grid click-focus fixture) has a long scrollable grid and an outside button.

The Fix

  1. GridDragScroll listens for dragstart on the document (capture, bound once in construct) and treats it as the end of any pointer gesture it is tracking: a live monitorDrag → onMonitorEnd(); a live activeDrag → onDragEnd({type: 'mouseup'}) (the unregister precedent — cursor restored, listeners removed, no kinetic tail from a drag the user did not release). The browser guarantees no further mouse events for that gesture; the addon must not wait for one.
  2. The dragstart listener lives on the document, not on registered bodies: the native drag may start on a row inside a registered body or on a foreign draggable — either way the monitor is dead. Registration count does not gate it; a page without grids pays one no-op listener.
  3. Witness in test/playwright/component/grid/: the grid-focus fixture gains NativeDragSource and a nativeDragZone on the long grid (delegate: '[data-record-id]', one text/plain type). Arms: (a) press on a row, move past minDistance so dragstart fires, continue outside, release → monitorDrag and activeDrag are null, body.style.cursor is empty — red on dev (monitorDrag survives); (b) then move the pointer over the long grid with no button → activeDrag stays null, the view's scrollTop does not change — red on dev (a phantom drag-scroll starts); (c) control: a plain press-move-release on the same grid without a native drag still scrolls it (the drag-scroll itself keeps working).

Acceptance Criteria

  • AC-1 GridDragScroll ends a live monitor and a live active drag on a document dragstart; no other behaviour of the addon changes.
  • AC-2 Witness arm (a): after a native row drag and drop, the addon holds no gesture state and the body cursor is restored — red-first on dev.
  • AC-3 Witness arm (b): a button-less pointer move over the grid after that drop neither starts a drag-scroll nor scrolls the view — red-first on dev.
  • AC-4 Witness arm (c): ordinary drag-to-scroll on the same fixture still scrolls (control green before and after).
  • AC-5 components + unit/manager/unit/grid suites green at head; NativeDragZone component spec unchanged and green.

Out of Scope

  • Whether the drop into the consumer's editor iframe resolves — that is the frame's drop handler and the payload protocol (cnetDragZone in the consumer), not the engine's gesture state; this ticket ends where the engine's own stuck state ends.
  • Touch gestures: a touchstart press never turns into a native drag.
  • #17889 (pointer-event sensors for DragDrop) — a different addon and pipeline.

Avoided Traps

  • Listening to dragend instead of dragstart. dragend arrives only after the drop, so the monitor would still be armed during the whole drag; and a source node removed mid-drag never fires dragend (Mnemosyne's finding on #18091). dragstart is the moment the browser stops talking to us; end the gesture there.
  • Excluding draggable nodes at mousedown. NativeDragSource arms draggable after the press, so the row is not draggable when the monitor is armed; a path check cannot see a drag that has not been declared yet.
  • Running the kinetic tail on the phantom end. onDragEnd computes a fling from the drag history; ending an activeDrag on dragstart must not fling the grid — pass through the same {type: 'mouseup'} path unregister uses and let the empty recent history yield zero velocity, or cancel the animation explicitly.

Related

  • #18065 / PR #18066 — the other GridDragScroll mousedown finding of the day (the cancelled default).
  • PR #18091 (#18087) — NativeDragSource stamps neo-native-drag-active for the drag's lifetime; a second signal the addon could read, but dragstart is the primary one.
  • #17889 — drag sensors to Pointer Events (adjacent, different pipeline).

Live latest-open sweep: checked the latest 20 open issues at 2026-09-02T14:31Z; no equivalent found. A2A claim sweep (last messages, all read-states): no overlapping [lane-claim]. Local corpus: no GridDragScroll ticket mentions a native drag.

Origin Session ID: 87c91db1-7fcd-4987-9932-2bf78d3dda25

Retrieval Hint: "GridDragScroll monitorDrag survives native dragstart grabbing cursor phantom scroll"

tobiu referenced in commit ec65c7e - "fix(grid): a native drag out of a grid ends the drag-to-scroll gesture it grew out of (#18099) (#18103)" on Sep 2, 2026, 4:54 PM
tobiu closed this issue on Sep 2, 2026, 4:54 PM