77 */
88
99import { computed , signal , SignalLike } from '../signal-like/signal-like' ;
10+ import { ExpansionItem , ListExpansion , ListExpansionInputs } from '../expansion/expansion' ;
1011import { ListFocus , ListFocusInputs , ListFocusItem } from '../list-focus/list-focus' ;
1112import {
1213 ListNavigation ,
@@ -28,15 +29,17 @@ import {NavOptions} from '../list/list';
2829
2930/** Represents an item in the tree. */
3031export interface TreeItem < V , T extends TreeItem < V , T > >
31- extends ListTypeaheadItem , ListNavigationItem , ListSelectionItem < V > , ListFocusItem {
32+ extends
33+ ListTypeaheadItem ,
34+ ListNavigationItem ,
35+ ListSelectionItem < V > ,
36+ ListFocusItem ,
37+ ExpansionItem {
3238 /** The children of this item. */
33- children ?: SignalLike < T [ ] > ;
34-
35- /** Whether this item is expanded. */
36- expanded ?: SignalLike < boolean > ;
39+ children : SignalLike < T [ ] | undefined > ;
3740
3841 /** The parent of this item. */
39- parent ?: T ;
42+ parent : SignalLike < T | undefined > ;
4043
4144 /** Whether this item is visible. */
4245 visible : SignalLike < boolean > ;
@@ -46,7 +49,8 @@ export interface TreeItem<V, T extends TreeItem<V, T>>
4649export type TreeInputs < T extends TreeItem < V , T > , V > = ListFocusInputs < T > &
4750 ListNavigationInputs < T > &
4851 ListSelectionInputs < T , V > &
49- ListTypeaheadInputs < T > ;
52+ ListTypeaheadInputs < T > &
53+ ListExpansionInputs ;
5054
5155/** Controls the state of a tree. */
5256export class Tree < T extends TreeItem < V , T > , V > {
@@ -62,6 +66,9 @@ export class Tree<T extends TreeItem<V, T>, V> {
6266 /** Controls focus for the tree. */
6367 focusBehavior : ListFocus < T > ;
6468
69+ /** Controls expansion for the tree. */
70+ expansionBehavior : ListExpansion ;
71+
6572 /** Whether the tree is disabled. */
6673 disabled = computed ( ( ) => this . focusBehavior . isListDisabled ( ) ) ;
6774
@@ -81,10 +88,11 @@ export class Tree<T extends TreeItem<V, T>, V> {
8188 private _wrap = signal ( true ) ;
8289
8390 constructor ( readonly inputs : TreeInputs < T , V > ) {
84- this . focusBehavior = new ListFocus ( inputs ) ;
85- this . selectionBehavior = new ListSelection ( { ...inputs , focusManager : this . focusBehavior } ) ;
86- this . typeaheadBehavior = new ListTypeahead ( { ...inputs , focusManager : this . focusBehavior } ) ;
87- this . navigationBehavior = new ListNavigation ( {
91+ this . focusBehavior = new ListFocus < T > ( inputs ) ;
92+ this . selectionBehavior = new ListSelection < T , V > ( { ...inputs , focusManager : this . focusBehavior } ) ;
93+ this . typeaheadBehavior = new ListTypeahead < T > ( { ...inputs , focusManager : this . focusBehavior } ) ;
94+ this . expansionBehavior = new ListExpansion ( inputs ) ;
95+ this . navigationBehavior = new ListNavigation < T > ( {
8896 ...inputs ,
8997 focusManager : this . focusBehavior ,
9098 wrap : computed ( ( ) => this . _wrap ( ) && this . inputs . wrap ( ) ) ,
@@ -138,7 +146,11 @@ export class Tree<T extends TreeItem<V, T>, V> {
138146 nextSibling ( opts ?: NavOptions < T > ) {
139147 this . _navigate ( opts , ( ) => {
140148 const item = this . inputs . activeItem ( ) ;
141- const items = item ?. parent ?. children ?.( ) ?. filter ( c => c . visible ( ) !== false ) ?? [ ] ;
149+ const items =
150+ item
151+ ?. parent ?.( )
152+ ?. children ?.( )
153+ ?. filter ( c => c . visible ( ) !== false ) ?? [ ] ;
142154 return this . navigationBehavior . next ( { items, ...opts } ) ;
143155 } ) ;
144156 }
@@ -147,15 +159,19 @@ export class Tree<T extends TreeItem<V, T>, V> {
147159 prevSibling ( opts ?: NavOptions < T > ) {
148160 this . _navigate ( opts , ( ) => {
149161 const item = this . inputs . activeItem ( ) ;
150- const items = item ?. parent ?. children ?.( ) ?. filter ( c => c . visible ( ) !== false ) ?? [ ] ;
162+ const items =
163+ item
164+ ?. parent ?.( )
165+ ?. children ?.( )
166+ ?. filter ( c => c . visible ( ) !== false ) ?? [ ] ;
151167 return this . navigationBehavior . prev ( { items, ...opts } ) ;
152168 } ) ;
153169 }
154170
155171 /** Navigates to the parent of the current active item. */
156172 parent ( opts ?: NavOptions < T > ) {
157173 this . _navigate ( opts , ( ) =>
158- this . navigationBehavior . goto ( this . inputs . activeItem ( ) ?. parent , opts ) ,
174+ this . navigationBehavior . goto ( this . inputs . activeItem ( ) ?. parent ?. ( ) , opts ) ,
159175 ) ;
160176 }
161177
@@ -219,11 +235,59 @@ export class Tree<T extends TreeItem<V, T>, V> {
219235 this . selectionBehavior . toggleAll ( ) ;
220236 }
221237
238+ /** Toggles the expansion of the given item. */
239+ toggleExpansion ( item ?: T ) {
240+ item ??= this . inputs . activeItem ( ) ;
241+ if ( ! item || ! this . isFocusable ( item ) ) return ;
242+
243+ if ( this . isExpandable ( item ) ) {
244+ this . expansionBehavior . toggle ( item ) ;
245+ }
246+ }
247+
248+ /** Expands the given item. */
249+ expand ( item : T ) {
250+ if ( this . isExpandable ( item ) ) {
251+ this . expansionBehavior . open ( item ) ;
252+ }
253+ }
254+
255+ /** Collapses the given item. */
256+ collapse ( item : T ) {
257+ this . expansionBehavior . close ( item ) ;
258+ }
259+
260+ /** Expands all sibling items of the given item (or active item). */
261+ expandSiblings ( item ?: T ) {
262+ item ??= this . inputs . activeItem ( ) ;
263+ if ( ! item ) return ;
264+
265+ const parent = item . parent ?.( ) ;
266+ // TODO: This assumes that items without a parent are root items.
267+ const siblings = parent ? parent . children ?.( ) : this . inputs . items ( ) . filter ( i => ! i . parent ?.( ) ) ;
268+ siblings ?. forEach ( s => this . expand ( s ) ) ;
269+ }
270+
271+ /** Expands all items in the tree. */
272+ expandAll ( ) {
273+ this . expansionBehavior . openAll ( ) ;
274+ }
275+
276+ /** Collapses all items in the tree. */
277+ collapseAll ( ) {
278+ this . expansionBehavior . closeAll ( ) ;
279+ }
280+
222281 /** Checks if the given item is able to receive focus. */
223282 isFocusable ( item : T ) {
224283 return this . focusBehavior . isFocusable ( item ) ;
225284 }
226285
286+ /** Checks if the given item is expandable. */
287+ isExpandable ( item : T ) {
288+ return this . expansionBehavior . isExpandable ( item ) ;
289+ }
290+
227291 /** Handles updating selection for the tree. */
228292 updateSelection ( opts : NavOptions < T > = { anchor : true } ) {
229293 if ( opts . toggle ) {
@@ -265,9 +329,7 @@ export class Tree<T extends TreeItem<V, T>, V> {
265329 * Constructs navigation options with the visible items subset.
266330 */
267331 private _getNavOpts ( opts ?: NavOptions < T > ) : ListNavigationOpts < T > {
268- const visibleItems = this . inputs . items ( ) . filter ( i => {
269- return i . visible ( ) !== false ;
270- } ) ;
332+ const visibleItems = this . inputs . items ( ) . filter ( i => i . visible ( ) !== false ) ;
271333
272334 return {
273335 items : visibleItems ,
0 commit comments