11import { flow } from '@lit-labs/virtualizer/layouts/flow.js' ;
22import { css , html , nothing } from 'lit' ;
33import { customElement , property , state } from 'lit/decorators.js' ;
4+ import { keyed } from 'lit/directives/keyed.js' ;
45import type { Ref } from 'lit/directives/ref.js' ;
56import { createRef , ref } from 'lit/directives/ref.js' ;
67import { when } from 'lit/directives/when.js' ;
@@ -71,6 +72,9 @@ export class GlTreeGenerator extends GlElement {
7172 @state ( )
7273 treeItems ?: TreeModelFlat [ ] = undefined ;
7374
75+ @state ( )
76+ private _virtualizerKey = 0 ;
77+
7478 @property ( { reflect : true } )
7579 guides ?: 'none' | 'onHover' | 'always' ;
7680
@@ -143,6 +147,10 @@ export class GlTreeGenerator extends GlElement {
143147 // This prevents stale node references when switching commits or toggling filters
144148 this . _nodeMap . clear ( ) ;
145149
150+ // Increment virtualizer key to force complete re-render
151+ // This prevents stale DOM node references in the repeat directive
152+ this . _virtualizerKey ++ ;
153+
146154 // Build both maps during tree flattening (single traversal)
147155 let treeItems : TreeModelFlat [ ] | undefined ;
148156 if ( this . _model != null ) {
@@ -271,6 +279,8 @@ export class GlTreeGenerator extends GlElement {
271279 // Use aria-activedescendant to indicate which tree item is active for screen readers
272280 const activeDescendant = this . _focusedItemPath ? `tree-item-${ this . _focusedItemPath } ` : undefined ;
273281
282+ // Use keyed directive to force virtualizer re-creation when model changes
283+ // This prevents stale DOM node references in the repeat directive
274284 return html `
275285 < div
276286 ${ ref ( this . scrollableRef ) }
@@ -284,14 +294,18 @@ export class GlTreeGenerator extends GlElement {
284294 @focus=${ this . handleContainerFocus }
285295 @blur=${ this . handleContainerBlur }
286296 >
287- < lit-virtualizer
288- ${ ref ( this . virtualizerRef ) }
289- .items =${ this . treeItems }
290- .keyFunction =${ ( item : TreeModelFlat ) => item . path }
291- .layout=${ flow ( { direction : 'vertical' } ) }
292- .renderItem=${ ( node : TreeModelFlat ) => this . renderTreeItem ( node ) }
293- .scroller=${ Boolean ( this . scrollableRef . value ) }
294- > </ lit-virtualizer >
297+ ${ keyed (
298+ this . _virtualizerKey ,
299+ html `< lit-virtualizer
300+ class ="scrollable "
301+ ${ ref ( this . virtualizerRef ) }
302+ .items =${ this . treeItems }
303+ .keyFunction =${ ( item : TreeModelFlat ) => item . path }
304+ .layout=${ flow ( { direction : 'vertical' } ) }
305+ .renderItem=${ ( node : TreeModelFlat ) => this . renderTreeItem ( node ) }
306+ scroller
307+ > </ lit-virtualizer > ` ,
308+ ) }
295309 </ div >
296310 ` ;
297311 }
@@ -317,9 +331,13 @@ export class GlTreeGenerator extends GlElement {
317331 private rebuildFlattenedTree ( ) {
318332 if ( ! this . _model ) return ;
319333
334+ // Clear stale node map before processing new model
335+ // This prevents stale node references when expanding/collapsing nodes
336+ this . _nodeMap . clear ( ) ;
337+
320338 const size = this . _model . length ;
321339 const newTreeItems = this . _model . reduce < TreeModelFlat [ ] > ( ( acc , node , index ) => {
322- acc . push ( ...flattenTree ( node , size , index + 1 ) ) ;
340+ acc . push ( ...flattenTree ( node , size , index + 1 , undefined , this . _nodeMap ) ) ;
323341 return acc ;
324342 } , [ ] ) ;
325343
0 commit comments