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
@@ -208,7 +214,14 @@ export class CdkTree<T, K = T> implements AfterContentChecked, CollectionViewer,
208214 new Map < K , CdkTreeNode < T , K > > ( ) ,
209215 ) ;
210216
211- constructor ( private _differs : IterableDiffers , private _changeDetectorRef : ChangeDetectorRef ) { }
217+ /** The key manager for this tree. Handles focus and activation based on user keyboard input. */
218+ _keyManager : TreeKeyManager < CdkTreeNode < T , K > > ;
219+
220+ constructor (
221+ private _differs : IterableDiffers ,
222+ private _changeDetectorRef : ChangeDetectorRef ,
223+ private _dir : Directionality ,
224+ ) { }
212225
213226 ngOnInit ( ) {
214227 this . _dataDiffer = this . _differs . find ( [ ] ) . create ( this . trackBy ) ;
@@ -250,6 +263,26 @@ export class CdkTree<T, K = T> implements AfterContentChecked, CollectionViewer,
250263 }
251264 }
252265
266+ ngAfterContentInit ( ) {
267+ this . _keyManager = new TreeKeyManager ( {
268+ items : combineLatest ( [ this . _dataNodes , this . _nodes ] ) . pipe (
269+ map ( ( [ dataNodes , nodes ] ) =>
270+ dataNodes . map ( data => nodes . get ( this . _getExpansionKey ( data ) ) ) . filter ( isNotNullish ) ,
271+ ) ,
272+ ) ,
273+ trackBy : node => this . _getExpansionKey ( node . data ) ,
274+ typeAheadDebounceInterval : true ,
275+ horizontalOrientation : this . _dir . value ,
276+ } ) ;
277+
278+ this . _keyManager . change
279+ . pipe ( startWith ( null ) , pairwise ( ) , takeUntil ( this . _onDestroy ) )
280+ . subscribe ( ( [ prev , next ] ) => {
281+ prev ?. _setTabUnfocusable ( ) ;
282+ next ?. _setTabFocusable ( ) ;
283+ } ) ;
284+ }
285+
253286 ngAfterContentChecked ( ) {
254287 const defaultNodeDefs = this . _nodeDefs . filter ( def => ! def . when ) ;
255288 if ( defaultNodeDefs . length > 1 && ( typeof ngDevMode === 'undefined' || ngDevMode ) ) {
@@ -262,13 +295,17 @@ export class CdkTree<T, K = T> implements AfterContentChecked, CollectionViewer,
262295 }
263296 }
264297
265- // TODO(tinayuangao): Work on keyboard traversal and actions, make sure it's working for RTL
266- // and nested trees.
298+ _getTabindex ( ) {
299+ // If the `TreeKeyManager` has no active item, then we know that we need to focus the initial
300+ // item when the tree is focused. We set the tabindex to be `0` so that we can capture
301+ // the focus event and redirect it. Otherwise, we unset it.
302+ return this . _keyManager . getActiveItem ( ) ? null : 0 ;
303+ }
267304
268305 /**
269306 * Switch to the provided data source by resetting the data and unsubscribing from the current
270307 * render change subscription if one exists. If the data source is null, interpret this by
271- * clearing the node outlet. Otherwise start listening for new data.
308+ * clearIng the node outlet. Otherwise start listening for new data.
272309 */
273310 private _switchDataSource ( dataSource : DataSource < T > | Observable < T [ ] > | T [ ] ) {
274311 if ( this . _dataSource && typeof ( this . _dataSource as DataSource < T > ) . disconnect === 'function' ) {
@@ -676,6 +713,37 @@ export class CdkTree<T, K = T> implements AfterContentChecked, CollectionViewer,
676713 this . _nodes . next ( this . _nodes . value ) ;
677714 }
678715
716+ /** Given a CdkTreeNode, gets the node that renders that node's parent's data. */
717+ _getNodeParent ( node : CdkTreeNode < T , K > ) {
718+ const parent = this . _parents . get ( node . data ) ;
719+ return parent && this . _nodes . value . get ( this . _getExpansionKey ( parent ) ) ;
720+ }
721+
722+ /** Given a CdkTreeNode, gets the nodes that renders that node's child data. */
723+ _getNodeChildren ( node : CdkTreeNode < T , K > ) {
724+ const children = coerceObservable ( this . _getChildrenAccessor ( ) ?.( node . data ) ?? [ ] ) ;
725+ return children . pipe (
726+ map ( children =>
727+ children
728+ . map ( child => this . _nodes . value . get ( this . _getExpansionKey ( child ) ) )
729+ . filter ( isNotNullish ) ,
730+ ) ,
731+ ) ;
732+ }
733+
734+ /** `keydown` event handler; this just passes the event to the `TreeKeyManager`. */
735+ _sendKeydownToKeyManager ( event : KeyboardEvent ) {
736+ this . _keyManager . onKeydown ( event ) ;
737+ }
738+
739+ /** `focus` event handler; this focuses the initial item if there isn't already one available. */
740+ _focusInitialTreeItem ( ) {
741+ if ( this . _keyManager . getActiveItem ( ) ) {
742+ return ;
743+ }
744+ this . _keyManager . onInitialFocus ( ) ;
745+ }
746+
679747 private _getAllDescendants ( ) : Observable < T [ ] > {
680748 return merge ( ...this . _dataNodes . value . map ( dataNode => this . _getDescendants ( dataNode ) ) ) ;
681749 }
@@ -810,9 +878,10 @@ export class CdkTree<T, K = T> implements AfterContentChecked, CollectionViewer,
810878 '[attr.aria-level]' : 'level + 1' ,
811879 '[attr.aria-posinset]' : '_getPositionInSet()' ,
812880 '[attr.aria-setsize]' : '_getSetSize()' ,
881+ 'tabindex' : '-1' ,
813882 } ,
814883} )
815- export class CdkTreeNode < T , K = T > implements FocusableOption , OnDestroy , OnInit {
884+ export class CdkTreeNode < T , K = T > implements OnDestroy , OnInit , TreeKeyManagerItem {
816885 /**
817886 * The role of the tree node.
818887 *
@@ -843,6 +912,16 @@ export class CdkTreeNode<T, K = T> implements FocusableOption, OnDestroy, OnInit
843912 }
844913 }
845914
915+ /**
916+ * Whether or not this node is disabled. If it's disabled, then the user won't be able to focus
917+ * or activate this node.
918+ */
919+ @Input ( ) isDisabled ?: boolean ;
920+
921+ /** This emits when the node has been programatically activated. */
922+ @Output ( )
923+ readonly activation : EventEmitter < T > = new EventEmitter < T > ( ) ;
924+
846925 /**
847926 * The most recently created `CdkTreeNode`. We save it in static variable so we can retrieve it
848927 * in `CdkTree` and set the data to it.
@@ -917,11 +996,42 @@ export class CdkTreeNode<T, K = T> implements FocusableOption, OnDestroy, OnInit
917996 this . _destroyed . complete ( ) ;
918997 }
919998
920- /** Focuses the menu item. Implements for FocusableOption. */
999+ getParent ( ) : CdkTreeNode < T , K > | null {
1000+ return this . _tree . _getNodeParent ( this ) ?? null ;
1001+ }
1002+
1003+ getChildren ( ) : Array < CdkTreeNode < T , K > > | Observable < Array < CdkTreeNode < T , K > > > {
1004+ return this . _tree . _getNodeChildren ( this ) ;
1005+ }
1006+
1007+ /** Focuses this data node. Implemented for TreeKeyManagerItem. */
9211008 focus ( ) : void {
9221009 this . _elementRef . nativeElement . focus ( ) ;
9231010 }
9241011
1012+ /** Emits an activation event. Implemented for TreeKeyManagerItem. */
1013+ activate ( ) : void {
1014+ this . activation . next ( this . _data ) ;
1015+ }
1016+
1017+ /** Collapses this data node. Implemented for TreeKeyManagerItem. */
1018+ collapse ( ) : void {
1019+ this . _tree . collapse ( this . _data ) ;
1020+ }
1021+
1022+ /** Expands this data node. Implemented for TreeKeyManagerItem. */
1023+ expand ( ) : void {
1024+ this . _tree . expand ( this . _data ) ;
1025+ }
1026+
1027+ _setTabFocusable ( ) {
1028+ this . _elementRef . nativeElement . setAttribute ( 'tabindex' , '0' ) ;
1029+ }
1030+
1031+ _setTabUnfocusable ( ) {
1032+ this . _elementRef . nativeElement . setAttribute ( 'tabindex' , '-1' ) ;
1033+ }
1034+
9251035 // TODO: role should eventually just be set in the component host
9261036 protected _setRoleFromData ( ) : void {
9271037 this . role = 'treeitem' ;
0 commit comments