Skip to content

nosferatu500/react-sortable-tree

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

745 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

React Sortable Tree

NPM version NPM license NPM total downloads NPM monthly downloads PRs Welcome

Drag-and-drop sortable representation of hierarchical data for React 18/19 with virtualized rendering powered by virtua and react-dnd. Storybook demos cover both basic and advanced scenarios.

Getting started

Install the package together with its peer dependencies:

npm install @nosferatu500/react-sortable-tree react-dnd react-dnd-html5-backend
# or
yarn add @nosferatu500/react-sortable-tree react-dnd react-dnd-html5-backend

The bundle is ESM-only and includes all styles via runtime injection (no separate CSS file is required).

Quick start

import { useState } from 'react'
import { SortableTree, TreeItem } from '@nosferatu500/react-sortable-tree'

const initialData: TreeItem[] = [
  { title: 'Chicken', children: [{ title: 'Egg' }] },
  { title: 'Fish', children: [{ title: 'Fingerling' }] },
]

export function ExampleTree() {
  const [treeData, setTreeData] = useState(initialData)

  return (
    <div style={{ height: 400 }}>
      <SortableTree
        treeData={treeData}
        onChange={setTreeData}
      />
    </div>
  )
}

Already have a surrounding react-dnd context? Use the context-less export instead:

import { SortableTreeWithoutDndContext } from '@nosferatu500/react-sortable-tree'

Component props

All props are typed in ReactSortableTreeProps (see src/react-sortable-tree.tsx).

Required props

Prop Type Description
treeData TreeItem[] Array of tree nodes with { title?, subtitle?, expanded?, children?, ...custom }
onChange (treeData: TreeItem[]) => void Called on every tree data change

Appearance & layout

Prop Type Default Description
rowHeight number | ((treeIndex, node, path) => number) 62 Height of each row in pixels
rowDirection 'ltr' | 'rtl' 'ltr' Layout direction
scaffoldBlockPxWidth number 44 Width of indent per level
slideRegionSize number 100 Size of the drag slide region
style CSSProperties - Styles for the outer container
innerStyle CSSProperties - Styles for the virtual list
className string - Class name for the outer container

Theming & custom renderers

Prop Type Description
theme ThemeProps Theme object (see Theming section)
nodeContentRenderer ComponentType Custom component for node content
treeNodeRenderer ComponentType Custom component for the entire tree row
placeholderRenderer ComponentType Custom component for empty tree state

Drag & drop

Prop Type Default Description
canDrag boolean | ((params) => boolean) true Whether nodes can be dragged
canDrop (params) => boolean - Validate if a drop is allowed
canNodeHaveChildren (node) => boolean () => true Whether a node can have children
maxDepth number - Maximum nesting depth
shouldCopyOnOutsideDrop boolean | ((params) => boolean) false Copy node when dropped outside
dndType string - Custom drag type for multi-tree setups
onMoveNode (params) => void - Called after a node is moved
onDragStateChanged (params) => void - Called when drag state changes

Search

Prop Type Description
searchQuery string Search query string
searchMethod (params) => boolean Custom search matching function
searchFocusOffset number Index of the focused match
searchFinishCallback (matches) => void Called when search completes
onlyExpandSearchedNodes boolean Collapse non-matching paths

Other

Prop Type Description
generateNodeProps (params) => object Add custom props to each node
getNodeKey (node) => string | number Generate stable node keys
onVisibilityToggle (params) => void Called when node expands/collapses
loadCollapsedLazyChildren boolean Load lazy children before expanding
virtuaRef RefObject<VListHandle> Direct access to the virtual list
dragDropManager object External react-dnd manager

Theming

The component supports theming through CSS variables, the theme prop, and custom renderers.

CSS Variables

Override these CSS variables on the .rst__tree class or a parent element:

.my-custom-theme .rst__tree {
  --rst-row-height: 62px;
  --rst-block-width: 44px;
  --rst-handle-width: 44px;
  --rst-line-color: #000;
  --rst-line-highlight: #36c2f6;
  --rst-line-highlight-arrow: white;
  --rst-primary-color: #36c2f6;
  --rst-focus-color: #fc6421;
  --rst-match-color: #0080ff;
  --rst-bg-landing: lightblue;
  --rst-bg-cancel: #e6a8ad;
  --rst-text-color: #333;
  --rst-icon-color: #6DB3F2;
  --rst-button-bg: #fff;
  --rst-button-border: #989898;
}

Theme prop

The theme prop accepts an object with these properties:

type ThemeProps = {
  style?: React.CSSProperties
  innerStyle?: React.CSSProperties
  scaffoldBlockPxWidth?: number
  slideRegionSize?: number
  treeNodeRenderer?: React.ComponentType
  nodeContentRenderer?: React.ComponentType
  placeholderRenderer?: React.ComponentType
  dndType?: string
}

Theme values are merged with component props, with direct props taking precedence.

Example: File Explorer Theme

The library includes a File Explorer theme example in the Storybook demos:

import { SortableTree } from '@nosferatu500/react-sortable-tree'
import { fileExplorerTheme, FILE_EXPLORER_THEME_CLASS } from './themes/file-explorer'

function FileTree() {
  const [treeData, setTreeData] = useState([
    { title: 'src', isDirectory: true, expanded: true, children: [
      { title: 'index.ts' },
      { title: 'App.tsx' },
    ]},
    { title: 'package.json' },
  ])

  return (
    <div className={FILE_EXPLORER_THEME_CLASS}>
      <SortableTree
        treeData={treeData}
        onChange={setTreeData}
        theme={fileExplorerTheme}
        rowHeight={28}
        // Only folders can have children
        canNodeHaveChildren={(node) => node.isDirectory === true}
        // Only allow dropping into folders
        canDrop={({ nextParent }) =>
          !nextParent || nextParent.isDirectory === true
        }
      />
    </div>
  )
}

For dark mode, add the rst__file-explorer-dark class to the wrapper.

Creating custom themes

To create a custom theme:

  1. Create a custom nodeContentRenderer component (see src/node-renderer-default.tsx for reference)
  2. Add CSS styles with your theme class
  3. Export a theme object:
export const myTheme = {
  nodeContentRenderer: MyCustomNodeRenderer,
  scaffoldBlockPxWidth: 24,
  slideRegionSize: 50,
}

Data helper functions

Utilities exported from the package:

Node manipulation

  • addNodeUnderParent({ treeData, newNode, parentKey, getNodeKey, expandParent?, addAsFirstChild? }) - Add a node under a parent
  • insertNode({ treeData, newNode, depth, minimumTreeIndex, getNodeKey, expandParent? }) - Insert a node at a specific position
  • removeNode({ treeData, path, getNodeKey }) - Remove a node by path
  • removeNodeAtPath({ treeData, path, getNodeKey }) - Remove a node at exact path
  • changeNodeAtPath({ treeData, path, newNode, getNodeKey }) - Update a node at path

Tree inspection

  • getNodeAtPath({ treeData, path, getNodeKey }) - Get node at path
  • getDescendantCount({ node }) - Count all descendants
  • getDepth(node) - Get nesting depth of a node
  • isDescendant(older, younger) - Check parent-child relationship
  • getVisibleNodeCount({ treeData }) - Count visible (expanded) nodes

Tree traversal

  • walk({ treeData, getNodeKey, callback, ignoreCollapsed? }) - Walk tree depth-first
  • map({ treeData, getNodeKey, callback, ignoreCollapsed? }) - Transform all nodes
  • toggleExpandedForAll({ treeData, expanded }) - Expand or collapse all nodes
  • find({ treeData, getNodeKey, searchQuery, searchMethod, expandAllMatchPaths? }) - Search with path expansion

Data conversion

  • getFlatDataFromTree({ treeData, getNodeKey, ignoreCollapsed? }) - Convert to flat array
  • getTreeFromFlatData({ flatData, getKey, getParentKey, rootKey? }) - Convert from flat array

Default handlers

  • defaultGetNodeKey({ treeIndex }) - Default key generator (uses index)
  • defaultSearchMethod({ node, searchQuery }) - Default search (matches title)

License

MIT

About

Drag-and-drop sortable component for nested data and hierarchies

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • TypeScript 85.7%
  • CSS 10.5%
  • JavaScript 3.8%