Show a dragged column's drop line at every boundary

The line was hidden while a drop would leave the column where it is, so a drag
showed nothing until it crossed a neighbour's middle. It now shows from the
first movement, on the nearer edge of the column's own slot when the drop
would change nothing, and at the first boundary over the mark column. The
choice of boundary moves to a tested function in the columns module.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Gregory SchierandClaude Opus 5.5 authored and Stetta task tk_mwadri4inaohxyiw committed 2026-11-10 09:03:19 -07:00
1 parent 78351f620e
commit 9ee37e4015
3 files changed
+49 -9

No files matched your search

@@ -38,6 +38,7 @@ import {
type ColumnPrefs,
type CustomColumnId,
canHide,
dropBoundary,
hideOtherColumns,
isCustomColumn,
moveColumn,
@@ -409,7 +410,7 @@ function useColumnDrag({
id: ColumnId;
/** The pointer, across the window */
x: number;
/** The boundary it would drop at, and that boundary's line, or null where it would not move */
/** The boundary it would move to, null where it would stay, and the line drawn on it */
to: number | null;
line: number | null;
frame: number;
@@ -433,6 +434,8 @@ function useColumnDrag({
if (column == null) return;
state.current = { id: column.id, x: pointer.current, to: null, line: null, frame: 0 };
setDragging(column);
// The line shows from the drag's first movement, not a frame later
target(state.current);
const tick = () => {
const s = state.current;
@@ -511,18 +514,14 @@ function useColumnDrag({
/** Sets where the drag would drop, from the pointer and the labels where they are now */
function target(s: NonNullable<typeof state.current>) {
// The boundary before the first label whose middle is right of the pointer
const cells = Array.from(
gridRef.current?.querySelectorAll<HTMLElement>(":scope > [data-column]") ?? [],
).map((el) => el.getBoundingClientRect());
let to = cells.findIndex((r) => s.x < r.left + r.width / 2);
if (to === -1) to = cells.length;
// Where it already is, it would not move, so no line. Its place is read each time, as the
// columns can change under a drag: Operation shows itself when a flow has one.
// Its place is read each time, as the columns can change under a drag: Operation shows
// itself when a flow has one.
const from = latest.current.columns.findIndex((c) => c.id === s.id);
const moves = from !== -1 && to !== from && to !== from + 1;
s.to = moves ? to : null;
const line = moves ? (cells[to]?.left ?? cells.at(-1)?.right ?? null) : null;
const { to, line } = dropBoundary(s.x, cells, from);
s.to = to;
// Told again only when it changes: another boundary, or the same one scrolled
if (line !== s.line) latest.current.onDropLine(line);
s.line = line;
+22
View File
@@ -11,6 +11,7 @@ import {
customPathProblem,
customValue,
defaultColumnPrefs,
dropBoundary,
fieldReference,
fieldReferenceChanges,
gridTemplate,
@@ -322,6 +323,27 @@ describe("column order", () => {
expect(parseColumnPrefs({ order: "time" }).order).toEqual([]);
});
test("the drop line shows at every boundary, its own slot's edges included", () => {
// Four columns 100px wide from x 24, after the mark column; the third is dragged
const cells = [24, 124, 224, 324].map((left) => ({ left, right: left + 100 }));
const at = (x: number) => dropBoundary(x, cells, 2);
// Over the mark column or the first's left half: the first boundary
expect(at(5)).toEqual({ to: 0, line: 24 });
expect(at(60)).toEqual({ to: 0, line: 24 });
expect(at(80)).toEqual({ to: 1, line: 124 });
// The left neighbour's near half and its own left half: its left edge, staying
expect(at(190)).toEqual({ to: null, line: 224 });
expect(at(250)).toEqual({ to: null, line: 224 });
// Its own right half and the right neighbour's near half: its right edge, staying
expect(at(290)).toEqual({ to: null, line: 324 });
expect(at(350)).toEqual({ to: null, line: 324 });
// Past the right neighbour's middle, and past the end
expect(at(380)).toEqual({ to: 4, line: 424 });
expect(at(900)).toEqual({ to: 4, line: 424 });
// The last column dragged to the end stays
expect(dropBoundary(900, cells, 3)).toEqual({ to: null, line: 424 });
});
test("Reset Column Order puts every column back", () => {
let prefs = moveColumn(defaultColumnPrefs, "time", 0);
prefs = toggleColumn(prefs, "latency");
+19
View File
@@ -298,6 +298,25 @@ export function resetOrder(prefs: ColumnPrefs): ColumnPrefs {
return { ...prefs, order: [] };
}
/**
* Where a dragged column would drop, from the pointer and the shown columns' edges, left to
* right: the boundary before the first column whose middle is right of the pointer, and the
* line's place on it. Over its own column or the near half of a neighbour, that is an edge of
* its own slot, where it would stay, so `to` is null; the line still shows there. Left of the
* first column, over the fixed mark column, it is the first boundary.
*/
export function dropBoundary(
x: number,
cells: { left: number; right: number }[],
from: number,
): { to: number | null; line: number | null } {
let at = cells.findIndex((r) => x < (r.left + r.right) / 2);
if (at === -1) at = cells.length;
const line = cells[at]?.left ?? cells.at(-1)?.right ?? null;
const stays = from === -1 || at === from || at === from + 1;
return { to: stays ? null : at, line };
}
/** Shows `id` alone */
export function hideOtherColumns(prefs: ColumnPrefs, id: ColumnId): ColumnPrefs {
const visible: ColumnPrefs["visible"] = {};