Fix drag-and-drop hover detection and slot targeting
Two root-cause bugs fixed: 1. Port <button> elements inside ModuleBlock had pointer-events:auto (browser default), so document.elementFromPoint() hit them instead of the RackSlot behind them whenever the cursor was over an occupied slot. Fixed by toggling body.rack-dragging during any drag, which applies a CSS rule that forces pointer-events:none !important on .module-block and all descendants. 2. onDragMove pointer-position reconstruction (activatorEvent.clientX + delta.x) was slightly off because delta is measured from the initial mousedown, not the activation point. Replaced with a native window pointermove listener (capture phase) that gives exact clientX/Y — no reconstruction needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -116,7 +116,7 @@ export function ModuleBlock({ module }: ModuleBlockProps) {
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
className={cn(
|
||||
'relative w-full border-l-4 select-none overflow-hidden transition-opacity',
|
||||
'module-block relative w-full border-l-4 select-none overflow-hidden transition-opacity',
|
||||
colors.bg,
|
||||
colors.border,
|
||||
isDragging ? 'opacity-0 pointer-events-none' : 'cursor-grab active:cursor-grabbing',
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
useSensors,
|
||||
type DragStartEvent,
|
||||
type DragEndEvent,
|
||||
type DragMoveEvent,
|
||||
} from '@dnd-kit/core';
|
||||
import { SortableContext, horizontalListSortingStrategy, arrayMove } from '@dnd-kit/sortable';
|
||||
import { toast } from 'sonner';
|
||||
@@ -61,13 +60,10 @@ function ModuleDragOverlay({ label }: { label: string }) {
|
||||
* Resolve which rack slot (if any) is under the pointer during a drag.
|
||||
*
|
||||
* Strategy: elementFromPoint at the current pointer coordinates.
|
||||
* - ModuleBlock has pointer-events:none when isDragging, so it is transparent.
|
||||
* - All ModuleBlocks get pointer-events:none via the body.rack-dragging CSS rule,
|
||||
* so elementFromPoint sees through them to the slot element beneath.
|
||||
* - DragOverlay has pointer-events:none natively (dnd-kit).
|
||||
* - RackSlot divs carry data-rack-id / data-u-pos attributes that we read here.
|
||||
*
|
||||
* This is intentionally independent of dnd-kit's collision detection, which
|
||||
* cannot reliably distinguish 44px slot elements from large (~1800px) rack
|
||||
* column sortable containers that share the same DndContext.
|
||||
*/
|
||||
function resolveSlotFromPoint(clientX: number, clientY: number): HoverSlot | null {
|
||||
const el = document.elementFromPoint(clientX, clientY);
|
||||
@@ -99,6 +95,10 @@ export function RackPlanner() {
|
||||
const [hoverSlot, setHoverSlot] = useState<HoverSlot | null>(null);
|
||||
const hoverSlotRef = useRef<HoverSlot | null>(null);
|
||||
|
||||
// Tracks whether ANY module/palette drag is in progress — used to
|
||||
// activate the body.rack-dragging CSS class and the pointermove listener.
|
||||
const isDraggingAnyRef = useRef(false);
|
||||
|
||||
function updateHoverSlot(slot: HoverSlot | null) {
|
||||
hoverSlotRef.current = slot;
|
||||
setHoverSlot(slot);
|
||||
@@ -112,36 +112,47 @@ export function RackPlanner() {
|
||||
fetchRacks().catch(() => toast.error('Failed to load racks'));
|
||||
}, [fetchRacks]);
|
||||
|
||||
/**
|
||||
* Native pointermove listener registered once on mount.
|
||||
* Only runs while isDraggingAnyRef is true — gives us the exact cursor
|
||||
* position without any reconstruction arithmetic, so resolveSlotFromPoint
|
||||
* is always called with accurate coordinates.
|
||||
*/
|
||||
useEffect(() => {
|
||||
function onPointerMove(e: PointerEvent) {
|
||||
if (!isDraggingAnyRef.current) return;
|
||||
const slot = resolveSlotFromPoint(e.clientX, e.clientY);
|
||||
hoverSlotRef.current = slot;
|
||||
setHoverSlot(slot);
|
||||
}
|
||||
|
||||
// Capture phase so we get the event before any element can stop propagation.
|
||||
window.addEventListener('pointermove', onPointerMove, { capture: true });
|
||||
return () => window.removeEventListener('pointermove', onPointerMove, { capture: true });
|
||||
}, []);
|
||||
|
||||
function handleDragStart(event: DragStartEvent) {
|
||||
const data = event.active.data.current as Record<string, unknown>;
|
||||
if (data?.dragType === 'palette') {
|
||||
setActivePaletteType(data.type as ModuleType);
|
||||
isDraggingAnyRef.current = true;
|
||||
document.body.classList.add('rack-dragging');
|
||||
} else if (data?.dragType === 'module') {
|
||||
setDraggingModuleId(data.moduleId as string);
|
||||
setActiveDragModuleLabel(data.label as string);
|
||||
isDraggingAnyRef.current = true;
|
||||
document.body.classList.add('rack-dragging');
|
||||
}
|
||||
updateHoverSlot(null);
|
||||
}
|
||||
|
||||
function handleDragMove(event: DragMoveEvent) {
|
||||
const data = event.active.data.current as Record<string, unknown>;
|
||||
// Only track slot hover for module and palette drags
|
||||
if (data?.dragType !== 'module' && data?.dragType !== 'palette') {
|
||||
updateHoverSlot(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute current pointer position from the activating event + accumulated delta
|
||||
const activatorEvent = event.activatorEvent as PointerEvent;
|
||||
const clientX = activatorEvent.clientX + event.delta.x;
|
||||
const clientY = activatorEvent.clientY + event.delta.y;
|
||||
|
||||
updateHoverSlot(resolveSlotFromPoint(clientX, clientY));
|
||||
}
|
||||
|
||||
async function handleDragEnd(event: DragEndEvent) {
|
||||
const { active, over } = event;
|
||||
|
||||
// Stop native hover tracking and remove body class FIRST
|
||||
isDraggingAnyRef.current = false;
|
||||
document.body.classList.remove('rack-dragging');
|
||||
|
||||
// Capture hoverSlot BEFORE resetting state
|
||||
const slot = hoverSlotRef.current;
|
||||
|
||||
@@ -210,7 +221,6 @@ export function RackPlanner() {
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragMove={handleDragMove}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<div className="flex flex-col h-screen bg-[#0f1117]">
|
||||
|
||||
@@ -28,3 +28,15 @@
|
||||
height: 1.75rem; /* 28px per U */
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* During any rack drag, make every module-block and ALL of its children
|
||||
* transparent to pointer-events so that document.elementFromPoint() can
|
||||
* "see through" them to the RackSlot elements underneath.
|
||||
* The `!important` is necessary because individual elements (port buttons,
|
||||
* resize handle) carry their own pointer-events values.
|
||||
*/
|
||||
body.rack-dragging .module-block,
|
||||
body.rack-dragging .module-block * {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user