@@ -46,9 +46,109 @@ export interface ControlledTreeProps<T> {
46
46
onSelect ?: ( nodes : TreeNode < T > [ ] ) => void ;
47
47
}
48
48
49
+ interface MantineTreeController {
50
+ isNodeChecked : ( value : string ) => boolean ;
51
+ isNodeIndeterminate : ( value : string ) => boolean ;
52
+ uncheckNode : ( value : string ) => void ;
53
+ checkNode : ( value : string ) => void ;
54
+ collapse : ( value : string ) => void ;
55
+ expand : ( value : string ) => void ;
56
+ deselect : ( value : string ) => void ;
57
+ select : ( value : string ) => void ;
58
+ }
59
+
60
+ interface TreeNodeRendererProps {
61
+ node : TreeNodeData ;
62
+ expanded : boolean ;
63
+ selected : boolean ;
64
+ hasChildren : boolean ;
65
+ elementProps : {
66
+ className ?: string ;
67
+ [ key : string ] : unknown ;
68
+ } ;
69
+ tree : MantineTreeController ;
70
+ selectionMode : TreeSelectionMode ;
71
+ }
72
+
49
73
// COMPONENTS
50
74
// -------------------------------------------------------------------------------------------------
51
75
76
+ const TreeNodeRenderer = memo ( function TreeNodeRenderer ( {
77
+ node,
78
+ expanded,
79
+ selected,
80
+ hasChildren,
81
+ elementProps,
82
+ tree,
83
+ selectionMode,
84
+ } : TreeNodeRendererProps ) {
85
+ const checked = tree . isNodeChecked ( node . value ) ;
86
+ const indeterminate = tree . isNodeIndeterminate ( node . value ) ;
87
+
88
+ const handleCheckboxClick = useCallback ( ( ) => {
89
+ if ( checked ) {
90
+ tree . uncheckNode ( node . value ) ;
91
+ } else {
92
+ tree . checkNode ( node . value ) ;
93
+ }
94
+ } , [ checked , node . value , tree ] ) ;
95
+
96
+ const handleExpandClick = useCallback ( ( ) => {
97
+ if ( expanded ) {
98
+ tree . collapse ( node . value ) ;
99
+ } else {
100
+ tree . expand ( node . value ) ;
101
+ }
102
+ } , [ expanded , node . value , tree ] ) ;
103
+
104
+ const handleNodeClick = useCallback ( ( ) => {
105
+ if ( selectionMode !== "single" ) {
106
+ return ;
107
+ }
108
+
109
+ if ( selected ) {
110
+ tree . deselect ( node . value ) ;
111
+ } else {
112
+ tree . select ( node . value ) ;
113
+ }
114
+ } , [ node . value , selected , selectionMode , tree ] ) ;
115
+
116
+ return (
117
+ < div
118
+ { ...elementProps }
119
+ className = { classNames ( styles . node , elementProps . className , {
120
+ [ styles . selectOnClick ] : selectionMode === "single" ,
121
+ [ styles . selectedPath ] : selected ,
122
+ } ) }
123
+ >
124
+ { selectionMode === "multiple" && (
125
+ < Checkbox . Indicator
126
+ className = { classNames ( styles . checkbox , { [ styles . filled ] : checked || indeterminate } ) }
127
+ checked = { checked }
128
+ indeterminate = { indeterminate }
129
+ onClick = { handleCheckboxClick }
130
+ />
131
+ ) }
132
+
133
+ { hasChildren && (
134
+ < ActionIcon
135
+ size = "compact-sm"
136
+ ml = { 2 }
137
+ color = "gray"
138
+ variant = "subtle"
139
+ onClick = { handleExpandClick }
140
+ >
141
+ { expanded ? < IoChevronDown /> : < IoChevronForward /> }
142
+ </ ActionIcon >
143
+ ) }
144
+
145
+ < div className = { styles . labelContainer } onClick = { handleNodeClick } >
146
+ < span > { node . label } </ span >
147
+ </ div >
148
+ </ div >
149
+ ) ;
150
+ } ) ;
151
+
52
152
/**
53
153
* Wrapper around Mantine's Tree component with controlled state management.
54
154
*
@@ -97,109 +197,52 @@ function ControlledTree<T extends object>({
97
197
return path ;
98
198
} , [ mantineNodes , nodes , selectedNodes , selectedMantineNodes ] ) ;
99
199
100
- const tree = useTree ( {
101
- initialExpandedState : getTreeExpandedState ( mantineNodes , pathToFirstSelectedNode ) ,
102
- initialSelectedState : selectedMantineNodes . map ( ( node ) => node . value ) ,
103
- } ) ;
200
+ // Initialize tree with memoized initial state
201
+ const initialTreeState = useMemo (
202
+ ( ) => ( {
203
+ initialExpandedState : getTreeExpandedState ( mantineNodes , pathToFirstSelectedNode ) ,
204
+ initialSelectedState : selectedMantineNodes . map ( ( node ) => node . value ) ,
205
+ } ) ,
206
+ [ mantineNodes , pathToFirstSelectedNode , selectedMantineNodes ] ,
207
+ ) ;
208
+
209
+ const tree = useTree ( initialTreeState ) ;
104
210
const { select, clearSelected, checkedState } = tree ;
105
211
106
212
const allNodesChecked = checkedState . length === numLeafNodes ;
107
213
const someNodesChecked = checkedState . length > 0 ;
108
- useEffect ( ( ) => {
109
- if ( selectionMode === "multiple" ) {
110
- onSelect ?.(
111
- filterUndefined (
112
- checkedState . map ( ( nodeId ) => findNodeById ( nodes , mantineNodeValueToId ( nodeId ) ) ) ,
113
- ) ,
114
- ) ;
115
- }
116
- } , [ checkedState , nodes , onSelect , selectionMode ] ) ;
117
214
118
- const renderTreeNode = useCallback (
119
- ( { node, expanded, selected, hasChildren, elementProps, tree } : RenderTreeNodePayload ) => {
120
- const checked = tree . isNodeChecked ( node . value ) ;
121
- const indeterminate = tree . isNodeIndeterminate ( node . value ) ;
122
-
123
- return (
124
- < div
125
- key = { node . value }
126
- { ...elementProps }
127
- className = { classNames ( styles . node , elementProps . className , {
128
- [ styles . selectOnClick ] : selectionMode === "single" ,
129
- [ styles . selectedPath ] : selected ,
130
- } ) }
131
- >
132
- { selectionMode === "multiple" && (
133
- < Checkbox . Indicator
134
- className = { classNames ( styles . checkbox , { [ styles . filled ] : checked || indeterminate } ) }
135
- checked = { checked }
136
- indeterminate = { indeterminate }
137
- onClick = { ( ) => {
138
- if ( checked ) {
139
- tree . uncheckNode ( node . value ) ;
140
- } else {
141
- tree . checkNode ( node . value ) ;
142
- }
143
- } }
144
- />
145
- ) }
146
-
147
- { hasChildren && (
148
- < ActionIcon
149
- size = "compact-sm"
150
- ml = { 2 }
151
- color = "gray"
152
- variant = "subtle"
153
- onClick = { ( ) => {
154
- if ( expanded ) {
155
- tree . collapse ( node . value ) ;
156
- } else {
157
- tree . expand ( node . value ) ;
158
- }
159
- } }
160
- >
161
- { expanded ? < IoChevronDown /> : < IoChevronForward /> }
162
- </ ActionIcon >
163
- ) }
164
-
165
- < div
166
- className = { styles . labelContainer }
167
- onClick = { ( ) => {
168
- if ( selectionMode !== "single" ) {
169
- return ;
170
- }
171
-
172
- if ( selected ) {
173
- tree . deselect ( node . value ) ;
174
- } else {
175
- tree . select ( node . value ) ;
176
- const selectedNode = findNodeById ( nodes , mantineNodeValueToId ( node . value ) ) ;
177
- if ( selectedNode ) {
178
- onSelect ?.( [ selectedNode ] ) ;
179
- }
180
- }
181
- } }
182
- >
183
- < span > { node . label } </ span >
184
- </ div >
185
- </ div >
186
- ) ;
215
+ const handleSelectionChange = useCallback (
216
+ ( nodes : TreeNode < T > [ ] ) => {
217
+ if ( selectionMode === "multiple" ) {
218
+ onSelect ?.(
219
+ filterUndefined (
220
+ checkedState . map ( ( nodeId ) => findNodeById ( nodes , mantineNodeValueToId ( nodeId ) ) ) ,
221
+ ) ,
222
+ ) ;
223
+ }
187
224
} ,
188
- [ nodes , onSelect , selectionMode ] ,
225
+ [ checkedState , onSelect , selectionMode ] ,
189
226
) ;
190
227
228
+ useEffect ( ( ) => {
229
+ handleSelectionChange ( nodes ) ;
230
+ } , [ handleSelectionChange , nodes ] ) ;
231
+
191
232
// Update tree state controlled selection changes
192
233
useEffect ( ( ) => {
193
234
if ( selectionMode === "single" ) {
194
235
clearSelected ( ) ;
195
236
const selectedNode = selectedMantineNodes [ 0 ] ;
196
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
197
- if ( selectedNode ) {
198
- select ( selectedNode . value ) ;
199
- }
237
+ select ( selectedNode . value ) ;
200
238
}
201
239
} , [ selectedMantineNodes , select , clearSelected , selectionMode ] ) ;
202
240
241
+ const renderTreeNode = useCallback (
242
+ ( props : RenderTreeNodePayload ) => < TreeNodeRenderer { ...props } selectionMode = { selectionMode } /> ,
243
+ [ selectionMode ] ,
244
+ ) ;
245
+
203
246
return (
204
247
< >
205
248
{ selectionMode === "multiple" && (
@@ -237,22 +280,25 @@ export default memo(ControlledTree) as typeof ControlledTree;
237
280
// UTILITIES
238
281
// -------------------------------------------------------------------------------------------------
239
282
240
- function mapNodesToMantineFormat < T > (
283
+ const nodeValuePathsMap = new WeakMap < TreeNode < unknown > [ ] , Map < string , string > > ( ) ;
284
+
285
+ function mapNodesToMantineFormat < T extends object > (
241
286
nodes : TreeNode < T > [ ] ,
242
- /** IDs of the selected node(s). */
243
287
selectedNodeIds : string [ ] ,
244
- /**
245
- * Accumulated map of node id to node value path.
246
- * Example:
247
- * - node 1: "1"
248
- * - node 2: "1/2"
249
- * - node 3: "1/2/3"
250
- */
251
- nodeValuePaths : Map < string , string > = new Map < string , string > ( ) ,
288
+ nodeValuePaths : Map < string , string > = nodeValuePathsMap . get ( nodes ) ?? new Map < string , string > ( ) ,
252
289
) : { mantineNodes : TreeNodeData [ ] ; numLeafNodes : number } {
290
+ // Cache the nodeValuePaths map for this nodes array
291
+ if ( ! nodeValuePathsMap . has ( nodes ) ) {
292
+ nodeValuePathsMap . set ( nodes , nodeValuePaths ) ;
293
+ }
294
+
253
295
let numLeafNodes = 0 ;
254
296
255
297
function getNodeValue ( node : TreeNode < T > ) {
298
+ // Check cache first
299
+ const cachedValue = nodeValuePaths . get ( node . id ) ;
300
+ if ( cachedValue ) return cachedValue ;
301
+
256
302
const parentNodeValue = node . parentId ? nodeValuePaths . get ( node . parentId ) : undefined ;
257
303
const value = parentNodeValue ? `${ parentNodeValue } /${ node . id } ` : node . id ;
258
304
nodeValuePaths . set ( node . id , value ) ;
0 commit comments