@@ -108,11 +108,20 @@ class CustomListSectionRenderable extends BoxRenderable {
108108class CustomListEmptyViewRenderable extends BoxRenderable {
109109 public emptyTitle : string
110110 public emptyDescription ?: string
111+ private parentList ?: CustomListRenderable
111112
112113 constructor ( ctx : RenderContext , options : CustomListEmptyViewOptions ) {
113114 super ( ctx , { ...options , height : 0 , overflow : 'hidden' } )
114- this . emptyTitle = options . emptyTitle
115+ this . emptyTitle = options . emptyTitle || 'No items'
115116 this . emptyDescription = options . emptyDescription
117+
118+ // Self-register with parent list after being added to tree
119+ this . onLifecyclePass = ( ) => {
120+ if ( ! this . parentList ) {
121+ this . parentList = findParent ( this , CustomListRenderable )
122+ this . parentList ?. registerEmptyView ( this )
123+ }
124+ }
116125 }
117126}
118127
@@ -223,22 +232,28 @@ class CustomListRenderable extends BoxRenderable {
223232 super . add ( this . statusText )
224233 }
225234
226- // Override add() so React children go into scrollBox
235+ // ─────────────────────────────────────────────────────────────────────────
236+ // Child Management
237+ //
238+ // All React children are redirected to scrollBox for proper scrolling.
239+ // Children self-register with the list via onLifecyclePass callback:
240+ //
241+ // 1. React calls list.add(child) for sections/items/emptyView
242+ // 2. We forward to scrollBox.add(child), which sets child.parent
243+ // 3. opentui calls child.onLifecyclePass() after parent is set
244+ // 4. Child traverses up via findParent() to find this list
245+ // 5. Child calls registerItem/Section/EmptyView to add itself to sets
246+ // 6. scheduleUpdate() debounces and triggers refilter/selection
247+ //
248+ // This avoids the list needing to traverse the tree to find children.
249+ // Stale refs are cleaned lazily in getAllItems/Sections by checking
250+ // if items are still connected to the tree via parent chain.
251+ // ─────────────────────────────────────────────────────────────────────────
252+
227253 add ( child : Renderable , index ?: number ) : number {
228- if ( child instanceof CustomListEmptyViewRenderable ) {
229- // EmptyView is data-only, not rendered - just store reference
230- this . emptyView = child
231- return - 1
232- }
233- // All other React children go into scrollBox
234- // Items/sections register themselves via onLifecyclePass
235254 return this . scrollBox . add ( child , index )
236255 }
237256
238- // ─────────────────────────────────────────────────────────────────────────
239- // Registration - items/sections register themselves via onLifecyclePass
240- // ─────────────────────────────────────────────────────────────────────────
241-
242257 registerItem ( item : CustomListItemRenderable ) {
243258 this . registeredItems . add ( item )
244259 this . scheduleUpdate ( )
@@ -249,6 +264,10 @@ class CustomListRenderable extends BoxRenderable {
249264 this . scheduleUpdate ( )
250265 }
251266
267+ registerEmptyView ( emptyView : CustomListEmptyViewRenderable ) {
268+ this . emptyView = emptyView
269+ }
270+
252271 private updateScheduled = false
253272 private scheduleUpdate ( ) {
254273 if ( this . updateScheduled ) return
0 commit comments