77 */
88
99import { Platform } from '@angular/cdk/platform' ;
10+ import { DOCUMENT } from '@angular/common' ;
1011import {
1112 AfterContentInit ,
13+ AfterViewInit ,
14+ ContentChildren ,
1215 Directive ,
1316 ElementRef ,
1417 HostBinding ,
18+ HostListener ,
19+ Inject ,
1520 NgZone ,
1621 OnDestroy ,
1722 QueryList
1823} from '@angular/core' ;
1924import { RippleConfig , RippleRenderer , RippleTarget , setLines } from '@angular/material/core' ;
25+ import { MDCListAdapter , MDCListFoundation } from '@material/list' ;
2026import { Subscription } from 'rxjs' ;
2127import { startWith } from 'rxjs/operators' ;
2228
@@ -28,17 +34,6 @@ function toggleClass(el: Element, className: string, on: boolean) {
2834 }
2935}
3036
31- @Directive ( )
32- /** @docs -private */
33- export abstract class MatListBase {
34- // @HostBinding is used in the class as it is expected to be extended. Since @Component decorator
35- // metadata is not inherited by child classes, instead the host binding data is defined in a way
36- // that can be inherited.
37- // tslint:disable-next-line:no-host-decorator-in-concrete
38- @HostBinding ( 'class.mdc-list--non-interactive' )
39- _isNonInteractive : boolean = false ;
40- }
41-
4237@Directive ( )
4338/** @docs -private */
4439export abstract class MatListItemBase implements AfterContentInit , OnDestroy , RippleTarget {
@@ -53,22 +48,37 @@ export abstract class MatListItemBase implements AfterContentInit, OnDestroy, Ri
5348
5449 private _rippleRenderer : RippleRenderer ;
5550
56- constructor ( protected _element : ElementRef , protected _ngZone : NgZone , listBase : MatListBase ,
57- platform : Platform ) {
58- const el = this . _element . nativeElement ;
59- this . rippleDisabled = listBase . _isNonInteractive ;
60- if ( ! listBase . _isNonInteractive ) {
61- el . classList . add ( 'mat-mdc-list-item-interactive' ) ;
62- }
63- this . _rippleRenderer =
64- new RippleRenderer ( this , this . _ngZone , el , platform ) ;
65- this . _rippleRenderer . setupTriggerEvents ( el ) ;
51+ protected constructor ( public _elementRef : ElementRef < HTMLElement > , protected _ngZone : NgZone ,
52+ private _listBase : MatListBase , private _platform : Platform ) {
53+ this . _initRipple ( ) ;
6654 }
6755
6856 ngAfterContentInit ( ) {
6957 this . _monitorLines ( ) ;
7058 }
7159
60+ ngOnDestroy ( ) {
61+ this . _subscriptions . unsubscribe ( ) ;
62+ this . _rippleRenderer . _removeTriggerEvents ( ) ;
63+ }
64+
65+ _initDefaultTabIndex ( tabIndex : number ) {
66+ const el = this . _elementRef . nativeElement ;
67+ if ( ! el . hasAttribute ( 'tabIndex' ) ) {
68+ el . tabIndex = tabIndex ;
69+ }
70+ }
71+
72+ private _initRipple ( ) {
73+ this . rippleDisabled = this . _listBase . _isNonInteractive ;
74+ if ( ! this . _listBase . _isNonInteractive ) {
75+ this . _elementRef . nativeElement . classList . add ( 'mat-mdc-list-item-interactive' ) ;
76+ }
77+ this . _rippleRenderer =
78+ new RippleRenderer ( this , this . _ngZone , this . _elementRef . nativeElement , this . _platform ) ;
79+ this . _rippleRenderer . setupTriggerEvents ( this . _elementRef . nativeElement ) ;
80+ }
81+
7282 /**
7383 * Subscribes to changes in `MatLine` content children and annotates them appropriately when they
7484 * change.
@@ -77,20 +87,137 @@ export abstract class MatListItemBase implements AfterContentInit, OnDestroy, Ri
7787 this . _ngZone . runOutsideAngular ( ( ) => {
7888 this . _subscriptions . add ( this . lines . changes . pipe ( startWith ( this . lines ) )
7989 . subscribe ( ( lines : QueryList < ElementRef < Element > > ) => {
80- this . _element . nativeElement . classList
90+ this . _elementRef . nativeElement . classList
8191 . toggle ( 'mat-mdc-list-item-single-line' , lines . length <= 1 ) ;
8292 lines . forEach ( ( line : ElementRef < Element > , index : number ) => {
8393 toggleClass ( line . nativeElement ,
8494 'mdc-list-item__primary-text' , index === 0 && lines . length > 1 ) ;
8595 toggleClass ( line . nativeElement , 'mdc-list-item__secondary-text' , index !== 0 ) ;
8696 } ) ;
87- setLines ( lines , this . _element , 'mat-mdc' ) ;
97+ setLines ( lines , this . _elementRef , 'mat-mdc' ) ;
8898 } ) ) ;
8999 } ) ;
90100 }
101+ }
102+
103+ @Directive ( )
104+ /** @docs -private */
105+ export abstract class MatListBase {
106+ @HostBinding ( 'class.mdc-list--non-interactive' )
107+ _isNonInteractive : boolean = true ;
108+ }
109+
110+ @Directive ( )
111+ export abstract class MatInteractiveListBase extends MatListBase
112+ implements AfterViewInit , OnDestroy {
113+ @HostListener ( 'keydown' , [ '$event' ] )
114+ _handleKeydown ( event : KeyboardEvent ) {
115+ const index = this . _indexForElement ( event . target as HTMLElement ) ;
116+ this . _foundation . handleKeydown (
117+ event , this . _elementAtIndex ( index ) === event . target , index ) ;
118+ }
119+
120+ @HostListener ( 'click' , [ '$event' ] )
121+ _handleClick ( event : MouseEvent ) {
122+ this . _foundation . handleClick ( this . _indexForElement ( event . target as HTMLElement ) , false ) ;
123+ }
124+
125+ @HostListener ( 'focusin' , [ '$event' ] )
126+ _handleFocusin ( event : FocusEvent ) {
127+ this . _foundation . handleFocusIn ( event , this . _indexForElement ( event . target as HTMLElement ) ) ;
128+ }
129+
130+ @HostListener ( 'focusout' , [ '$event' ] )
131+ _handleFocusout ( event : FocusEvent ) {
132+ this . _foundation . handleFocusOut ( event , this . _indexForElement ( event . target as HTMLElement ) ) ;
133+ }
134+
135+ @ContentChildren ( MatListItemBase , { descendants : true } ) _items : QueryList < MatListItemBase > ;
136+
137+ protected _adapter : MDCListAdapter = {
138+ getListItemCount : ( ) => this . _items . length ,
139+ listItemAtIndexHasClass :
140+ ( index , className ) => this . _elementAtIndex ( index ) . classList . contains ( className ) ,
141+ addClassForElementIndex :
142+ ( index , className ) => this . _elementAtIndex ( index ) . classList . add ( className ) ,
143+ removeClassForElementIndex :
144+ ( index , className ) => this . _elementAtIndex ( index ) . classList . remove ( className ) ,
145+ getAttributeForElementIndex : ( index , attr ) => this . _elementAtIndex ( index ) . getAttribute ( attr ) ,
146+ setAttributeForElementIndex :
147+ ( index , attr , value ) => this . _elementAtIndex ( index ) . setAttribute ( attr , value ) ,
148+ getFocusedElementIndex : ( ) => this . _indexForElement ( this . _document ?. activeElement ) ,
149+ isFocusInsideList : ( ) => this . _element . nativeElement . contains ( this . _document ?. activeElement ) ,
150+ isRootFocused : ( ) => this . _element . nativeElement === this . _document ?. activeElement ,
151+ focusItemAtIndex : index => this . _elementAtIndex ( index ) . focus ( ) ,
152+
153+ // MDC uses this method to disable focusable children of list items. However, we believe that
154+ // this is not an accessible pattern and should be avoided, therefore we intentionally do not
155+ // implement this method. In addition, implementing this would require violating Angular
156+ // Material's general principle of not having components modify DOM elements they do not own.
157+ // A user who feels they really need this feature can simply listen to the `(focus)` and
158+ // `(blur)` events on the list item and enable/disable focus on the children themselves as
159+ // appropriate.
160+ setTabIndexForListItemChildren : ( ) => { } ,
161+
162+ // The following methods have a dummy implementation in the base class because they are only
163+ // applicable to certain types of lists. They should be implemented for the concrete classes
164+ // where they are applicable.
165+ hasCheckboxAtIndex : ( ) => false ,
166+ hasRadioAtIndex : ( ) => false ,
167+ setCheckedCheckboxOrRadioAtIndex : ( ) => { } ,
168+ isCheckboxCheckedAtIndex : ( ) => false ,
169+
170+ // TODO(mmalerba): Determine if we need to implement these.
171+ getPrimaryTextAtIndex : ( ) => '' ,
172+ notifyAction : ( ) => { } ,
173+ } ;
174+
175+ protected _foundation : MDCListFoundation ;
176+
177+ protected _document : Document ;
178+
179+ private _itemsArr : MatListItemBase [ ] = [ ] ;
180+
181+ private _subscriptions = new Subscription ( ) ;
182+
183+ constructor ( protected _element : ElementRef < HTMLElement > , @Inject ( DOCUMENT ) document : any ) {
184+ super ( ) ;
185+ this . _document = document ;
186+ this . _isNonInteractive = false ;
187+ this . _foundation = new MDCListFoundation ( this . _adapter ) ;
188+ }
189+
190+ ngAfterViewInit ( ) {
191+ this . _initItems ( ) ;
192+ this . _foundation . init ( ) ;
193+ this . _foundation . layout ( ) ;
194+ }
91195
92196 ngOnDestroy ( ) {
197+ this . _foundation . destroy ( ) ;
93198 this . _subscriptions . unsubscribe ( ) ;
94- this . _rippleRenderer . _removeTriggerEvents ( ) ;
199+ }
200+
201+ private _initItems ( ) {
202+ this . _subscriptions . add (
203+ this . _items . changes . pipe ( startWith ( null ) )
204+ . subscribe ( ( ) => this . _itemsArr = this . _items . toArray ( ) ) ) ;
205+ for ( let i = 0 ; this . _itemsArr . length ; i ++ ) {
206+ this . _itemsArr [ i ] . _initDefaultTabIndex ( i === 0 ? 0 : - 1 ) ;
207+ }
208+ }
209+
210+ private _itemAtIndex ( index : number ) : MatListItemBase {
211+ return this . _itemsArr [ index ] ;
212+ }
213+
214+ private _elementAtIndex ( index : number ) : HTMLElement {
215+ return this . _itemAtIndex ( index ) . _elementRef . nativeElement ;
216+ }
217+
218+ private _indexForElement ( element : Element | null ) {
219+ return element ?
220+ this . _itemsArr . findIndex ( i => i . _elementRef . nativeElement . contains ( element ) ) : - 1 ;
95221 }
96222}
223+
0 commit comments