CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-rc-tree

Tree UI component for React with selection, checkboxes, drag-drop, and virtual scrolling features

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

drag-drop.mddocs/

Drag & Drop

RC Tree provides built-in drag and drop functionality with customizable drop validation, drag constraints, and comprehensive event handling for tree reorganization.

Capabilities

Drag & Drop Configuration

Enable and configure drag-and-drop functionality with flexible constraints and validation.

/**
 * Drag and drop configuration options
 */
interface DragDropConfig<TreeDataType extends BasicDataNode = DataNode> {
  /** Enable drag and drop functionality */
  draggable?: DraggableFn | boolean | DraggableConfig;
  /** Function to validate drop operations */
  allowDrop?: AllowDrop<TreeDataType>;
  /** Custom drop indicator renderer */
  dropIndicatorRender?: (props: DropIndicatorProps) => React.ReactNode;
  /** Layout direction for drag logic */
  direction?: Direction;
}

/**
 * Function to determine if a node can be dragged
 */
type DraggableFn = (node: DataNode) => boolean;

/**
 * Advanced drag configuration object
 */
type DraggableConfig = {
  /** Custom drag icon (false to hide) */
  icon?: React.ReactNode | false;
  /** Function to determine per-node draggability */
  nodeDraggable?: DraggableFn;
};

/**
 * Function to validate drop operations
 */
type AllowDrop<TreeDataType extends BasicDataNode = DataNode> = (
  options: AllowDropOptions<TreeDataType>,
) => boolean;

/**
 * Information provided to drop validation function
 */
interface AllowDropOptions<TreeDataType extends BasicDataNode = DataNode> {
  /** The node being dragged */
  dragNode: TreeDataType;
  /** The node being dropped onto */
  dropNode: TreeDataType;
  /** Drop position relative to dropNode (-1: before, 0: inside, 1: after) */
  dropPosition: -1 | 0 | 1;
}

Drag & Drop Events

Comprehensive event handling for all phases of drag and drop operations.

/**
 * Drag and drop event handlers
 */
interface DragDropEvents<TreeDataType extends BasicDataNode = DataNode> {
  /** Fired when drag operation starts */
  onDragStart?: (info: NodeDragEventParams<TreeDataType>) => void;
  /** Fired when dragged item enters a drop target */
  onDragEnter?: (info: NodeDragEventParams<TreeDataType> & { expandedKeys: Key[] }) => void;
  /** Fired continuously while dragging over a drop target */
  onDragOver?: (info: NodeDragEventParams<TreeDataType>) => void;
  /** Fired when dragged item leaves a drop target */
  onDragLeave?: (info: NodeDragEventParams<TreeDataType>) => void;
  /** Fired when drag operation ends (regardless of success) */
  onDragEnd?: (info: NodeDragEventParams<TreeDataType>) => void;
  /** Fired when drop operation completes successfully */
  onDrop?: (
    info: NodeDragEventParams<TreeDataType> & {
      dragNode: EventDataNode<TreeDataType>;
      dragNodesKeys: Key[];
      dropPosition: number;
      dropToGap: boolean;
    },
  ) => void;
}

/**
 * Event parameters for drag operations
 */
type NodeDragEventParams<
  TreeDataType extends BasicDataNode = DataNode,
  T = HTMLDivElement,
> = {
  event: React.DragEvent<T>;
  node: EventDataNode<TreeDataType>;
};

/**
 * Properties for custom drop indicator rendering
 */
interface DropIndicatorProps {
  dropPosition: -1 | 0 | 1;
  dropLevelOffset: number;
  indent: number;
  prefixCls: string;
  direction: Direction;
}

Usage Examples:

Basic Drag & Drop

import React, { useState } from "react";
import Tree from "rc-tree";

const BasicDragDrop = () => {
  const [treeData, setTreeData] = useState([
    {
      key: '0-0',
      title: 'Parent Node',
      children: [
        { key: '0-0-0', title: 'Child 1' },
        { key: '0-0-1', title: 'Child 2' },
        { key: '0-0-2', title: 'Child 3' },
      ],
    },
    {
      key: '0-1',
      title: 'Another Parent',
      children: [
        { key: '0-1-0', title: 'Another Child' },
      ],
    },
  ]);

  const onDrop = (info: any) => {
    console.log('Drop info:', info);
    
    const { dragNode, node, dropPosition, dropToGap } = info;
    const dragKey = dragNode.key;
    const dropKey = node.key;

    // Implement your tree reorganization logic here
    console.log(`Moving ${dragKey} to ${dropKey} at position ${dropPosition}`);
    
    // This is a simplified example - real implementation would
    // need to properly update the tree data structure
  };

  return (
    <Tree
      prefixCls="rc-tree"
      draggable
      treeData={treeData}
      onDrop={onDrop}
      defaultExpandAll
    />
  );
};

Advanced Drag & Drop with Validation

import React, { useState } from "react";
import Tree from "rc-tree";

interface FileNode {
  key: string;
  title: string;
  type: 'file' | 'folder';
  children?: FileNode[];
}

const AdvancedDragDrop = () => {
  const [treeData, setTreeData] = useState<FileNode[]>([
    {
      key: 'folder-1',
      title: 'Documents',
      type: 'folder',
      children: [
        { key: 'file-1', title: 'document.pdf', type: 'file' },
        { key: 'file-2', title: 'image.jpg', type: 'file' },
      ],
    },
    {
      key: 'folder-2',
      title: 'Projects',
      type: 'folder',
      children: [
        { key: 'file-3', title: 'project.zip', type: 'file' },
      ],
    },
    { key: 'file-4', title: 'readme.txt', type: 'file' },
  ]);

  // Validate drop operations
  const allowDrop = ({ dragNode, dropNode, dropPosition }: any) => {
    // Don't allow dropping files into files
    if (dropNode.type === 'file' && dropPosition === 0) {
      return false;
    }
    
    // Don't allow dropping a folder into its own descendants
    if (dragNode.type === 'folder' && dropNode.key.startsWith(dragNode.key)) {
      return false;
    }
    
    return true;
  };

  // Control which nodes can be dragged
  const nodeDraggable = (node: FileNode) => {
    // Example: Don't allow dragging system files
    return !node.title.startsWith('system');
  };

  const onDrop = (info: any) => {
    const { dragNode, node, dropPosition, dropToGap } = info;
    
    console.log('Valid drop:', {
      dragNode: dragNode.key,
      dropNode: node.key,
      dropPosition,
      dropToGap,
    });

    // Implement tree data update logic here
    // This would involve removing the dragNode from its current position
    // and inserting it at the new position
  };

  return (
    <Tree
      prefixCls="rc-tree"
      draggable={{
        icon: <span>🔄</span>, // Custom drag icon
        nodeDraggable,
      }}
      allowDrop={allowDrop}
      treeData={treeData}
      onDrop={onDrop}
      titleRender={(node) => (
        <span>
          {node.type === 'folder' ? '📁' : '📄'} {node.title}
        </span>
      )}
      defaultExpandAll
    />
  );
};

Drag & Drop with State Management

import React, { useState } from "react";
import Tree from "rc-tree";

const StatefulDragDrop = () => {
  const [treeData, setTreeData] = useState([
    {
      key: '0-0',
      title: 'Root',
      children: [
        { key: '0-0-0', title: 'Item 1' },
        { key: '0-0-1', title: 'Item 2' },
        { key: '0-0-2', title: 'Item 3' },
      ],
    },
  ]);
  const [expandedKeys, setExpandedKeys] = useState<string[]>(['0-0']);

  // Utility function to find and remove a node
  const removeNode = (data: any[], key: string): [any[], any | null] => {
    for (let i = 0; i < data.length; i++) {
      const item = data[i];
      if (item.key === key) {
        return [data.filter((_, index) => index !== i), item];
      }
      if (item.children) {
        const [updatedChildren, removedNode] = removeNode(item.children, key);
        if (removedNode) {
          return [
            data.map((node, index) => 
              index === i ? { ...node, children: updatedChildren } : node
            ),
            removedNode
          ];
        }
      }
    }
    return [data, null];
  };

  // Utility function to insert a node at a specific position
  const insertNode = (data: any[], node: any, dropKey: string, dropPosition: number): any[] => {
    return data.map(item => {
      if (item.key === dropKey) {
        if (dropPosition === 0) {
          // Insert as child
          return {
            ...item,
            children: [...(item.children || []), node],
          };
        }
      }
      if (item.children) {
        return {
          ...item,
          children: insertNode(item.children, node, dropKey, dropPosition),
        };
      }
      return item;
    });
  };

  const onDrop = (info: any) => {
    const { dragNode, node, dropPosition } = info;
    const dragKey = dragNode.key;
    const dropKey = node.key;

    // Remove the dragged node
    const [updatedData, draggedNode] = removeNode([...treeData], dragKey);
    
    if (draggedNode) {
      // Insert at new position
      const finalData = insertNode(updatedData, draggedNode, dropKey, dropPosition);
      setTreeData(finalData);
      
      // Auto-expand the drop target if dropping inside
      if (dropPosition === 0 && !expandedKeys.includes(dropKey)) {
        setExpandedKeys([...expandedKeys, dropKey]);
      }
    }
  };

  const onDragEnter = (info: any) => {
    console.log('Drag enter:', info);
    // Auto-expand folders when dragging over them
    const { node } = info;
    if (node.children && !expandedKeys.includes(node.key)) {
      setExpandedKeys([...expandedKeys, node.key]);
    }
  };

  return (
    <Tree
      prefixCls="rc-tree"
      draggable
      treeData={treeData}
      expandedKeys={expandedKeys}
      onExpand={setExpandedKeys}
      onDrop={onDrop}
      onDragEnter={onDragEnter}
    />
  );
};

Custom Drop Indicator

import React, { useState } from "react";
import Tree from "rc-tree";

const CustomDropIndicator = () => {
  const [treeData, setTreeData] = useState([
    {
      key: '0-0',
      title: 'Folder 1',
      children: [
        { key: '0-0-0', title: 'File 1' },
        { key: '0-0-1', title: 'File 2' },
      ],
    },
    {
      key: '0-1',
      title: 'Folder 2',
      children: [],
    },
  ]);

  const dropIndicatorRender = (props: any) => {
    const { dropPosition, dropLevelOffset, prefixCls } = props;
    
    const style: React.CSSProperties = {
      position: 'absolute',
      right: 0,
      left: dropLevelOffset,
      height: 2,
      backgroundColor: '#1890ff',
      zIndex: 1,
      pointerEvents: 'none',
    };

    let className = `${prefixCls}-drop-indicator`;
    
    if (dropPosition === -1) {
      style.top = -1;
      className += ' drop-before';
    } else if (dropPosition === 1) {
      style.bottom = -1;
      className += ' drop-after';
    } else {
      // dropPosition === 0 (inside)
      style.top = -1;
      style.backgroundColor = '#52c41a';
      className += ' drop-inside';
    }

    return <div className={className} style={style} />;
  };

  return (
    <Tree
      prefixCls="rc-tree"
      draggable
      treeData={treeData}
      dropIndicatorRender={dropIndicatorRender}
      onDrop={(info) => console.log('Drop with custom indicator:', info)}
      defaultExpandAll
    />
  );
};

Drag & Drop Event Details

Event Sequence

/**
 * Typical drag and drop event sequence:
 * 1. onDragStart - User begins dragging a node
 * 2. onDragEnter - Dragged item enters a potential drop target
 * 3. onDragOver - Continuously fired while over drop target
 * 4. onDragLeave - Dragged item leaves the drop target
 * 5. onDrop - Drop operation completes (if valid)
 * 6. onDragEnd - Drag operation ends (always fired)
 */

/**
 * Enhanced drop event information
 */
interface DropEventInfo<TreeDataType extends BasicDataNode = DataNode> {
  /** Native drag event */
  event: React.DragEvent<HTMLDivElement>;
  /** The node being dropped onto */
  node: EventDataNode<TreeDataType>;
  /** The node being dragged */
  dragNode: EventDataNode<TreeDataType>;
  /** Keys of all nodes being dragged (for multi-select drag) */
  dragNodesKeys: Key[];
  /** Drop position (-1: before, 0: inside, 1: after) */
  dropPosition: number;
  /** Whether dropping into a gap between nodes */
  dropToGap: boolean;
}

Drag State Management

/**
 * Internal drag state (available in tree context)
 */
interface DragState {
  /** Key of the node currently being dragged */
  draggingNodeKey?: Key;
  /** Key of the node being dragged over */
  dragOverNodeKey: Key | null;
  /** Current drop container key */
  dropContainerKey: Key | null;
  /** Current drop target key */
  dropTargetKey: Key | null;
  /** Current drop position */
  dropPosition: -1 | 0 | 1 | null;
  /** Drop level offset for visual feedback */
  dropLevelOffset?: number;
}

Advanced Drag & Drop Patterns

Multi-Node Drag

import React, { useState } from "react";
import Tree from "rc-tree";

const MultiNodeDrag = () => {
  const [treeData, setTreeData] = useState([
    {
      key: '0-0',
      title: 'Source Folder',
      children: [
        { key: '0-0-0', title: 'File 1' },
        { key: '0-0-1', title: 'File 2' },
        { key: '0-0-2', title: 'File 3' },
      ],
    },
    {
      key: '0-1',
      title: 'Target Folder',
      children: [],
    },
  ]);
  const [selectedKeys, setSelectedKeys] = useState<string[]>([]);

  const onDrop = (info: any) => {
    const { dragNodesKeys, node, dropPosition } = info;
    
    console.log('Multi-node drop:', {
      draggedNodes: dragNodesKeys,
      dropTarget: node.key,
      position: dropPosition,
    });

    // Handle multiple node movement
    // Implementation would move all selected nodes
  };

  return (
    <Tree
      prefixCls="rc-tree"
      draggable
      selectable
      multiple
      treeData={treeData}
      selectedKeys={selectedKeys}
      onSelect={setSelectedKeys}
      onDrop={onDrop}
      defaultExpandAll
    />
  );
};

Conditional Drag & Drop

import React, { useState } from "react";
import Tree from "rc-tree";

const ConditionalDragDrop = () => {
  const [treeData, setTreeData] = useState([
    {
      key: 'readonly-folder',
      title: '🔒 Read-only Folder',
      type: 'readonly',
      children: [
        { key: 'readonly-file', title: 'Protected File', type: 'readonly' },
      ],
    },
    {
      key: 'editable-folder',
      title: '📝 Editable Folder',
      type: 'editable',
      children: [
        { key: 'editable-file', title: 'Normal File', type: 'editable' },
      ],
    },
  ]);

  const nodeDraggable = (node: any) => {
    // Only allow dragging editable items
    return node.type === 'editable';
  };

  const allowDrop = ({ dragNode, dropNode, dropPosition }: any) => {
    // Can't drop into readonly containers
    if (dropNode.type === 'readonly' && dropPosition === 0) {
      return false;
    }
    
    // Can't drop readonly items
    if (dragNode.type === 'readonly') {
      return false;
    }
    
    return true;
  };

  return (
    <Tree
      prefixCls="rc-tree"
      draggable={{ nodeDraggable }}
      allowDrop={allowDrop}
      treeData={treeData}
      onDrop={(info) => console.log('Conditional drop:', info)}
      defaultExpandAll
    />
  );
};

Install with Tessl CLI

npx tessl i tessl/npm-rc-tree

docs

async-loading.md

data-management.md

drag-drop.md

index.md

selection-checking.md

tree-component.md

tree-node.md

virtual-scrolling.md

tile.json