Drag and Drop Troubleshooting Guide
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
dragoverhandlers to parent card, card content, and child preview containers - Implemented boundary detection in
onDragEndto 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
mousemoveevent 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
mouseuplistener during drag - Use
setTimeoutto 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:
- Image area (168×112px) with CENTER dropzone overlay
- BOTTOM bar (4px high) positioned immediately below image
- Child icons area positioned below BOTTOM bar with clear separation
- 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()indragoverhandlers - 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
requestAnimationFramefor 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
useEffectcleanup 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:
- User Action: Click anywhere outside draggable objects
- Developer: Call cleanup functions manually in console
- 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)
Related Documentation
- Create/Edit Spatial Relationships Use Case - User-facing functionality
- Spatial Dashboard Requirements - Technical specifications
- Component Architecture - Overall system design
Last Updated: 2025-07-08 - Added comprehensive troubleshooting for drag-and-drop functionality and dropzone layout optimization