From 871a500fb808aaa9823ecac86df20ce8740c92fa Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Mon, 25 Oct 2021 18:50:13 +0200 Subject: [PATCH] feat(material-experimental/mdc-list): rework API to support secondary text with wrapping Reworks the API to support secondary text with wrapping into the third line. The overall API changes are made to ease the integration with the MDC-list foundation/component. https://docs.google.com/document/d/15yC_W2-NuNvVqHgA1zjpE8V28-Y5XRpdFOMdLjMbWAo/edit --- scripts/check-mdc-exports-config.ts | 12 + scripts/check-mdc-tests-config.ts | 12 +- scripts/check-mdc-tests.ts | 2 +- src/dev-app/mdc-list/mdc-list-demo.html | 103 ++++++--- src/dev-app/mdc-list/mdc-list-demo.scss | 4 + src/dev-app/mdc-list/mdc-list-demo.ts | 5 +- .../mdc-core/public-api.ts | 2 - .../mdc-list/BUILD.bazel | 1 + .../mdc-list/list-base.ts | 210 ++++++++++++++---- .../mdc-list/list-item-sections.ts | 100 +++++++++ .../mdc-list/list-item.html | 23 +- .../mdc-list/list-option.html | 11 +- .../mdc-list/list-option.ts | 23 +- .../mdc-list/list-styling.ts | 66 ------ src/material-experimental/mdc-list/list.scss | 38 ++-- .../mdc-list/list.spec.ts | 72 ++---- src/material-experimental/mdc-list/list.ts | 15 +- src/material-experimental/mdc-list/module.ts | 39 ++-- .../mdc-list/public-api.ts | 4 +- .../mdc-list/selection-list.spec.ts | 119 +++++++--- .../mdc-list/subheader.ts | 21 ++ 21 files changed, 571 insertions(+), 311 deletions(-) create mode 100644 src/material-experimental/mdc-list/list-item-sections.ts delete mode 100644 src/material-experimental/mdc-list/list-styling.ts create mode 100644 src/material-experimental/mdc-list/subheader.ts diff --git a/scripts/check-mdc-exports-config.ts b/scripts/check-mdc-exports-config.ts index 780fa76f489b..da69cdf7e4ed 100644 --- a/scripts/check-mdc-exports-config.ts +++ b/scripts/check-mdc-exports-config.ts @@ -5,6 +5,18 @@ export const config = { // Exclude them from this check since they aren't part of the public API. skippedSymbols: [/^_/], skippedExports: { + 'mdc-core': [ + // The line directive is not used by the MDC-based list and therefore does + // not need to be re-exposed. + 'MatLine', + 'MatLineModule', + ], + 'mdc-list': [ + // These classes are docs-private and have actual public classes in the + // MDC-based list, such as `MatListItemIcon` or `MatListItemAvatar`. + 'MatListAvatarCssMatStyler', + 'MatListIconCssMatStyler', + ], 'mdc-chips': [ // These components haven't been implemented for MDC due to a different accessibility pattern. 'MatChipListChange', diff --git a/scripts/check-mdc-tests-config.ts b/scripts/check-mdc-tests-config.ts index 17017e95e54f..a5f6bd9d0b59 100644 --- a/scripts/check-mdc-tests-config.ts +++ b/scripts/check-mdc-tests-config.ts @@ -100,13 +100,11 @@ export const config = { 'should not change focus when pressing HOME with a modifier key', 'should not change focus when pressing END with a modifier key', - // MDC does not support the common CTRL + A keyboard shortcut. - // Tracked with: https://github.com/material-components/material-components-web/issues/6366 - 'should select all items using ctrl + a', - 'should not select disabled items when pressing ctrl + a', - 'should select all items using ctrl + a if some items are selected', - 'should deselect all with ctrl + a if all options are selected', - 'should dispatch the selectionChange event when selecting via ctrl + a', + // The indirect descendant test scenario never worked (even in the non-MDC list) and + // therefore this test has been removed. + 'should pick up indirect descendant lines', + // MDC-based list does not support more than three lines. + 'should apply a particular class to lists with more than 3 lines', ], 'mdc-progress-bar': [ // These tests are verifying implementation details that are not relevant for MDC. diff --git a/scripts/check-mdc-tests.ts b/scripts/check-mdc-tests.ts index 2b7e9f54781c..9a8af1e73a3b 100644 --- a/scripts/check-mdc-tests.ts +++ b/scripts/check-mdc-tests.ts @@ -91,7 +91,7 @@ function getTestNames(files: string[]): string[] { if ( ts.isCallExpression(node) && ts.isIdentifier(node.expression) && - node.expression.text === 'it' + (node.expression.text === 'it' || node.expression.text === 'xit') ) { // Note that this is a little naive since it'll take the literal text of the test // name expression which could include things like string concatenation. It's fine diff --git a/src/dev-app/mdc-list/mdc-list-demo.html b/src/dev-app/mdc-list/mdc-list-demo.html index 31baa520f704..e8b2d376c888 100644 --- a/src/dev-app/mdc-list/mdc-list-demo.html +++ b/src/dev-app/mdc-list/mdc-list-demo.html @@ -1,9 +1,11 @@

mat-list demo

- + + + +
-

Normal lists

@@ -16,18 +18,18 @@

Normal lists

-
{{contact.name}}
-
extra line
-
{{contact.headline}}
+
{{contact.name}}
+
extra line
+
{{contact.headline}}
Today
- Image of {{message.from}} -
{{message.from}}
-
+ Image of {{message.from}} +
{{message.from}}
+
{{message.subject}} -- {{message.message}}
@@ -35,16 +37,16 @@

Normal lists

-
{{message.from}}
-
{{message.subject}}
-
{{message.message}}
+
{{message.from}}
+
{{message.subject}}
+
{{message.message}}
- {{ link.name }} - @@ -62,25 +64,25 @@

Dense lists

-
{{contact.name}}
-
{{contact.headline}}
+
{{contact.name}}
+
{{contact.headline}}
Today
- Image of {{message.from}} -
{{message.from}}
-
{{message.subject}}
-
{{message.message}}
+ Image of {{message.from}} +
{{message.from}}
+
{{message.subject}}
+
{{message.message}}
- {{ link.name }} - @@ -98,8 +100,8 @@

Nav lists

- folder - {{ link.name }} + folder + {{ link.name }}
@@ -134,13 +136,13 @@

Selection list

Dogs
- - Shiba Inu + + Shiba Inu - - Other Shiba Inu + + Other Shiba Inu @@ -183,21 +185,52 @@

Single Selection list

Selected: {{favoriteOptions | json}}

+
+

Line scenarios

+ + + Title + + Title + This is unscoped text content that is supposed to not wrap. The list has only + acquired two lines and therefore there is no space for wrapping. + + + Title + This is unscoped text content that is supposed to wrap to the third line. + The list item acquire spaces for three lines and text should have an ellipsis in the + third line upon text overflow. + + + Title + This is unscoped text content that is supposed to not wrap. The list has only + acquired two lines (automatically) and therefore there is no space for wrapping. + + + Title + Secondary line + Tertiary line + + + + +
+

Line alignment

- {{ link.name }} - Not in an matLine + {{ link.name }} + Unscoped content First - Second - Not in an matLine + Second + Unscoped content
@@ -207,19 +240,19 @@

Icon alignment in selection list

- info + info Bananas - info + info Oranges - info + info Cake - info + info Fries diff --git a/src/dev-app/mdc-list/mdc-list-demo.scss b/src/dev-app/mdc-list/mdc-list-demo.scss index 3f5fd9822baf..30bed5b5e262 100644 --- a/src/dev-app/mdc-list/mdc-list-demo.scss +++ b/src/dev-app/mdc-list/mdc-list-demo.scss @@ -15,6 +15,10 @@ .mat-mdc-icon-button { color: rgba(0, 0, 0, 0.12); } + + &.demo-show-boxes .mat-mdc-list-item { + border: 1px solid grey; + } } .demo-secondary-text { diff --git a/src/dev-app/mdc-list/mdc-list-demo.ts b/src/dev-app/mdc-list/mdc-list-demo.ts index 5296c897d33f..a33fb58b67d4 100644 --- a/src/dev-app/mdc-list/mdc-list-demo.ts +++ b/src/dev-app/mdc-list/mdc-list-demo.ts @@ -27,10 +27,10 @@ export class MdcListDemo { messages: {from: string; subject: string; message: string; image: string}[] = [ { - from: 'Nancy', + from: 'John', subject: 'Brunch?', message: 'Did you want to go on Sunday? I was thinking that might work.', - image: 'https://angular.io/generated/images/bios/cindygreenekaplan.jpg', + image: 'https://angular.io/generated/images/bios/devversion.jpg', }, { from: 'Mary', @@ -49,6 +49,7 @@ export class MdcListDemo { links: {name: string}[] = [{name: 'Inbox'}, {name: 'Outbox'}, {name: 'Spam'}, {name: 'Trash'}]; thirdLine = false; + showBoxes = false; infoClicked = false; selectionListDisabled = false; selectionListRippleDisabled = false; diff --git a/src/material-experimental/mdc-core/public-api.ts b/src/material-experimental/mdc-core/public-api.ts index 00cbeab3a498..7ca80f2d379c 100644 --- a/src/material-experimental/mdc-core/public-api.ts +++ b/src/material-experimental/mdc-core/public-api.ts @@ -29,8 +29,6 @@ export { MatCommonModule, MatDateFormats, MATERIAL_SANITY_CHECKS, - MatLine, - MatLineModule, MatNativeDateModule, MatPseudoCheckbox, MatPseudoCheckboxModule, diff --git a/src/material-experimental/mdc-list/BUILD.bazel b/src/material-experimental/mdc-list/BUILD.bazel index bc18968cb298..43fb6793d02c 100644 --- a/src/material-experimental/mdc-list/BUILD.bazel +++ b/src/material-experimental/mdc-list/BUILD.bazel @@ -26,6 +26,7 @@ ng_module( deps = [ "//src:dev_mode_types", "//src/cdk/collections", + "//src/cdk/observers", "//src/material-experimental/mdc-core", "//src/material/divider", "//src/material/list", diff --git a/src/material-experimental/mdc-list/list-base.ts b/src/material-experimental/mdc-list/list-base.ts index fb9547ac74b6..13081d1d950c 100644 --- a/src/material-experimental/mdc-list/list-base.ts +++ b/src/material-experimental/mdc-list/list-base.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; +import {BooleanInput, coerceBooleanProperty, coerceNumberProperty} from '@angular/cdk/coercion'; import {Platform} from '@angular/cdk/platform'; import { - AfterContentInit, + AfterViewInit, ContentChildren, Directive, ElementRef, @@ -26,20 +26,15 @@ import { RippleGlobalOptions, RippleRenderer, RippleTarget, - setLines, } from '@angular/material-experimental/mdc-core'; import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations'; -import {Subscription} from 'rxjs'; -import {startWith} from 'rxjs/operators'; -import {MatListAvatarCssMatStyler, MatListIconCssMatStyler} from './list-styling'; - -function toggleClass(el: Element, className: string, on: boolean) { - if (on) { - el.classList.add(className); - } else { - el.classList.remove(className); - } -} +import {Subscription, merge} from 'rxjs'; +import { + MatListItemLine, + MatListItemTitle, + MatListItemIcon, + MatListItemAvatar, +} from './list-item-sections'; @Directive({ host: { @@ -48,12 +43,20 @@ function toggleClass(el: Element, className: string, on: boolean) { }, }) /** @docs-private */ -export abstract class MatListItemBase implements AfterContentInit, OnDestroy, RippleTarget { +export abstract class MatListItemBase implements AfterViewInit, OnDestroy, RippleTarget { /** Query list matching list-item line elements. */ - abstract lines: QueryList>; + abstract _lines: QueryList | undefined; - /** Element reference referring to the primary list item text. */ - abstract _itemText: ElementRef; + /** Query list matching list-item title elements. */ + abstract _titles: QueryList | undefined; + + /** + * Element reference to the unscoped content in a list item. + * + * Unscoped content is user-projected text content in a list item that is + * not part of an explicit line or title. + */ + abstract _unscopedContent: ElementRef | undefined; /** Host element for the list item. */ _hostElement: HTMLElement; @@ -61,8 +64,25 @@ export abstract class MatListItemBase implements AfterContentInit, OnDestroy, Ri /** Whether animations are disabled. */ _noopAnimations: boolean; - @ContentChildren(MatListAvatarCssMatStyler, {descendants: false}) _avatars: QueryList; - @ContentChildren(MatListIconCssMatStyler, {descendants: false}) _icons: QueryList; + @ContentChildren(MatListItemAvatar, {descendants: false}) _avatars: QueryList; + @ContentChildren(MatListItemIcon, {descendants: false}) _icons: QueryList; + + /** + * The number of lines this list item should reserve space for. If not specified, + * lines are inferred based on the projected content. + * + * Explicitly specifying the number of lines is useful if you want to acquire additional + * space and enable the wrapping of text. The unscoped text content of a list item will + * always be able to take up the remaining space of the item, unless it represents the title. + * + * A maximum of three lines is supported as per the Material Design specification. + */ + @Input() + set lines(lines: number | string | null) { + this._explicitLines = coerceNumberProperty(lines, null); + this._updateItemLines(false); + } + _explicitLines: number | null = null; @Input() get disableRipple(): boolean { @@ -88,6 +108,9 @@ export abstract class MatListItemBase implements AfterContentInit, OnDestroy, Ri private _subscriptions = new Subscription(); private _rippleRenderer: RippleRenderer | null = null; + /** Whether the list item has unscoped text content. */ + _hasUnscopedTextContent: boolean = false; + /** * Implemented as part of `RippleTarget`. * @docs-private @@ -102,7 +125,7 @@ export abstract class MatListItemBase implements AfterContentInit, OnDestroy, Ri return this.disableRipple || !!this.rippleConfig.disabled; } - constructor( + protected constructor( public _elementRef: ElementRef, protected _ngZone: NgZone, private _listBase: MatListBase, @@ -131,8 +154,9 @@ export abstract class MatListItemBase implements AfterContentInit, OnDestroy, Ri } } - ngAfterContentInit() { - this._monitorLines(); + ngAfterViewInit() { + this._monitorProjectedLinesAndTitle(); + this._updateItemLines(true); } ngOnDestroy() { @@ -144,7 +168,11 @@ export abstract class MatListItemBase implements AfterContentInit, OnDestroy, Ri /** Gets the label for the list item. This is used for the typeahead. */ _getItemLabel(): string { - return this._itemText ? this._itemText.nativeElement.textContent || '' : ''; + const titleElement = this._titles?.get(0)?._elementRef.nativeElement; + // If there is no explicit title element, the unscoped text content + // is treated as the list item title. + const labelEl = titleElement ?? this._unscopedContent?.nativeElement; + return labelEl ? labelEl.textContent ?? '' : ''; } /** Whether the list item has icons or avatars. */ @@ -164,33 +192,95 @@ export abstract class MatListItemBase implements AfterContentInit, OnDestroy, Ri } /** - * Subscribes to changes in `MatLine` content children and annotates them - * appropriately when they change. + * Subscribes to changes in the projected title and lines. Triggers a + * item lines update whenever a change occurs. */ - private _monitorLines() { + private _monitorProjectedLinesAndTitle() { this._ngZone.runOutsideAngular(() => { this._subscriptions.add( - this.lines.changes - .pipe(startWith(this.lines)) - .subscribe((lines: QueryList>) => { - toggleClass(this._hostElement, 'mat-mdc-list-item-single-line', lines.length <= 1); - toggleClass(this._hostElement, 'mdc-list-item--with-one-line', lines.length <= 1); - - lines.forEach((line: ElementRef, index: number) => { - toggleClass(this._hostElement, 'mdc-list-item--with-two-lines', lines.length === 2); - toggleClass(this._hostElement, 'mdc-list-item--with-three-lines', lines.length === 3); - 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._elementRef, 'mat-mdc'); - }), + merge(this._lines!.changes, this._titles!.changes).subscribe(() => + this._updateItemLines(false), + ), ); }); } + + /** + * Updates the lines of the list item. Based on the projected user content and optional + * explicit lines setting, the visual appearance of the list item is determined. + * + * This method should be invoked whenever the projected user content changes, or + * when the explicit lines have been updated. + * + * @param recheckUnscopedContent Whether the projected unscoped content should be re-checked. + * The unscoped content is not re-checked for every update as it is a rather expensive check + * for content that is expected to not change very often. + */ + _updateItemLines(recheckUnscopedContent: boolean) { + // If the updated is triggered too early before the view and content is initialized, + // we just skip the update. After view initialization the update is triggered again. + if (!this._lines || !this._titles || !this._unscopedContent) { + return; + } + + // Re-check the DOM for unscoped text content if requested. This needs to + // happen before any computation or sanity checks run as these rely on the + // result of whether there is unscoped text content or not. + if (recheckUnscopedContent) { + this._checkDomForUnscopedTextContent(); + } + + // Sanity check the list item lines and title in the content. This is a dev-mode only + // check that can be dead-code eliminated by Terser in production. + if (typeof ngDevMode === 'undefined' || ngDevMode) { + sanityCheckListItemContent(this); + } + + const numberOfLines = this._explicitLines ?? this._inferLinesFromContent(); + const unscopedContentEl = this._unscopedContent.nativeElement; + + // Update the list item element to reflect the number of lines. + this._hostElement.classList.toggle('mat-mdc-list-item-single-line', numberOfLines <= 1); + this._hostElement.classList.toggle('mdc-list-item--with-one-line', numberOfLines <= 1); + this._hostElement.classList.toggle('mdc-list-item--with-two-lines', numberOfLines === 2); + this._hostElement.classList.toggle('mdc-list-item--with-three-lines', numberOfLines === 3); + + // If there is no title and the unscoped content is the is the only line, the + // unscoped text content will be treated as the title of the list-item. + if (this._hasUnscopedTextContent) { + const treatAsTitle = this._titles.length === 0 && numberOfLines === 1; + unscopedContentEl.classList.toggle('mdc-list-item__primary-text', treatAsTitle); + unscopedContentEl.classList.toggle('mdc-list-item__secondary-text', !treatAsTitle); + } else { + unscopedContentEl.classList.remove('mdc-list-item__primary-text'); + unscopedContentEl.classList.remove('mdc-list-item__secondary-text'); + } + } + + /** + * Infers the number of lines based on the projected user content. This is useful + * if no explicit number of lines has been specified on the list item. + * + * The number of lines is inferred based on whether there is a title, the number of + * additional lines (secondary/tertiary). An additional line is acquired if there is + * unscoped text content. + */ + private _inferLinesFromContent() { + let numOfLines = this._titles!.length + this._lines!.length; + if (this._hasUnscopedTextContent) { + numOfLines += 1; + } + return numOfLines; + } + + /** Checks whether the list item has unscoped text content. */ + private _checkDomForUnscopedTextContent() { + this._hasUnscopedTextContent = Array.from( + this._unscopedContent!.nativeElement.childNodes, + ) + .filter(node => node.nodeType !== node.COMMENT_NODE) + .some(node => !!(node.textContent && node.textContent.trim())); + } } @Directive({ @@ -223,3 +313,33 @@ export abstract class MatListBase { } private _disabled = false; } + +/** + * Sanity checks the configuration of the list item with respect to the amount + * of lines, whether there is a title, or if there is unscoped text content. + * + * The checks are extracted into a top-level function that can be dead-code + * eliminated by Terser or other optimizers in production mode. + */ +function sanityCheckListItemContent(item: MatListItemBase) { + const numTitles = item._titles!.length; + const numLines = item._titles!.length; + + if (numTitles > 1) { + throw Error('A list item cannot have multiple titles.'); + } + if (numTitles === 0 && numLines > 0) { + throw Error('A list item line can only be used if there is a list item title.'); + } + if ( + numTitles === 0 && + item._hasUnscopedTextContent && + item._explicitLines !== null && + item._explicitLines > 1 + ) { + throw Error('A list item cannot have wrapping content without a title.'); + } + if (numLines > 2 || (numLines === 2 && item._hasUnscopedTextContent)) { + throw Error('A list item can have at maximum three lines.'); + } +} diff --git a/src/material-experimental/mdc-list/list-item-sections.ts b/src/material-experimental/mdc-list/list-item-sections.ts new file mode 100644 index 000000000000..78101de89688 --- /dev/null +++ b/src/material-experimental/mdc-list/list-item-sections.ts @@ -0,0 +1,100 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Directive, ElementRef, Inject, Optional} from '@angular/core'; +import {LIST_OPTION, ListOption} from './list-option-types'; + +/** + * Directive capturing the title of a list item. A list item usually consists of a + * title and optional secondary or tertiary lines. + * + * Text content for the title never wraps. There can only be a single title per list item. + */ +@Directive({ + selector: '[matListItemTitle]', + host: {'class': 'mat-mdc-list-item-title mdc-list-item__primary-text'}, +}) +export class MatListItemTitle { + constructor(public _elementRef: ElementRef) {} +} + +/** + * Directive capturing a line in a list item. A list item usually consists of a + * title and optional secondary or tertiary lines. + * + * Text content inside a line never wraps. There can be at maximum two lines per list item. + */ +@Directive({ + selector: '[matListItemLine]', + host: {'class': 'mat-mdc-list-item-line mdc-list-item__secondary-text'}, +}) +export class MatListItemLine { + constructor(public _elementRef: ElementRef) {} +} + +/** + * Directive matching an optional meta section for list items. + * + * List items can reserve space at the end of an item to display a control, + * button or additional text content. + */ +@Directive({ + selector: '[matListItemMeta]', + host: {'class': 'mat-mdc-list-item-meta mdc-list-item__end'}, +}) +export class MatListItemMeta {} + +/** + * @docs-private + * + * MDC uses the very intuitively named classes `.mdc-list-item__start` and `.mat-list-item__end` + * to position content such as icons or checkboxes that comes either before or after the text + * content respectively. This directive detects the placement of the checkbox and applies the + * correct MDC class to position the icon/avatar on the opposite side. + */ +@Directive({ + host: { + // MDC uses intuitively named classes `.mdc-list-item__start` and `.mat-list-item__end` + // to position content such as icons or checkboxes that comes either before or after the text + // content respectively. This directive detects the placement of the checkbox and applies the + // correct MDC class to position the icon/avatar on the opposite side. + '[class.mdc-list-item__start]': '_isAlignedAtStart()', + '[class.mdc-list-item__end]': '!_isAlignedAtStart()', + }, +}) +export class _MatListItemGraphicBase { + constructor(@Optional() @Inject(LIST_OPTION) public _listOption: ListOption) {} + + _isAlignedAtStart() { + // By default, in all list items the graphic is aligned at start. In list options, + // the graphic is only aligned at start if the checkbox is at the end. + return !this._listOption || this._listOption?._getCheckboxPosition() === 'after'; + } +} + +/** + * Directive matching an optional avatar within a list item. + * + * List items can reserve space at the beginning of an item to display an avatar. + */ +@Directive({ + selector: '[matListItemAvatar]', + host: {'class': 'mat-mdc-list-item-avatar'}, +}) +export class MatListItemAvatar extends _MatListItemGraphicBase {} + +/** + * Directive matching an optional icon within a list item. + * + * List items can reserve space at the beginning of an item to display an icon. + */ +@Directive({ + selector: '[matListItemIcon]', + host: {'class': 'mat-mdc-list-item-icon'}, +}) +export class MatListItemIcon extends _MatListItemGraphicBase {} diff --git a/src/material-experimental/mdc-list/list-item.html b/src/material-experimental/mdc-list/list-item.html index e00076915380..e52a1e9491c4 100644 --- a/src/material-experimental/mdc-list/list-item.html +++ b/src/material-experimental/mdc-list/list-item.html @@ -1,19 +1,16 @@ - + - - - - - - - - + + + + + + + + - + @@ -36,8 +36,13 @@ - - + + + + + + diff --git a/src/material-experimental/mdc-list/list-option.ts b/src/material-experimental/mdc-list/list-option.ts index 52eef6d07eb8..89b9de13b32c 100644 --- a/src/material-experimental/mdc-list/list-option.ts +++ b/src/material-experimental/mdc-list/list-option.ts @@ -29,7 +29,6 @@ import { ViewEncapsulation, } from '@angular/core'; import { - MatLine, MAT_RIPPLE_GLOBAL_OPTIONS, RippleGlobalOptions, ThemePalette, @@ -37,6 +36,7 @@ import { import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations'; import {MatListBase, MatListItemBase} from './list-base'; import {LIST_OPTION, ListOption, MatListOptionCheckboxPosition} from './list-option-types'; +import {MatListItemLine, MatListItemTitle} from './list-item-sections'; /** * Injection token that can be used to reference instances of an `SelectionList`. It serves @@ -94,11 +94,10 @@ export interface SelectionList extends MatListBase { ], }) export class MatListOption extends MatListItemBase implements ListOption, OnInit, OnDestroy { - /** - * This is set to true after the first OnChanges cycle so we don't - * clear the value of `selected` in the first cycle. - */ - private _inputsInitialized = false; + @ContentChildren(MatListItemLine, {descendants: true}) _lines: QueryList; + @ContentChildren(MatListItemTitle, {descendants: true}) _titles: QueryList; + @ViewChild('unscopedContent') _unscopedContent: ElementRef; + @ViewChild('text') _itemText: ElementRef; /** * Emits when the selected state of the option has changed. @@ -108,12 +107,6 @@ export class MatListOption extends MatListItemBase implements ListOption, OnInit @Output() readonly selectedChange: EventEmitter = new EventEmitter(); - @ViewChild('text') _itemText: ElementRef; - - @ContentChildren(MatLine, {read: ElementRef, descendants: true}) lines: QueryList< - ElementRef - >; - /** Whether the label should appear before or after the checkbox. Defaults to 'after' */ @Input() checkboxPosition: MatListOptionCheckboxPosition = 'after'; @@ -159,6 +152,12 @@ export class MatListOption extends MatListItemBase implements ListOption, OnInit } private _selected = false; + /** + * This is set to true after the first OnChanges cycle so we don't + * clear the value of `selected` in the first cycle. + */ + private _inputsInitialized = false; + constructor( element: ElementRef, ngZone: NgZone, diff --git a/src/material-experimental/mdc-list/list-styling.ts b/src/material-experimental/mdc-list/list-styling.ts deleted file mode 100644 index 571340f3fa4e..000000000000 --- a/src/material-experimental/mdc-list/list-styling.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import {Directive, Inject, Optional} from '@angular/core'; -import {LIST_OPTION, ListOption} from './list-option-types'; - -/** - * MDC uses the very intuitively named classes `.mdc-list-item__start` and `.mat-list-item__end` - * to position content such as icons or checkboxes that comes either before or after the text - * content respectively. This directive detects the placement of the checkbox and applies the - * correct MDC class to position the icon/avatar on the opposite side. - * @docs-private - */ -@Directive({ - selector: '[mat-list-avatar], [matListAvatar], [mat-list-icon], [matListIcon]', - host: { - '[class.mdc-list-item__start]': '_isAlignedAtStart()', - '[class.mdc-list-item__end]': '!_isAlignedAtStart()', - }, -}) -export class MatListGraphicAlignmentStyler { - constructor(@Optional() @Inject(LIST_OPTION) public _listOption: ListOption) {} - - _isAlignedAtStart() { - // By default, in all list items the graphic is aligned at start. In list options, - // the graphic is only aligned at start if the checkbox is at the end. - return !this._listOption || this._listOption?._getCheckboxPosition() === 'after'; - } -} - -/** - * Directive whose purpose is to add the mat- CSS styling to this selector. - * @docs-private - */ -@Directive({ - selector: '[mat-list-avatar], [matListAvatar]', - host: {'class': 'mat-mdc-list-avatar'}, -}) -export class MatListAvatarCssMatStyler {} - -/** - * Directive whose purpose is to add the mat- CSS styling to this selector. - * @docs-private - */ -@Directive({ - selector: '[mat-list-icon], [matListIcon]', - host: {'class': 'mat-mdc-list-icon'}, -}) -export class MatListIconCssMatStyler {} - -/** - * Directive whose purpose is to add the mat- CSS styling to this selector. - * @docs-private - */ -@Directive({ - selector: '[mat-subheader], [matSubheader]', - // TODO(mmalerba): MDC's subheader font looks identical to the list item font, figure out why and - // make a change in one of the repos to visually distinguish. - host: {'class': 'mat-mdc-subheader mdc-list-group__subheader'}, -}) -export class MatListSubheaderCssMatStyler {} diff --git a/src/material-experimental/mdc-list/list.scss b/src/material-experimental/mdc-list/list.scss index 4a27011f7146..039dc1c320a4 100644 --- a/src/material-experimental/mdc-list/list.scss +++ b/src/material-experimental/mdc-list/list.scss @@ -29,7 +29,7 @@ bottom: 0; } - .mat-mdc-list-avatar ~ .mat-divider-inset { + .mat-mdc-list-item-avatar ~ .mat-divider-inset { margin-left: 72px; [dir='rtl'] & { @@ -60,23 +60,23 @@ pointer-events: none; } -// If there are projected lines, we project any remaining content into the list-item's end -// container and set the `--with-trailing-meta` class. If this container is empty due to no -// projected content though, we want to hide the element because otherwise the empty container -// would take up horizontal space. There is no good way to check for remaining projected content -// in the template, so we just use CSS with the `:empty` selector. Note that we need increased -// specificity here because MDC overrides the `display` with the `--trailing-meta` class too. -.mat-mdc-list-item.mdc-list-item--with-trailing-meta > .mdc-list-item__end:empty { - display: none; -} +.mat-mdc-list-item.mdc-list-item--with-three-lines { + // List item lines or titles never wrap. MDC always enables wrapping for secondary text + // if the list item has acquired three lines. We unset these styles for line elements. + // https://github.com/material-components/material-components-web/blob/348665978ce73694ad4518626dd70cdf5b984113/packages/mdc-list/_evolution-mixins.scss#L205-L206. + // TODO: Consider removing once MDC supports the explicit tertiary line list variant. + .mat-mdc-list-item-line.mdc-list-item__secondary-text { + white-space: nowrap; + line-height: normal; + } -// Unset MDC's styles for wrapping secondary text in three-line lists. MDC implements three-line -// lists in a way where they assume that the second list line should wrap into the third line. -// This is different to the three-line list variant we want to support. We support a dedicated -// third line that can be controlled by consumers using a third `matLine` span. -// https://github.com/material-components/material-components-web/blob/master/packages/mdc-list/_evolution-mixins.scss#L199. -// TODO: Consider removing once MDC supports the various three-line list variants. -.mat-mdc-list-item.mdc-list-item--with-three-lines .mdc-list-item__secondary-text { - white-space: nowrap; - line-height: normal; + // Unscoped content can wrap if the list item has acquired three lines. MDC implements + // this functionality for secondary text but there is no proper text ellipsis when + // text overflows the third line. These styles ensure the overflow is handled properly. + // TODO: Move this to the the MDC list once it drops IE11 support. + .mat-mdc-list-item-unscoped-content.mdc-list-item__secondary-text { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + } } diff --git a/src/material-experimental/mdc-list/list.spec.ts b/src/material-experimental/mdc-list/list.spec.ts index 12c62b7f1a0c..6fe496a663ec 100644 --- a/src/material-experimental/mdc-list/list.spec.ts +++ b/src/material-experimental/mdc-list/list.spec.ts @@ -26,7 +26,6 @@ describe('MDC-based MatList', () => { NavListWithOneAnchorItem, ActionListWithoutType, ActionListWithType, - ListWithIndirectDescendantLines, ListWithDisabledItems, ], }); @@ -41,7 +40,6 @@ describe('MDC-based MatList', () => { fixture.detectChanges(); expect(listItem.nativeElement.classList).toContain('mat-mdc-list-item'); expect(listItem.nativeElement.classList).toContain('mdc-list-item'); - expect(listItem.nativeElement.classList).toContain('mat-mdc-list-item-single-line'); expect(listItem.nativeElement.classList).toContain('mdc-list-item--with-one-line'); }); @@ -50,8 +48,7 @@ describe('MDC-based MatList', () => { fixture.detectChanges(); const listItems = fixture.debugElement.children[0].queryAll(By.css('mat-list-item')); - expect(listItems[0].nativeElement.className).toContain('mat-mdc-2-line'); - expect(listItems[1].nativeElement.className).toContain('mat-mdc-2-line'); + expect(listItems[0].nativeElement.className).toContain('mdc-list-item--with-two-lines'); }); it('should apply a particular class to lists with three lines', () => { @@ -59,17 +56,7 @@ describe('MDC-based MatList', () => { fixture.detectChanges(); const listItems = fixture.debugElement.children[0].queryAll(By.css('mat-list-item')); - expect(listItems[0].nativeElement.className).toContain('mat-mdc-3-line'); - expect(listItems[1].nativeElement.className).toContain('mat-mdc-3-line'); - }); - - it('should apply a particular class to lists with more than 3 lines', () => { - const fixture = TestBed.createComponent(ListWithManyLines); - fixture.detectChanges(); - - const listItems = fixture.debugElement.children[0].queryAll(By.css('mat-list-item')); - expect(listItems[0].nativeElement.className).toContain('mat-mdc-multi-line'); - expect(listItems[1].nativeElement.className).toContain('mat-mdc-multi-line'); + expect(listItems[0].nativeElement.className).toContain('mdc-list-item--with-three-lines'); }); it('should apply a class to list items with avatars', () => { @@ -110,13 +97,11 @@ describe('MDC-based MatList', () => { const listItem = fixture.debugElement.children[0].query(By.css('mat-list-item'))!; expect(listItem.nativeElement.classList).toContain('mdc-list-item--with-two-lines'); - expect(listItem.nativeElement.classList).toContain('mat-mdc-2-line'); expect(listItem.nativeElement.classList).toContain('mat-mdc-list-item'); expect(listItem.nativeElement.classList).toContain('mdc-list-item'); fixture.debugElement.componentInstance.showThirdLine = true; fixture.detectChanges(); - expect(listItem.nativeElement.className).toContain('mat-mdc-3-line'); expect(listItem.nativeElement.className).toContain('mdc-list-item--with-three-lines'); }); @@ -309,15 +294,6 @@ describe('MDC-based MatList', () => { .toBe(0); })); - it('should pick up indirect descendant lines', () => { - const fixture = TestBed.createComponent(ListWithIndirectDescendantLines); - fixture.detectChanges(); - - const listItems = fixture.debugElement.children[0].queryAll(By.css('mat-list-item')); - expect(listItems[0].nativeElement.className).toContain('mat-mdc-2-line'); - expect(listItems[1].nativeElement.className).toContain('mat-mdc-2-line'); - }); - it('should be able to disable a single list item', () => { const fixture = TestBed.createComponent(ListWithDisabledItems); const listItems: HTMLElement[] = Array.from( @@ -435,8 +411,8 @@ class ListWithOneItem extends BaseTestList {} -

{{item.name}}

-

{{item.description}}

+

{{item.name}}

+

{{item.description}}

`, }) @@ -446,9 +422,9 @@ class ListWithTwoLineItem extends BaseTestList {} template: ` -

{{item.name}}

-

{{item.description}}

-

Some other text

+

{{item.name}}

+

{{item.description}}

+

Some other text

`, }) @@ -458,10 +434,10 @@ class ListWithThreeLineItem extends BaseTestList {} template: ` -

Line 1

-

Line 2

-

Line 3

-

Line 4

+

Line 1

+

Line 2

+

Line 3

+

Line 4

`, }) @@ -471,7 +447,7 @@ class ListWithManyLines extends BaseTestList {} template: ` - + Paprika @@ -485,8 +461,8 @@ class ListWithAvatar extends BaseTestList {} template: ` -

{{item.name}}

-

{{item.description}}

+

{{item.name}}

+

{{item.description}}

`, }) @@ -496,9 +472,9 @@ class ListWithItemWithCssClass extends BaseTestList {} template: ` -

{{item.name}}

-

{{item.description}}

-

Some other text

+

{{item.name}}

+

{{item.description}}

+

Some other text

`, }) @@ -514,20 +490,6 @@ class ListWithDynamicNumberOfLines extends BaseTestList {} }) class ListWithMultipleItems extends BaseTestList {} -// Note the blank `ngSwitch` which we need in order to hit the bug that we're testing. -@Component({ - template: ` - - - -

{{item.name}}

-

{{item.description}}

-
-
-
`, -}) -class ListWithIndirectDescendantLines extends BaseTestList {} - @Component({ template: ` diff --git a/src/material-experimental/mdc-list/list.ts b/src/material-experimental/mdc-list/list.ts index f1692ecbac8e..58643391d646 100644 --- a/src/material-experimental/mdc-list/list.ts +++ b/src/material-experimental/mdc-list/list.ts @@ -20,12 +20,12 @@ import { ViewEncapsulation, } from '@angular/core'; import { - MatLine, MAT_RIPPLE_GLOBAL_OPTIONS, RippleGlobalOptions, } from '@angular/material-experimental/mdc-core'; import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations'; import {MatListBase, MatListItemBase} from './list-base'; +import {MatListItemLine, MatListItemMeta, MatListItemTitle} from './list-item-sections'; @Component({ selector: 'mat-list', @@ -48,11 +48,7 @@ export class MatList extends MatListBase {} 'class': 'mat-mdc-list-item mdc-list-item', '[class.mdc-list-item--with-leading-avatar]': '_avatars.length !== 0', '[class.mdc-list-item--with-leading-icon]': '_icons.length !== 0', - // If there are projected lines, we project the remaining content into the `mdc-list-item__end` - // container. In order to make sure the container aligns properly (if there is content), we add - // the trailing meta class. Note that we also add this even if there is no projected `meta` - // content. This is because there is no good way to check for remaining projected content. - '[class.mdc-list-item--with-trailing-meta]': 'lines.length !== 0', + '[class.mdc-list-item--with-trailing-meta]': '_meta.length !== 0', '[class._mat-animation-noopable]': '_noopAnimations', }, templateUrl: 'list-item.html', @@ -60,9 +56,10 @@ export class MatList extends MatListBase {} changeDetection: ChangeDetectionStrategy.OnPush, }) export class MatListItem extends MatListItemBase { - @ContentChildren(MatLine, {read: ElementRef, descendants: true}) lines: QueryList< - ElementRef - >; + @ContentChildren(MatListItemLine, {descendants: true}) _lines: QueryList; + @ContentChildren(MatListItemTitle, {descendants: true}) _titles: QueryList; + @ContentChildren(MatListItemMeta, {descendants: true}) _meta: QueryList; + @ViewChild('unscopedContent') _unscopedContent: ElementRef; @ViewChild('text') _itemText: ElementRef; constructor( diff --git a/src/material-experimental/mdc-list/module.ts b/src/material-experimental/mdc-list/module.ts index 878d43adcb5a..c0af078cf855 100644 --- a/src/material-experimental/mdc-list/module.ts +++ b/src/material-experimental/mdc-list/module.ts @@ -9,7 +9,6 @@ import {CommonModule} from '@angular/common'; import {NgModule} from '@angular/core'; import { - MatLineModule, MatPseudoCheckboxModule, MatRippleModule, MatCommonModule, @@ -18,17 +17,26 @@ import {MatDividerModule} from '@angular/material/divider'; import {MatActionList} from './action-list'; import {MatList, MatListItem} from './list'; import {MatListOption} from './list-option'; +import {MatListSubheaderCssMatStyler} from './subheader'; import { - MatListAvatarCssMatStyler, - MatListGraphicAlignmentStyler, - MatListIconCssMatStyler, - MatListSubheaderCssMatStyler, -} from './list-styling'; + MatListItemLine, + MatListItemTitle, + MatListItemMeta, + MatListItemAvatar, + MatListItemIcon, +} from './list-item-sections'; import {MatNavList} from './nav-list'; import {MatSelectionList} from './selection-list'; +import {ObserversModule} from '@angular/cdk/observers'; @NgModule({ - imports: [CommonModule, MatCommonModule, MatLineModule, MatRippleModule, MatPseudoCheckboxModule], + imports: [ + ObserversModule, + CommonModule, + MatCommonModule, + MatRippleModule, + MatPseudoCheckboxModule, + ], exports: [ MatList, MatActionList, @@ -36,12 +44,13 @@ import {MatSelectionList} from './selection-list'; MatSelectionList, MatListItem, MatListOption, - MatListAvatarCssMatStyler, - MatListIconCssMatStyler, + MatListItemAvatar, + MatListItemIcon, MatListSubheaderCssMatStyler, - MatListGraphicAlignmentStyler, MatDividerModule, - MatLineModule, + MatListItemLine, + MatListItemTitle, + MatListItemMeta, ], declarations: [ MatList, @@ -50,10 +59,12 @@ import {MatSelectionList} from './selection-list'; MatSelectionList, MatListItem, MatListOption, - MatListAvatarCssMatStyler, - MatListIconCssMatStyler, MatListSubheaderCssMatStyler, - MatListGraphicAlignmentStyler, + MatListItemAvatar, + MatListItemIcon, + MatListItemLine, + MatListItemTitle, + MatListItemMeta, ], }) export class MatListModule {} diff --git a/src/material-experimental/mdc-list/public-api.ts b/src/material-experimental/mdc-list/public-api.ts index 37d7928d805b..adc02bd749df 100644 --- a/src/material-experimental/mdc-list/public-api.ts +++ b/src/material-experimental/mdc-list/public-api.ts @@ -12,7 +12,9 @@ export * from './module'; export * from './nav-list'; export * from './selection-list'; export * from './list-option'; -export * from './list-styling'; +export * from './subheader'; +export * from './list-item-sections'; + export {MatListOptionCheckboxPosition} from './list-option-types'; export {MatListOption} from './list-option'; diff --git a/src/material-experimental/mdc-list/selection-list.spec.ts b/src/material-experimental/mdc-list/selection-list.spec.ts index 3bde5c3da64f..9ee28517e1ed 100644 --- a/src/material-experimental/mdc-list/selection-list.spec.ts +++ b/src/material-experimental/mdc-list/selection-list.spec.ts @@ -50,7 +50,6 @@ describe('MDC-based MatSelectionList without forms', () => { SelectionListWithOnlyOneOption, SelectionListWithIndirectChildOptions, SelectionListWithSelectedOptionAndValue, - SelectionListWithIndirectDescendantLines, ], }); @@ -361,6 +360,94 @@ describe('MDC-based MatSelectionList without forms', () => { expect(event.defaultPrevented).toBe(true); }); + it('should select all items using ctrl + a', () => { + listOptions.forEach(option => (option.componentInstance.disabled = false)); + fixture.detectChanges(); + + expect(listOptions.some(option => option.componentInstance.selected)).toBe(false); + + listOptions[2].nativeElement.focus(); + dispatchKeyboardEvent(listOptions[2].nativeElement, 'keydown', A, 'A', {control: true}); + fixture.detectChanges(); + + expect(listOptions.every(option => option.componentInstance.selected)).toBe(true); + }); + + it('should not select disabled items when pressing ctrl + a', () => { + listOptions.slice(0, 2).forEach(option => (option.componentInstance.disabled = true)); + fixture.detectChanges(); + + expect(listOptions.map(option => option.componentInstance.selected)).toEqual([ + false, + false, + false, + false, + false, + ]); + + listOptions[3].nativeElement.focus(); + dispatchKeyboardEvent(listOptions[3].nativeElement, 'keydown', A, 'A', {control: true}); + fixture.detectChanges(); + + expect(listOptions.map(option => option.componentInstance.selected)).toEqual([ + false, + false, + true, + true, + true, + ]); + }); + + it('should select all items using ctrl + a if some items are selected', () => { + listOptions.slice(0, 2).forEach(option => (option.componentInstance.selected = true)); + fixture.detectChanges(); + + expect(listOptions.some(option => option.componentInstance.selected)).toBe(true); + + listOptions[2].nativeElement.focus(); + dispatchKeyboardEvent(listOptions[2].nativeElement, 'keydown', A, 'A', {control: true}); + fixture.detectChanges(); + + expect(listOptions.every(option => option.componentInstance.selected)).toBe(true); + }); + + it('should deselect all with ctrl + a if all options are selected', () => { + listOptions.forEach(option => (option.componentInstance.selected = true)); + fixture.detectChanges(); + + expect(listOptions.every(option => option.componentInstance.selected)).toBe(true); + + listOptions[2].nativeElement.focus(); + dispatchKeyboardEvent(listOptions[2].nativeElement, 'keydown', A, 'A', {control: true}); + fixture.detectChanges(); + + expect(listOptions.every(option => option.componentInstance.selected)).toBe(false); + }); + + // This is temporarily disabled as the MDC list does not emit a proper event when + // items are interactively toggled with e.g. `CTRL + A`. + // TODO(devversion): look more into this. MDC does not expose an `onChange` adapter + // function. Authors are required to emit a change event on checkbox/radio change, but + // that is not an viable option for us since we also allow for programmatic selection updates. + // https://github.com/material-components/material-components-web/blob/a986df922b6b4c1ef5c59925107281d1d40287a8/packages/mdc-list/component.ts#L300-L308. + // tslint:disable-next-line:ban + xit('should dispatch the selectionChange event when selecting via ctrl + a', () => { + const spy = spyOn(fixture.componentInstance, 'onSelectionChange'); + listOptions.forEach(option => (option.componentInstance.disabled = false)); + fixture.detectChanges(); + + listOptions[2].nativeElement.focus(); + dispatchKeyboardEvent(listOptions[2].nativeElement, 'keydown', A, 'A', {control: true}); + fixture.detectChanges(); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith( + jasmine.objectContaining({ + options: listOptions.map(option => option.componentInstance), + }), + ); + }); + it('should be able to jump focus down to an item by typing', fakeAsync(() => { const firstOption = listOptions[0].nativeElement; @@ -566,14 +653,6 @@ describe('MDC-based MatSelectionList without forms', () => { expect(listItemEl.componentInstance.value).toBe(componentFixture.componentInstance.itemValue); }); - it('should pick up indirect descendant lines', () => { - const componentFixture = TestBed.createComponent(SelectionListWithIndirectDescendantLines); - componentFixture.detectChanges(); - - const option = componentFixture.nativeElement.querySelector('mat-list-option'); - expect(option.classList).toContain('mat-mdc-2-line'); - }); - it('should have a focus indicator', () => { const optionNativeElements = listOptions.map(option => option.nativeElement as HTMLElement); @@ -814,7 +893,7 @@ describe('MDC-based MatSelectionList without forms', () => { * ensures no avatar is shown at the specified position. */ function expectIconAt(item: HTMLElement, position: 'before' | 'after') { - const icon = item.querySelector('.mat-mdc-list-icon')!; + const icon = item.querySelector('.mat-mdc-list-item-icon')!; expect(item.classList).not.toContain('mdc-list-item--with-leading-avatar'); expect(item.classList).not.toContain('mat-mdc-list-option-with-trailing-avatar'); @@ -835,7 +914,7 @@ describe('MDC-based MatSelectionList without forms', () => { * ensures that no icon is shown at the specified position. */ function expectAvatarAt(item: HTMLElement, position: 'before' | 'after') { - const avatar = item.querySelector('.mat-mdc-list-avatar')!; + const avatar = item.querySelector('.mat-mdc-list-item-avatar')!; expect(item.classList).not.toContain('mdc-list-item--with-leading-icon'); expect(item.classList).not.toContain('mdc-list-item--with-trailing-icon'); @@ -1733,7 +1812,7 @@ class SelectionListWithCustomComparator { template: ` -
I
+
I
Inbox
@@ -1747,7 +1826,7 @@ class SelectionListWithAvatar { template: ` -
I
+
I
Inbox
@@ -1771,20 +1850,6 @@ class SelectionListWithIndirectChildOptions { @ViewChildren(MatListOption) optionInstances: QueryList; } -// Note the blank `ngSwitch` which we need in order to hit the bug that we're testing. -@Component({ - template: ` - - - -

Item

-

Item description

-
-
-
`, -}) -class SelectionListWithIndirectDescendantLines {} - @Component({ template: ` diff --git a/src/material-experimental/mdc-list/subheader.ts b/src/material-experimental/mdc-list/subheader.ts new file mode 100644 index 000000000000..b1fbfb21da2e --- /dev/null +++ b/src/material-experimental/mdc-list/subheader.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Directive} from '@angular/core'; + +/** + * Directive whose purpose is to add the mat- CSS styling to this selector. + * @docs-private + */ +@Directive({ + selector: '[mat-subheader], [matSubheader]', + // TODO(mmalerba): MDC's subheader font looks identical to the list item font, figure out why and + // make a change in one of the repos to visually distinguish. + host: {'class': 'mat-mdc-subheader mdc-list-group__subheader'}, +}) +export class MatListSubheaderCssMatStyler {}