Tree UI component for React with selection, checkboxes, drag-drop, and virtual scrolling features
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
RC Tree provides built-in drag and drop functionality with customizable drop validation, drag constraints, and comprehensive event handling for tree reorganization.
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;
}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:
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
/>
);
};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
/>
);
};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}
/>
);
};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
/>
);
};/**
* 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;
}/**
* 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;
}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
/>
);
};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