Fix module drag-and-drop: custom collision detection to hit slots over racks

Root cause: SortableContext registers each rack column as a droppable.
Each column is ~1800px tall (42U x 44px). The default closestCenter
algorithm compared center-to-center distances, so the rack column's
center consistently beat the 44px RackSlot's center — meaning over.data
resolved to { dragType: 'rack' } and handleDragEnd's check for
dropType === 'slot' never matched. Drops silently did nothing.

Fix: replace closestCenter with a two-phase collision detection:
  1. pointerWithin — returns droppables whose bounding rect contains
     the actual pointer position. Slots are exactly hit-tested.
  2. closestCenter fallback — used when the pointer is not within any
     registered droppable (e.g. dragging a rack header between columns
     for sortable reorder where the pointer may be in the gap).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-22 07:55:35 -05:00
parent 7c04c4633f
commit 172896b85f

View File

@@ -3,10 +3,13 @@ import {
DndContext,
DragOverlay,
PointerSensor,
pointerWithin,
closestCenter,
useSensor,
useSensors,
type DragStartEvent,
type DragEndEvent,
type CollisionDetection,
} from '@dnd-kit/core';
import { SortableContext, horizontalListSortingStrategy, arrayMove } from '@dnd-kit/sortable';
import { toast } from 'sonner';
@@ -50,6 +53,23 @@ function ModuleDragOverlay({ label }: { label: string }) {
);
}
/**
* Collision detection strategy:
* - pointerWithin first: finds droppables whose rect contains the pointer.
* This correctly hits the 44px-tall RackSlots since the pointer is inside them.
* - closestCenter fallback: used when the pointer is not within any droppable,
* which is when dragging a rack header between columns (sortable reorder).
*
* Without this, the default closestCenter alone would favour the large rack-column
* sortable elements (~1800px tall) over the tiny slot droppables, so over.data
* would be { dragType: 'rack' } instead of { dropType: 'slot' }.
*/
const slotFirstCollision: CollisionDetection = (args) => {
const within = pointerWithin(args);
if (within.length > 0) return within;
return closestCenter(args);
};
export function RackPlanner() {
const { racks, loading, fetchRacks, moveModule, updateRack } = useRackStore();
const canvasRef = useRef<HTMLDivElement>(null);
@@ -143,7 +163,7 @@ export function RackPlanner() {
const rackIds = racks.map((r) => r.id);
return (
<DndContext sensors={sensors} onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<DndContext sensors={sensors} collisionDetection={slotFirstCollision} onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<div className="flex flex-col h-screen bg-[#0f1117]">
<RackToolbar rackCanvasRef={canvasRef} />