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:
@@ -3,10 +3,13 @@ import {
|
|||||||
DndContext,
|
DndContext,
|
||||||
DragOverlay,
|
DragOverlay,
|
||||||
PointerSensor,
|
PointerSensor,
|
||||||
|
pointerWithin,
|
||||||
|
closestCenter,
|
||||||
useSensor,
|
useSensor,
|
||||||
useSensors,
|
useSensors,
|
||||||
type DragStartEvent,
|
type DragStartEvent,
|
||||||
type DragEndEvent,
|
type DragEndEvent,
|
||||||
|
type CollisionDetection,
|
||||||
} from '@dnd-kit/core';
|
} from '@dnd-kit/core';
|
||||||
import { SortableContext, horizontalListSortingStrategy, arrayMove } from '@dnd-kit/sortable';
|
import { SortableContext, horizontalListSortingStrategy, arrayMove } from '@dnd-kit/sortable';
|
||||||
import { toast } from 'sonner';
|
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() {
|
export function RackPlanner() {
|
||||||
const { racks, loading, fetchRacks, moveModule, updateRack } = useRackStore();
|
const { racks, loading, fetchRacks, moveModule, updateRack } = useRackStore();
|
||||||
const canvasRef = useRef<HTMLDivElement>(null);
|
const canvasRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -143,7 +163,7 @@ export function RackPlanner() {
|
|||||||
const rackIds = racks.map((r) => r.id);
|
const rackIds = racks.map((r) => r.id);
|
||||||
|
|
||||||
return (
|
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]">
|
<div className="flex flex-col h-screen bg-[#0f1117]">
|
||||||
<RackToolbar rackCanvasRef={canvasRef} />
|
<RackToolbar rackCanvasRef={canvasRef} />
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user