react-native-tree-multi-select 3.0.0-beta.3 → 3.0.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +100 -30
  2. package/lib/module/TreeView.js +36 -31
  3. package/lib/module/TreeView.js.map +1 -1
  4. package/lib/module/components/CheckboxView.js +8 -4
  5. package/lib/module/components/CheckboxView.js.map +1 -1
  6. package/lib/module/components/CustomExpandCollapseIcon.js +2 -2
  7. package/lib/module/components/CustomExpandCollapseIcon.js.map +1 -1
  8. package/lib/module/components/DragOverlay.js +17 -5
  9. package/lib/module/components/DragOverlay.js.map +1 -1
  10. package/lib/module/components/DropIndicator.js +2 -2
  11. package/lib/module/components/DropIndicator.js.map +1 -1
  12. package/lib/module/components/NodeList.js +78 -58
  13. package/lib/module/components/NodeList.js.map +1 -1
  14. package/lib/module/constants/treeView.constants.js +3 -0
  15. package/lib/module/constants/treeView.constants.js.map +1 -1
  16. package/lib/module/helpers/expandCollapse.helper.js.map +1 -1
  17. package/lib/module/helpers/moveTreeNode.helper.js +30 -0
  18. package/lib/module/helpers/moveTreeNode.helper.js.map +1 -1
  19. package/lib/module/helpers/selectAll.helper.js.map +1 -1
  20. package/lib/module/helpers/toggleCheckbox.helper.js +43 -60
  21. package/lib/module/helpers/toggleCheckbox.helper.js.map +1 -1
  22. package/lib/module/hooks/useDragDrop.js +146 -65
  23. package/lib/module/hooks/useDragDrop.js.map +1 -1
  24. package/lib/module/{handlers/ScrollToNodeHandler.js → hooks/useScrollToNode.js} +27 -26
  25. package/lib/module/hooks/useScrollToNode.js.map +1 -0
  26. package/lib/module/index.js +1 -0
  27. package/lib/module/index.js.map +1 -1
  28. package/lib/module/jest.setup.js +14 -1
  29. package/lib/module/jest.setup.js.map +1 -1
  30. package/lib/module/store/treeView.store.js +3 -0
  31. package/lib/module/store/treeView.store.js.map +1 -1
  32. package/lib/module/utils/typedMemo.js +3 -3
  33. package/lib/module/utils/typedMemo.js.map +1 -1
  34. package/lib/module/utils/useDeepCompareEffect.js +5 -5
  35. package/lib/module/utils/useDeepCompareEffect.js.map +1 -1
  36. package/lib/typescript/src/TreeView.d.ts +3 -3
  37. package/lib/typescript/src/TreeView.d.ts.map +1 -1
  38. package/lib/typescript/src/components/CheckboxView.d.ts +1 -2
  39. package/lib/typescript/src/components/CheckboxView.d.ts.map +1 -1
  40. package/lib/typescript/src/components/CustomExpandCollapseIcon.d.ts +1 -2
  41. package/lib/typescript/src/components/CustomExpandCollapseIcon.d.ts.map +1 -1
  42. package/lib/typescript/src/components/DragOverlay.d.ts +1 -0
  43. package/lib/typescript/src/components/DragOverlay.d.ts.map +1 -1
  44. package/lib/typescript/src/components/DropIndicator.d.ts +1 -2
  45. package/lib/typescript/src/components/DropIndicator.d.ts.map +1 -1
  46. package/lib/typescript/src/components/NodeList.d.ts.map +1 -1
  47. package/lib/typescript/src/constants/treeView.constants.d.ts +2 -0
  48. package/lib/typescript/src/constants/treeView.constants.d.ts.map +1 -1
  49. package/lib/typescript/src/helpers/expandCollapse.helper.d.ts +2 -2
  50. package/lib/typescript/src/helpers/expandCollapse.helper.d.ts.map +1 -1
  51. package/lib/typescript/src/helpers/moveTreeNode.helper.d.ts.map +1 -1
  52. package/lib/typescript/src/helpers/selectAll.helper.d.ts +4 -4
  53. package/lib/typescript/src/helpers/selectAll.helper.d.ts.map +1 -1
  54. package/lib/typescript/src/helpers/toggleCheckbox.helper.d.ts +3 -0
  55. package/lib/typescript/src/helpers/toggleCheckbox.helper.d.ts.map +1 -1
  56. package/lib/typescript/src/hooks/useDragDrop.d.ts +24 -8
  57. package/lib/typescript/src/hooks/useDragDrop.d.ts.map +1 -1
  58. package/lib/typescript/src/{handlers/ScrollToNodeHandler.d.ts → hooks/useScrollToNode.d.ts} +13 -15
  59. package/lib/typescript/src/hooks/useScrollToNode.d.ts.map +1 -0
  60. package/lib/typescript/src/index.d.ts +4 -3
  61. package/lib/typescript/src/index.d.ts.map +1 -1
  62. package/lib/typescript/src/jest.setup.d.ts +1 -1
  63. package/lib/typescript/src/jest.setup.d.ts.map +1 -1
  64. package/lib/typescript/src/store/treeView.store.d.ts +2 -1
  65. package/lib/typescript/src/store/treeView.store.d.ts.map +1 -1
  66. package/lib/typescript/src/types/dragDrop.types.d.ts +10 -0
  67. package/lib/typescript/src/types/dragDrop.types.d.ts.map +1 -1
  68. package/lib/typescript/src/types/treeView.types.d.ts +79 -41
  69. package/lib/typescript/src/types/treeView.types.d.ts.map +1 -1
  70. package/lib/typescript/src/utils/typedMemo.d.ts +1 -1
  71. package/lib/typescript/src/utils/typedMemo.d.ts.map +1 -1
  72. package/lib/typescript/src/utils/useDeepCompareEffect.d.ts +2 -2
  73. package/lib/typescript/src/utils/useDeepCompareEffect.d.ts.map +1 -1
  74. package/package.json +32 -15
  75. package/src/TreeView.tsx +57 -35
  76. package/src/components/CheckboxView.tsx +7 -4
  77. package/src/components/CustomExpandCollapseIcon.tsx +2 -2
  78. package/src/components/DragOverlay.tsx +19 -6
  79. package/src/components/DropIndicator.tsx +2 -2
  80. package/src/components/NodeList.tsx +87 -60
  81. package/src/constants/treeView.constants.ts +4 -1
  82. package/src/helpers/expandCollapse.helper.ts +5 -5
  83. package/src/helpers/moveTreeNode.helper.ts +33 -0
  84. package/src/helpers/selectAll.helper.ts +10 -10
  85. package/src/helpers/toggleCheckbox.helper.ts +56 -68
  86. package/src/hooks/useDragDrop.ts +190 -80
  87. package/src/{handlers/ScrollToNodeHandler.tsx → hooks/useScrollToNode.ts} +48 -45
  88. package/src/index.tsx +11 -0
  89. package/src/jest.setup.ts +14 -1
  90. package/src/store/treeView.store.ts +6 -1
  91. package/src/types/dragDrop.types.ts +12 -0
  92. package/src/types/treeView.types.ts +87 -43
  93. package/src/utils/typedMemo.ts +3 -3
  94. package/src/utils/useDeepCompareEffect.ts +13 -7
  95. package/lib/module/handlers/ScrollToNodeHandler.js.map +0 -1
  96. package/lib/typescript/src/handlers/ScrollToNodeHandler.d.ts.map +0 -1
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import { memo, useCallback } from "react";
2
2
  import {
3
3
  Platform,
4
4
  StyleSheet,
@@ -13,6 +13,8 @@ import type {
13
13
  } from "../types/treeView.types";
14
14
  import { Checkbox } from "@futurejj/react-native-checkbox";
15
15
 
16
+ // Intentionally narrow: only re-render when the checkbox value or label text changes.
17
+ // Other props (callbacks, styles) are stable references from parent memoization.
16
18
  function arePropsEqual(
17
19
  prevProps: BuiltInCheckBoxViewProps,
18
20
  nextProps: BuiltInCheckBoxViewProps
@@ -23,7 +25,7 @@ function arePropsEqual(
23
25
  );
24
26
  }
25
27
 
26
- export const CheckboxView = React.memo(_CheckboxView, arePropsEqual);
28
+ export const CheckboxView = memo(_CheckboxView, arePropsEqual);
27
29
 
28
30
  function _CheckboxView(props: BuiltInCheckBoxViewProps) {
29
31
  const {
@@ -43,7 +45,7 @@ function _CheckboxView(props: BuiltInCheckBoxViewProps) {
43
45
  },
44
46
  } = props;
45
47
 
46
- const customCheckboxValToCheckboxValType = React.useCallback((
48
+ const customCheckboxValToCheckboxValType = useCallback((
47
49
  customCheckboxValueType: CheckboxValueType
48
50
  ) => {
49
51
  return customCheckboxValueType === "indeterminate"
@@ -59,7 +61,7 @@ function _CheckboxView(props: BuiltInCheckBoxViewProps) {
59
61
  *
60
62
  * @param newValue This represents the updated CheckBox value after it's clicked.
61
63
  */
62
- const onValueChangeModifier = React.useCallback(() => {
64
+ const onValueChangeModifier = useCallback(() => {
63
65
  // If the previous state was 'indeterminate', set checked to true
64
66
  if (value === "indeterminate") onValueChange(true);
65
67
  else onValueChange(!value);
@@ -106,6 +108,7 @@ export const defaultCheckboxViewStyles = StyleSheet.create({
106
108
  },
107
109
  checkboxTextStyle: {
108
110
  color: "black",
111
+ /* istanbul ignore next -- Platform.OS is never "android" in jest */
109
112
  marginTop: Platform.OS === "android" ? 2 : undefined,
110
113
  },
111
114
  });
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import { memo } from "react";
2
2
  import { type ExpandIconProps } from "../types/treeView.types";
3
3
 
4
4
  // Function to dynamically load FontAwesomeIcon from either Expo or React Native
@@ -20,7 +20,7 @@ function loadFontAwesomeIcon() {
20
20
  // Load the FontAwesomeIcon component
21
21
  const FontAwesomeIcon = loadFontAwesomeIcon();
22
22
 
23
- export const CustomExpandCollapseIcon = React.memo(
23
+ export const CustomExpandCollapseIcon = memo(
24
24
  _CustomExpandCollapseIcon
25
25
  );
26
26
 
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import { type ComponentType } from "react";
2
2
  import { Animated, StyleSheet, View } from "react-native";
3
3
 
4
4
  import type {
@@ -10,8 +10,12 @@ import type {
10
10
  import { CheckboxView } from "./CheckboxView";
11
11
  import { CustomExpandCollapseIcon } from "./CustomExpandCollapseIcon";
12
12
  import { defaultIndentationMultiplier } from "../constants/treeView.constants";
13
+ import { getTreeViewStore } from "../store/treeView.store";
14
+ import { getCheckboxValue } from "../helpers";
15
+ import { typedMemo } from "../utils/typedMemo";
13
16
 
14
17
  interface DragOverlayProps<ID> extends TreeItemCustomizations<ID> {
18
+ storeId: string;
15
19
  overlayY: Animated.Value;
16
20
  overlayX: Animated.Value;
17
21
  node: __FlattenedTreeNode__<ID>;
@@ -21,18 +25,24 @@ interface DragOverlayProps<ID> extends TreeItemCustomizations<ID> {
21
25
 
22
26
  function _DragOverlay<ID>(props: DragOverlayProps<ID>) {
23
27
  const {
28
+ storeId,
24
29
  overlayY,
25
30
  overlayX,
26
31
  node,
27
32
  level,
28
33
  indentationMultiplier = defaultIndentationMultiplier,
29
- CheckboxComponent = CheckboxView as React.ComponentType<CheckBoxViewProps>,
34
+ CheckboxComponent = CheckboxView as ComponentType<CheckBoxViewProps>,
30
35
  ExpandCollapseIconComponent = CustomExpandCollapseIcon,
31
36
  CustomNodeRowComponent,
32
37
  checkBoxViewStyleProps,
33
38
  dragDropCustomizations,
34
39
  } = props;
35
40
 
41
+ // Read the actual checked state for the dragged node
42
+ const store = getTreeViewStore<ID>(storeId);
43
+ const { checked, indeterminate } = store.getState();
44
+ const checkedValue = getCheckboxValue(checked.has(node.id), indeterminate.has(node.id));
45
+
36
46
  const overlayStyleProps = dragDropCustomizations?.dragOverlayStyleProps;
37
47
  const CustomOverlay = dragDropCustomizations?.CustomDragOverlayComponent;
38
48
 
@@ -52,13 +62,16 @@ function _DragOverlay<ID>(props: DragOverlayProps<ID>) {
52
62
  { transform: [{ translateX: overlayX }, { translateY: overlayY }] },
53
63
  ]}
54
64
  >
65
+ {/* Render priority: CustomDragOverlayComponent > CustomNodeRowComponent > built-in.
66
+ The overlay is display-only (pointerEvents="none" on parent), so handlers are no-ops.
67
+ isExpanded is always false because useDragDrop collapses the node at drag start. */}
55
68
  {CustomOverlay ? (
56
- <CustomOverlay node={node} level={level} />
69
+ <CustomOverlay node={node} level={level} checkedValue={checkedValue} />
57
70
  ) : CustomNodeRowComponent ? (
58
71
  <CustomNodeRowComponent
59
72
  node={node}
60
73
  level={level}
61
- checkedValue={false}
74
+ checkedValue={checkedValue}
62
75
  isExpanded={false}
63
76
  onCheck={() => {}}
64
77
  onExpand={() => {}}
@@ -73,7 +86,7 @@ function _DragOverlay<ID>(props: DragOverlayProps<ID>) {
73
86
  <CheckboxComponent
74
87
  text={node.name}
75
88
  onValueChange={() => {}}
76
- value={false}
89
+ value={checkedValue}
77
90
  {...checkBoxViewStyleProps}
78
91
  />
79
92
  {node.children?.length ? (
@@ -87,7 +100,7 @@ function _DragOverlay<ID>(props: DragOverlayProps<ID>) {
87
100
  );
88
101
  }
89
102
 
90
- export const DragOverlay = React.memo(_DragOverlay) as typeof _DragOverlay;
103
+ export const DragOverlay = typedMemo(_DragOverlay);
91
104
 
92
105
  const styles = StyleSheet.create({
93
106
  overlay: {
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import { memo } from "react";
2
2
  import { Animated, View, StyleSheet } from "react-native";
3
3
  import type { DropPosition } from "../types/dragDrop.types";
4
4
 
@@ -10,7 +10,7 @@ interface DropIndicatorProps {
10
10
  indentationMultiplier: number;
11
11
  }
12
12
 
13
- export const DropIndicator = React.memo(function DropIndicator(
13
+ export const DropIndicator = memo(function DropIndicator(
14
14
  props: DropIndicatorProps
15
15
  ) {
16
16
  const {
@@ -1,4 +1,10 @@
1
- import React from "react";
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ } from "react";
2
8
  import {
3
9
  View,
4
10
  StyleSheet,
@@ -9,7 +15,6 @@ import {
9
15
  import { FlashList } from "@shopify/flash-list";
10
16
 
11
17
  import type {
12
- CheckboxValueType,
13
18
  __FlattenedTreeNode__,
14
19
  DropIndicatorStyleProps,
15
20
  NodeListProps,
@@ -21,6 +26,7 @@ import { useTreeViewStore } from "../store/treeView.store";
21
26
  import {
22
27
  getFilteredTreeData,
23
28
  getFlattenedTreeData,
29
+ getCheckboxValue,
24
30
  getInnerMostChildrenIdsInTree,
25
31
  handleToggleExpand,
26
32
  toggleCheckboxes
@@ -29,10 +35,13 @@ import { CheckboxView } from "./CheckboxView";
29
35
  import { CustomExpandCollapseIcon } from "./CustomExpandCollapseIcon";
30
36
  import { DragOverlay } from "./DragOverlay";
31
37
  import type { DropPosition } from "../types/dragDrop.types";
32
- import { defaultIndentationMultiplier } from "../constants/treeView.constants";
38
+ import {
39
+ defaultIndentationMultiplier,
40
+ listHeaderFooterPadding
41
+ } from "../constants/treeView.constants";
33
42
  import { useShallow } from "zustand/react/shallow";
34
43
  import { typedMemo } from "../utils/typedMemo";
35
- import { ScrollToNodeHandler } from "../handlers/ScrollToNodeHandler";
44
+ import { useScrollToNode } from "../hooks/useScrollToNode";
36
45
  import { useDragDrop } from "../hooks/useDragDrop";
37
46
 
38
47
  const NodeList = typedMemo(_NodeList);
@@ -54,15 +63,30 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
54
63
  ExpandCollapseTouchableComponent,
55
64
  CustomNodeRowComponent,
56
65
 
57
- dragEnabled,
66
+ dragAndDrop,
67
+ } = props;
68
+
69
+ const {
70
+ enabled: _dragEnabled,
71
+ onDragStart,
58
72
  onDragEnd,
73
+ onDragCancel,
59
74
  longPressDuration = 400,
60
75
  autoScrollThreshold = 60,
61
76
  autoScrollSpeed = 1.0,
62
- dragOverlayOffset = -4,
77
+ dragOverlayOffset = -2,
63
78
  autoExpandDelay = 800,
64
- dragDropCustomizations,
65
- } = props;
79
+ customizations: dragDropCustomizations,
80
+ canDrop: canDropCallback,
81
+ maxDepth,
82
+ canNodeHaveChildren,
83
+ canDrag,
84
+ autoScrollToDroppedNode,
85
+ } = dragAndDrop ?? {};
86
+
87
+ // When the dragAndDrop prop is provided, drag is enabled by default.
88
+ // Users can still toggle it off with enabled: false at runtime.
89
+ const dragEnabled = dragAndDrop ? (_dragEnabled ?? true) : false;
66
90
 
67
91
  const {
68
92
  expanded,
@@ -80,34 +104,43 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
80
104
  })
81
105
  ));
82
106
 
83
- const flashListRef = React.useRef<FlashList<__FlattenedTreeNode__<ID>> | null>(null);
84
- const containerRef = React.useRef<View>(null);
85
- const internalDataRef = React.useRef<TreeNode<ID>[] | null>(null);
86
- const measuredItemHeightRef = React.useRef(0);
107
+ const flashListRef = useRef<FlashList<__FlattenedTreeNode__<ID>> | null>(null);
108
+ const containerRef = useRef<View>(null);
109
+ const internalDataRef = useRef<TreeNode<ID>[] | null>(null);
110
+ const measuredItemHeightRef = useRef(0);
87
111
 
88
- const handleItemLayout = React.useCallback((height: number) => {
112
+ const handleItemLayout = useCallback((height: number) => {
89
113
  if (measuredItemHeightRef.current === 0 && height > 0) {
90
114
  measuredItemHeightRef.current = height;
91
115
  }
92
116
  }, []);
93
117
 
94
- const [initialScrollIndex, setInitialScrollIndex] = React.useState<number>(-1);
118
+ const [initialScrollIndex, setInitialScrollIndex] = useState<number>(-1);
95
119
 
96
120
  // First we filter the tree as per the search term and keys
97
- const filteredTree = React.useMemo(() => getFilteredTreeData<ID>(
121
+ const filteredTree = useMemo(() => getFilteredTreeData<ID>(
98
122
  initialTreeViewData,
99
123
  searchText.trim().toLowerCase(),
100
124
  searchKeys
101
125
  ), [initialTreeViewData, searchText, searchKeys]);
102
126
 
103
127
  // Then we flatten the tree to make it "render-compatible" in a "flat" list
104
- const flattenedFilteredNodes = React.useMemo(() => getFlattenedTreeData<ID>(
128
+ const flattenedFilteredNodes = useMemo(() => getFlattenedTreeData<ID>(
105
129
  filteredTree,
106
130
  expanded,
107
131
  ), [filteredTree, expanded]);
108
132
 
133
+ useScrollToNode<ID>({
134
+ storeId,
135
+ scrollToNodeHandlerRef,
136
+ flashListRef,
137
+ flattenedFilteredNodes,
138
+ setInitialScrollIndex,
139
+ initialScrollNodeID,
140
+ });
141
+
109
142
  // And update the innermost children id -> required to un/select filtered tree
110
- React.useEffect(() => {
143
+ useEffect(() => {
111
144
  const updatedInnerMostChildrenIds = getInnerMostChildrenIdsInTree<ID>(
112
145
  filteredTree
113
146
  );
@@ -133,8 +166,10 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
133
166
  flattenedNodes: flattenedFilteredNodes,
134
167
  flashListRef,
135
168
  containerRef,
136
- dragEnabled: dragEnabled ?? false,
169
+ dragEnabled,
170
+ onDragStart,
137
171
  onDragEnd,
172
+ onDragCancel,
138
173
  longPressDuration,
139
174
  autoScrollThreshold,
140
175
  autoScrollSpeed,
@@ -143,10 +178,16 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
143
178
  dragOverlayOffset,
144
179
  autoExpandDelay,
145
180
  indentationMultiplier: effectiveIndentationMultiplier,
181
+ canDrop: canDropCallback,
182
+ maxDepth,
183
+ canNodeHaveChildren,
184
+ canDrag,
185
+ scrollToNodeHandlerRef,
186
+ autoScrollToDroppedNode,
146
187
  });
147
188
 
148
189
  // Combined onScroll handler
149
- const handleScroll = React.useCallback((
190
+ const handleScroll = useCallback((
150
191
  event: NativeSyntheticEvent<NativeScrollEvent>
151
192
  ) => {
152
193
  scrollOffsetRef.current = event.nativeEvent.contentOffset.y;
@@ -156,7 +197,7 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
156
197
  treeFlashListProps?.onScroll?.(event as any);
157
198
  }, [scrollOffsetRef, cancelLongPressTimer, treeFlashListProps]);
158
199
 
159
- const nodeRenderer = React.useCallback((
200
+ const nodeRenderer = useCallback((
160
201
  { item, index }: { item: __FlattenedTreeNode__<ID>; index: number; }
161
202
  ) => {
162
203
  return (
@@ -223,14 +264,6 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
223
264
 
224
265
  return (
225
266
  <>
226
- <ScrollToNodeHandler
227
- ref={scrollToNodeHandlerRef}
228
- storeId={storeId}
229
- flashListRef={flashListRef}
230
- flattenedFilteredNodes={flattenedFilteredNodes}
231
- setInitialScrollIndex={setInitialScrollIndex}
232
- initialScrollNodeID={initialScrollNodeID} />
233
-
234
267
  {dragEnabled ? (
235
268
  <View
236
269
  ref={containerRef}
@@ -240,6 +273,7 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
240
273
  {flashListElement}
241
274
  {isDragging && draggedNode && (
242
275
  <DragOverlay<ID>
276
+ storeId={storeId}
243
277
  overlayY={overlayY}
244
278
  overlayX={overlayX}
245
279
  node={draggedNode}
@@ -262,22 +296,10 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
262
296
 
263
297
  function HeaderFooterView() {
264
298
  return (
265
- <View style={styles.defaultHeaderFooter} />
299
+ <View style={{ padding: listHeaderFooterPadding }} />
266
300
  );
267
301
  }
268
302
 
269
- function getValue(
270
- isChecked: boolean,
271
- isIndeterminate: boolean
272
- ): CheckboxValueType {
273
- if (isIndeterminate) {
274
- return "indeterminate";
275
- } else if (isChecked) {
276
- return true;
277
- } else {
278
- return false;
279
- }
280
- }
281
303
 
282
304
  const Node = typedMemo(_Node);
283
305
  function _Node<ID>(props: NodeProps<ID>) {
@@ -315,7 +337,7 @@ function _Node<ID>(props: NodeProps<ID>) {
315
337
  } = useTreeViewStore<ID>(storeId)(useShallow(
316
338
  state => ({
317
339
  isExpanded: state.expanded.has(node.id),
318
- value: getValue(
340
+ value: getCheckboxValue(
319
341
  state.checked.has(node.id),
320
342
  state.indeterminate.has(node.id)
321
343
  ),
@@ -332,45 +354,51 @@ function _Node<ID>(props: NodeProps<ID>) {
332
354
  // The flag is set during render (synchronous) and cleared on the next touch start.
333
355
  // It is also cleared via effect when dragging ends, to prevent stale `true`
334
356
  // values surviving FlashList recycling (where refs persist across items).
335
- const wasDraggedRef = React.useRef(false);
357
+ const wasDraggedRef = useRef(false);
336
358
  if (isDraggingGlobal && isBeingDragged) {
337
359
  wasDraggedRef.current = true;
338
360
  }
339
361
 
340
- React.useEffect(() => {
362
+ useEffect(() => {
341
363
  if (!isDraggingGlobal) {
342
364
  wasDraggedRef.current = false;
343
365
  }
344
366
  }, [isDraggingGlobal]);
345
367
 
346
- const _onToggleExpand = React.useCallback(() => {
368
+ const _onToggleExpand = useCallback(() => {
347
369
  if (wasDraggedRef.current) return;
348
370
  handleToggleExpand(storeId, node.id);
349
371
  }, [storeId, node.id]);
350
372
 
351
- const _onCheck = React.useCallback(() => {
373
+ const _onCheck = useCallback(() => {
352
374
  if (wasDraggedRef.current) return;
353
375
  toggleCheckboxes(storeId, [node.id]);
354
376
  }, [storeId, node.id]);
355
377
 
356
- const handleTouchStart = React.useCallback((e: any) => {
378
+ const handleTouchStart = useCallback((e: any) => {
357
379
  wasDraggedRef.current = false;
358
380
  if (!onNodeTouchStart) return;
359
381
  const { pageY, locationY } = e.nativeEvent;
360
382
  onNodeTouchStart(node.id, pageY, locationY, nodeIndex);
361
383
  }, [node.id, nodeIndex, onNodeTouchStart]);
362
384
 
363
- const handleTouchEnd = React.useCallback(() => {
385
+ const handleTouchEnd = useCallback(() => {
364
386
  onNodeTouchEnd?.();
365
387
  }, [onNodeTouchEnd]);
366
388
 
367
- // Determine opacity for drag state
368
- const dragOpacity = dragDropCustomizations?.draggedNodeOpacity ?? 0.3;
369
- const nodeOpacity = (isDraggingGlobal && (isBeingDragged || isDragInvalid))
370
- ? dragOpacity
371
- : 1.0;
372
-
373
- const handleLayout = React.useCallback((e: any) => {
389
+ // Determine opacity for drag state (separate values for dragged node vs invalid targets).
390
+ // When CustomNodeRowComponent is used, hand off all visual control
391
+ // (including drag opacity) to the custom component - it receives
392
+ // isDraggedNode / isInvalidDropTarget / isDragging props.
393
+ const draggedOpacity = dragDropCustomizations?.draggedNodeOpacity ?? 0.3;
394
+ const invalidOpacity = dragDropCustomizations?.invalidTargetOpacity ?? 0.3;
395
+ const nodeOpacity = CustomNodeRowComponent
396
+ ? 1.0
397
+ : isDraggingGlobal
398
+ ? (isBeingDragged ? draggedOpacity : isDragInvalid ? invalidOpacity : 1.0)
399
+ : 1.0;
400
+
401
+ const handleLayout = useCallback((e: any) => {
374
402
  onItemLayout?.(e.nativeEvent.layout.height);
375
403
  }, [onItemLayout]);
376
404
 
@@ -424,7 +452,6 @@ function _Node<ID>(props: NodeProps<ID>) {
424
452
  else {
425
453
  return (
426
454
  <View
427
- {...touchHandlers}
428
455
  onLayout={onItemLayout ? handleLayout : undefined}
429
456
  style={[
430
457
  { opacity: nodeOpacity },
@@ -439,9 +466,12 @@ function _Node<ID>(props: NodeProps<ID>) {
439
466
  isExpanded={isExpanded}
440
467
  onCheck={_onCheck}
441
468
  onExpand={_onToggleExpand}
442
- isDragTarget={isDragInvalid}
469
+ isInvalidDropTarget={isDragInvalid}
470
+ isDropTarget={isDropTarget}
471
+ dropPosition={nodeDropPosition ?? undefined}
443
472
  isDragging={isDraggingGlobal}
444
473
  isDraggedNode={isBeingDragged}
474
+ dragHandleProps={touchHandlers}
445
475
  />
446
476
  </View>
447
477
  );
@@ -511,9 +541,6 @@ function NodeDropIndicator({ position, level, indentationMultiplier, styleProps
511
541
  }
512
542
 
513
543
  const styles = StyleSheet.create({
514
- defaultHeaderFooter: {
515
- padding: 5
516
- },
517
544
  nodeExpandableArrowTouchable: {
518
545
  flex: 1
519
546
  },
@@ -1 +1,4 @@
1
- export const defaultIndentationMultiplier = 15;
1
+ export const defaultIndentationMultiplier = 15;
2
+
3
+ /** Padding used by the FlashList header/footer component. Total header height = 2 * this value. */
4
+ export const listHeaderFooterPadding = 5;
@@ -49,8 +49,8 @@ export function handleToggleExpand<ID>(storeId: string, id: ID) {
49
49
  /**
50
50
  * Expand all nodes in the tree.
51
51
  */
52
- export function expandAll(storeId: string) {
53
- const treeViewStore = getTreeViewStore(storeId);
52
+ export function expandAll<ID>(storeId: string) {
53
+ const treeViewStore = getTreeViewStore<ID>(storeId);
54
54
  const { nodeMap, updateExpanded } = treeViewStore.getState();
55
55
  // Create a new Set containing the IDs of all nodes
56
56
  const newExpanded = new Set(nodeMap.keys());
@@ -60,11 +60,11 @@ export function expandAll(storeId: string) {
60
60
  /**
61
61
  * Collapse all nodes in the tree.
62
62
  */
63
- export function collapseAll(storeId: string) {
64
- const treeViewStore = getTreeViewStore(storeId);
63
+ export function collapseAll<ID>(storeId: string) {
64
+ const treeViewStore = getTreeViewStore<ID>(storeId);
65
65
  const { updateExpanded } = treeViewStore.getState();
66
66
  // Clear the expanded state
67
- updateExpanded(new Set<string>());
67
+ updateExpanded(new Set());
68
68
  }
69
69
 
70
70
  /**
@@ -18,6 +18,9 @@ export function moveTreeNode<ID>(
18
18
  ): TreeNode<ID>[] {
19
19
  if (draggedNodeId === targetNodeId) return data;
20
20
 
21
+ // Prevent moving a node into its own descendant (would create a cycle)
22
+ if (isDescendant(data, draggedNodeId, targetNodeId)) return data;
23
+
21
24
  // Step 1: Deep clone the tree
22
25
  const cloned = deepCloneTree(data);
23
26
 
@@ -32,6 +35,36 @@ export function moveTreeNode<ID>(
32
35
  return cloned;
33
36
  }
34
37
 
38
+ /**
39
+ * Check if `candidateDescendantId` is a descendant of `ancestorId` in the tree.
40
+ */
41
+ function isDescendant<ID>(
42
+ nodes: TreeNode<ID>[],
43
+ ancestorId: ID,
44
+ candidateDescendantId: ID,
45
+ ): boolean {
46
+ for (const node of nodes) {
47
+ if (node.id === ancestorId) {
48
+ // Found the ancestor - search its subtree for the candidate
49
+ return containsNode(node.children ?? [], candidateDescendantId);
50
+ }
51
+ if (node.children && isDescendant(node.children, ancestorId, candidateDescendantId)) {
52
+ return true;
53
+ }
54
+ }
55
+ return false;
56
+ }
57
+
58
+ /** Check if a node with the given ID exists anywhere in the subtree. */
59
+ function containsNode<ID>(nodes: TreeNode<ID>[], nodeId: ID): boolean {
60
+ for (const node of nodes) {
61
+ if (node.id === nodeId) return true;
62
+ if (node.children && containsNode(node.children, nodeId)) return true;
63
+ }
64
+ return false;
65
+ }
66
+
67
+ /** Deep clone a tree structure so mutations don't affect the original. */
35
68
  function deepCloneTree<ID>(nodes: TreeNode<ID>[]): TreeNode<ID>[] {
36
69
  return nodes.map(node => ({
37
70
  ...node,
@@ -7,13 +7,13 @@ import { toggleCheckboxes } from "./toggleCheckbox.helper";
7
7
  *
8
8
  * If there is no search text, then it selects all nodes; otherwise, it selects all visible nodes.
9
9
  */
10
- export function selectAllFiltered(storeId: string) {
11
- const treeViewStore = getTreeViewStore(storeId);
10
+ export function selectAllFiltered<ID>(storeId: string) {
11
+ const treeViewStore = getTreeViewStore<ID>(storeId);
12
12
  const { searchText, innerMostChildrenIds } = treeViewStore.getState();
13
13
 
14
14
  // If there's no search text, select all nodes
15
15
  if (!searchText) {
16
- selectAll(storeId);
16
+ selectAll<ID>(storeId);
17
17
  } else {
18
18
  // If there's search text, only select the visible nodes
19
19
  toggleCheckboxes(storeId, innerMostChildrenIds, true);
@@ -25,13 +25,13 @@ export function selectAllFiltered(storeId: string) {
25
25
  *
26
26
  * If there is no search text, then it unselects all nodes; otherwise, it unselects all visible nodes.
27
27
  */
28
- export function unselectAllFiltered(storeId: string) {
29
- const treeViewStore = getTreeViewStore(storeId);
28
+ export function unselectAllFiltered<ID>(storeId: string) {
29
+ const treeViewStore = getTreeViewStore<ID>(storeId);
30
30
  const { searchText, innerMostChildrenIds } = treeViewStore.getState();
31
31
 
32
32
  // If there's no search text, unselect all nodes
33
33
  if (!searchText) {
34
- unselectAll(storeId);
34
+ unselectAll<ID>(storeId);
35
35
  } else {
36
36
  // If there's search text, only unselect the visible nodes
37
37
  toggleCheckboxes(storeId, innerMostChildrenIds, false);
@@ -43,8 +43,8 @@ export function unselectAllFiltered(storeId: string) {
43
43
  *
44
44
  * This function selects all nodes by adding all node ids to the checked set and clearing the indeterminate set.
45
45
  */
46
- export function selectAll(storeId: string) {
47
- const treeViewStore = getTreeViewStore(storeId);
46
+ export function selectAll<ID>(storeId: string) {
47
+ const treeViewStore = getTreeViewStore<ID>(storeId);
48
48
  const {
49
49
  nodeMap,
50
50
  updateChecked,
@@ -64,8 +64,8 @@ export function selectAll(storeId: string) {
64
64
  *
65
65
  * This function unselects all nodes by clearing both the checked and indeterminate sets.
66
66
  */
67
- export function unselectAll(storeId: string) {
68
- const treeViewStore = getTreeViewStore(storeId);
67
+ export function unselectAll<ID>(storeId: string) {
68
+ const treeViewStore = getTreeViewStore<ID>(storeId);
69
69
  const { updateChecked, updateIndeterminate } = treeViewStore.getState();
70
70
  // Update the state to mark all nodes as unchecked
71
71