Skip to content

Auto-scroll long Dashboards while Command/Ctrl-dragging tiles #338

Description

@BorisTyshkevich

Summary

Allow users to move a Dashboard tile across a long Dashboard without repeatedly dropping, scrolling, and starting another drag.

During an active Dashboard tile move, placing the pointer near or beyond the top or bottom of the visible Dashboard area must start continuous vertical browser scrolling. Scrolling stops when the pointer leaves the edge zone, the document reaches its scroll boundary, or the move ends.

This extends the Command/Ctrl-drag tile movement implemented by #332. It does not restore native HTML drag-and-drop and does not change Dashboard ordering semantics.

Problem

A tile can currently be moved only among destinations visible in the viewport when the move begins. On a long Dashboard, moving a tile from the end to the beginning requires several separate reorder operations.

Expected behavior is the standard editor/board interaction:

  1. start Command/Ctrl-dragging a tile;
  2. move the pointer near the viewport top or bottom;
  3. the page scrolls continuously in that direction;
  4. destination feedback follows the newly revealed tiles;
  5. release once at the final destination.

Activation rules

Auto-scroll starts only after a valid tile move has crossed the existing movement threshold.

It must not activate during:

  • plain text selection;
  • chart range selection;
  • table or log cell interaction;
  • tile resize;
  • modifier hover before an actual move starts;
  • read-only Dashboard mode;
  • a cancelled or completed move.

The existing platform gesture remains authoritative:

  • macOS: Command-drag;
  • Windows/Linux: Ctrl-drag.

Plain drag must remain available to tile content and must never start auto-scroll.

Scroll viewport

For the current Dashboard page, scroll the browser/document viewport used by the Dashboard route.

Keep the implementation structured around an explicit scroll target so a future nested Dashboard scroll container can reuse the same controller without rewriting drag logic.

Use the scroll target's visible bounds rather than assuming window.innerHeight everywhere. Account for any sticky Dashboard header/toolbar that visually occludes the top of the content: the effective upper interaction boundary should be the first visible Dashboard content coordinate below sticky chrome.

Edge zones

Define top and bottom activation zones inside the visible scroll viewport.

Recommended defaults:

const AUTO_SCROLL_EDGE_PX = 80;
const AUTO_SCROLL_MIN_PX_PER_FRAME = 3;
const AUTO_SCROLL_MAX_PX_PER_FRAME = 24;

Normative behavior:

  • pointer above the top edge zone: scroll upward at maximum speed;
  • pointer within the top edge zone: scroll upward, faster as it approaches the edge;
  • pointer in the center region: no auto-scroll;
  • pointer within the bottom edge zone: scroll downward, faster as it approaches the edge;
  • pointer below the bottom edge zone: scroll downward at maximum speed;
  • no horizontal auto-scroll in this issue.

The exact constants may be adjusted in implementation, but speed must be bounded and proportional to edge proximity rather than a single abrupt rate.

Animation loop

Use one requestAnimationFrame loop owned by the active move gesture.

Requirements:

  • start the loop only when edge scrolling is needed;
  • continue scrolling while the pointer remains stationary in an edge zone;
  • use the latest pointer coordinates on every frame;
  • call the scroll target's immediate scroll operation; do not use smooth scrolling;
  • stop requesting frames when no movement is possible or needed;
  • never create more than one loop for the same drag;
  • do not dispatch a persisted Dashboard command on each frame.

The loop must tolerate throttled frames and must clamp movement to the scroll target's actual boundaries.

Pointer capture and lifecycle

The active tile move must retain pointer events when the pointer reaches or temporarily leaves the viewport.

Use pointer capture or the existing equivalent gesture ownership established by #332.

Stop auto-scroll and remove all transient listeners/state on:

  • pointer release;
  • pointercancel;
  • Escape;
  • window blur;
  • lost pointer capture;
  • Dashboard rerender;
  • route teardown;
  • tile removal during the gesture;
  • session destruction.

A cancelled gesture must not change tile order or workspace revision.

Destination recomputation while scrolling

Scrolling changes which tiles are under the stationary pointer. Destination feedback must therefore be recomputed during auto-scroll, not only on pointermove.

On every frame that changes scroll position:

  1. resolve the current destination from the latest pointer coordinates and current DOM geometry;
  2. update the insertion indicator / moving preview;
  3. keep the dragged tile identity stable;
  4. do not persist the move yet.

On pointer release, dispatch the existing atomic move-tile command exactly once using the final resolved destination.

If no valid destination is resolved at release, cancel the move and preserve the original order.

Do not repeatedly reorder the persisted document while scrolling. Preview may update continuously; persistence occurs once at successful drop.

Layout behavior

Support every editable Dashboard layout in which tile reordering is available:

  • Grid Tiles;
  • transient Full view;
  • Report;
  • 2 columns;
  • 3 columns.

The destination resolver must retain each layout engine's existing canonical ordering and packing behavior. Auto-scroll only reveals additional destinations; it does not define a new ordering algorithm.

Changing responsive breakpoints during a drag is not a primary use case, but the implementation must fail safely: cancel or recompute without persisting an invalid destination.

Visual feedback

During edge scrolling:

  • retain the existing active-move styling;
  • retain a visible destination indicator;
  • optionally show a subtle top/bottom edge gradient or directional cue;
  • do not cover tile content with a large overlay;
  • do not cause layout shift;
  • do not display a browser-native drag ghost.

An edge cue is optional. Functional auto-scroll and destination feedback are required.

Accessibility and reduced motion

  • Existing non-pointer reorder alternatives, if any, remain unchanged.
  • Auto-scroll must not start from keyboard focus movement alone.
  • Respect prefers-reduced-motion by using a lower bounded scroll speed or a less accelerated profile, not by disabling the functionality.
  • Announce only the final completed reorder through the existing move feedback; do not announce every scroll frame or intermediate destination.

Shared controller

Keep edge-scroll mechanics outside Dashboard document/persistence code. Prefer a small gesture utility/controller with injected dependencies such as:

interface DragAutoScrollTarget {
  visibleTop(): number;
  visibleBottom(): number;
  scrollBy(deltaY: number): number; // actual applied delta
  canScrollUp(): boolean;
  canScrollDown(): boolean;
}

The exact shape may differ. The important boundary is:

  • pointer gesture owns pointer coordinates and start/stop lifecycle;
  • auto-scroll controller owns edge velocity and animation frames;
  • Dashboard destination resolver owns tile hit-testing/order preview;
  • move-tile remains the only persistence command.

Do not embed workspace commits, layout mutation, or query/session work in the animation loop.

Tests

Unit tests

Cover pure edge velocity behavior:

  • zero velocity in the center;
  • upward/downward direction in each edge zone;
  • proportional acceleration;
  • maximum-speed clamping outside viewport bounds;
  • reduced-motion profile;
  • no scrolling when the target is already at its boundary.

Cover controller lifecycle:

  • one animation loop only;
  • stationary pointer continues scrolling;
  • leaving the edge zone stops the loop;
  • pointer release/cancel/Escape/blur/lost capture destroys the loop;
  • teardown is idempotent;
  • no post-teardown frame mutates state.

Dashboard integration tests

  • active Command/Ctrl-drag near the bottom scrolls downward;
  • active drag near the top scrolls upward;
  • plain drag/text selection never scrolls the page;
  • tile resize and chart brushing never start tile auto-scroll;
  • destination updates as newly revealed tiles pass under a stationary pointer;
  • one final move-tile command is dispatched on release;
  • cancellation dispatches no command;
  • read-only mode never enables the behavior;
  • Grid Tiles and flow layouts preserve their existing order semantics.

Real-browser tests

Use a Dashboard tall enough to exceed several viewports.

Verify:

  • move a tile from near the bottom to near the beginning in one gesture;
  • move a tile from near the beginning to near the bottom in one gesture;
  • continuous scroll with the pointer held stationary in an edge zone;
  • sticky header does not hide or corrupt the top destination;
  • Chrome, Firefox, and WebKit;
  • light and dark themes;
  • narrow viewport;
  • no accidental page horizontal scrolling.

Acceptance criteria

  • Active Command/Ctrl tile moves auto-scroll near the visible top and bottom edges.
  • Scroll direction and speed respond to pointer distance from the edge.
  • Scrolling continues without additional pointer movement.
  • Destination feedback recomputes while the document scrolls.
  • A successful gesture persists exactly one final move-tile command.
  • Cancellation leaves tile order and revision unchanged.
  • Plain text selection, chart brushing, cell interaction, and resizing never trigger auto-scroll.
  • All listeners, pointer capture, and animation frames are cleaned up on every exit path.
  • Grid Tiles, Full view, Report, 2-column, and 3-column layouts work.
  • Unit and real-browser tests pass.

Non-goals

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions