Drag and Drop Troubleshooting Guide

Overview

This guide covers common issues and solutions for the drag-and-drop functionality in the Spatial Navigator. The drag-and-drop system uses HTML5 Drag and Drop API with custom ghost images and manual event handling for child object icons.

Common Issues and Solutions

1. Drag Operations End Prematurely

Symptoms:

  • Drag ends immediately when mouse leaves the small child icon
  • Cannot move objects between containers
  • Ghost disappears unexpectedly

Root Cause: Browser fires dragend event when mouse leaves the draggable element, even if the drag operation should continue.

Solution: Implemented multiple levels of dragover event handlers:

// Prevent drag end when moving within card boundaries
onDragOver={(e) => {
  if (isDragMode && draggedObjectId) {
    e.preventDefault();
    e.stopPropagation();
  }
}}

Implementation Details:

  • Added dragover handlers to parent card, card content, and child preview containers
  • Implemented boundary detection in onDragEnd to ignore premature drag end events
  • Added padding around child icons to create larger interaction areas

2. Custom Drag Ghost Not Visible

Symptoms:

  • No visual feedback when dragging
  • Browser default ghost appears instead of custom ghost
  • Drag operations work but without visual guidance

Root Cause: Browser limitations with setDragImage() API and timing issues with ghost element creation.

Solution: Implemented manual ghost tracking system:

// Hide browser's default ghost
const emptyImg = new Image();
emptyImg.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=';
e.dataTransfer.setDragImage(emptyImg, 0, 0);

// Create manual ghost that follows mouse
const ghost = document.createElement('div');
// ... style ghost element
document.body.appendChild(ghost);

// Track mouse movement
document.addEventListener('mousemove', updateGhostPosition);

Implementation Details:

  • Creates DOM element positioned at mouse cursor
  • Uses mousemove event to update ghost position
  • Shows either object image or colored circle with first letter
  • Proper cleanup on drag end with global mouse event handling

3. Ghost Gets Stuck After Drop

Symptoms:

  • Ghost element remains visible after drag completes
  • Mouse cursor shows dragging state permanently
  • Multiple ghost elements accumulate

Root Cause: Drop events may not fire correctly, leaving cleanup code unexecuted.

Solution: Implemented global cleanup mechanism:

// Global mouse up handler for cleanup
const handleGlobalMouseUp = () => {
  setTimeout(() => {
    if (dragGhostRef.current && document.body.contains(dragGhostRef.current)) {
      document.body.removeChild(dragGhostRef.current);
      dragGhostRef.current = null;
    }
    // Clean up event listeners
    document.removeEventListener('mousemove', mouseMoveHandler);
    document.removeEventListener('mouseup', handleGlobalMouseUp);
  }, 100); // Delay allows drop events to fire first
};

document.addEventListener('mouseup', handleGlobalMouseUp);

Prevention:

  • Always add global mouseup listener during drag
  • Use setTimeout to allow drop events to complete first
  • Check if ghost element exists before removal

4. Drop Zones Appear on Wrong Cards

Symptoms:

  • Drop zones show on the card being dragged from
  • Cannot distinguish valid drop targets
  • Confusing visual feedback

Root Cause: Logic didn’t account for child objects being dragged from parent containers.

Solution: Enhanced dropzone visibility logic:

// Check if dragged object is a child of this object
const draggedObjectIsChild = draggedObjectId && 
  object.children.some(child => child.id === draggedObjectId);

const showDropzones = isDragMode && draggedObjectId && 
                     draggedObjectId !== object.id && 
                     !draggedObjectIsChild;

Implementation:

  • Prevent dropzones on the object being dragged
  • Prevent dropzones on parent of the dragged child object
  • Only show dropzones on valid target containers

6. Dropzone Layout Conflicts

Symptoms:

  • CENTER dropzone covers entire card instead of just image area
  • BOTTOM dropzone overlaps with child object icons
  • Confusing visual hierarchy and interaction areas

Root Cause: Dropzones were positioned to cover entire card areas without considering component layout.

Solution: Optimized dropzone positioning:

// CENTER zone limited to image area only
<div className="relative w-full flex justify-center">
  <ObjectImage />
  {showDropzones && (
    <div className="absolute inset-0 z-10 rounded-lg"> {/* Only covers image */}
      CENTER dropzone
    </div>
  )}
</div>

// BOTTOM zone as distinct bar below image
<div 
  className="absolute left-0 right-0 h-4 z-10"
  style= // Positioned after image height
>
  BOTTOM dropzone bar
</div>

Layout Hierarchy:

  1. Image area (168×112px) with CENTER dropzone overlay
  2. BOTTOM bar (4px high) positioned immediately below image
  3. Child icons area positioned below BOTTOM bar with clear separation
  4. LEFT/RIGHT/TOP zones remain on card edges

Prevention:

  • Use precise positioning instead of full-card overlays
  • Separate dropzone positioning from content layout
  • Test dropzone interactions with actual content placement

5. Drop Events Not Firing

Symptoms:

  • Can drag objects but drops are ignored
  • No error messages or feedback
  • Objects return to original position

Root Cause: Missing preventDefault() in dragover handlers or ghost element intercepting events.

Solution: Ensure proper event handling:

const handleDragOver = (e: React.DragEvent, zone: DropZoneType) => {
  e.preventDefault(); // Required for drop to work
  e.dataTransfer.dropEffect = 'move';
  setActiveDropZone(zone);
};

// Ghost element must not intercept events
ghost.style.pointerEvents = 'none';

Requirements:

  • Always call preventDefault() in dragover handlers
  • Set ghost element to pointerEvents: 'none'
  • Ensure drop zones are properly positioned above other content

Browser-Specific Issues

Safari Limitations

Issue: Safari has stricter requirements for setDragImage() Solution: Use actual <img> elements instead of div elements for ghost images

if (isSafari && object.mainImageUrl) {
  const img = new Image();
  img.src = object.mainImageUrl;
  img.width = 48;
  img.height = 48;
  document.body.appendChild(img);
  e.dataTransfer.setDragImage(img, 24, 24);
}

Mobile/Touch Devices

Issue: Limited support for HTML5 drag-and-drop on touch devices Considerations:

  • Implement touch event handlers as fallback
  • Use longer touch-and-hold delays
  • Provide alternative interaction methods for critical functionality

Performance Considerations

Ghost Element Management

Best Practices:

  • Always clean up ghost elements to prevent memory leaks
  • Use requestAnimationFrame for smooth ghost movement
  • Limit ghost element complexity to maintain 60fps
// Smooth ghost updates
const updateGhostPosition = (e: MouseEvent) => {
  requestAnimationFrame(() => {
    if (ghostRef.current) {
      ghostRef.current.style.left = `${e.clientX - 32}px`;
      ghostRef.current.style.top = `${e.clientY - 32}px`;
    }
  });
};

Event Listener Cleanup

Prevention:

  • Always remove event listeners when components unmount
  • Use useEffect cleanup functions
  • Store handler references for proper cleanup
useEffect(() => {
  return () => {
    // Cleanup on unmount
    if (mouseMoveHandlerRef.current) {
      document.removeEventListener('mousemove', mouseMoveHandlerRef.current);
    }
    if (dragGhostRef.current && document.body.contains(dragGhostRef.current)) {
      document.body.removeChild(dragGhostRef.current);
    }
  };
}, []);

Debugging Techniques

Console Logging

Enable detailed logging to trace drag operations:

// Log drag lifecycle
console.log('🎯 Drag started:', objectName);
console.log('🎯 Ghost created:', ghostElement);
console.log('🎯 Drop fired:', targetZone);
console.log('🧹 Cleanup completed');

Visual Debugging

Add temporary visual indicators:

// Highlight dropzones for debugging
const debugStyle = process.env.NODE_ENV === 'development' 
  ? 'border: 2px solid red !important;' 
  : '';

Browser DevTools

Check for:

  • Orphaned DOM elements (ghost elements not cleaned up)
  • Event listeners not removed (Memory tab)
  • Console errors during drag operations
  • Network requests during optimistic updates

Testing Recommendations

Manual Testing Checklist

  • Can drag child icons from container cards
  • Ghost appears and follows mouse cursor
  • Drop zones appear on valid targets only
  • Drops create correct relationships
  • Ghost disappears after successful drop
  • Ghost disappears when drag is cancelled
  • Works across different container types
  • Error handling for invalid drops

Automated Testing

Puppeteer Test Example:

// Test drag ghost visibility
await page.evaluate(() => {
  const icon = document.querySelector('[draggable="true"]');
  const ghost = document.querySelector('[id^="drag-ghost-"]');
  
  // Simulate drag start
  icon.dispatchEvent(new DragEvent('dragstart'));
  
  // Check ghost exists and is visible
  return ghost && ghost.style.opacity === '0.9';
});

Recovery Procedures

Stuck Drag State

If the UI gets stuck in drag mode:

  1. User Action: Click anywhere outside draggable objects
  2. Developer: Call cleanup functions manually in console
  3. Automatic: Global mouseup handler should resolve within 100ms

Ghost Element Cleanup

Remove orphaned ghost elements:

// Manual cleanup in browser console
document.querySelectorAll('[id^="drag-ghost-"]').forEach(el => el.remove());

Reset Apollo Cache

If optimistic updates cause state issues:

// Reset specific object cache
apolloClient.cache.evict({ id: 'ObjectInstance:' + objectId });
apolloClient.cache.gc();

// Or full refetch
await refetch();

Architecture Insights

Why Manual Ghost Tracking?

Problem: Browser setDragImage() is unreliable across different browsers and scenarios Solution: Manual ghost tracking provides:

  • Consistent visual feedback across all browsers
  • Full control over ghost appearance and behavior
  • Better performance and debugging capabilities

Event Propagation Strategy

Key Principle: Stop propagation at child level, allow it at parent level

  • Child icons: stopPropagation() to prevent parent drag
  • Parent cards: Allow events to bubble for dropzone detection
  • Container elements: preventDefault() in dragover for drop enabling

State Management

Separation of Concerns:

  • React state for UI state (isDragging, activeDropZone)
  • Apollo cache for data state (object relationships)
  • DOM state for ghost positioning (performance)

Last Updated: 2025-07-08 - Added comprehensive troubleshooting for drag-and-drop functionality and dropzone layout optimization