react-native-tree-multi-select
Version:
Super-fast, customizable tree view component for React Native with drag-and-drop reordering, multi-selection, and search filtering.
441 lines (388 loc) • 19.1 kB
text/typescript
import { ComponentType, RefObject } from "react";
import type {
GestureResponderEvent,
StyleProp,
TextProps,
TouchableOpacityProps,
ViewStyle
} from "react-native";
import type { FlashListProps } from "@shopify/flash-list";
import type {
ScrollToNodeHandlerRef,
ScrollToNodeParams
} from "../hooks/useScrollToNode";
import type {
CheckboxProps as _CheckboxProps
} from "@futurejj/react-native-checkbox";
import type { DragCancelEvent, DragEndEvent, DragStartEvent, DropPosition, MoveResult } from "./dragDrop.types";
/** The tri-state value of a checkbox: checked, unchecked, or indeterminate */
export type CheckboxValueType = boolean | "indeterminate";
/** Props passed to a custom expand/collapse icon component */
export interface ExpandIconProps {
/** Whether the node is currently expanded */
isExpanded: boolean;
}
/** A single node in the tree data structure */
export interface TreeNode<ID = string> {
/** Unique identifier for this node */
id: ID;
/** Display name for this node */
name: string;
/** Child nodes (omit or empty array for leaf nodes) */
children?: TreeNode<ID>[];
/** Additional custom data fields */
[key: string]: any;
}
/** Internal flattened representation of a tree node with its nesting level */
export interface __FlattenedTreeNode__<ID = string> extends TreeNode<ID> {
/** Nesting depth of this node (0 = root level) */
level?: number;
}
/** FlashList props available for customization (excludes `data` and `renderItem`) */
export type TreeFlatListProps<ItemT = any> = Omit<
FlashListProps<ItemT>,
"data"
| "renderItem"
>;
/** Props passed to a custom node row component */
export interface NodeRowProps<ID = string> {
/** The tree node to render */
node: TreeNode<ID>;
/** Nesting depth of this node (0 = root level) */
level: number;
/** Current checkbox state of this node */
checkedValue: CheckboxValueType;
/** Whether this node's children are currently visible */
isExpanded: boolean;
/** Callback to toggle this node's checked state */
onCheck: () => void;
/** Callback to toggle this node's expanded/collapsed state */
onExpand: () => void;
/** Whether this node is an invalid drop target during a drag operation */
isInvalidDropTarget?: boolean;
/** Whether this node is the current valid drop target */
isDropTarget?: boolean;
/** The drop position if this node is the current drop target */
dropPosition?: DropPosition;
/** Whether a drag operation is currently in progress */
isDragging?: boolean;
/** Whether this node is the one being dragged */
isDraggedNode?: boolean;
/** Props to spread on a drag handle element. Attach to a specific View to
* make only that area initiate drag, or spread on the root for whole-row drag.
* Only present when drag-and-drop is enabled. */
dragHandleProps?: DragHandleProps;
}
/** Touch handlers to spread on a drag handle element within a custom node row */
export interface DragHandleProps {
/** Arms the long-press timer that initiates a drag. Fires on finger down. */
onTouchStart: (e: GestureResponderEvent) => void;
/** Cancels a pending long-press, or ends a drag the PanResponder never
* captured (finger lifted without movement). Fires on finger up. */
onTouchEnd: () => void;
/** Cancels a pending long-press when the system interrupts the touch. */
onTouchCancel: () => void;
}
/** Customization options for tree item appearance and behavior */
export interface TreeItemCustomizations<ID = string> {
/** Style props for the built-in checkbox view */
checkBoxViewStyleProps?: BuiltInCheckBoxViewStyleProps;
/** Pixels of indentation per nesting level. Default: 15 */
indentationMultiplier?: number;
/** Custom checkbox component replacing the built-in checkbox */
CheckboxComponent?: ComponentType<CheckBoxViewProps>;
/** Custom expand/collapse icon component */
ExpandCollapseIconComponent?: ComponentType<ExpandIconProps>;
/** Custom touchable component wrapping the expand/collapse icon */
ExpandCollapseTouchableComponent?: ComponentType<TouchableOpacityProps>;
/** Fully custom node row component replacing the entire built-in row */
CustomNodeRowComponent?: ComponentType<NodeRowProps<ID>>;
}
/** Internal props for a single node in the list (extends TreeItemCustomizations) */
export interface NodeProps<ID = string> extends TreeItemCustomizations<ID> {
/** The flattened tree node data */
node: __FlattenedTreeNode__<ID>;
/** Nesting depth of this node */
level: number;
/** Internal store identifier */
storeId: string;
/** Index of this node in the flattened list */
nodeIndex?: number;
/** Whether drag-and-drop is enabled */
dragEnabled?: boolean;
/** Whether a drag operation is currently in progress */
isDragging?: boolean;
/** Callback when a touch starts on this node (used for drag initiation) */
onNodeTouchStart?: (
nodeId: ID,
pageY: number,
locationY: number,
nodeIndex: number
) => void;
/** Callback when a touch ends on this node */
onNodeTouchEnd?: () => void;
/** Callback reporting this node's measured height, keyed by its stable node id
* (used for accurate variable-height drop targeting) */
onItemLayout?: (id: ID, height: number) => void;
/** Customizations for drag-and-drop visuals */
dragDropCustomizations?: DragDropCustomizations<ID>;
}
/** Options for auto-scrolling to the dropped node after a successful drop.
* Uses the same scroll parameters as `scrollToNodeID` (minus internally managed fields). */
export interface DropAutoScrollOptions extends Omit<ScrollToNodeParams<never>, "nodeId" | "expandScrolledNode"> {
/** Enable auto-scroll to the dropped node. Default: true */
enabled?: boolean;
}
/** Configuration options for drag-and-drop reordering */
export interface DragAndDropOptions<ID = string> {
/** Enable drag-and-drop reordering. Default: true (when dragAndDrop is provided) */
enabled?: boolean;
/** Callback fired when a drag operation begins */
onDragStart?: (event: DragStartEvent<ID>) => void;
/** Callback fired after a node is successfully dropped at a new position */
onDragEnd?: (event: DragEndEvent<ID>) => void;
/** Callback fired when a drag is cancelled without a successful drop */
onDragCancel?: (event: DragCancelEvent<ID>) => void;
/** Long press duration in ms to start drag. Default: 400 */
longPressDuration?: number;
/** Distance from edge (px) to trigger auto-scroll during drag. Default: 60 */
autoScrollThreshold?: number;
/** Speed multiplier for auto-scroll during drag. Default: 1.0 */
autoScrollSpeed?: number;
/** Offset of the dragged overlay from the finger, in item-height units. Default: -2 (two items above finger) */
dragOverlayOffset?: number;
/** Advanced: extra vertical correction for the drag overlay, in item-height units,
* added on top of `dragOverlayOffset`. Compensates for Android reporting touch
* `locationY` differently from iOS. Default: -2 on Android, 0 on other platforms.
* Override only if the overlay sits noticeably off from the finger on a device. */
overlayYCorrection?: number;
/** Delay in ms before auto-expanding a collapsed node during drag hover. Default: 800 */
autoExpandDelay?: number;
/** Auto-expand a collapsed node while hovering "inside" it during a drag.
* Default: true. Set false to disable hover-to-expand entirely. */
autoExpand?: boolean;
/** Animate the drag overlay with a magnetic "snap" spring when the effective drop
* level changes. Default: true. Set false to keep the overlay tracking the level
* without the spring animation (e.g. for reduced-motion preferences). */
magneticSnap?: boolean;
/** Customizations for drag-and-drop visuals (overlay, indicator, opacity) */
customizations?: DragDropCustomizations<ID>;
/** Callback to determine if a node can be dropped on a specific target.
* Return false to grey out the target and suppress the drop indicator. */
canDrop?: (draggedNode: TreeNode<ID>, targetNode: TreeNode<ID>, position: DropPosition) => boolean;
/** Maximum nesting depth allowed. Drops that would exceed this depth are suppressed. */
maxDepth?: number;
/** Callback to determine if a node can accept children.
* Return false to suppress the "inside" drop zone for that node. */
canNodeHaveChildren?: (node: TreeNode<ID>) => boolean;
/** Callback to determine if a node can be dragged.
* Return false to prevent dragging this node. Default: all nodes are draggable. */
canDrag?: (node: TreeNode<ID>) => boolean;
/** Auto-scroll to the dropped node after a successful drop, if it ended up
* outside the viewport (no scroll when the node is already visible).
* Pass `false` to disable, `true` for defaults, or an object to customize.
* Default: `{ enabled: true, animated: true, viewPosition: 0.5 }` */
autoScrollToDroppedNode?: boolean | DropAutoScrollOptions;
}
/** Props for the NodeList component that renders the flattened tree */
export interface NodeListProps<ID = string> extends TreeItemCustomizations<ID> {
/** Additional props passed to the underlying FlashList */
treeFlashListProps?: TreeFlatListProps;
/** Ref for programmatic scroll-to-node functionality */
scrollToNodeHandlerRef: RefObject<ScrollToNodeHandlerRef<ID>>;
/** Node ID to scroll to on initial render */
initialScrollNodeID?: ID;
/** Internal store identifier */
storeId: string;
/** Drag-and-drop configuration */
dragAndDrop?: DragAndDropOptions<ID>;
}
/** Props for the TreeView component */
export interface TreeViewProps<ID = string> extends Omit<
NodeListProps<ID>, "storeId" | "scrollToNodeHandlerRef"
> {
/** The tree data to render */
data: TreeNode<ID>[];
/** Callback fired when checked nodes change. Receives checked and indeterminate node IDs. */
onCheck?: (checkedIds: ID[], indeterminateIds: ID[]) => void;
/** Callback fired when expanded nodes change. Receives all currently expanded node IDs. */
onExpand?: (expandedIds: ID[]) => void;
/** Node IDs that should be checked on initial render */
preselectedIds?: ID[];
/** Node IDs that should be expanded on initial render */
preExpandedIds?: ID[];
/** Controls whether checking a node propagates to its children and/or parents */
selectionPropagation?: SelectionPropagation;
/** Drag-and-drop configuration */
dragAndDrop?: DragAndDropOptions<ID>;
}
type CheckboxProps = Omit<_CheckboxProps, "onPress" | "status">;
/** Props for the checkbox view component */
export interface CheckBoxViewProps {
/** Current checkbox state */
value: CheckboxValueType;
/** Callback when the checkbox value changes */
onValueChange: (value: boolean) => void;
/** Label text displayed next to the checkbox */
text: string;
/** Test ID for testing frameworks */
testID?: string;
}
/** Style props for customizing the built-in checkbox view */
export interface BuiltInCheckBoxViewStyleProps {
/** Style for the outermost container wrapping the checkbox and text */
outermostParentViewStyle?: StyleProp<ViewStyle>;
/** Style for the view wrapping the checkbox itself */
checkboxParentViewStyle?: StyleProp<ViewStyle>;
/** Style for the touchable area wrapping the label text */
textTouchableStyle?: StyleProp<ViewStyle>;
/** Additional props passed to the underlying Checkbox component */
checkboxProps?: CheckboxProps;
/** Props passed to the label Text component */
textProps?: TextProps;
}
/** Combined props for the built-in checkbox view (CheckBoxViewProps + style props) */
export type BuiltInCheckBoxViewProps =
CheckBoxViewProps
& BuiltInCheckBoxViewStyleProps;
/** Ref handle exposed by the TreeView component for imperative operations */
export interface TreeViewRef<ID = string> {
/** Select (check) all nodes in the tree */
selectAll: () => void;
/** Unselect (uncheck) all nodes in the tree */
unselectAll: () => void;
/** Select all nodes that match the current search filter */
selectAllFiltered: () => void;
/** Unselect all nodes that match the current search filter */
unselectAllFiltered: () => void;
/** Expand all nodes in the tree */
expandAll: () => void;
/** Collapse all nodes in the tree */
collapseAll: () => void;
/** Expand specific nodes by their IDs */
expandNodes: (ids: ID[]) => void;
/** Collapse specific nodes by their IDs */
collapseNodes: (ids: ID[]) => void;
/** Select (check) specific nodes by their IDs */
selectNodes: (ids: ID[]) => void;
/** Unselect (uncheck) specific nodes by their IDs */
unselectNodes: (ids: ID[]) => void;
/** Set the search text and optionally specify which node fields to search */
setSearchText: (searchText: string, searchKeys?: string[]) => void;
/** Programmatically scroll to a specific node by its ID */
scrollToNodeID: (scrollToNodeParams: ScrollToNodeParams<ID>) => void;
/** Get a map of child node IDs to their parent node IDs */
getChildToParentMap: () => Map<ID, ID>;
/** Get the current tree data held by the component (the original `data` prop
* before any move, or the reordered structure after a drag-and-drop / `moveNode`).
* Useful for reading the full tree without pushing it through the `onDragEnd` /
* `moveNode` move delta.
*
* NOTE: this returns a LIVE internal reference - treat it as read-only and do
* NOT mutate it in place. Mutating it desyncs the internal node maps and the
* reinit-skip diff. Clone it (e.g. with the exported `moveTreeNode`, or
* `structuredClone`) before modifying. */
getTreeData: () => TreeNode<ID>[];
/** Programmatically move a node to a new position in the tree.
* Works like a drag-and-drop but without user interaction.
* Returns a lightweight {@link MoveResult} describing the move, or `null` if it
* was a no-op / invalid (e.g. moving a node onto itself or into its own
* descendant, or - with `{ validate: true }` - blocked by `canDrop`,
* `maxDepth`, or `canNodeHaveChildren`).
*
* `{ validate: true }` enforces the `canDrop` / `maxDepth` / `canNodeHaveChildren`
* rules - but those rules live on the `dragAndDrop` prop, so validation only runs
* when a `dragAndDrop` prop is configured. Without it, `validate` is ignored (in
* dev a warning is logged) and the move proceeds subject only to the structural
* no-op checks.
*
* `{ scrollToNode: true }` scrolls the moved node into view after the move (the
* interactive drag does this automatically; programmatic moves do not by default).
* Pass a {@link DropAutoScrollOptions} object to customize the scroll. */
moveNode: (
nodeId: ID,
targetNodeId: ID,
position: DropPosition,
options?: {
validate?: boolean;
scrollToNode?: boolean | DropAutoScrollOptions;
}
) => MoveResult<ID> | null;
}
/** Controls how checkbox selection propagates through the tree hierarchy */
export interface SelectionPropagation {
/** Whether checking a parent node automatically checks all its children. Default: true */
toChildren?: boolean;
/** Whether checking all children automatically checks their parent. Default: true */
toParents?: boolean;
}
// --- Drag-and-drop customization types ---
/** Props for the drop indicator rendered on the target node during drag */
export interface DropIndicatorComponentProps {
/** Whether the indicator is above, below, or inside the target node */
position: DropPosition;
/** The nesting level of the target node (useful for indenting the indicator) */
level: number;
/** The indentation multiplier used for each level (pixels per level) */
indentationMultiplier: number;
}
/** Style props for customizing the built-in drop indicator appearance */
export interface DropIndicatorStyleProps {
/** Color of the line indicator (above/below). Default: "#0078FF" */
lineColor?: string;
/** Thickness of the line indicator. Default: 3 */
lineThickness?: number;
/** Diameter of the circle at the line's start. Default: 10 */
circleSize?: number;
/** Background color of the "inside" highlight. Default: "rgba(0, 120, 255, 0.15)" */
highlightColor?: string;
/** Border color of the "inside" highlight. Default: "rgba(0, 120, 255, 0.5)" */
highlightBorderColor?: string;
/** Border width of the "inside" highlight box. Default: 2 */
highlightBorderWidth?: number;
/** Corner radius of the "inside" highlight box. Default: 4 */
highlightBorderRadius?: number;
}
/** Style props for customizing the drag overlay (the "lifted" node ghost) */
export interface DragOverlayStyleProps {
/** Background color of the overlay. Default: "rgba(255, 255, 255, 0.95)" */
backgroundColor?: string;
/** Shadow color. Default: "#000" */
shadowColor?: string;
/** Shadow offset (iOS). Default: { width: 0, height: 2 } */
shadowOffset?: { width: number; height: number; };
/** Shadow opacity. Default: 0.25 */
shadowOpacity?: number;
/** Shadow radius. Default: 4 */
shadowRadius?: number;
/** Android elevation. Default: 10 */
elevation?: number;
/** Stacking order of the overlay. Default: 9999 */
zIndex?: number;
/** Custom style applied to the overlay container */
style?: StyleProp<ViewStyle>;
}
/** Combined drag-and-drop customization props */
export interface DragDropCustomizations<ID = string> {
/** Opacity applied to the node being dragged. Default: 0.3 */
draggedNodeOpacity?: number;
/** Opacity applied to invalid drop targets during drag. Default: 0.3 */
invalidTargetOpacity?: number;
/** Style props for the built-in drop indicator */
dropIndicatorStyleProps?: DropIndicatorStyleProps;
/** Style props for the drag overlay (lifted node ghost) */
dragOverlayStyleProps?: DragOverlayStyleProps;
/** Fully custom drop indicator component - replaces the built-in line/highlight */
CustomDropIndicatorComponent?: ComponentType<DropIndicatorComponentProps>;
/** Fully custom drag overlay component - replaces the built-in ghost node */
CustomDragOverlayComponent?: ComponentType<DragOverlayComponentProps<ID>>;
}
/** Props passed to a custom drag overlay component */
export interface DragOverlayComponentProps<ID = string> {
/** The node being dragged */
node: TreeNode<ID>;
/** The nesting level of the dragged node */
level: number;
/** The current checkbox value of the dragged node */
checkedValue: CheckboxValueType;
}