Fix module drag drop (collision detection) + widen rack to fix port clipping
Collision detection root cause:
pointerWithin returns ALL droppables containing the pointer, in
registration order — not sorted by element size. Rack columns register
via useSortable before their child RackSlots, so they always came first
in the result list. over.data.current was { dragType: 'rack' }, never
{ dropType: 'slot' }, so handleDragEnd's slot check never matched and
the module snapped back.
Fix: filter droppableContainers to elements with data.current.dropType
=== 'slot' before running pointerWithin. This does an exact pointer
hit-test against only the 44px slot rects. If no slot is hit (e.g. the
pointer is in a gap or over a rack header), fall back to closestCenter
over all droppables so rack-column reorder still works.
Width fix:
24 ports * 10px + 23 gaps * 3px = 309px
+ px-2 padding (16px) + border-l-4 (4px) = 329px minimum
w-80 (320px) was 9px short, clipping port 24.
Increased to w-96 (384px) / min-w-[384px] — 55px of breathing room.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -51,7 +51,7 @@ export function RackColumn({ rack, draggingModuleId }: RackColumnProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={setNodeRef} style={style} className="flex flex-col min-w-[320px] w-80 shrink-0">
|
||||
<div ref={setNodeRef} style={style} className="flex flex-col min-w-[384px] w-96 shrink-0">
|
||||
{/* Rack header — drag handle for reorder */}
|
||||
<div className="flex items-center gap-1 bg-slate-700 border border-slate-600 rounded-t-lg px-2 py-1.5 group">
|
||||
{/* Drag handle */}
|
||||
|
||||
@@ -55,18 +55,31 @@ 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' }.
|
||||
* Problem: SortableContext registers each rack column as a droppable. Columns
|
||||
* are ~1800px tall; individual slots are 44px. Default closestCenter picks the
|
||||
* column centre over any slot centre. pointerWithin returns multiple matches
|
||||
* sorted by registration order (columns register before their child slots), so
|
||||
* the rack column still wins.
|
||||
*
|
||||
* Fix:
|
||||
* 1. First try pointerWithin restricted to ONLY elements whose data has
|
||||
* dropType === 'slot'. This is an exact hit-test against the 44px slot rects.
|
||||
* 2. Fall back to closestCenter over ALL droppables so rack-header reorder
|
||||
* (which needs the sortable rack targets) still works.
|
||||
*/
|
||||
const slotFirstCollision: CollisionDetection = (args) => {
|
||||
const within = pointerWithin(args);
|
||||
if (within.length > 0) return within;
|
||||
// Restrict to slot droppables only
|
||||
const slotContainers = args.droppableContainers.filter(
|
||||
(c) => c.data.current?.dropType === 'slot'
|
||||
);
|
||||
|
||||
if (slotContainers.length > 0) {
|
||||
const slotHits = pointerWithin({ ...args, droppableContainers: slotContainers });
|
||||
if (slotHits.length > 0) return slotHits;
|
||||
}
|
||||
|
||||
// Nothing hit a slot — use full closestCenter for rack reorder
|
||||
return closestCenter(args);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user