react-native-tree-multi-select 1.8.0 → 1.9.0-beta.1

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 (31) hide show
  1. package/README.md +52 -24
  2. package/lib/commonjs/TreeView.js +17 -6
  3. package/lib/commonjs/TreeView.js.map +1 -1
  4. package/lib/commonjs/components/NodeList.js +26 -10
  5. package/lib/commonjs/components/NodeList.js.map +1 -1
  6. package/lib/commonjs/handlers/ScrollToNodeHandler.js +169 -0
  7. package/lib/commonjs/handlers/ScrollToNodeHandler.js.map +1 -0
  8. package/lib/commonjs/helpers/expandCollapse.helper.js +7 -1
  9. package/lib/commonjs/helpers/expandCollapse.helper.js.map +1 -1
  10. package/lib/module/TreeView.js +18 -5
  11. package/lib/module/TreeView.js.map +1 -1
  12. package/lib/module/components/NodeList.js +27 -11
  13. package/lib/module/components/NodeList.js.map +1 -1
  14. package/lib/module/handlers/ScrollToNodeHandler.js +165 -0
  15. package/lib/module/handlers/ScrollToNodeHandler.js.map +1 -0
  16. package/lib/module/helpers/expandCollapse.helper.js +7 -1
  17. package/lib/module/helpers/expandCollapse.helper.js.map +1 -1
  18. package/lib/typescript/TreeView.d.ts.map +1 -1
  19. package/lib/typescript/components/NodeList.d.ts.map +1 -1
  20. package/lib/typescript/handlers/ScrollToNodeHandler.d.ts +58 -0
  21. package/lib/typescript/handlers/ScrollToNodeHandler.d.ts.map +1 -0
  22. package/lib/typescript/helpers/expandCollapse.helper.d.ts +3 -1
  23. package/lib/typescript/helpers/expandCollapse.helper.d.ts.map +1 -1
  24. package/lib/typescript/types/treeView.types.d.ts +6 -1
  25. package/lib/typescript/types/treeView.types.d.ts.map +1 -1
  26. package/package.json +1 -1
  27. package/src/TreeView.tsx +182 -152
  28. package/src/components/NodeList.tsx +31 -11
  29. package/src/handlers/ScrollToNodeHandler.tsx +222 -0
  30. package/src/helpers/expandCollapse.helper.ts +16 -1
  31. package/src/types/treeView.types.ts +15 -1
package/src/TreeView.tsx CHANGED
@@ -1,210 +1,240 @@
1
- import React, { useMemo } from 'react';
1
+ import React from 'react';
2
2
  import { InteractionManager } from 'react-native';
3
3
  import type {
4
- TreeNode,
5
- TreeViewProps,
6
- TreeViewRef
4
+ TreeNode,
5
+ TreeViewProps,
6
+ TreeViewRef
7
7
  } from './types/treeView.types';
8
8
  import NodeList from './components/NodeList';
9
9
  import {
10
- selectAll,
11
- selectAllFiltered,
12
- unselectAll,
13
- unselectAllFiltered,
14
- initializeNodeMaps,
15
- expandAll,
16
- collapseAll,
17
- toggleCheckboxes,
18
- expandNodes,
19
- collapseNodes
10
+ selectAll,
11
+ selectAllFiltered,
12
+ unselectAll,
13
+ unselectAllFiltered,
14
+ initializeNodeMaps,
15
+ expandAll,
16
+ collapseAll,
17
+ toggleCheckboxes,
18
+ expandNodes,
19
+ collapseNodes
20
20
  } from './helpers';
21
- import { useTreeViewStore } from './store/treeView.store';
21
+ import { getTreeViewStore, useTreeViewStore } from './store/treeView.store';
22
22
  import usePreviousState from './utils/usePreviousState';
23
23
  import { useShallow } from "zustand/react/shallow";
24
24
  import uuid from "react-native-uuid";
25
25
  import useDeepCompareEffect from "./utils/useDeepCompareEffect";
26
26
  import { typedMemo } from './utils/typedMemo';
27
+ import {
28
+ ScrollToNodeHandlerRef,
29
+ ScrollToNodeParams
30
+ } from "./handlers/ScrollToNodeHandler";
31
+
32
+ function _innerTreeView<ID>(
33
+ props: TreeViewProps<ID>,
34
+ ref: React.ForwardedRef<TreeViewRef<ID>>
35
+ ) {
36
+ const {
37
+ data,
27
38
 
28
- function _innerTreeView<ID>(props: TreeViewProps<ID>, ref: React.ForwardedRef<TreeViewRef<ID>>) {
29
- const {
30
- data,
39
+ onCheck,
40
+ onExpand,
31
41
 
32
- onCheck,
33
- onExpand,
42
+ selectionPropagation,
34
43
 
35
- selectionPropagation,
44
+ preselectedIds = [],
36
45
 
37
- preselectedIds = [],
46
+ preExpandedIds = [],
38
47
 
39
- preExpandedIds = [],
48
+ initialScrollNodeID,
40
49
 
41
- treeFlashListProps,
42
- checkBoxViewStyleProps,
43
- indentationMultiplier,
50
+ treeFlashListProps,
51
+ checkBoxViewStyleProps,
52
+ indentationMultiplier,
44
53
 
45
- CheckboxComponent,
46
- ExpandCollapseIconComponent,
47
- ExpandCollapseTouchableComponent,
54
+ CheckboxComponent,
55
+ ExpandCollapseIconComponent,
56
+ ExpandCollapseTouchableComponent,
48
57
 
49
- CustomNodeRowComponent,
50
- } = props;
58
+ CustomNodeRowComponent,
59
+ } = props;
51
60
 
52
- const storeId = useMemo(() => uuid.v4(), []);
61
+ const storeId = React.useMemo(() => uuid.v4(), []);
53
62
 
54
- const {
55
- expanded,
56
- updateExpanded,
63
+ const {
64
+ expanded,
65
+ updateExpanded,
57
66
 
58
- initialTreeViewData,
59
- updateInitialTreeViewData,
67
+ initialTreeViewData,
68
+ updateInitialTreeViewData,
60
69
 
61
- searchText,
62
- updateSearchText,
70
+ searchText,
71
+ updateSearchText,
63
72
 
64
- updateSearchKeys,
73
+ updateSearchKeys,
65
74
 
66
- checked,
67
- indeterminate,
75
+ checked,
76
+ indeterminate,
68
77
 
69
- setSelectionPropagation,
78
+ setSelectionPropagation,
70
79
 
71
- cleanUpTreeViewStore,
72
- } = useTreeViewStore<ID>(storeId)(useShallow(
73
- state => ({
74
- expanded: state.expanded,
75
- updateExpanded: state.updateExpanded,
80
+ cleanUpTreeViewStore,
81
+ } = useTreeViewStore<ID>(storeId)(useShallow(
82
+ state => ({
83
+ expanded: state.expanded,
84
+ updateExpanded: state.updateExpanded,
76
85
 
77
- initialTreeViewData: state.initialTreeViewData,
78
- updateInitialTreeViewData: state.updateInitialTreeViewData,
86
+ initialTreeViewData: state.initialTreeViewData,
87
+ updateInitialTreeViewData: state.updateInitialTreeViewData,
79
88
 
80
- searchText: state.searchText,
81
- updateSearchText: state.updateSearchText,
89
+ searchText: state.searchText,
90
+ updateSearchText: state.updateSearchText,
82
91
 
83
- updateSearchKeys: state.updateSearchKeys,
92
+ updateSearchKeys: state.updateSearchKeys,
84
93
 
85
- checked: state.checked,
86
- indeterminate: state.indeterminate,
94
+ checked: state.checked,
95
+ indeterminate: state.indeterminate,
87
96
 
88
- setSelectionPropagation: state.setSelectionPropagation,
97
+ setSelectionPropagation: state.setSelectionPropagation,
89
98
 
90
- cleanUpTreeViewStore: state.cleanUpTreeViewStore,
91
- })
92
- ));
99
+ cleanUpTreeViewStore: state.cleanUpTreeViewStore,
100
+ })
101
+ ));
93
102
 
94
- React.useImperativeHandle(ref, () => ({
95
- selectAll: () => selectAll(storeId),
96
- unselectAll: () => unselectAll(storeId),
103
+ React.useImperativeHandle(ref, () => ({
104
+ selectAll: () => selectAll(storeId),
105
+ unselectAll: () => unselectAll(storeId),
97
106
 
98
- selectAllFiltered: () => selectAllFiltered(storeId),
99
- unselectAllFiltered: () => unselectAllFiltered(storeId),
107
+ selectAllFiltered: () => selectAllFiltered(storeId),
108
+ unselectAllFiltered: () => unselectAllFiltered(storeId),
100
109
 
101
- expandAll: () => expandAll(storeId),
102
- collapseAll: () => collapseAll(storeId),
110
+ expandAll: () => expandAll(storeId),
111
+ collapseAll: () => collapseAll(storeId),
103
112
 
104
- expandNodes: (ids: ID[]) => expandNodes(storeId, ids),
105
- collapseNodes: (ids: ID[]) => collapseNodes(storeId, ids),
113
+ expandNodes: (ids: ID[]) => expandNodes(storeId, ids),
114
+ collapseNodes: (ids: ID[]) => collapseNodes(storeId, ids),
106
115
 
107
- selectNodes: (ids: ID[]) => selectNodes(ids),
108
- unselectNodes: (ids: ID[]) => unselectNodes(ids),
116
+ selectNodes: (ids: ID[]) => selectNodes(ids),
117
+ unselectNodes: (ids: ID[]) => unselectNodes(ids),
109
118
 
110
- setSearchText
111
- }));
119
+ setSearchText,
112
120
 
113
- const prevSearchText = usePreviousState(searchText);
121
+ scrollToNodeID,
114
122
 
115
- useDeepCompareEffect(() => {
116
- cleanUpTreeViewStore();
123
+ getChildToParentMap
124
+ }));
117
125
 
118
- updateInitialTreeViewData(data);
126
+ const scrollToNodeHandlerRef = React.useRef<ScrollToNodeHandlerRef<ID>>(null);
127
+ const prevSearchText = usePreviousState(searchText);
119
128
 
120
- if (selectionPropagation)
121
- setSelectionPropagation(selectionPropagation);
129
+ useDeepCompareEffect(() => {
130
+ cleanUpTreeViewStore();
122
131
 
123
- initializeNodeMaps(storeId, data);
132
+ updateInitialTreeViewData(data);
124
133
 
125
- // Check any pre-selected nodes
126
- toggleCheckboxes(storeId, preselectedIds, true);
134
+ if (selectionPropagation)
135
+ setSelectionPropagation(selectionPropagation);
127
136
 
128
- // Expand pre-expanded nodes
129
- expandNodes(storeId, preExpandedIds);
130
- }, [data]);
137
+ initializeNodeMaps(storeId, data);
131
138
 
132
- function selectNodes(ids: ID[]) {
133
- toggleCheckboxes(storeId, ids, true);
134
- }
139
+ // Check any pre-selected nodes
140
+ toggleCheckboxes(storeId, preselectedIds, true);
135
141
 
136
- function unselectNodes(ids: ID[]) {
137
- toggleCheckboxes(storeId, ids, false);
138
- }
142
+ // Expand pre-expanded nodes
143
+ expandNodes(storeId, [
144
+ ...preExpandedIds,
145
+ ...(initialScrollNodeID ? [initialScrollNodeID] : [])
146
+ ]);
147
+ }, [data]);
139
148
 
140
- function setSearchText(text: string, keys: string[] = ["name"]) {
141
- updateSearchText(text);
142
- updateSearchKeys(keys);
143
- }
149
+ function selectNodes(ids: ID[]) {
150
+ toggleCheckboxes(storeId, ids, true);
151
+ }
144
152
 
145
- const getIds = React.useCallback((node: TreeNode<ID>): ID[] => {
146
- if (!node.children || node.children.length === 0) {
147
- return [node.id];
148
- } else {
149
- return [node.id, ...node.children.flatMap((item) => getIds(item))];
150
- }
151
- }, []);
153
+ function unselectNodes(ids: ID[]) {
154
+ toggleCheckboxes(storeId, ids, false);
155
+ }
152
156
 
153
- React.useEffect(() => {
154
- onCheck?.(Array.from(checked), Array.from(indeterminate));
155
- }, [onCheck, checked, indeterminate]);
157
+ function setSearchText(text: string, keys: string[] = ["name"]) {
158
+ updateSearchText(text);
159
+ updateSearchKeys(keys);
160
+ }
156
161
 
157
- React.useEffect(() => {
158
- onExpand?.(Array.from(expanded));
159
- }, [onExpand, expanded]);
162
+ function scrollToNodeID(params: ScrollToNodeParams<ID>) {
163
+ scrollToNodeHandlerRef.current?.scrollToNodeID(params);
164
+ }
160
165
 
161
- React.useEffect(() => {
162
- if (searchText) {
163
- InteractionManager.runAfterInteractions(() => {
164
- updateExpanded(new Set(initialTreeViewData.flatMap(
165
- (item) => getIds(item)
166
- )));
167
- });
168
- }
169
- else if (prevSearchText && prevSearchText !== "") {
170
- /* Collapse all nodes only if previous search query was non-empty: this is
171
- done to prevent node collapse on first render if preExpandedIds is provided */
172
- InteractionManager.runAfterInteractions(() => {
173
- updateExpanded(new Set());
174
- });
175
- }
176
- }, [
177
- getIds,
178
- initialTreeViewData,
179
- prevSearchText,
180
- searchText,
181
- updateExpanded
182
- ]);
183
-
184
- React.useEffect(() => {
185
- return () => {
186
- cleanUpTreeViewStore();
187
- };
188
- }, [cleanUpTreeViewStore]);
189
-
190
- return (
191
- <NodeList
192
- storeId={storeId}
193
-
194
- treeFlashListProps={treeFlashListProps}
195
- checkBoxViewStyleProps={checkBoxViewStyleProps}
196
- indentationMultiplier={indentationMultiplier}
197
-
198
- CheckboxComponent={CheckboxComponent}
199
- ExpandCollapseIconComponent={ExpandCollapseIconComponent}
200
- ExpandCollapseTouchableComponent={ExpandCollapseTouchableComponent}
201
-
202
- CustomNodeRowComponent={CustomNodeRowComponent}
203
- />
204
- );
166
+ function getChildToParentMap() {
167
+ const treeViewStore = getTreeViewStore<ID>(storeId);
168
+ return treeViewStore.getState().childToParentMap;
169
+ }
170
+
171
+ const getIds = React.useCallback((node: TreeNode<ID>): ID[] => {
172
+ if (!node.children || node.children.length === 0) {
173
+ return [node.id];
174
+ } else {
175
+ return [node.id, ...node.children.flatMap((item) => getIds(item))];
176
+ }
177
+ }, []);
178
+
179
+ React.useEffect(() => {
180
+ onCheck?.(Array.from(checked), Array.from(indeterminate));
181
+ }, [onCheck, checked, indeterminate]);
182
+
183
+ React.useEffect(() => {
184
+ onExpand?.(Array.from(expanded));
185
+ }, [onExpand, expanded]);
186
+
187
+ React.useEffect(() => {
188
+ if (searchText) {
189
+ InteractionManager.runAfterInteractions(() => {
190
+ updateExpanded(new Set(initialTreeViewData.flatMap(
191
+ (item) => getIds(item)
192
+ )));
193
+ });
194
+ }
195
+ else if (prevSearchText && prevSearchText !== "") {
196
+ /* Collapse all nodes only if previous search query was non-empty: this is
197
+ done to prevent node collapse on first render if preExpandedIds is provided */
198
+ InteractionManager.runAfterInteractions(() => {
199
+ updateExpanded(new Set());
200
+ });
201
+ }
202
+ }, [
203
+ getIds,
204
+ initialTreeViewData,
205
+ prevSearchText,
206
+ searchText,
207
+ updateExpanded
208
+ ]);
209
+
210
+ React.useEffect(() => {
211
+ return () => {
212
+ cleanUpTreeViewStore();
213
+ };
214
+ }, [cleanUpTreeViewStore]);
215
+
216
+ return (
217
+ <NodeList
218
+ storeId={storeId}
219
+
220
+ scrollToNodeHandlerRef={scrollToNodeHandlerRef}
221
+ initialScrollNodeID={initialScrollNodeID}
222
+
223
+ treeFlashListProps={treeFlashListProps}
224
+ checkBoxViewStyleProps={checkBoxViewStyleProps}
225
+ indentationMultiplier={indentationMultiplier}
226
+
227
+ CheckboxComponent={CheckboxComponent}
228
+ ExpandCollapseIconComponent={ExpandCollapseIconComponent}
229
+ ExpandCollapseTouchableComponent={ExpandCollapseTouchableComponent}
230
+
231
+ CustomNodeRowComponent={CustomNodeRowComponent}
232
+ />
233
+ );
205
234
  }
235
+
206
236
  const _TreeView = React.forwardRef(_innerTreeView) as <ID>(
207
- props: TreeViewProps<ID> & { ref?: React.ForwardedRef<TreeViewRef<ID>> }
237
+ props: TreeViewProps<ID> & { ref?: React.ForwardedRef<TreeViewRef<ID>>; }
208
238
  ) => ReturnType<typeof _innerTreeView>;
209
239
 
210
240
  export const TreeView = typedMemo<typeof _TreeView>(_TreeView);
@@ -27,6 +27,7 @@ import { CustomExpandCollapseIcon } from "./CustomExpandCollapseIcon";
27
27
  import { defaultIndentationMultiplier } from "../constants/treeView.constants";
28
28
  import { useShallow } from 'zustand/react/shallow';
29
29
  import { typedMemo } from "../utils/typedMemo";
30
+ import { ScrollToNodeHandler } from "../handlers/ScrollToNodeHandler";
30
31
 
31
32
  const NodeList = typedMemo(_NodeList);
32
33
  export default NodeList;
@@ -35,6 +36,9 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
35
36
  const {
36
37
  storeId,
37
38
 
39
+ scrollToNodeHandlerRef,
40
+ initialScrollNodeID,
41
+
38
42
  treeFlashListProps,
39
43
  checkBoxViewStyleProps,
40
44
  indentationMultiplier,
@@ -61,6 +65,10 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
61
65
  })
62
66
  ));
63
67
 
68
+ const flashListRef = React.useRef<FlashList<__FlattenedTreeNode__<ID>> | null>(null);
69
+
70
+ const [initialScrollIndex, setInitialScrollIndex] = React.useState<number>(-1);
71
+
64
72
  // First we filter the tree as per the search term and keys
65
73
  const filteredTree = React.useMemo(() => getFilteredTreeData<ID>(
66
74
  initialTreeViewData,
@@ -112,17 +120,29 @@ function _NodeList<ID>(props: NodeListProps<ID>) {
112
120
  ]);
113
121
 
114
122
  return (
115
- <FlashList
116
- estimatedItemSize={36}
117
- removeClippedSubviews={true}
118
- keyboardShouldPersistTaps="handled"
119
- drawDistance={50}
120
- ListHeaderComponent={<HeaderFooterView />}
121
- ListFooterComponent={<HeaderFooterView />}
122
- {...treeFlashListProps}
123
- data={flattenedFilteredNodes}
124
- renderItem={nodeRenderer}
125
- />
123
+ <>
124
+ <ScrollToNodeHandler
125
+ ref={scrollToNodeHandlerRef}
126
+ storeId={storeId}
127
+ flashListRef={flashListRef}
128
+ flattenedFilteredNodes={flattenedFilteredNodes}
129
+ setInitialScrollIndex={setInitialScrollIndex}
130
+ initialScrollNodeID={initialScrollNodeID} />
131
+
132
+ <FlashList
133
+ ref={flashListRef}
134
+ estimatedItemSize={36}
135
+ initialScrollIndex={initialScrollIndex}
136
+ removeClippedSubviews={true}
137
+ keyboardShouldPersistTaps="handled"
138
+ drawDistance={50}
139
+ ListHeaderComponent={<HeaderFooterView />}
140
+ ListFooterComponent={<HeaderFooterView />}
141
+ {...treeFlashListProps}
142
+ data={flattenedFilteredNodes}
143
+ renderItem={nodeRenderer}
144
+ />
145
+ </>
126
146
  );
127
147
  };
128
148
 
@@ -0,0 +1,222 @@
1
+ /**
2
+ * ScrollToNodeHandler Component
3
+ *
4
+ * This component provides an imperative handle to scroll to a specified node within a tree view.
5
+ * The scrolling action is orchestrated via a two-step "milestone" mechanism that ensures the target
6
+ * node is both expanded in the tree and that the rendered list reflects this expansion before the scroll
7
+ * is performed.
8
+ *
9
+ * The two key milestones tracked by the `expandAndScrollToNodeQueue` state are:
10
+ * 1. EXPANDED: Indicates that the expansion logic for the target node has been initiated.
11
+ * 2. RENDERED: Indicates that the list has re-rendered with the expanded node included.
12
+ *
13
+ * When the `scrollToNodeID` method is called:
14
+ * - The scroll parameters (target node ID, animation preferences, view offset/position) are stored in a ref.
15
+ * - The target node's expansion is triggered via the `expandNodes` helper.
16
+ * - The `expandAndScrollToNodeQueue` state is updated to mark that expansion has begun.
17
+ *
18
+ * As the component re-renders (e.g., after the node expansion changes the rendered list):
19
+ * - A useEffect monitors changes to the list, and once it detects the expansion has occurred,
20
+ * it updates the queue to include the RENDERED milestone.
21
+ *
22
+ * A layout effect then waits for both conditions to be met:
23
+ * - The target node is confirmed to be in the expanded set.
24
+ * - The `expandAndScrollToNodeQueue` exactly matches the expected milestones ([EXPANDED, RENDERED]).
25
+ *
26
+ * Once both conditions are satisfied:
27
+ * - The index of the target node is determined within the latest flattened node list.
28
+ * - The flash list is scrolled to that index.
29
+ * - The queued scroll parameters and milestone queue are reset.
30
+ *
31
+ * This design ensures that the scroll action is performed only after the target node is fully present
32
+ * in the UI, thus preventing issues with attempting to scroll to an element that does not exist yet.
33
+ */
34
+
35
+ import React from "react";
36
+ import { expandNodes } from "../helpers/expandCollapse.helper";
37
+ import { useTreeViewStore } from "../store/treeView.store";
38
+ import { useShallow } from "zustand/react/shallow";
39
+ import { __FlattenedTreeNode__ } from "../types/treeView.types";
40
+ import { typedMemo } from "../utils/typedMemo";
41
+ import { isEqual } from "lodash";
42
+
43
+ interface Props<ID> {
44
+ storeId: string;
45
+ flashListRef: React.MutableRefObject<any>;
46
+ flattenedFilteredNodes: __FlattenedTreeNode__<ID>[];
47
+ setInitialScrollIndex: React.Dispatch<React.SetStateAction<number>>;
48
+ initialScrollNodeID: ID | undefined;
49
+ }
50
+
51
+ export interface ScrollToNodeParams<ID> {
52
+ nodeId: ID;
53
+ expandScrolledNode?: boolean;
54
+
55
+ animated?: boolean;
56
+ viewOffset?: number;
57
+ viewPosition?: number;
58
+ }
59
+
60
+ // Enum representing the two milestones needed before scrolling
61
+ enum ExpandQueueAction {
62
+ EXPANDED,
63
+ RENDERED,
64
+ }
65
+
66
+ export interface ScrollToNodeHandlerRef<ID> {
67
+ scrollToNodeID: (params: ScrollToNodeParams<ID>) => void;
68
+ }
69
+
70
+ function _innerScrollToNodeHandler<ID>(
71
+ props: Props<ID>,
72
+ ref: React.ForwardedRef<ScrollToNodeHandlerRef<ID>>
73
+ ) {
74
+ const {
75
+ storeId,
76
+ flashListRef,
77
+ flattenedFilteredNodes,
78
+ setInitialScrollIndex,
79
+ initialScrollNodeID
80
+ } = props;
81
+
82
+ const { expanded, childToParentMap } = useTreeViewStore<ID>(storeId)(useShallow(
83
+ state => ({
84
+ expanded: state.expanded,
85
+ childToParentMap: state.childToParentMap
86
+ })
87
+ ));
88
+
89
+ React.useImperativeHandle(ref, () => ({
90
+ scrollToNodeID: (params: ScrollToNodeParams<ID>) => {
91
+ queuedScrollToNodeParams.current = params;
92
+ // Mark that expansion is initiated.
93
+ setExpandAndScrollToNodeQueue([ExpandQueueAction.EXPANDED]);
94
+ // Trigger expansion logic (this may update the store and subsequently re-render the list).
95
+ expandNodes(
96
+ storeId,
97
+ [queuedScrollToNodeParams.current.nodeId],
98
+ !queuedScrollToNodeParams.current.expandScrolledNode
99
+ );
100
+ }
101
+ }), [storeId]);
102
+
103
+ // Ref to store the scroll parameters for the queued action.
104
+ const queuedScrollToNodeParams = React.useRef<ScrollToNodeParams<ID> | null>(null);
105
+
106
+ // State to track progression: first the expansion is triggered, then the list is rendered.
107
+ const [expandAndScrollToNodeQueue, setExpandAndScrollToNodeQueue]
108
+ = React.useState<ExpandQueueAction[]>([]);
109
+
110
+ const latestFlattenedFilteredNodesRef = React.useRef(flattenedFilteredNodes);
111
+
112
+ /* When the rendered node list changes, update the ref.
113
+ If an expansion was triggered, mark that the list is now rendered. */
114
+ React.useEffect(() => {
115
+ setExpandAndScrollToNodeQueue(prevQueue => {
116
+ if (prevQueue.includes(ExpandQueueAction.EXPANDED)) {
117
+ latestFlattenedFilteredNodesRef.current = flattenedFilteredNodes;
118
+ return [
119
+ ExpandQueueAction.EXPANDED,
120
+ ExpandQueueAction.RENDERED
121
+ ];
122
+ } else {
123
+ return prevQueue;
124
+ }
125
+ });
126
+ }, [flattenedFilteredNodes]);
127
+
128
+ /* Once the target node is expanded and the list is updated (milestones reached),
129
+ perform the scroll using the latest node list. */
130
+ React.useLayoutEffect(() => {
131
+ if (queuedScrollToNodeParams.current === null)
132
+ return;
133
+
134
+ if (!isEqual(
135
+ expandAndScrollToNodeQueue,
136
+ [ExpandQueueAction.EXPANDED, ExpandQueueAction.RENDERED]
137
+ )) {
138
+ return;
139
+ }
140
+
141
+ // If node is set to not expand
142
+ if (!queuedScrollToNodeParams.current.expandScrolledNode) {
143
+ let parentId: ID | undefined;
144
+ // Get the parent's id of the node to scroll to
145
+ if (childToParentMap.has(queuedScrollToNodeParams.current.nodeId)) {
146
+ parentId = childToParentMap.get(queuedScrollToNodeParams.current.nodeId) as ID;
147
+ }
148
+
149
+ // Ensure if the parent is expanded before proceeding to scroll to the node
150
+ if (parentId && !expanded.has(parentId))
151
+ return;
152
+ }
153
+ // If node is set to expand
154
+ else {
155
+ if (!expanded.has(queuedScrollToNodeParams.current.nodeId))
156
+ return;
157
+ }
158
+
159
+ const {
160
+ nodeId,
161
+ animated,
162
+ viewOffset,
163
+ viewPosition
164
+ } = queuedScrollToNodeParams.current!;
165
+
166
+ function scrollToItem() {
167
+ const index = latestFlattenedFilteredNodesRef.current.findIndex(
168
+ item => item.id === nodeId
169
+ );
170
+
171
+ if (index !== -1 && flashListRef.current) {
172
+ // Scroll to the target index.
173
+ flashListRef.current.scrollToIndex({
174
+ index,
175
+ animated,
176
+ viewOffset,
177
+ viewPosition
178
+ });
179
+ } else {
180
+ if (__DEV__) {
181
+ console.info("Cannot find the item of the mentioned id to scroll in the rendered tree view list data!");
182
+ }
183
+ }
184
+
185
+ // Clear the queued parameters and reset the expansion/render queue.
186
+ queuedScrollToNodeParams.current = null;
187
+ setExpandAndScrollToNodeQueue([]);
188
+ }
189
+
190
+ scrollToItem();
191
+ }, [childToParentMap, expanded, flashListRef, expandAndScrollToNodeQueue]);
192
+
193
+ ////////////////////////////// Handle Initial Scroll /////////////////////////////
194
+ /* On first render, if an initial scroll target is provided, determine its index.
195
+ This is done only once. */
196
+ const initialScrollDone = React.useRef(false);
197
+ React.useLayoutEffect(() => {
198
+ if (initialScrollDone.current) return;
199
+
200
+ const index = flattenedFilteredNodes.findIndex(
201
+ item => item.id === initialScrollNodeID
202
+ );
203
+
204
+ setInitialScrollIndex(index);
205
+
206
+ if (index !== -1) {
207
+ initialScrollDone.current = true;
208
+ }
209
+ // eslint-disable-next-line react-hooks/exhaustive-deps
210
+ }, [flattenedFilteredNodes, initialScrollNodeID]);
211
+ /////////////////////////////////////////////////////////////////////////////////
212
+
213
+ return null;
214
+ }
215
+
216
+ const _ScrollToNodeHandler = React.forwardRef(_innerScrollToNodeHandler) as <ID>(
217
+ props: Props<ID> & { ref?: React.ForwardedRef<ScrollToNodeHandlerRef<ID>>; }
218
+ ) => ReturnType<typeof _innerScrollToNodeHandler>;
219
+
220
+ export const ScrollToNodeHandler = typedMemo<
221
+ typeof _ScrollToNodeHandler
222
+ >(_ScrollToNodeHandler);