1
1
import { VirtualItem , Virtualizer , useVirtualizer } from "@tanstack/react-virtual" ;
2
2
import { motion } from "framer-motion" ;
3
3
import { MutableRefObject , RefObject , useCallback , useEffect , useReducer , useRef } from "react" ;
4
- import { UnmountClosed } from "react-collapse" ;
5
4
import { cn } from "~/utils/cn" ;
6
5
import { NodeState , NodesState , reducer } from "./reducer" ;
7
- import { applyFilterToState , concreteStateFromInput , selectedIdFromState } from "./utils" ;
6
+ import { concreteStateFromInput , selectedIdFromState } from "./utils" ;
8
7
9
8
export type TreeViewProps < TData > = {
10
9
tree : FlatTree < TData > ;
@@ -104,23 +103,22 @@ export function TreeView<TData>({
104
103
if ( ! node ) return null ;
105
104
const state = nodes [ node . id ] ;
106
105
if ( ! state ) return null ;
106
+ if ( ! state . visible ) return null ;
107
107
return (
108
108
< div
109
109
key = { node . id }
110
110
data-index = { virtualItem . index }
111
111
ref = { virtualizer . measureElement }
112
- className = "overflow-clip [&_.ReactCollapse--collapse]:transition-all "
112
+ className = "overflow-clip"
113
113
{ ...getNodeProps ( node . id ) }
114
114
>
115
- < UnmountClosed key = { node . id } isOpened = { state . visible } >
116
- { renderNode ( {
117
- node,
118
- state,
119
- index : virtualItem . index ,
120
- virtualizer : virtualizer ,
121
- virtualItem,
122
- } ) }
123
- </ UnmountClosed >
115
+ { renderNode ( {
116
+ node,
117
+ state,
118
+ index : virtualItem . index ,
119
+ virtualizer : virtualizer ,
120
+ virtualItem,
121
+ } ) }
124
122
</ div >
125
123
) ;
126
124
} ) }
@@ -130,19 +128,23 @@ export function TreeView<TData>({
130
128
) ;
131
129
}
132
130
133
- type TreeStateHookProps < TData > = {
131
+ export type Filter < TData , TFilterValue > = {
132
+ value ?: TFilterValue ;
133
+ fn : ( value : TFilterValue , node : FlatTreeItem < TData > ) => boolean ;
134
+ } ;
135
+
136
+ type TreeStateHookProps < TData , TFilterValue > = {
134
137
tree : FlatTree < TData > ;
135
138
selectedId ?: string ;
136
139
collapsedIds ?: string [ ] ;
137
140
onSelectedIdChanged ?: ( selectedId : string | undefined ) => void ;
138
- onCollapsedIdsChanged ?: ( collapsedIds : string [ ] ) => void ;
139
141
estimatedRowHeight : ( params : {
140
142
node : FlatTreeItem < TData > ;
141
143
state : NodeState ;
142
144
index : number ;
143
145
} ) => number ;
144
146
parentRef : RefObject < any > ;
145
- filter ?: ( node : FlatTreeItem < TData > ) => boolean ;
147
+ filter ?: Filter < TData , TFilterValue > ;
146
148
} ;
147
149
148
150
//this is so Framer Motion can be used to render the components
@@ -178,24 +180,24 @@ export type UseTreeStateOutput = {
178
180
scrollToNode : ( id : string ) => void ;
179
181
} ;
180
182
181
- export function useTree < TData > ( {
183
+ export function useTree < TData , TFilterValue > ( {
182
184
tree,
183
185
selectedId,
184
186
collapsedIds,
185
187
onSelectedIdChanged,
186
- onCollapsedIdsChanged,
187
188
parentRef,
188
189
estimatedRowHeight,
189
190
filter,
190
- } : TreeStateHookProps < TData > ) : UseTreeStateOutput {
191
+ } : TreeStateHookProps < TData , TFilterValue > ) : UseTreeStateOutput {
191
192
const previousNodeCount = useRef ( tree . length ) ;
192
193
const previousSelectedId = useRef < string | undefined > ( selectedId ) ;
193
194
194
195
const [ state , dispatch ] = useReducer (
195
196
reducer ,
196
- concreteStateFromInput ( { tree, selectedId, collapsedIds } )
197
+ concreteStateFromInput ( { tree, selectedId, collapsedIds, filter } )
197
198
) ;
198
199
200
+ //fire onSelectedIdChanged()
199
201
useEffect ( ( ) => {
200
202
const selectedId = selectedIdFromState ( state . nodes ) ;
201
203
if ( selectedId !== previousSelectedId . current ) {
@@ -204,22 +206,33 @@ export function useTree<TData>({
204
206
}
205
207
} , [ state . changes . selectedId ] ) ;
206
208
207
- useEffect ( ( ) => {
208
- if ( state . changes . collapsedIds ) {
209
- onCollapsedIdsChanged ?.( state . changes . collapsedIds ) ;
210
- }
211
- } , [ state . changes . collapsedIds ] ) ;
212
-
209
+ //update tree when the number of nodes changes
213
210
useEffect ( ( ) => {
214
211
if ( tree . length !== previousNodeCount . current ) {
215
212
previousNodeCount . current = tree . length ;
216
213
dispatch ( { type : "UPDATE_TREE" , payload : { tree } } ) ;
217
214
}
218
215
} , [ previousNodeCount . current , tree . length ] ) ;
219
216
217
+ //update the filter, if it's changed
218
+ const previousFilter = useRef ( filter ) ;
219
+ useEffect ( ( ) => {
220
+ //check if the value (not reference) of the filter is the same
221
+ const previousValue = previousFilter . current
222
+ ? JSON . stringify ( previousFilter . current . value )
223
+ : undefined ;
224
+ const newValue = filter ? JSON . stringify ( filter . value ) : undefined ;
225
+
226
+ previousFilter . current = filter ;
227
+
228
+ if ( previousValue !== newValue ) {
229
+ dispatch ( { type : "UPDATE_FILTER" , payload : { filter } } ) ;
230
+ }
231
+ } , [ filter ?. value ] ) ;
232
+
220
233
const virtualizer = useVirtualizer ( {
221
- count : tree . length ,
222
- getItemKey : ( index ) => tree [ index ] . id ,
234
+ count : state . visibleNodeIds . length ,
235
+ getItemKey : ( index ) => state . visibleNodeIds [ index ] ,
223
236
getScrollElement : ( ) => parentRef . current ,
224
237
estimateSize : ( index : number ) => {
225
238
return estimatedRowHeight ( {
@@ -269,21 +282,21 @@ export function useTree<TData>({
269
282
270
283
const expandNode = useCallback (
271
284
( id : string , scrollToNode = true ) => {
272
- dispatch ( { type : "EXPAND_NODE" , payload : { id, tree , scrollToNode, scrollToNodeFn } } ) ;
285
+ dispatch ( { type : "EXPAND_NODE" , payload : { id, scrollToNode, scrollToNodeFn } } ) ;
273
286
} ,
274
287
[ state ]
275
288
) ;
276
289
277
290
const collapseNode = useCallback (
278
291
( id : string ) => {
279
- dispatch ( { type : "COLLAPSE_NODE" , payload : { id, tree } } ) ;
292
+ dispatch ( { type : "COLLAPSE_NODE" , payload : { id } } ) ;
280
293
} ,
281
294
[ state ]
282
295
) ;
283
296
284
297
const toggleExpandNode = useCallback (
285
298
( id : string , scrollToNode = true ) => {
286
- dispatch ( { type : "TOGGLE_EXPAND_NODE" , payload : { id, tree , scrollToNode, scrollToNodeFn } } ) ;
299
+ dispatch ( { type : "TOGGLE_EXPAND_NODE" , payload : { id, scrollToNode, scrollToNodeFn } } ) ;
287
300
} ,
288
301
[ state ]
289
302
) ;
@@ -292,7 +305,7 @@ export function useTree<TData>({
292
305
( scrollToNode = true ) => {
293
306
dispatch ( {
294
307
type : "SELECT_FIRST_VISIBLE_NODE" ,
295
- payload : { tree , scrollToNode, scrollToNodeFn } ,
308
+ payload : { scrollToNode, scrollToNodeFn } ,
296
309
} ) ;
297
310
} ,
298
311
[ tree , state ]
@@ -302,7 +315,7 @@ export function useTree<TData>({
302
315
( scrollToNode = true ) => {
303
316
dispatch ( {
304
317
type : "SELECT_LAST_VISIBLE_NODE" ,
305
- payload : { tree , scrollToNode, scrollToNodeFn } ,
318
+ payload : { scrollToNode, scrollToNodeFn } ,
306
319
} ) ;
307
320
} ,
308
321
[ tree , state ]
@@ -312,7 +325,7 @@ export function useTree<TData>({
312
325
( scrollToNode = true ) => {
313
326
dispatch ( {
314
327
type : "SELECT_NEXT_VISIBLE_NODE" ,
315
- payload : { tree , scrollToNode, scrollToNodeFn } ,
328
+ payload : { scrollToNode, scrollToNodeFn } ,
316
329
} ) ;
317
330
} ,
318
331
[ state ]
@@ -322,7 +335,7 @@ export function useTree<TData>({
322
335
( scrollToNode = true ) => {
323
336
dispatch ( {
324
337
type : "SELECT_PREVIOUS_VISIBLE_NODE" ,
325
- payload : { tree , scrollToNode, scrollToNodeFn } ,
338
+ payload : { scrollToNode, scrollToNodeFn } ,
326
339
} ) ;
327
340
} ,
328
341
[ state ]
@@ -332,43 +345,43 @@ export function useTree<TData>({
332
345
( scrollToNode = true ) => {
333
346
dispatch ( {
334
347
type : "SELECT_PARENT_NODE" ,
335
- payload : { tree , scrollToNode, scrollToNodeFn } ,
348
+ payload : { scrollToNode, scrollToNodeFn } ,
336
349
} ) ;
337
350
} ,
338
351
[ state ]
339
352
) ;
340
353
341
354
const expandAllBelowDepth = useCallback (
342
355
( depth : number ) => {
343
- dispatch ( { type : "EXPAND_ALL_BELOW_DEPTH" , payload : { tree , depth } } ) ;
356
+ dispatch ( { type : "EXPAND_ALL_BELOW_DEPTH" , payload : { depth } } ) ;
344
357
} ,
345
358
[ state ]
346
359
) ;
347
360
348
361
const collapseAllBelowDepth = useCallback (
349
362
( depth : number ) => {
350
- dispatch ( { type : "COLLAPSE_ALL_BELOW_DEPTH" , payload : { tree , depth } } ) ;
363
+ dispatch ( { type : "COLLAPSE_ALL_BELOW_DEPTH" , payload : { depth } } ) ;
351
364
} ,
352
365
[ state ]
353
366
) ;
354
367
355
368
const expandLevel = useCallback (
356
369
( level : number ) => {
357
- dispatch ( { type : "EXPAND_LEVEL" , payload : { tree , level } } ) ;
370
+ dispatch ( { type : "EXPAND_LEVEL" , payload : { level } } ) ;
358
371
} ,
359
372
[ state ]
360
373
) ;
361
374
362
375
const collapseLevel = useCallback (
363
376
( level : number ) => {
364
- dispatch ( { type : "COLLAPSE_LEVEL" , payload : { tree , level } } ) ;
377
+ dispatch ( { type : "COLLAPSE_LEVEL" , payload : { level } } ) ;
365
378
} ,
366
379
[ state ]
367
380
) ;
368
381
369
382
const toggleExpandLevel = useCallback (
370
383
( level : number ) => {
371
- dispatch ( { type : "TOGGLE_EXPAND_LEVEL" , payload : { tree , level } } ) ;
384
+ dispatch ( { type : "TOGGLE_EXPAND_LEVEL" , payload : { level } } ) ;
372
385
} ,
373
386
[ state ]
374
387
) ;
@@ -480,7 +493,7 @@ export function useTree<TData>({
480
493
481
494
return {
482
495
selected : selectedIdFromState ( state . nodes ) ,
483
- nodes : filter ? applyFilterToState ( tree , state . nodes , filter ) : state . nodes ,
496
+ nodes : state . nodes ,
484
497
getTreeProps,
485
498
getNodeProps,
486
499
selectNode,
0 commit comments