LearnNewsExamplesServices
Frontmatter
id12929
titleScrolled-state column drop desyncs header toolbar from body (scrollLeft clamps at drag-end)
stateClosed
labels
bugairegressiongrid
assigneesneo-fable
createdAtJun 12, 2026, 1:12 AM
updatedAtJun 12, 2026, 3:55 AM
githubUrlhttps://github.com/neomjs/neo/issues/12929
authorneo-fable
commentsCount2
parentIssue12883
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 12, 2026, 3:55 AM

Scrolled-state column drop desyncs header toolbar from body (scrollLeft clamps at drag-end)

Closed v13.0.0/archive-v13-0-0-chunk-18 bugairegressiongrid
neo-fable
neo-fable commented on Jun 12, 2026, 1:12 AM

Context

Operator L4 re-test on dev tip (post #12908 merge) found devindex "fully broken": cell content rendering under wrong headers (his screenshot: identical dates under four distinct headers; numbers under text headers) and header movements breaking, especially across heterogeneous widths. Headless reproduction tonight isolated the mechanism with a forensic probe.

Release classification: ON the board — this IS the v13 gate failure (parent #12883).

The Problem (reproduced + convicted)

Probe sequence: clean drags audit green; overdrag + drop in the scrolled state then breaks the world:

  • Post-drop forensics: headers render at HOME positions (Total@387, Commits %@487, …) while the first-row cells beneath are year-block cells (y2024, y2021, y2019 — 80px cells under 90-200px headers). Header row vs body offset by the FULL drop-time scroll (~1878px).
  • The desync self-heals on the next manual scroll (the GridHorizontalScrollSync addon copies scrollbar→header on the scroll event) — which is why post-scroll audits read clean while the post-drop window looks destroyed.
  • Compounding failure: a second drag started inside the desynced window has the zone's scroll term (fed from the worker pipeline: ~1878) contradicting the toolbar element's actual scroll (0) — the drag-start itemRects snapshot is taken in toolbar space, so every delta is off by the scroll amount: switch storms, wrong landings, and switchItems' columnPositions x/width writes land wrong — the persistent "columns duplicate / widths broken" corruption the operator accumulated interactively.

The Mechanism

At drag end, the items' wrapperStyle restore removes position: absolute. During the reflow there is a frame where the center toolbar has no scrollable overflow → the browser clamps the toolbar element's native scrollLeft to 0. Nothing re-applies it: the scrollbar element and the body's --grid-scroll-left var still hold the drop-time value; the addon only copies on scrollbar scroll events, and a same-value scrollTo on the scrollbar fires no event. The toolbar is left as the odd surface out.

The Fix

In draggable.grid.header.toolbar.SortZone#processDragEnd, after the restore/update chain settles (both branches — within-region landing AND lock-change re-home): re-apply the worker-side scroll truth to the toolbar element via Neo.main.DomAccess.scrollTo({direction: 'left', id: owner.id, value: owner.scrollLeft}) when owner.scrollLeft > 0. The body var and scrollbar are already correct; the toolbar rejoins them. Idempotent, scoped to the one surface that clamps.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback / Edge Case Docs Evidence
grid header SortZone#processDragEnd (drag-end scroll re-sync) This conviction (probe forensics) + the #12908 pipeline model (scrollbar = SSOT; toolbar = display copy) After drop in a scrolled state, the header toolbar's native scrollLeft is re-applied from owner.scrollLeft (the worker mirror fed by grid.ScrollManager) owner.scrollLeft === 0 → no-op (unscrolled drops unaffected); lock-change branch re-applies after onColumnLockChange settles JSDoc on the re-sync block L3 headless: post-drop forensics show header↔cell alignment WITHOUT any subsequent scroll (new regression leg in GridColumnOverdragScroll.spec.mjs); L4: operator re-pass on #12883
Second-drag-after-scrolled-drop math Zone scroll-term semantics (absolute, seeded from owner.scrollLeft) With the toolbar re-synced, the next drag's itemRects snapshot space and term agree Covered by the same re-sync; no separate code path n/a Probe: post-fix drag-after-scrolled-drop audits clean

Acceptance Criteria

  • Post-drop (scrolled state), headers and cells align immediately — no scroll required (probe forensics + the new spec leg).
  • A second drag in that state lands at the pointer-indicated slot with bounded switches.
  • Existing overdrag spec legs stay green.
  • Operator L4 re-pass (parent #12883's human gate).

Out of Scope

  • The deeper architectural question the operator raised (multi-body vs the main-branch single-body model) — if the re-sync + re-pass still leaves width-class failures, that conversation escalates to a Discussion.

Related

  • Parent: #12883 · Pipeline model: #12908 (#12906/#12907) · Mid-drag mapping: #12894

Live latest-open sweep: checked at 2026-06-11T23:1xZ; no equivalent. A2A in-flight sweep: clean.

Origin Session ID: e605ce21-3668-445c-bc00-45896aa9a092

Retrieval Hint: "scrolled drop header toolbar scrollLeft clamp desync body var drag-end reflow"

tobiu referenced in commit c3d3720 - "fix: scrolled-drop re-sync, single-fire lock rebuild, stale-echo gate (#12929, #12932) (#12938) on Jun 12, 2026, 3:55 AM
tobiu closed this issue on Jun 12, 2026, 3:55 AM