Skip to content

Commit

Permalink
feat(material-experimental/mdc-list): add MDC foundation for action/n…
Browse files Browse the repository at this point in the history
…av list (#19601)

* feat(material-expeirmental/mdc-list): add support for focus/hover states
and ripples

* add state styles

* add adapter for MDCList

* set up MDCListFoundation

* refactor so only interactive lists set up the foundation

* address feedback

* use descendants:true for content children

* don't change tabindex on child elements of list-item

* fix tabIndex initialization

* move logic out of lifecycle hooks
  • Loading branch information
mmalerba committed Jun 19, 2020
1 parent 10888f3 commit 5f15763
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 31 deletions.
4 changes: 2 additions & 2 deletions src/material-experimental/mdc-list/action-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core';
import {MatListBase} from './list-base';
import {MatInteractiveListBase, MatListBase} from './list-base';

@Component({
selector: 'mat-action-list',
Expand All @@ -23,4 +23,4 @@ import {MatListBase} from './list-base';
{provide: MatListBase, useExisting: MatActionList},
]
})
export class MatActionList extends MatListBase {}
export class MatActionList extends MatInteractiveListBase {}
175 changes: 151 additions & 24 deletions src/material-experimental/mdc-list/list-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,22 @@
*/

import {Platform} from '@angular/cdk/platform';
import {DOCUMENT} from '@angular/common';
import {
AfterContentInit,
AfterViewInit,
ContentChildren,
Directive,
ElementRef,
HostBinding,
HostListener,
Inject,
NgZone,
OnDestroy,
QueryList
} from '@angular/core';
import {RippleConfig, RippleRenderer, RippleTarget, setLines} from '@angular/material/core';
import {MDCListAdapter, MDCListFoundation} from '@material/list';
import {Subscription} from 'rxjs';
import {startWith} from 'rxjs/operators';

Expand All @@ -28,17 +34,6 @@ function toggleClass(el: Element, className: string, on: boolean) {
}
}

@Directive()
/** @docs-private */
export abstract class MatListBase {
// @HostBinding is used in the class as it is expected to be extended. Since @Component decorator
// metadata is not inherited by child classes, instead the host binding data is defined in a way
// that can be inherited.
// tslint:disable-next-line:no-host-decorator-in-concrete
@HostBinding('class.mdc-list--non-interactive')
_isNonInteractive: boolean = false;
}

@Directive()
/** @docs-private */
export abstract class MatListItemBase implements AfterContentInit, OnDestroy, RippleTarget {
Expand All @@ -53,22 +48,37 @@ export abstract class MatListItemBase implements AfterContentInit, OnDestroy, Ri

private _rippleRenderer: RippleRenderer;

constructor(protected _element: ElementRef, protected _ngZone: NgZone, listBase: MatListBase,
platform: Platform) {
const el = this._element.nativeElement;
this.rippleDisabled = listBase._isNonInteractive;
if (!listBase._isNonInteractive) {
el.classList.add('mat-mdc-list-item-interactive');
}
this._rippleRenderer =
new RippleRenderer(this, this._ngZone, el, platform);
this._rippleRenderer.setupTriggerEvents(el);
protected constructor(public _elementRef: ElementRef<HTMLElement>, protected _ngZone: NgZone,
private _listBase: MatListBase, private _platform: Platform) {
this._initRipple();
}

ngAfterContentInit() {
this._monitorLines();
}

ngOnDestroy() {
this._subscriptions.unsubscribe();
this._rippleRenderer._removeTriggerEvents();
}

_initDefaultTabIndex(tabIndex: number) {
const el = this._elementRef.nativeElement;
if (!el.hasAttribute('tabIndex')) {
el.tabIndex = tabIndex;
}
}

private _initRipple() {
this.rippleDisabled = this._listBase._isNonInteractive;
if (!this._listBase._isNonInteractive) {
this._elementRef.nativeElement.classList.add('mat-mdc-list-item-interactive');
}
this._rippleRenderer =
new RippleRenderer(this, this._ngZone, this._elementRef.nativeElement, this._platform);
this._rippleRenderer.setupTriggerEvents(this._elementRef.nativeElement);
}

/**
* Subscribes to changes in `MatLine` content children and annotates them appropriately when they
* change.
Expand All @@ -77,20 +87,137 @@ export abstract class MatListItemBase implements AfterContentInit, OnDestroy, Ri
this._ngZone.runOutsideAngular(() => {
this._subscriptions.add(this.lines.changes.pipe(startWith(this.lines))
.subscribe((lines: QueryList<ElementRef<Element>>) => {
this._element.nativeElement.classList
this._elementRef.nativeElement.classList
.toggle('mat-mdc-list-item-single-line', lines.length <= 1);
lines.forEach((line: ElementRef<Element>, index: number) => {
toggleClass(line.nativeElement,
'mdc-list-item__primary-text', index === 0 && lines.length > 1);
toggleClass(line.nativeElement, 'mdc-list-item__secondary-text', index !== 0);
});
setLines(lines, this._element, 'mat-mdc');
setLines(lines, this._elementRef, 'mat-mdc');
}));
});
}
}

@Directive()
/** @docs-private */
export abstract class MatListBase {
@HostBinding('class.mdc-list--non-interactive')
_isNonInteractive: boolean = true;
}

@Directive()
export abstract class MatInteractiveListBase extends MatListBase
implements AfterViewInit, OnDestroy {
@HostListener('keydown', ['$event'])
_handleKeydown(event: KeyboardEvent) {
const index = this._indexForElement(event.target as HTMLElement);
this._foundation.handleKeydown(
event, this._elementAtIndex(index) === event.target, index);
}

@HostListener('click', ['$event'])
_handleClick(event: MouseEvent) {
this._foundation.handleClick(this._indexForElement(event.target as HTMLElement), false);
}

@HostListener('focusin', ['$event'])
_handleFocusin(event: FocusEvent) {
this._foundation.handleFocusIn(event, this._indexForElement(event.target as HTMLElement));
}

@HostListener('focusout', ['$event'])
_handleFocusout(event: FocusEvent) {
this._foundation.handleFocusOut(event, this._indexForElement(event.target as HTMLElement));
}

@ContentChildren(MatListItemBase, {descendants: true}) _items: QueryList<MatListItemBase>;

protected _adapter: MDCListAdapter = {
getListItemCount: () => this._items.length,
listItemAtIndexHasClass:
(index, className) => this._elementAtIndex(index).classList.contains(className),
addClassForElementIndex:
(index, className) => this._elementAtIndex(index).classList.add(className),
removeClassForElementIndex:
(index, className) => this._elementAtIndex(index).classList.remove(className),
getAttributeForElementIndex: (index, attr) => this._elementAtIndex(index).getAttribute(attr),
setAttributeForElementIndex:
(index, attr, value) => this._elementAtIndex(index).setAttribute(attr, value),
getFocusedElementIndex: () => this._indexForElement(this._document?.activeElement),
isFocusInsideList: () => this._element.nativeElement.contains(this._document?.activeElement),
isRootFocused: () => this._element.nativeElement === this._document?.activeElement,
focusItemAtIndex: index => this._elementAtIndex(index).focus(),

// MDC uses this method to disable focusable children of list items. However, we believe that
// this is not an accessible pattern and should be avoided, therefore we intentionally do not
// implement this method. In addition, implementing this would require violating Angular
// Material's general principle of not having components modify DOM elements they do not own.
// A user who feels they really need this feature can simply listen to the `(focus)` and
// `(blur)` events on the list item and enable/disable focus on the children themselves as
// appropriate.
setTabIndexForListItemChildren: () => {},

// The following methods have a dummy implementation in the base class because they are only
// applicable to certain types of lists. They should be implemented for the concrete classes
// where they are applicable.
hasCheckboxAtIndex: () => false,
hasRadioAtIndex: () => false,
setCheckedCheckboxOrRadioAtIndex: () => {},
isCheckboxCheckedAtIndex: () => false,

// TODO(mmalerba): Determine if we need to implement these.
getPrimaryTextAtIndex: () => '',
notifyAction: () => {},
};

protected _foundation: MDCListFoundation;

protected _document: Document;

private _itemsArr: MatListItemBase[] = [];

private _subscriptions = new Subscription();

constructor(protected _element: ElementRef<HTMLElement>, @Inject(DOCUMENT) document: any) {
super();
this._document = document;
this._isNonInteractive = false;
this._foundation = new MDCListFoundation(this._adapter);
}

ngAfterViewInit() {
this._initItems();
this._foundation.init();
this._foundation.layout();
}

ngOnDestroy() {
this._foundation.destroy();
this._subscriptions.unsubscribe();
this._rippleRenderer._removeTriggerEvents();
}

private _initItems() {
this._subscriptions.add(
this._items.changes.pipe(startWith(null))
.subscribe(() => this._itemsArr = this._items.toArray()));
for (let i = 0; this._itemsArr.length; i++) {
this._itemsArr[i]._initDefaultTabIndex(i === 0 ? 0 : -1);
}
}

private _itemAtIndex(index: number): MatListItemBase {
return this._itemsArr[index];
}

private _elementAtIndex(index: number): HTMLElement {
return this._itemAtIndex(index)._elementRef.nativeElement;
}

private _indexForElement(element: Element | null) {
return element ?
this._itemsArr.findIndex(i => i._elementRef.nativeElement.contains(element)) : -1;
}
}

7 changes: 4 additions & 3 deletions src/material-experimental/mdc-list/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,7 @@ export class MatListSubheaderCssMatStyler {}
{provide: MatListBase, useExisting: MatList},
]
})
export class MatList extends MatListBase {
_isNonInteractive = true;
}
export class MatList extends MatListBase {}

@Component({
selector: 'mat-list-item, a[mat-list-item], button[mat-list-item]',
Expand All @@ -79,6 +77,9 @@ export class MatList extends MatListBase {
templateUrl: 'list-item.html',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{provide: MatListItemBase, useExisting: MatListItem},
]
})
export class MatListItem extends MatListItemBase {
@ContentChildren(MatLine, {read: ElementRef, descendants: true}) lines:
Expand Down
4 changes: 2 additions & 2 deletions src/material-experimental/mdc-list/nav-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core';
import {MatList} from './list';
import {MatListBase} from './list-base';
import {MatInteractiveListBase, MatListBase} from './list-base';

@Component({
selector: 'mat-nav-list',
Expand All @@ -33,4 +33,4 @@ import {MatListBase} from './list-base';
{provide: MatList, useExisting: MatNavList},
]
})
export class MatNavList extends MatListBase {}
export class MatNavList extends MatInteractiveListBase {}
3 changes: 3 additions & 0 deletions src/material-experimental/mdc-list/selection-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ export class MatSelectionList extends MatListBase implements ControlValueAccesso
templateUrl: 'list-option.html',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{provide: MatListItemBase, useExisting: MatListOption},
]
})
export class MatListOption extends MatListItemBase {
static ngAcceptInputType_disabled: BooleanInput;
Expand Down

0 comments on commit 5f15763

Please sign in to comment.