55 * Use of this source code is governed by an MIT-style license that can be
66 * found in the LICENSE file at https://angular.io/license
77 */
8- import { FocusableOption } from '@angular/cdk/a11y' ;
8+ import { TreeKeyManager , TreeKeyManagerItem } from '@angular/cdk/a11y' ;
9+ import { Directionality } from '@angular/cdk/bidi' ;
910import { coerceNumberProperty } from '@angular/cdk/coercion' ;
1011import { CollectionViewer , DataSource , isDataSource , SelectionModel } from '@angular/cdk/collections' ;
1112import {
@@ -16,12 +17,14 @@ import {
1617 ContentChildren ,
1718 Directive ,
1819 ElementRef ,
20+ EventEmitter ,
1921 Input ,
2022 IterableChangeRecord ,
2123 IterableDiffer ,
2224 IterableDiffers ,
2325 OnDestroy ,
2426 OnInit ,
27+ Output ,
2528 QueryList ,
2629 TrackByFunction ,
2730 ViewChild ,
@@ -75,6 +78,9 @@ function isNotNullish<T>(val: T | null | undefined): val is T {
7578 host : {
7679 'class' : 'cdk-tree' ,
7780 'role' : 'tree' ,
81+ '[attr.tabindex]' : '_getTabindex()' ,
82+ '(keydown)' : '_sendKeydownToKeyManager($event)' ,
83+ '(focus)' : '_focusInitialTreeItem()' ,
7884 } ,
7985 encapsulation : ViewEncapsulation . None ,
8086
@@ -214,7 +220,14 @@ export class CdkTree<T, K = T> implements AfterContentChecked, CollectionViewer,
214220 new Map < K , CdkTreeNode < T , K > > ( ) ,
215221 ) ;
216222
217- constructor ( private _differs : IterableDiffers , private _changeDetectorRef : ChangeDetectorRef ) { }
223+ /** The key manager for this tree. Handles focus and activation based on user keyboard input. */
224+ _keyManager : TreeKeyManager < CdkTreeNode < T , K > > ;
225+
226+ constructor (
227+ private _differs : IterableDiffers ,
228+ private _changeDetectorRef : ChangeDetectorRef ,
229+ private _dir : Directionality ,
230+ ) { }
218231
219232 ngOnInit ( ) {
220233 this . _dataDiffer = this . _differs . find ( [ ] ) . create ( this . trackBy ) ;
@@ -256,6 +269,26 @@ export class CdkTree<T, K = T> implements AfterContentChecked, CollectionViewer,
256269 }
257270 }
258271
272+ ngAfterContentInit ( ) {
273+ this . _keyManager = new TreeKeyManager ( {
274+ items : combineLatest ( [ this . _dataNodes , this . _nodes ] ) . pipe (
275+ map ( ( [ dataNodes , nodes ] ) =>
276+ dataNodes . map ( data => nodes . get ( this . _getExpansionKey ( data ) ) ) . filter ( isNotNullish ) ,
277+ ) ,
278+ ) ,
279+ trackBy : node => this . _getExpansionKey ( node . data ) ,
280+ typeAheadDebounceInterval : true ,
281+ horizontalOrientation : this . _dir . value ,
282+ } ) ;
283+
284+ this . _keyManager . change
285+ . pipe ( startWith ( null ) , pairwise ( ) , takeUntil ( this . _onDestroy ) )
286+ . subscribe ( ( [ prev , next ] ) => {
287+ prev ?. _setTabUnfocusable ( ) ;
288+ next ?. _setTabFocusable ( ) ;
289+ } ) ;
290+ }
291+
259292 ngAfterContentChecked ( ) {
260293 const defaultNodeDefs = this . _nodeDefs . filter ( def => ! def . when ) ;
261294 if ( defaultNodeDefs . length > 1 && ( typeof ngDevMode === 'undefined' || ngDevMode ) ) {
@@ -268,13 +301,17 @@ export class CdkTree<T, K = T> implements AfterContentChecked, CollectionViewer,
268301 }
269302 }
270303
271- // TODO(tinayuangao): Work on keyboard traversal and actions, make sure it's working for RTL
272- // and nested trees.
304+ _getTabindex ( ) {
305+ // If the `TreeKeyManager` has no active item, then we know that we need to focus the initial
306+ // item when the tree is focused. We set the tabindex to be `0` so that we can capture
307+ // the focus event and redirect it. Otherwise, we unset it.
308+ return this . _keyManager . getActiveItem ( ) ? null : 0 ;
309+ }
273310
274311 /**
275312 * Switch to the provided data source by resetting the data and unsubscribing from the current
276313 * render change subscription if one exists. If the data source is null, interpret this by
277- * clearing the node outlet. Otherwise start listening for new data.
314+ * clearIng the node outlet. Otherwise start listening for new data.
278315 */
279316 private _switchDataSource ( dataSource : DataSource < T > | Observable < T [ ] > | T [ ] ) {
280317 if ( this . _dataSource && typeof ( this . _dataSource as DataSource < T > ) . disconnect === 'function' ) {
@@ -649,6 +686,37 @@ export class CdkTree<T, K = T> implements AfterContentChecked, CollectionViewer,
649686 return group . indexOf ( dataNode ) + 1 ;
650687 }
651688
689+ /** Given a CdkTreeNode, gets the node that renders that node's parent's data. */
690+ _getNodeParent ( node : CdkTreeNode < T , K > ) {
691+ const parent = this . _parents . get ( node . data ) ;
692+ return parent && this . _nodes . value . get ( this . _getExpansionKey ( parent ) ) ;
693+ }
694+
695+ /** Given a CdkTreeNode, gets the nodes that renders that node's child data. */
696+ _getNodeChildren ( node : CdkTreeNode < T , K > ) {
697+ const children = coerceObservable ( this . _getChildrenAccessor ( ) ?.( node . data ) ?? [ ] ) ;
698+ return children . pipe (
699+ map ( children =>
700+ children
701+ . map ( child => this . _nodes . value . get ( this . _getExpansionKey ( child ) ) )
702+ . filter ( isNotNullish ) ,
703+ ) ,
704+ ) ;
705+ }
706+
707+ /** `keydown` event handler; this just passes the event to the `TreeKeyManager`. */
708+ _sendKeydownToKeyManager ( event : KeyboardEvent ) {
709+ this . _keyManager . onKeydown ( event ) ;
710+ }
711+
712+ /** `focus` event handler; this focuses the initial item if there isn't already one available. */
713+ _focusInitialTreeItem ( ) {
714+ if ( this . _keyManager . getActiveItem ( ) ) {
715+ return ;
716+ }
717+ this . _keyManager . onInitialFocus ( ) ;
718+ }
719+
652720 /**
653721 * Gets all nodes in the tree, through recursive expansion.
654722 *
@@ -811,9 +879,10 @@ export class CdkTree<T, K = T> implements AfterContentChecked, CollectionViewer,
811879 '[attr.aria-level]' : 'level + 1' ,
812880 '[attr.aria-posinset]' : '_getPositionInSet()' ,
813881 '[attr.aria-setsize]' : '_getSetSize()' ,
882+ 'tabindex' : '-1' ,
814883 } ,
815884} )
816- export class CdkTreeNode < T , K = T > implements FocusableOption , OnDestroy , OnInit {
885+ export class CdkTreeNode < T , K = T > implements OnDestroy , OnInit , TreeKeyManagerItem {
817886 /**
818887 * The role of the tree node.
819888 *
@@ -844,6 +913,16 @@ export class CdkTreeNode<T, K = T> implements FocusableOption, OnDestroy, OnInit
844913 }
845914 }
846915
916+ /**
917+ * Whether or not this node is disabled. If it's disabled, then the user won't be able to focus
918+ * or activate this node.
919+ */
920+ @Input ( ) isDisabled ?: boolean ;
921+
922+ /** This emits when the node has been programatically activated. */
923+ @Output ( )
924+ readonly activation : EventEmitter < T > = new EventEmitter < T > ( ) ;
925+
847926 /**
848927 * The most recently created `CdkTreeNode`. We save it in static variable so we can retrieve it
849928 * in `CdkTree` and set the data to it.
@@ -918,11 +997,42 @@ export class CdkTreeNode<T, K = T> implements FocusableOption, OnDestroy, OnInit
918997 this . _destroyed . complete ( ) ;
919998 }
920999
921- /** Focuses the menu item. Implements for FocusableOption. */
1000+ getParent ( ) : CdkTreeNode < T , K > | null {
1001+ return this . _tree . _getNodeParent ( this ) ?? null ;
1002+ }
1003+
1004+ getChildren ( ) : Array < CdkTreeNode < T , K > > | Observable < Array < CdkTreeNode < T , K > > > {
1005+ return this . _tree . _getNodeChildren ( this ) ;
1006+ }
1007+
1008+ /** Focuses this data node. Implemented for TreeKeyManagerItem. */
9221009 focus ( ) : void {
9231010 this . _elementRef . nativeElement . focus ( ) ;
9241011 }
9251012
1013+ /** Emits an activation event. Implemented for TreeKeyManagerItem. */
1014+ activate ( ) : void {
1015+ this . activation . next ( this . _data ) ;
1016+ }
1017+
1018+ /** Collapses this data node. Implemented for TreeKeyManagerItem. */
1019+ collapse ( ) : void {
1020+ this . _tree . collapse ( this . _data ) ;
1021+ }
1022+
1023+ /** Expands this data node. Implemented for TreeKeyManagerItem. */
1024+ expand ( ) : void {
1025+ this . _tree . expand ( this . _data ) ;
1026+ }
1027+
1028+ _setTabFocusable ( ) {
1029+ this . _elementRef . nativeElement . setAttribute ( 'tabindex' , '0' ) ;
1030+ }
1031+
1032+ _setTabUnfocusable ( ) {
1033+ this . _elementRef . nativeElement . setAttribute ( 'tabindex' , '-1' ) ;
1034+ }
1035+
9261036 // TODO: role should eventually just be set in the component host
9271037 protected _setRoleFromData ( ) : void {
9281038 this . role = 'treeitem' ;
0 commit comments