Skip to content

feat(#338): auto-scroll long Dashboards while dragging a tile - #339

Merged
BorisTyshkevich merged 3 commits into
mainfrom
feat/dashboard-drag-autoscroll-338
Jul 20, 2026
Merged

BorisTyshkevich merged 3 commits into
mainfrom
feat/dashboard-drag-autoscroll-338

Conversation

@BorisTyshkevich

@BorisTyshkevich BorisTyshkevich commented Jul 20, 2026 •

Copy link
Copy Markdown
Collaborator

What & why

Closes #338.

On a long Dashboard, moving a tile from the end to the beginning previously required several separate drops-and-scrolls, because a move could only reach destinations visible when the drag began. This adds the standard board interaction: during an active tile move, holding the pointer near the top/bottom edge of the visible Dashboard viewport scrolls it continuously, and the destination follows the newly revealed tiles.

Extends the #332 grip / ⌘-Ctrl tile move — no native HTML drag, no change to ordering semantics or schemas.

How it works

  • Pure, injected-seam module src/core/dashboard-autoscroll.ts (100/100/100/100):
    • edgeScrollVelocity(...) — signed px/frame from the pointer's distance to the visible edge; bounded, proportional acceleration; a lower bounded cap under prefers-reduced-motion; 0 in the center and on a degenerate viewport.
    • createDragAutoScroll(target, scheduler, opts) — a single-requestAnimationFrame-loop controller behind an injected DragAutoScrollTarget (visibleTop/visibleBottom/scrollBy/canScrollUp/canScrollDown) and FrameScheduler seam. At most one frame is ever pending; a stationary pointer keeps scrolling; leaving the edge zone or hitting a boundary idles the loop; stop() is idempotent and a frame already in flight can't mutate after it. A future nested scroll container can reuse the controller unchanged.
  • DOM glue in src/ui/dashboard.ts wireTileDrag: builds the DragAutoScrollTarget over .dash-page (effective top = page top + sticky .dash-topbar height), injects real requestAnimationFrame, and stops it in the existing single cleanup() (so every exit path — pointerup / pointercancel / blur / Escape / rerender — tears it down).
  • Destination stays correct while scrolling: the Use command-drag for Dashboard tile moves and add cell-detail preview to table/log tiles #332 gesture captures tile home rects once; those go stale as the page scrolls, so a currentRects() helper shifts them by -(scrollTop - scrollTop0) at all four resolution sites (onMove grid+flow, onUp grid+flow) and on the per-frame recompute. The floating dragged card is position:fixed (viewport-anchored) and is correctly left unadjusted. Persistence is unchanged: exactly one atomic move-tile on release, nothing during the gesture; a cancel changes neither order nor revision.

Both the Grid Tiles live-reflow path and the flow (Report / 2-column / 3-column) reorder path auto-scroll. No new bundled runtime dependency.

Tests

  • Unit (tests/unit/dashboard-autoscroll.test.ts): edge-velocity zero/direction/accel/clamp/reduced-motion/degenerate/short-viewport; controller single-loop, stationary-keeps-scrolling, leave-zone idle, boundary idle, stop() cancel + idempotent, no post-teardown mutation.
  • Integration (tests/unit/dashboard.test.ts): edge scroll down/up, threshold-gated non-start, destination recomputes from a scroll frame alone with no new pointermove, exactly-one move-tile after several frames (landing on the scroll-shifted slot), Escape cancels the loop + dispatches nothing, read-only never scrolls, sticky-topbar offset shifts the top edge zone, flow-engine scroll+reorder.

Reviewed by an adversarial pass (single-loop invariant, teardown on every exit, scroll-adjustment across all sites, non-vacuous tests) — no blockers/majors; the one minor it raised (untested nonzero sticky-topbar offset) is now covered by a dedicated test.

E2E (Playwright) not run locally (no engine in this sandbox) — relying on GitHub Actions.

Checklist

🤖 Generated with Claude Code


Follow-up fixes from live verification on github.demo (same PR)

Live-testing the drag on a tall dashboard surfaced three real defects, now fixed and re-verified live:

  1. Flow-layout tiles now float + follow the cursor (commit 1acf43b). They were position:static ("dim in place"), so the new auto-scroll scrolled the grabbed tile off-screen. Both engines now lift the tile to a floating follower (grid keeps its placeholder/FLIP reflow; flow has no slot grid).
  2. Max-overlap tile-move commit (commit 1acf43b). The old rule ("cover ≥2/3 of the destination slot's area") was aspect-ratio-dependent and unreachable for short tiles — a 403×120 KPI can cover at most ~0.40 of a 403×296 slot, so it always snapped back and could never be moved. Replaced with max-overlap (commit to whichever slot the tile overlaps most; snap back only when it still overlaps its origin most). core/tile-reorder.ts, 100/100/100/100.
  3. Fixed horizontal cursor/tile desync (commit eb6a3c3). The flow-float refactor reordered beginMove so the grid placeholder was inserted before the home rect was captured, displacing the card into the next CSS-grid cell → the floated tile sat ~one column right of the cursor (vertical fine). Now the rect is captured pre-placeholder; added a happy-dom regression test that models the grid displacement.

Verified live (github.demo, Google-auth, SHA-matched build): auto-scroll through an 11.6k-px dashboard; flow tile stays under the cursor while scrolling; grab point 0px offset on both axes; a KPI tile commits a move (index 55→54) where the old rule snapped back.

Known follow-up (separate, pre-existing): flow-layout KPI band tiles have no drag surface — filed as #340 (KPI tiles ARE movable in Grid Tiles).

BorisTyshkevich and others added 3 commits July 20, 2026 17:07
Extends the #332 grip / Command-Ctrl tile move: once an active move crosses
the movement threshold, holding the pointer near the top/bottom edge of the
visible .dash-page viewport scrolls it continuously (bounded, edge-proportional
speed; reduced-motion honored). Destination preview recomputes on every scroll
frame by shifting the captured tile home rects by the scroll delta, so a
stationary pointer lands on the newly revealed slot; the floating fixed card is
left unadjusted. Exactly one atomic move-tile persists on release and nothing
during the gesture; cancel (pointercancel / blur / Escape) changes neither
order nor revision. The effective top edge sits below the sticky topbar.

Edge-velocity math + the single-requestAnimationFrame-loop controller live in a
new pure, injected-seam module core/dashboard-autoscroll.ts (DragAutoScrollTarget
+ FrameScheduler), 100/100/100/100; the DOM glue in ui/dashboard.ts builds the
.dash-page adapter and injects real rAF. Both the Grid Tiles live-reflow and the
flow reorder paths auto-scroll. No new bundled runtime dependency.

Tests: pure edge-velocity + controller-lifecycle units (single loop, idempotent
stop, no post-teardown mutation, boundary idle, reduced-motion), and dashboard
integration tests (edge scroll up/down, threshold-gated non-start, destination
recompute from a scroll frame alone, exactly-one commit, cancel dispatches none,
read-only never scrolls, sticky-topbar offset, flow-engine scroll+reorder).
CHANGELOG [Unreleased] updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDsUDSPoDYa1M1rbpgdG3f
Two drag fixes found while live-verifying #338 on a tall dashboard:

1. Flow-layout tiles (Report / 2-column / 3-column) now LIFT + float + follow
   the cursor during a drag, exactly like Grid Tiles — previously they stayed
   position:static ("dim in place"), so the new #338 auto-scroll made the
   grabbed tile scroll off-screen with the content. The float/lift is now shared
   by both engines (placeholder + FLIP reflow stays grid-only); the KPI band
   card gets a real box for the duration of the drag if its computed display is
   'contents'. Verified live: a flow tile stays under the cursor while the page
   auto-scrolls thousands of px, and restores to static on release.

2. Replace the fragile "commit when the dragged tile covers >=2/3 of the
   DESTINATION slot's AREA" rule with MAX-OVERLAP: the move commits to whichever
   slot the dragged tile overlaps most, and snaps back only when it still
   overlaps its own origin most (or overlaps nothing). The old rule was
   aspect-ratio-dependent and unreachable for short tiles — a 403x120 KPI can
   cover at most ~0.40 of a 403x296 neighbor, so it ALWAYS snapped back and
   could never be moved in Grid Tiles. Max-overlap has no threshold to fight and
   works for any aspect ratio. Dropped OVERLAP_COMMIT_RATIO and the ratio param.
   Verified live: the KPI tile now commits its move (index changed) where it
   previously always snapped back.

Pure logic stays in core/tile-reorder.ts (max-overlap, 100/100/100/100) and the
DOM float/lift in ui/dashboard.ts behind the existing gesture. Tests updated:
tile-reorder unit block rewritten for max-overlap incl. the short-over-tall
regression; dashboard flow-drag tests assert the float/lift + cursor-follow +
restore. CHANGELOG [Unreleased] reconciled.

Known follow-up (pre-existing, separate): flow-layout KPI band tiles render
presentational cards with no drag surface, so they can't be grabbed in flow
layouts (they ARE movable in Grid Tiles). To be filed as its own issue.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDsUDSPoDYa1M1rbpgdG3f
Horizontal cursor/tile desync during a Grid Tiles move (found live): the
flow-float refactor reordered beginMove so `grid.insertBefore(placeholder,
card)` ran BEFORE `r0 = card.getBoundingClientRect()`. Inserting the
placeholder pushes the card into the next CSS-grid cell, so r0.left captured
the displaced (wrong-column) position and the floated tile sat ~one column
right of the cursor (vertical fine — same row). Capture r0 + savedHeight/
savedDisplay BEFORE the placeholder insertion. Real-browser only (happy-dom
ignores grid placement); added a regression test that models the displacement
(the dragged card reports its home left until a placeholder occupies its slot)
and asserts the float uses the pre-placeholder home left.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDsUDSPoDYa1M1rbpgdG3f
@BorisTyshkevich
BorisTyshkevich merged commit 7ea72bc into main Jul 20, 2026
9 checks passed
@BorisTyshkevich
BorisTyshkevich deleted the feat/dashboard-drag-autoscroll-338 branch August 6, 2026 15:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant