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:
- start Command/Ctrl-dragging a tile;
- move the pointer near the viewport top or bottom;
- the page scrolls continuously in that direction;
- destination feedback follows the newly revealed tiles;
- 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:
- resolve the current destination from the latest pointer coordinates and current DOM geometry;
- update the insertion indicator / moving preview;
- keep the dragged tile identity stable;
- 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
Non-goals
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:
Activation rules
Auto-scroll starts only after a valid tile move has crossed the existing movement threshold.
It must not activate during:
The existing platform gesture remains authoritative:
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.innerHeighteverywhere. 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:
Normative behavior:
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
requestAnimationFrameloop owned by the active move gesture.Requirements:
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:
pointercancel;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:
On pointer release, dispatch the existing atomic
move-tilecommand 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:
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:
An edge cue is optional. Functional auto-scroll and destination feedback are required.
Accessibility and reduced motion
prefers-reduced-motionby using a lower bounded scroll speed or a less accelerated profile, not by disabling the functionality.Shared controller
Keep edge-scroll mechanics outside Dashboard document/persistence code. Prefer a small gesture utility/controller with injected dependencies such as:
The exact shape may differ. The important boundary is:
move-tileremains 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:
Cover controller lifecycle:
Dashboard integration tests
move-tilecommand is dispatched on release;Real-browser tests
Use a Dashboard tall enough to exceed several viewports.
Verify:
Acceptance criteria
move-tilecommand.Non-goals