diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 4adf92417953..caba5eb81526 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -16,7 +16,7 @@
/src/material/grid-list/** @andrewseguin
/src/material/icon/** @andrewseguin
/src/material/legacy-input/** @mmalerba
-/src/material/list/** @andrewseguin @crisbeto @devversion
+/src/material/legacy-list/** @andrewseguin @crisbeto @devversion
/src/material/legacy-menu/** @crisbeto
/src/material/paginator/** @andrewseguin
/src/material/prebuilt-themes/** @andrewseguin
@@ -118,7 +118,7 @@
/src/material-experimental/mdc-core/** @crisbeto
/src/material/dialog/** @devversion
/src/material/form-field/** @devversion @mmalerba
-/src/material-experimental/mdc-list/** @mmalerba @devversion
+/src/material/list/** @mmalerba @devversion
/src/material/menu/** @crisbeto
/src/material/select/** @crisbeto
/src/material-experimental/mdc-paginator/** @crisbeto
diff --git a/.ng-dev/commit-message.mts b/.ng-dev/commit-message.mts
index 2502ea5d4752..ded36e2cbded 100644
--- a/.ng-dev/commit-message.mts
+++ b/.ng-dev/commit-message.mts
@@ -47,7 +47,6 @@ export const commitMessage: CommitMessageConfig = {
'material/dialog',
'material/form-field',
'material/input',
- 'material-experimental/mdc-list',
'material-experimental/mdc-paginator',
'material/progress-bar',
'material-experimental/mdc-progress-spinner',
@@ -85,6 +84,7 @@ export const commitMessage: CommitMessageConfig = {
'material/icon',
'material/legacy-input',
'material/list',
+ 'material/legacy-list',
'material/menu',
'material/legacy-menu',
'material/paginator',
diff --git a/goldens/tsec-exemption.json b/goldens/tsec-exemption.json
index 035ce976b9dd..9afff1aeabf5 100644
--- a/goldens/tsec-exemption.json
+++ b/goldens/tsec-exemption.json
@@ -4,7 +4,7 @@
"ban-element-setattribute": [
"../src/cdk/a11y/aria-describer/aria-reference.ts",
"../src/material/checkbox/checkbox.ts",
- "../src/material-experimental/mdc-list/interactive-list-base.ts",
+ "../src/material/list/interactive-list-base.ts",
"../src/material-experimental/mdc-progress-spinner/progress-spinner.ts",
"../src/material/slide-toggle/slide-toggle.ts",
"../src/material/icon/icon-registry.ts",
diff --git a/integration/size-test/material/list/BUILD.bazel b/integration/size-test/material/list/BUILD.bazel
index c349ba35a86c..3f49d3df4120 100644
--- a/integration/size-test/material/list/BUILD.bazel
+++ b/integration/size-test/material/list/BUILD.bazel
@@ -3,5 +3,5 @@ load("//integration/size-test:index.bzl", "size_test")
size_test(
name = "nav-list",
file = "nav-list.ts",
- deps = ["//src/material/list"],
+ deps = ["//src/material/legacy-list"],
)
diff --git a/integration/size-test/material/list/nav-list.ts b/integration/size-test/material/list/nav-list.ts
index 9c605aadcfef..4c8a3e5ad5c7 100644
--- a/integration/size-test/material/list/nav-list.ts
+++ b/integration/size-test/material/list/nav-list.ts
@@ -1,5 +1,5 @@
import {Component, NgModule} from '@angular/core';
-import {MatListModule} from '@angular/material/list';
+import {MatLegacyListModule} from '@angular/material/legacy-list';
/**
* Basic component using `MatNavList` and `MatListItem`. Other parts of the list
@@ -18,7 +18,7 @@ import {MatListModule} from '@angular/material/list';
export class TestComponent {}
@NgModule({
- imports: [MatListModule],
+ imports: [MatLegacyListModule],
declarations: [TestComponent],
bootstrap: [TestComponent],
})
diff --git a/src/components-examples/material-experimental/popover-edit/BUILD.bazel b/src/components-examples/material-experimental/popover-edit/BUILD.bazel
index 7c75580b5486..9682b28ac100 100644
--- a/src/components-examples/material-experimental/popover-edit/BUILD.bazel
+++ b/src/components-examples/material-experimental/popover-edit/BUILD.bazel
@@ -15,7 +15,7 @@ ng_module(
"//src/material/icon",
"//src/material/legacy-checkbox",
"//src/material/legacy-input",
- "//src/material/list",
+ "//src/material/legacy-list",
"//src/material/snack-bar",
"//src/material/table",
"@npm//@angular/common",
diff --git a/src/components-examples/material-experimental/popover-edit/index.ts b/src/components-examples/material-experimental/popover-edit/index.ts
index a469fbc4b3b4..5896571fc4b9 100644
--- a/src/components-examples/material-experimental/popover-edit/index.ts
+++ b/src/components-examples/material-experimental/popover-edit/index.ts
@@ -6,7 +6,7 @@ import {MatButtonModule} from '@angular/material/button';
import {MatLegacyCheckboxModule} from '@angular/material/legacy-checkbox';
import {MatIconModule} from '@angular/material/icon';
import {MatLegacyInputModule} from '@angular/material/legacy-input';
-import {MatListModule} from '@angular/material/list';
+import {MatLegacyListModule} from '@angular/material/legacy-list';
import {MatSnackBarModule} from '@angular/material/snack-bar';
import {MatTableModule} from '@angular/material/table';
import {PopoverEditCellSpanMatTableExample} from './popover-edit-cell-span-mat-table/popover-edit-cell-span-mat-table-example';
@@ -35,7 +35,7 @@ const EXAMPLES = [
MatLegacyCheckboxModule,
MatIconModule,
MatLegacyInputModule,
- MatListModule,
+ MatLegacyListModule,
MatPopoverEditModule,
MatSnackBarModule,
MatTableModule,
diff --git a/src/components-examples/material/bottom-sheet/BUILD.bazel b/src/components-examples/material/bottom-sheet/BUILD.bazel
index d2eaac889ff2..de008a3b6873 100644
--- a/src/components-examples/material/bottom-sheet/BUILD.bazel
+++ b/src/components-examples/material/bottom-sheet/BUILD.bazel
@@ -19,7 +19,7 @@ ng_module(
"//src/material/bottom-sheet",
"//src/material/bottom-sheet/testing",
"//src/material/button",
- "//src/material/list",
+ "//src/material/legacy-list",
"@npm//@angular/platform-browser",
"@npm//@angular/platform-browser-dynamic",
"@npm//@types/jasmine",
diff --git a/src/components-examples/material/bottom-sheet/index.ts b/src/components-examples/material/bottom-sheet/index.ts
index 35b5fcc016f8..adec0b5c174e 100644
--- a/src/components-examples/material/bottom-sheet/index.ts
+++ b/src/components-examples/material/bottom-sheet/index.ts
@@ -1,7 +1,7 @@
import {NgModule} from '@angular/core';
import {MatBottomSheetModule} from '@angular/material/bottom-sheet';
import {MatButtonModule} from '@angular/material/button';
-import {MatListModule} from '@angular/material/list';
+import {MatLegacyListModule} from '@angular/material/legacy-list';
import {
BottomSheetOverviewExample,
BottomSheetOverviewExampleSheet,
@@ -17,7 +17,7 @@ const EXAMPLES = [
];
@NgModule({
- imports: [MatBottomSheetModule, MatButtonModule, MatListModule],
+ imports: [MatBottomSheetModule, MatButtonModule, MatLegacyListModule],
declarations: EXAMPLES,
exports: EXAMPLES,
})
diff --git a/src/components-examples/material/divider/BUILD.bazel b/src/components-examples/material/divider/BUILD.bazel
index 0203ff33234a..9e798951feb5 100644
--- a/src/components-examples/material/divider/BUILD.bazel
+++ b/src/components-examples/material/divider/BUILD.bazel
@@ -17,7 +17,7 @@ ng_module(
"//src/cdk/testing/testbed",
"//src/material/divider",
"//src/material/divider/testing",
- "//src/material/list",
+ "//src/material/legacy-list",
"@npm//@angular/platform-browser",
"@npm//@angular/platform-browser-dynamic",
"@npm//@types/jasmine",
diff --git a/src/components-examples/material/divider/index.ts b/src/components-examples/material/divider/index.ts
index d6fc46ef4909..4a269b477f4a 100644
--- a/src/components-examples/material/divider/index.ts
+++ b/src/components-examples/material/divider/index.ts
@@ -1,6 +1,6 @@
import {NgModule} from '@angular/core';
import {MatDividerModule} from '@angular/material/divider';
-import {MatListModule} from '@angular/material/list';
+import {MatLegacyListModule} from '@angular/material/legacy-list';
import {DividerOverviewExample} from './divider-overview/divider-overview-example';
import {DividerHarnessExample} from './divider-harness/divider-harness-example';
@@ -9,7 +9,7 @@ export {DividerHarnessExample, DividerOverviewExample};
const EXAMPLES = [DividerHarnessExample, DividerOverviewExample];
@NgModule({
- imports: [MatDividerModule, MatListModule],
+ imports: [MatDividerModule, MatLegacyListModule],
declarations: EXAMPLES,
exports: EXAMPLES,
})
diff --git a/src/components-examples/material/list/BUILD.bazel b/src/components-examples/material/list/BUILD.bazel
index fc7e0e692d46..1d381c70f426 100644
--- a/src/components-examples/material/list/BUILD.bazel
+++ b/src/components-examples/material/list/BUILD.bazel
@@ -16,8 +16,8 @@ ng_module(
"//src/cdk/testing",
"//src/cdk/testing/testbed",
"//src/material/icon",
- "//src/material/list",
- "//src/material/list/testing",
+ "//src/material/legacy-list",
+ "//src/material/legacy-list/testing",
"@npm//@angular/platform-browser",
"@npm//@angular/platform-browser-dynamic",
"@npm//@types/jasmine",
@@ -40,8 +40,8 @@ ng_test_library(
":list",
"//src/cdk/testing",
"//src/cdk/testing/testbed",
- "//src/material/list",
- "//src/material/list/testing",
+ "//src/material/legacy-list",
+ "//src/material/legacy-list/testing",
"@npm//@angular/platform-browser-dynamic",
],
)
diff --git a/src/components-examples/material/list/index.ts b/src/components-examples/material/list/index.ts
index 394651accd7b..1507d89eea19 100644
--- a/src/components-examples/material/list/index.ts
+++ b/src/components-examples/material/list/index.ts
@@ -1,7 +1,7 @@
import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
import {MatIconModule} from '@angular/material/icon';
-import {MatListModule} from '@angular/material/list';
+import {MatLegacyListModule} from '@angular/material/legacy-list';
import {ListOverviewExample} from './list-overview/list-overview-example';
import {ListSectionsExample} from './list-sections/list-sections-example';
import {ListSelectionExample} from './list-selection/list-selection-example';
@@ -25,7 +25,7 @@ const EXAMPLES = [
];
@NgModule({
- imports: [CommonModule, MatIconModule, MatListModule],
+ imports: [CommonModule, MatIconModule, MatLegacyListModule],
declarations: EXAMPLES,
exports: EXAMPLES,
})
diff --git a/src/components-examples/material/list/list-harness/list-harness-example.spec.ts b/src/components-examples/material/list/list-harness/list-harness-example.spec.ts
index 463bb0cf31cd..10db84963547 100644
--- a/src/components-examples/material/list/list-harness/list-harness-example.spec.ts
+++ b/src/components-examples/material/list/list-harness/list-harness-example.spec.ts
@@ -1,8 +1,8 @@
import {HarnessLoader, parallel} from '@angular/cdk/testing';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
-import {MatListHarness} from '@angular/material/list/testing';
-import {MatListModule} from '@angular/material/list';
+import {MatLegacyListHarness} from '@angular/material/legacy-list/testing';
+import {MatLegacyListModule} from '@angular/material/legacy-list';
import {ListHarnessExample} from './list-harness-example';
describe('ListHarnessExample', () => {
@@ -11,7 +11,7 @@ describe('ListHarnessExample', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
- imports: [MatListModule],
+ imports: [MatLegacyListModule],
declarations: [ListHarnessExample],
}).compileComponents();
fixture = TestBed.createComponent(ListHarnessExample);
@@ -20,7 +20,7 @@ describe('ListHarnessExample', () => {
});
it('should get all items', async () => {
- const list = await loader.getHarness(MatListHarness);
+ const list = await loader.getHarness(MatLegacyListHarness);
const items = await list.getItems();
expect(await parallel(() => items.map(i => i.getText()))).toEqual([
'Item 1',
@@ -30,13 +30,13 @@ describe('ListHarnessExample', () => {
});
it('should get all items matching text', async () => {
- const list = await loader.getHarness(MatListHarness);
+ const list = await loader.getHarness(MatLegacyListHarness);
const items = await list.getItems({text: /[13]/});
expect(await parallel(() => items.map(i => i.getText()))).toEqual(['Item 1', 'Item 3']);
});
it('should get items by subheader', async () => {
- const list = await loader.getHarness(MatListHarness);
+ const list = await loader.getHarness(MatLegacyListHarness);
const sections = await list.getItemsGroupedBySubheader();
expect(sections.length).toBe(3);
expect(sections[0].heading).toBeUndefined();
@@ -51,7 +51,7 @@ describe('ListHarnessExample', () => {
});
it('should get list item text and lines', async () => {
- const list = await loader.getHarness(MatListHarness);
+ const list = await loader.getHarness(MatLegacyListHarness);
const items = await list.getItems();
expect(items.length).toBe(3);
expect(await items[0].getText()).toBe('Item 1');
diff --git a/src/components-examples/material/sidenav/BUILD.bazel b/src/components-examples/material/sidenav/BUILD.bazel
index 4057d76f6d45..e59545343d9e 100644
--- a/src/components-examples/material/sidenav/BUILD.bazel
+++ b/src/components-examples/material/sidenav/BUILD.bazel
@@ -19,9 +19,9 @@ ng_module(
"//src/material/button",
"//src/material/icon",
"//src/material/legacy-checkbox",
+ "//src/material/legacy-list",
"//src/material/legacy-radio",
"//src/material/legacy-select",
- "//src/material/list",
"//src/material/sidenav",
"//src/material/toolbar",
"@npm//@angular/forms",
diff --git a/src/components-examples/material/sidenav/index.ts b/src/components-examples/material/sidenav/index.ts
index 0b38ebb7d244..d3177169f00f 100644
--- a/src/components-examples/material/sidenav/index.ts
+++ b/src/components-examples/material/sidenav/index.ts
@@ -4,7 +4,7 @@ import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MatButtonModule} from '@angular/material/button';
import {MatLegacyCheckboxModule} from '@angular/material/legacy-checkbox';
import {MatIconModule} from '@angular/material/icon';
-import {MatListModule} from '@angular/material/list';
+import {MatLegacyListModule} from '@angular/material/legacy-list';
import {MatLegacyRadioModule} from '@angular/material/legacy-radio';
import {MatLegacySelectModule} from '@angular/material/legacy-select';
import {MatSidenavModule} from '@angular/material/sidenav';
@@ -56,7 +56,7 @@ const EXAMPLES = [
MatButtonModule,
MatLegacyCheckboxModule,
MatIconModule,
- MatListModule,
+ MatLegacyListModule,
MatLegacyRadioModule,
MatSidenavModule,
MatLegacySelectModule,
diff --git a/src/dev-app/bottom-sheet/BUILD.bazel b/src/dev-app/bottom-sheet/BUILD.bazel
index 1a4dc0d9440e..4f394b3b46e6 100644
--- a/src/dev-app/bottom-sheet/BUILD.bazel
+++ b/src/dev-app/bottom-sheet/BUILD.bazel
@@ -17,8 +17,8 @@ ng_module(
"//src/material/legacy-checkbox",
"//src/material/legacy-form-field",
"//src/material/legacy-input",
+ "//src/material/legacy-list",
"//src/material/legacy-select",
- "//src/material/list",
"@npm//@angular/forms",
],
)
diff --git a/src/dev-app/bottom-sheet/bottom-sheet-demo.ts b/src/dev-app/bottom-sheet/bottom-sheet-demo.ts
index 0ca5a7c93bb9..664ef88c9829 100644
--- a/src/dev-app/bottom-sheet/bottom-sheet-demo.ts
+++ b/src/dev-app/bottom-sheet/bottom-sheet-demo.ts
@@ -21,7 +21,7 @@ import {MatLegacyCheckboxModule} from '@angular/material/legacy-checkbox';
import {MatLegacyFormFieldModule} from '@angular/material/legacy-form-field';
import {MatIconModule} from '@angular/material/icon';
import {MatLegacyInputModule} from '@angular/material/legacy-input';
-import {MatListModule} from '@angular/material/list';
+import {MatLegacyListModule} from '@angular/material/legacy-list';
import {MatLegacySelectModule} from '@angular/material/legacy-select';
const defaultConfig = new MatBottomSheetConfig();
@@ -42,7 +42,7 @@ const defaultConfig = new MatBottomSheetConfig();
MatIconModule,
MatLegacyInputModule,
MatLegacySelectModule,
- MatListModule,
+ MatLegacyListModule,
],
})
export class BottomSheetDemo {
@@ -77,7 +77,7 @@ export class BottomSheetDemo {
`,
standalone: true,
- imports: [CommonModule, MatListModule],
+ imports: [CommonModule, MatLegacyListModule],
})
export class ExampleBottomSheet {
constructor(private _bottomSheet: MatBottomSheetRef) {}
diff --git a/src/dev-app/dev-app/BUILD.bazel b/src/dev-app/dev-app/BUILD.bazel
index 1208882e0a48..d7e0f048c38b 100644
--- a/src/dev-app/dev-app/BUILD.bazel
+++ b/src/dev-app/dev-app/BUILD.bazel
@@ -15,7 +15,7 @@ ng_module(
"//src/material/button",
"//src/material/core",
"//src/material/icon",
- "//src/material/list",
+ "//src/material/legacy-list",
"//src/material/sidenav",
"//src/material/toolbar",
"@npm//@angular/router",
diff --git a/src/dev-app/dev-app/dev-app-layout.ts b/src/dev-app/dev-app/dev-app-layout.ts
index fb16ccc68b94..1dbf750a5a60 100644
--- a/src/dev-app/dev-app/dev-app-layout.ts
+++ b/src/dev-app/dev-app/dev-app-layout.ts
@@ -13,7 +13,7 @@ import {DevAppDirectionality} from './dev-app-directionality';
import {DevAppRippleOptions} from './ripple-options';
import {CommonModule, DOCUMENT} from '@angular/common';
import {MatSidenavModule} from '@angular/material/sidenav';
-import {MatListModule} from '@angular/material/list';
+import {MatLegacyListModule} from '@angular/material/legacy-list';
import {MatButtonModule} from '@angular/material/button';
import {RouterModule} from '@angular/router';
import {MatIconModule} from '@angular/material/icon';
@@ -34,7 +34,7 @@ export const ANIMATIONS_STORAGE_KEY = 'ANGULAR_COMPONENTS_ANIMATIONS_DISABLED';
CommonModule,
MatButtonModule,
MatIconModule,
- MatListModule,
+ MatLegacyListModule,
MatSidenavModule,
MatToolbarModule,
RouterModule,
diff --git a/src/dev-app/drawer/BUILD.bazel b/src/dev-app/drawer/BUILD.bazel
index 84a126e8831a..2f91c505260d 100644
--- a/src/dev-app/drawer/BUILD.bazel
+++ b/src/dev-app/drawer/BUILD.bazel
@@ -11,7 +11,7 @@ ng_module(
],
deps = [
"//src/material/button",
- "//src/material/list",
+ "//src/material/legacy-list",
"//src/material/sidenav",
],
)
diff --git a/src/dev-app/drawer/drawer-demo.ts b/src/dev-app/drawer/drawer-demo.ts
index 367ba3a6094d..61da9bc65720 100644
--- a/src/dev-app/drawer/drawer-demo.ts
+++ b/src/dev-app/drawer/drawer-demo.ts
@@ -8,7 +8,7 @@
import {Component} from '@angular/core';
import {MatButtonModule} from '@angular/material/button';
-import {MatListModule} from '@angular/material/list';
+import {MatLegacyListModule} from '@angular/material/legacy-list';
import {MatSidenavModule} from '@angular/material/sidenav';
@Component({
@@ -16,7 +16,7 @@ import {MatSidenavModule} from '@angular/material/sidenav';
templateUrl: 'drawer-demo.html',
styleUrls: ['drawer-demo.css'],
standalone: true,
- imports: [MatButtonModule, MatListModule, MatSidenavModule],
+ imports: [MatButtonModule, MatLegacyListModule, MatSidenavModule],
})
export class DrawerDemo {
invert = false;
diff --git a/src/dev-app/list/BUILD.bazel b/src/dev-app/list/BUILD.bazel
index 36376529141c..e010b3180b5e 100644
--- a/src/dev-app/list/BUILD.bazel
+++ b/src/dev-app/list/BUILD.bazel
@@ -13,7 +13,7 @@ ng_module(
"//src/material/button",
"//src/material/icon",
"//src/material/legacy-checkbox",
- "//src/material/list",
+ "//src/material/legacy-list",
],
)
diff --git a/src/dev-app/list/list-demo.ts b/src/dev-app/list/list-demo.ts
index 69c9c2b21c14..7d0c3eeb8003 100644
--- a/src/dev-app/list/list-demo.ts
+++ b/src/dev-app/list/list-demo.ts
@@ -12,7 +12,10 @@ import {FormsModule} from '@angular/forms';
import {MatButtonModule} from '@angular/material/button';
import {MatLegacyCheckboxModule} from '@angular/material/legacy-checkbox';
import {MatIconModule} from '@angular/material/icon';
-import {MatListModule, MatListOptionCheckboxPosition} from '@angular/material/list';
+import {
+ MatLegacyListModule,
+ MatLegacyListOptionCheckboxPosition,
+} from '@angular/material/legacy-list';
@Component({
selector: 'list-demo',
@@ -25,7 +28,7 @@ import {MatListModule, MatListOptionCheckboxPosition} from '@angular/material/li
MatButtonModule,
MatLegacyCheckboxModule,
MatIconModule,
- MatListModule,
+ MatLegacyListModule,
],
})
export class ListDemo {
@@ -37,7 +40,7 @@ export class ListDemo {
{name: 'Bobby', headline: 'UX designer'},
];
- checkboxPosition: MatListOptionCheckboxPosition = 'before';
+ checkboxPosition: MatLegacyListOptionCheckboxPosition = 'before';
messages: {from: string; subject: string; message: string; image: string}[] = [
{
diff --git a/src/dev-app/mdc-list/BUILD.bazel b/src/dev-app/mdc-list/BUILD.bazel
index 11cd909993f1..0b60ff60c322 100644
--- a/src/dev-app/mdc-list/BUILD.bazel
+++ b/src/dev-app/mdc-list/BUILD.bazel
@@ -11,8 +11,8 @@ ng_module(
],
deps = [
"//src/material-experimental/mdc-button",
- "//src/material-experimental/mdc-list",
"//src/material/icon",
+ "//src/material/list",
],
)
diff --git a/src/dev-app/mdc-list/mdc-list-demo.ts b/src/dev-app/mdc-list/mdc-list-demo.ts
index 7341a89a582c..508a084d087d 100644
--- a/src/dev-app/mdc-list/mdc-list-demo.ts
+++ b/src/dev-app/mdc-list/mdc-list-demo.ts
@@ -9,10 +9,7 @@
import {Component} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {MatButtonModule} from '@angular/material-experimental/mdc-button';
-import {
- MatListModule,
- MatListOptionCheckboxPosition,
-} from '@angular/material-experimental/mdc-list';
+import {MatListModule, MatListOptionCheckboxPosition} from '@angular/material/list';
import {MatIconModule} from '@angular/material/icon';
import {CommonModule} from '@angular/common';
diff --git a/src/e2e-app/BUILD.bazel b/src/e2e-app/BUILD.bazel
index 7f21255e09bf..5aa4082c4d59 100644
--- a/src/e2e-app/BUILD.bazel
+++ b/src/e2e-app/BUILD.bazel
@@ -59,11 +59,11 @@ ng_module(
"//src/material/legacy-dialog",
"//src/material/legacy-form-field",
"//src/material/legacy-input",
+ "//src/material/legacy-list",
"//src/material/legacy-menu",
"//src/material/legacy-progress-bar",
"//src/material/legacy-radio",
"//src/material/legacy-slide-toggle",
- "//src/material/list",
"//src/material/menu",
"//src/material/progress-bar",
"//src/material/progress-spinner",
diff --git a/src/e2e-app/e2e-app/e2e-app-module.ts b/src/e2e-app/e2e-app/e2e-app-module.ts
index 807c74be264a..eadbc26f82ea 100644
--- a/src/e2e-app/e2e-app/e2e-app-module.ts
+++ b/src/e2e-app/e2e-app/e2e-app-module.ts
@@ -8,12 +8,12 @@
import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
-import {MatListModule} from '@angular/material/list';
+import {MatLegacyListModule} from '@angular/material/legacy-list';
import {RouterModule} from '@angular/router';
import {E2eAppLayout, Home} from './e2e-app-layout';
@NgModule({
- imports: [CommonModule, MatListModule, RouterModule],
+ imports: [CommonModule, MatLegacyListModule, RouterModule],
declarations: [E2eAppLayout, Home],
exports: [E2eAppLayout],
})
diff --git a/src/material-experimental/_index.scss b/src/material-experimental/_index.scss
index 464e1561d044..0fbdc9d6fbf3 100644
--- a/src/material-experimental/_index.scss
+++ b/src/material-experimental/_index.scss
@@ -23,8 +23,6 @@
mdc-fab-density, mdc-fab-theme;
@forward './mdc-button/icon-button-theme' as mdc-icon-button-* show mdc-icon-button-color,
mdc-icon-button-typography, mdc-icon-button-density, mdc-icon-button-theme;
-@forward './mdc-list/list-theme' as mdc-list-* show mdc-list-color, mdc-list-typography,
- mdc-list-density, mdc-list-theme;
@forward './mdc-paginator/paginator-theme' as mdc-paginator-* show mdc-paginator-color,
mdc-paginator-typography, mdc-paginator-density, mdc-paginator-theme;
@forward './mdc-progress-spinner/progress-spinner-theme' as mdc-progress-spinner-* show
diff --git a/src/material-experimental/config.bzl b/src/material-experimental/config.bzl
index 679125071412..c14b6bd1e3c2 100644
--- a/src/material-experimental/config.bzl
+++ b/src/material-experimental/config.bzl
@@ -3,8 +3,6 @@ entryPoints = [
"mdc-button",
"mdc-button/testing",
"mdc-core",
- "mdc-list",
- "mdc-list/testing",
"mdc-paginator",
"mdc-paginator/testing",
"mdc-progress-spinner",
diff --git a/src/material-experimental/mdc-core/color/_all-color.import.scss b/src/material-experimental/mdc-core/color/_all-color.import.scss
index fe8a5f7af90c..fbf07b4fdd69 100644
--- a/src/material-experimental/mdc-core/color/_all-color.import.scss
+++ b/src/material-experimental/mdc-core/color/_all-color.import.scss
@@ -23,9 +23,6 @@ $mat-mdc-table-mdc-data-table-sort-icon-active-color, $mat-mdc-table-mdc-data-ta
$mat-mdc-table-mdc-data-table-stroke-color, $mat-mdc-table-mdc-data-table-table-divider-color;
@forward '../../mdc-paginator/paginator-variables' as mat-mdc-paginator-*;
@forward '../core-theme.import';
-@forward '../../mdc-list/interactive-list-theme' as mat-mdc-*;
-@forward '../../mdc-list/list-option-theme' as mat-mdc-*;
-@forward '../../mdc-list/list-theme' as mat-mdc-list-*;
@forward '../../mdc-paginator/paginator-theme' as mat-mdc-paginator-*;
@forward '../../mdc-progress-spinner/progress-spinner-theme' as mat-mdc-progress-spinner-*;
@forward '../theming/all-theme';
diff --git a/src/material-experimental/mdc-core/density/_all-density.import.scss b/src/material-experimental/mdc-core/density/_all-density.import.scss
index 749a371e6775..9ec7db33a77f 100644
--- a/src/material-experimental/mdc-core/density/_all-density.import.scss
+++ b/src/material-experimental/mdc-core/density/_all-density.import.scss
@@ -23,9 +23,6 @@ $mat-mdc-table-mdc-data-table-sort-icon-active-color, $mat-mdc-table-mdc-data-ta
$mat-mdc-table-mdc-data-table-stroke-color, $mat-mdc-table-mdc-data-table-table-divider-color;
@forward '../../mdc-paginator/paginator-variables' as mat-mdc-paginator-*;
@forward '../core-theme.import';
-@forward '../../mdc-list/interactive-list-theme' as mat-mdc-*;
-@forward '../../mdc-list/list-option-theme' as mat-mdc-*;
-@forward '../../mdc-list/list-theme' as mat-mdc-list-*;
@forward '../../mdc-paginator/paginator-theme' as mat-mdc-paginator-*;
@forward '../../mdc-progress-spinner/progress-spinner-theme' as mat-mdc-progress-spinner-*;
@forward '../theming/all-theme';
diff --git a/src/material-experimental/mdc-core/theming/BUILD.bazel b/src/material-experimental/mdc-core/theming/BUILD.bazel
index 1cb9bbb453bb..3bff49de16c2 100644
--- a/src/material-experimental/mdc-core/theming/BUILD.bazel
+++ b/src/material-experimental/mdc-core/theming/BUILD.bazel
@@ -22,7 +22,6 @@ sass_library(
"//src/material:sass_lib",
"//src/material-experimental/mdc-button:mdc_button_scss_lib",
"//src/material-experimental/mdc-core:mdc_core_scss_lib",
- "//src/material-experimental/mdc-list:mdc_list_scss_lib",
"//src/material-experimental/mdc-paginator:mdc_paginator_scss_lib",
"//src/material-experimental/mdc-progress-spinner:mdc_progress_spinner_scss_lib",
"//src/material-experimental/mdc-snack-bar:mdc_snack_bar_scss_lib",
diff --git a/src/material-experimental/mdc-core/theming/_all-theme.import.scss b/src/material-experimental/mdc-core/theming/_all-theme.import.scss
index 879c7d2984b0..d75a592df8f1 100644
--- a/src/material-experimental/mdc-core/theming/_all-theme.import.scss
+++ b/src/material-experimental/mdc-core/theming/_all-theme.import.scss
@@ -23,16 +23,12 @@ $mat-mdc-table-mdc-data-table-sort-icon-active-color, $mat-mdc-table-mdc-data-ta
$mat-mdc-table-mdc-data-table-stroke-color, $mat-mdc-table-mdc-data-table-table-divider-color;
@forward '../../mdc-paginator/paginator-variables' as mat-mdc-paginator-*;
@forward '../core-theme.import';
-@forward '../../mdc-list/interactive-list-theme' as mat-mdc-*;
-@forward '../../mdc-list/list-option-theme' as mat-mdc-*;
-@forward '../../mdc-list/list-theme' as mat-mdc-list-*;
@forward '../../mdc-paginator/paginator-theme' as mat-mdc-paginator-*;
@forward '../../mdc-progress-spinner/progress-spinner-theme' as mat-mdc-progress-spinner-*;
@forward 'all-theme' hide all-mdc-component-themes;;
@import '../core-theme';
@import '../../mdc-button/button-theme';
-@import '../../mdc-list/list-theme';
@import '../../mdc-snack-bar/snack-bar-theme';
@import '../../mdc-tabs/tabs-theme';
@import '../../mdc-table/table-theme';
diff --git a/src/material-experimental/mdc-core/theming/_all-theme.scss b/src/material-experimental/mdc-core/theming/_all-theme.scss
index 91ec7206bed6..9856e1caa820 100644
--- a/src/material-experimental/mdc-core/theming/_all-theme.scss
+++ b/src/material-experimental/mdc-core/theming/_all-theme.scss
@@ -4,7 +4,6 @@
@use '../../mdc-button/button-theme';
@use '../../mdc-button/fab-theme';
@use '../../mdc-button/icon-button-theme';
-@use '../../mdc-list/list-theme';
@use '../../mdc-snack-bar/snack-bar-theme';
@use '../../mdc-tabs/tabs-theme';
@use '../../mdc-table/table-theme';
@@ -23,7 +22,7 @@
@include mat.card-theme($theme-or-color-config);
@include mat.checkbox-theme($theme-or-color-config);
@include mat.chips-theme($theme-or-color-config);
- @include list-theme.theme($theme-or-color-config);
+ @include mat.list-theme($theme-or-color-config);
@include mat.menu-theme($theme-or-color-config);
@include paginator-theme.theme($theme-or-color-config);
@include mat.progress-bar-theme($theme-or-color-config);
diff --git a/src/material-experimental/mdc-core/typography/_all-typography.import.scss b/src/material-experimental/mdc-core/typography/_all-typography.import.scss
index d51b573734da..900ec9d33dfe 100644
--- a/src/material-experimental/mdc-core/typography/_all-typography.import.scss
+++ b/src/material-experimental/mdc-core/typography/_all-typography.import.scss
@@ -23,9 +23,6 @@ $mat-mdc-table-mdc-data-table-sort-icon-active-color, $mat-mdc-table-mdc-data-ta
$mat-mdc-table-mdc-data-table-stroke-color, $mat-mdc-table-mdc-data-table-table-divider-color;
@forward '../../mdc-paginator/paginator-variables' as mat-mdc-paginator-*;
@forward '../core-theme.import';
-@forward '../../mdc-list/interactive-list-theme' as mat-mdc-*;
-@forward '../../mdc-list/list-option-theme' as mat-mdc-*;
-@forward '../../mdc-list/list-theme' as mat-mdc-list-*;
@forward '../../mdc-paginator/paginator-theme' as mat-mdc-paginator-*;
@forward '../../mdc-progress-spinner/progress-spinner-theme' as mat-mdc-progress-spinner-*;
@forward '../theming/all-theme';
diff --git a/src/material-experimental/mdc-list/README.md b/src/material-experimental/mdc-list/README.md
deleted file mode 100644
index 481def9a6cb8..000000000000
--- a/src/material-experimental/mdc-list/README.md
+++ /dev/null
@@ -1 +0,0 @@
-TODO: WIP
diff --git a/src/material-experimental/mdc-list/_list-theme.import.scss b/src/material-experimental/mdc-list/_list-theme.import.scss
deleted file mode 100644
index 0e0a4a5684aa..000000000000
--- a/src/material-experimental/mdc-list/_list-theme.import.scss
+++ /dev/null
@@ -1,6 +0,0 @@
-@forward 'interactive-list-theme' as mat-mdc-*;
-@forward 'list-option-theme' as mat-mdc-*;
-@forward 'list-theme' as mat-mdc-list-*;
-
-@import './interactive-list-theme';
-@import './list-option-theme';
diff --git a/src/material-experimental/mdc-list/_list-theme.scss b/src/material-experimental/mdc-list/_list-theme.scss
deleted file mode 100644
index fe7078186932..000000000000
--- a/src/material-experimental/mdc-list/_list-theme.scss
+++ /dev/null
@@ -1,80 +0,0 @@
-@use 'sass:map';
-@use '@angular/material' as mat;
-@use '@material/list/evolution-mixins' as mdc-list;
-
-@use './interactive-list-theme';
-@use './list-option-theme';
-
-@mixin color($config-or-theme) {
- $config: mat.get-color-config($config-or-theme);
- $primary: mat.get-color-from-palette(map.get($config, primary));
- $accent: mat.get-color-from-palette(map.get($config, accent));
- $warn: mat.get-color-from-palette(map.get($config, warn));
-
- @include mat.private-using-mdc-theme($config) {
- // MDC's state styles are tied in with their ripple. Since we don't use the MDC
- // ripple, we need to add the hover, focus and selected states manually.
- @include interactive-list-theme.private-interactive-list-item-state-colors($config);
- @include mdc-list.without-ripple($query: mat.$private-mdc-theme-styles-query);
-
- .mat-mdc-list-option {
- @include list-option-theme.private-list-option-color-override($config, $primary, primary);
- }
- .mat-mdc-list-option.mat-accent {
- @include list-option-theme.private-list-option-color-override($config, $accent, secondary);
- }
- .mat-mdc-list-option.mat-warn {
- @include list-option-theme.private-list-option-color-override($config, $warn, error);
- }
- }
-}
-
-@mixin density($config-or-theme) {
- $density-scale: mat.get-density-config($config-or-theme);
-
- @include mat.private-disable-mdc-fallback-declarations {
- .mat-mdc-list-item {
- @include mdc-list.one-line-item-density($density-scale);
- @include mdc-list.two-line-item-density($density-scale);
- @include mdc-list.three-line-item-density($density-scale);
- }
-
- @include list-option-theme.private-list-option-density-styles($density-scale);
- }
-}
-
-@mixin typography($config-or-theme) {
- $config: mat.private-typography-to-2018-config(
- mat.get-typography-config($config-or-theme));
- @include mat.private-using-mdc-typography($config) {
- @include mdc-list.without-ripple($query: mat.$private-mdc-typography-styles-query);
- @include list-option-theme.private-list-option-typography-styles();
- }
-
- // According to the public spec this should be subtitle-1.
- // However, body-1 and subtitle-1 are nearly identical in the public spec,
- // and the Google-specific spec states that it should be body-1.
- // For consistency, we use body-1 for both public and Google internal.
- .mat-mdc-list-item .mdc-list-item__primary-text {
- @include mat.typography-level($config, body-1);
- }
-}
-
-@mixin theme($theme-or-color-config) {
- $theme: mat.private-legacy-get-theme($theme-or-color-config);
- @include mat.private-check-duplicate-theme-styles($theme, 'mat-mdc-list') {
- $color: mat.get-color-config($theme);
- $density: mat.get-density-config($theme);
- $typography: mat.get-typography-config($theme);
-
- @if $color != null {
- @include color($color);
- }
- @if $density != null {
- @include density($density);
- }
- @if $typography != null {
- @include typography($typography);
- }
- }
-}
diff --git a/src/material-experimental/mdc-list/list-item.html b/src/material-experimental/mdc-list/list-item.html
deleted file mode 100644
index e52a1e9491c4..000000000000
--- a/src/material-experimental/mdc-list/list-item.html
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/material-experimental/mdc-list/list-option.html b/src/material-experimental/mdc-list/list-option.html
deleted file mode 100644
index 3dd89050aa65..000000000000
--- a/src/material-experimental/mdc-list/list-option.html
+++ /dev/null
@@ -1,64 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/material-experimental/mdc-list/list.e2e.spec.ts b/src/material-experimental/mdc-list/list.e2e.spec.ts
deleted file mode 100644
index da97d573c380..000000000000
--- a/src/material-experimental/mdc-list/list.e2e.spec.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-it('should have e2e tests', () => {
- // TODO: Implement.
-});
diff --git a/src/material-experimental/mdc-list/list.scss b/src/material-experimental/mdc-list/list.scss
deleted file mode 100644
index 76c3cce765db..000000000000
--- a/src/material-experimental/mdc-list/list.scss
+++ /dev/null
@@ -1,113 +0,0 @@
-@use '@angular/material' as mat;
-@use '@material/list/evolution-mixins' as mdc-list;
-
-
-@include mat.private-disable-mdc-fallback-declarations {
- @include mdc-list.without-ripple($query: mat.$private-mdc-base-styles-query);
-}
-
-// MDC expects the list element to be a ``, since we use `` instead we need to
-// explicitly set `display: block`
-.mat-mdc-list-base {
- display: block;
-}
-
-// MDC expects that the list items are always ``, since we actually use `` in some
-// cases, we need to make sure it expands to fill the available width.
-.mat-mdc-list-item,
-.mat-mdc-list-option {
- width: 100%;
- box-sizing: border-box;
-}
-
-// MDC doesn't have list dividers, so we use mat-divider and style appropriately.
-// TODO(devversion): check if we can use the MDC dividers.
-.mat-mdc-list-item,
-.mat-mdc-list-option {
- .mat-divider-inset {
- position: absolute;
- left: 0;
- right: 0;
- bottom: 0;
- }
-
- .mat-mdc-list-item-avatar ~ .mat-divider-inset {
- margin-left: 72px;
-
- [dir='rtl'] & {
- margin-right: 72px;
- }
- }
-}
-
-// MDC's hover and focus state styles are included with their ripple which we don't use.
-// Instead we add the focus, hover and selected styles ourselves using this pseudo-element
-.mat-mdc-list-item-interactive::before {
- @include mat.private-fill();
- content: '';
- opacity: 0;
-}
-
-// MDC always sets the cursor to `pointer`. We do not want to show this for non-interactive
-// lists. See: https://github.com/material-components/material-components-web/issues/6443
-.mat-mdc-list-non-interactive .mdc-list-item {
- cursor: default;
-}
-
-// The MDC-based list items already use the `::before` pseudo element for the standard
-// focus/selected/hover state. Hence, we need to have a separate list-item spanning
-// element that can be used for strong focus indicators.
-.mat-mdc-list-item {
- > .mat-mdc-focus-indicator {
- @include mat.private-fill();
- pointer-events: none;
- }
-
- // For list items, render the focus indicator when the parent
- // listem item is focused.
- &:focus > .mat-mdc-focus-indicator::before {
- content: '';
- }
-}
-
-.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;
- }
-
- // 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;
- }
-}
-
-// MDC doesn't account for button being used as a list item. We override some of
-// the default button styles here so that they look right when used as a list
-// item.
-mat-action-list button {
- background: none;
- color: inherit;
- border: none;
- font: inherit;
- outline: inherit;
- -webkit-tap-highlight-color: transparent;
- text-align: left;
-
- [dir='rtl'] & {
- text-align: right;
- }
-
- &::-moz-focus-inner {
- border: 0;
- }
-}
diff --git a/src/material-experimental/mdc-list/list.ts b/src/material-experimental/mdc-list/list.ts
deleted file mode 100644
index dc35500c9ca5..000000000000
--- a/src/material-experimental/mdc-list/list.ts
+++ /dev/null
@@ -1,85 +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 {Platform} from '@angular/cdk/platform';
-import {
- ChangeDetectionStrategy,
- Component,
- Input,
- ContentChildren,
- ElementRef,
- Inject,
- NgZone,
- Optional,
- QueryList,
- ViewChild,
- ViewEncapsulation,
-} from '@angular/core';
-import {MAT_RIPPLE_GLOBAL_OPTIONS, RippleGlobalOptions} from '@angular/material/core';
-import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
-import {MatListBase, MatListItemBase} from './list-base';
-import {MatListItemLine, MatListItemMeta, MatListItemTitle} from './list-item-sections';
-import {coerceBooleanProperty} from '@angular/cdk/coercion';
-
-@Component({
- selector: 'mat-list',
- exportAs: 'matList',
- template: ' ',
- host: {
- 'class': 'mat-mdc-list mat-mdc-list-base mdc-list',
- },
- styleUrls: ['list.css'],
- encapsulation: ViewEncapsulation.None,
- changeDetection: ChangeDetectionStrategy.OnPush,
- providers: [{provide: MatListBase, useExisting: MatList}],
-})
-export class MatList extends MatListBase {}
-
-@Component({
- selector: 'mat-list-item, a[mat-list-item], button[mat-list-item]',
- exportAs: 'matListItem',
- host: {
- 'class': 'mat-mdc-list-item mdc-list-item',
- '[class.mdc-list-item--activated]': 'activated',
- '[class.mdc-list-item--with-leading-avatar]': '_avatars.length !== 0',
- '[class.mdc-list-item--with-leading-icon]': '_icons.length !== 0',
- '[class.mdc-list-item--with-trailing-meta]': '_meta.length !== 0',
- '[class._mat-animation-noopable]': '_noopAnimations',
- },
- templateUrl: 'list-item.html',
- encapsulation: ViewEncapsulation.None,
- changeDetection: ChangeDetectionStrategy.OnPush,
-})
-export class MatListItem extends MatListItemBase {
- @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;
-
- /** Indicates whether an item in a `` is the currently active page. */
- @Input()
- get activated() {
- return this._activated;
- }
- set activated(activated) {
- this._activated = coerceBooleanProperty(activated);
- }
- _activated = false;
-
- constructor(
- element: ElementRef,
- ngZone: NgZone,
- listBase: MatListBase,
- platform: Platform,
- @Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) globalRippleOptions?: RippleGlobalOptions,
- @Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string,
- ) {
- super(element, ngZone, listBase, platform, globalRippleOptions, animationMode);
- }
-}
diff --git a/src/material-experimental/mdc-list/module.ts b/src/material-experimental/mdc-list/module.ts
deleted file mode 100644
index 8b1ec7903d19..000000000000
--- a/src/material-experimental/mdc-list/module.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 {CommonModule} from '@angular/common';
-import {NgModule} from '@angular/core';
-import {MatPseudoCheckboxModule, MatRippleModule, MatCommonModule} from '@angular/material/core';
-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 {
- 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: [
- ObserversModule,
- CommonModule,
- MatCommonModule,
- MatRippleModule,
- MatPseudoCheckboxModule,
- ],
- exports: [
- MatList,
- MatActionList,
- MatNavList,
- MatSelectionList,
- MatListItem,
- MatListOption,
- MatListItemAvatar,
- MatListItemIcon,
- MatListSubheaderCssMatStyler,
- MatDividerModule,
- MatListItemLine,
- MatListItemTitle,
- MatListItemMeta,
- ],
- declarations: [
- MatList,
- MatActionList,
- MatNavList,
- MatSelectionList,
- MatListItem,
- MatListOption,
- MatListSubheaderCssMatStyler,
- 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
deleted file mode 100644
index adc02bd749df..000000000000
--- a/src/material-experimental/mdc-list/public-api.ts
+++ /dev/null
@@ -1,21 +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
- */
-
-export * from './action-list';
-export * from './list';
-export * from './module';
-export * from './nav-list';
-export * from './selection-list';
-export * from './list-option';
-export * from './subheader';
-export * from './list-item-sections';
-
-export {MatListOptionCheckboxPosition} from './list-option-types';
-export {MatListOption} from './list-option';
-
-export {MAT_LIST, MAT_NAV_LIST, MAT_SELECTION_LIST_VALUE_ACCESSOR} from '@angular/material/list';
diff --git a/src/material-experimental/mdc-list/selection-list.ts b/src/material-experimental/mdc-list/selection-list.ts
deleted file mode 100644
index 91a22d0fef1f..000000000000
--- a/src/material-experimental/mdc-list/selection-list.ts
+++ /dev/null
@@ -1,415 +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 {FocusKeyManager} from '@angular/cdk/a11y';
-import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
-import {SelectionModel} from '@angular/cdk/collections';
-import {A, ENTER, hasModifierKey, SPACE} from '@angular/cdk/keycodes';
-import {_getFocusedElementPierceShadowDom} from '@angular/cdk/platform';
-import {
- AfterViewInit,
- ChangeDetectionStrategy,
- Component,
- ContentChildren,
- ElementRef,
- EventEmitter,
- forwardRef,
- Input,
- NgZone,
- OnChanges,
- OnDestroy,
- Output,
- QueryList,
- SimpleChanges,
- ViewEncapsulation,
-} from '@angular/core';
-import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
-import {ThemePalette} from '@angular/material/core';
-import {Subject} from 'rxjs';
-import {takeUntil} from 'rxjs/operators';
-import {MatListBase} from './list-base';
-import {MatListOption, SELECTION_LIST, SelectionList} from './list-option';
-
-const MAT_SELECTION_LIST_VALUE_ACCESSOR: any = {
- provide: NG_VALUE_ACCESSOR,
- useExisting: forwardRef(() => MatSelectionList),
- multi: true,
-};
-
-/** Change event that is being fired whenever the selected state of an option changes. */
-export class MatSelectionListChange {
- constructor(
- /** Reference to the selection list that emitted the event. */
- public source: MatSelectionList,
- /**
- * Reference to the option that has been changed.
- * @deprecated Use `options` instead, because some events may change more than one option.
- * @breaking-change 12.0.0
- */
- public option: MatListOption,
- /** Reference to the options that have been changed. */
- public options: MatListOption[],
- ) {}
-}
-
-@Component({
- selector: 'mat-selection-list',
- exportAs: 'matSelectionList',
- host: {
- 'class': 'mat-mdc-selection-list mat-mdc-list-base mdc-list',
- 'role': 'listbox',
- '[attr.aria-multiselectable]': 'multiple',
- '(keydown)': '_handleKeydown($event)',
- },
- template: ' ',
- styleUrls: ['list.css'],
- encapsulation: ViewEncapsulation.None,
- providers: [
- MAT_SELECTION_LIST_VALUE_ACCESSOR,
- {provide: MatListBase, useExisting: MatSelectionList},
- {provide: SELECTION_LIST, useExisting: MatSelectionList},
- ],
- changeDetection: ChangeDetectionStrategy.OnPush,
-})
-export class MatSelectionList
- extends MatListBase
- implements SelectionList, ControlValueAccessor, AfterViewInit, OnChanges, OnDestroy
-{
- private _initialized = false;
- private _keyManager: FocusKeyManager;
-
- /** Emits when the list has been destroyed. */
- private _destroyed = new Subject();
-
- /** Whether the list has been destroyed. */
- private _isDestroyed: boolean;
-
- /** View to model callback that should be called whenever the selected options change. */
- private _onChange: (value: any) => void = (_: any) => {};
-
- @ContentChildren(MatListOption, {descendants: true}) _items: QueryList;
-
- /** Emits a change event whenever the selected state of an option changes. */
- @Output() readonly selectionChange: EventEmitter =
- new EventEmitter();
-
- /** Theme color of the selection list. This sets the checkbox color for all list options. */
- @Input() color: ThemePalette = 'accent';
-
- /**
- * Function used for comparing an option against the selected value when determining which
- * options should appear as selected. The first argument is the value of an options. The second
- * one is a value from the selected value. A boolean must be returned.
- */
- @Input() compareWith: (o1: any, o2: any) => boolean = (a1, a2) => a1 === a2;
-
- /** Whether selection is limited to one or multiple items (default multiple). */
- @Input()
- get multiple(): boolean {
- return this._multiple;
- }
- set multiple(value: BooleanInput) {
- const newValue = coerceBooleanProperty(value);
-
- if (newValue !== this._multiple) {
- if ((typeof ngDevMode === 'undefined' || ngDevMode) && this._initialized) {
- throw new Error(
- 'Cannot change `multiple` mode of mat-selection-list after initialization.',
- );
- }
-
- this._multiple = newValue;
- this.selectedOptions = new SelectionModel(this._multiple, this.selectedOptions.selected);
- }
- }
- private _multiple = true;
-
- /** The currently selected options. */
- selectedOptions = new SelectionModel(this._multiple);
-
- /** Keeps track of the currently-selected value. */
- _value: string[] | null;
-
- /** View to model callback that should be called if the list or its options lost focus. */
- _onTouched: () => void = () => {};
-
- constructor(public _element: ElementRef, private _ngZone: NgZone) {
- super();
- this._isNonInteractive = false;
- }
-
- ngAfterViewInit() {
- // Mark the selection list as initialized so that the `multiple`
- // binding can no longer be changed.
- this._initialized = true;
- this._setupRovingTabindex();
-
- // These events are bound outside the zone, because they don't change
- // any change-detected properties and they can trigger timeouts.
- this._ngZone.runOutsideAngular(() => {
- this._element.nativeElement.addEventListener('focusin', this._handleFocusin);
- this._element.nativeElement.addEventListener('focusout', this._handleFocusout);
- });
-
- if (this._value) {
- this._setOptionsFromValues(this._value);
- }
-
- this._watchForSelectionChange();
- }
-
- ngOnChanges(changes: SimpleChanges) {
- const disabledChanges = changes['disabled'];
- const disableRippleChanges = changes['disableRipple'];
-
- if (
- (disableRippleChanges && !disableRippleChanges.firstChange) ||
- (disabledChanges && !disabledChanges.firstChange)
- ) {
- this._markOptionsForCheck();
- }
- }
-
- ngOnDestroy() {
- this._element.nativeElement.removeEventListener('focusin', this._handleFocusin);
- this._element.nativeElement.removeEventListener('focusout', this._handleFocusout);
- this._destroyed.next();
- this._destroyed.complete();
- this._isDestroyed = true;
- }
-
- /** Focuses the selection list. */
- focus(options?: FocusOptions) {
- this._element.nativeElement.focus(options);
- }
-
- /** Selects all of the options. Returns the options that changed as a result. */
- selectAll(): MatListOption[] {
- return this._setAllOptionsSelected(true);
- }
-
- /** Deselects all of the options. Returns the options that changed as a result. */
- deselectAll(): MatListOption[] {
- return this._setAllOptionsSelected(false);
- }
-
- /** Reports a value change to the ControlValueAccessor */
- _reportValueChange() {
- // Stop reporting value changes after the list has been destroyed. This avoids
- // cases where the list might wrongly reset its value once it is removed, but
- // the form control is still live.
- if (this.options && !this._isDestroyed) {
- const value = this._getSelectedOptionValues();
- this._onChange(value);
- this._value = value;
- }
- }
-
- /** Emits a change event if the selected state of an option changed. */
- _emitChangeEvent(options: MatListOption[]) {
- this.selectionChange.emit(new MatSelectionListChange(this, options[0], options));
- }
-
- /** Implemented as part of ControlValueAccessor. */
- writeValue(values: string[]): void {
- this._value = values;
-
- if (this.options) {
- this._setOptionsFromValues(values || []);
- }
- }
-
- /** Implemented as a part of ControlValueAccessor. */
- setDisabledState(isDisabled: boolean): void {
- this.disabled = isDisabled;
- }
-
- /** Implemented as part of ControlValueAccessor. */
- registerOnChange(fn: (value: any) => void): void {
- this._onChange = fn;
- }
-
- /** Implemented as part of ControlValueAccessor. */
- registerOnTouched(fn: () => void): void {
- this._onTouched = fn;
- }
-
- /** Watches for changes in the selected state of the options and updates the list accordingly. */
- private _watchForSelectionChange() {
- this.selectedOptions.changed.pipe(takeUntil(this._destroyed)).subscribe(event => {
- // Sync external changes to the model back to the options.
- for (let item of event.added) {
- item.selected = true;
- }
-
- for (let item of event.removed) {
- item.selected = false;
- }
-
- if (!this._containsFocus()) {
- this._resetActiveOption();
- }
- });
- }
-
- /** Sets the selected options based on the specified values. */
- private _setOptionsFromValues(values: string[]) {
- this.options.forEach(option => option._setSelected(false));
-
- values.forEach(value => {
- const correspondingOption = this.options.find(option => {
- // Skip options that are already in the model. This allows us to handle cases
- // where the same primitive value is selected multiple times.
- return option.selected ? false : this.compareWith(option.value, value);
- });
-
- if (correspondingOption) {
- correspondingOption._setSelected(true);
- }
- });
- }
-
- /** Returns the values of the selected options. */
- private _getSelectedOptionValues(): string[] {
- return this.options.filter(option => option.selected).map(option => option.value);
- }
-
- /** Marks all the options to be checked in the next change detection run. */
- private _markOptionsForCheck() {
- if (this.options) {
- this.options.forEach(option => option._markForCheck());
- }
- }
-
- /**
- * Sets the selected state on all of the options
- * and emits an event if anything changed.
- */
- private _setAllOptionsSelected(isSelected: boolean, skipDisabled?: boolean): MatListOption[] {
- // Keep track of whether anything changed, because we only want to
- // emit the changed event when something actually changed.
- const changedOptions: MatListOption[] = [];
-
- this.options.forEach(option => {
- if ((!skipDisabled || !option.disabled) && option._setSelected(isSelected)) {
- changedOptions.push(option);
- }
- });
-
- if (changedOptions.length) {
- this._reportValueChange();
- }
-
- return changedOptions;
- }
-
- // Note: This getter exists for backwards compatibility. The `_items` query list
- // cannot be named `options` as it will be picked up by the interactive list base.
- /** The option components contained within this selection-list. */
- get options(): QueryList {
- return this._items;
- }
-
- /** Handles keydown events within the list. */
- _handleKeydown(event: KeyboardEvent) {
- const activeItem = this._keyManager.activeItem;
-
- if (
- (event.keyCode === ENTER || event.keyCode === SPACE) &&
- !this._keyManager.isTyping() &&
- activeItem &&
- !activeItem.disabled
- ) {
- event.preventDefault();
- activeItem._toggleOnInteraction();
- } else if (
- event.keyCode === A &&
- this.multiple &&
- !this._keyManager.isTyping() &&
- hasModifierKey(event, 'ctrlKey')
- ) {
- const shouldSelect = this.options.some(option => !option.disabled && !option.selected);
- event.preventDefault();
- this._emitChangeEvent(this._setAllOptionsSelected(shouldSelect, true));
- } else {
- this._keyManager.onKeydown(event);
- }
- }
-
- /** Handles focusout events within the list. */
- private _handleFocusout = () => {
- // Focus takes a while to update so we have to wrap our call in a timeout.
- setTimeout(() => {
- if (!this._containsFocus()) {
- this._resetActiveOption();
- }
- });
- };
-
- /** Handles focusin events within the list. */
- private _handleFocusin = (event: FocusEvent) => {
- const activeIndex = this._items
- .toArray()
- .findIndex(item => item._elementRef.nativeElement.contains(event.target as HTMLElement));
-
- if (activeIndex > -1) {
- this._setActiveOption(activeIndex);
- } else {
- this._resetActiveOption();
- }
- };
-
- /** Sets up the logic for maintaining the roving tabindex. */
- private _setupRovingTabindex() {
- this._keyManager = new FocusKeyManager(this._items)
- .withHomeAndEnd()
- .withTypeAhead()
- .withWrap()
- // Allow navigation to disabled items.
- .skipPredicate(() => false);
-
- // Set the initial focus.
- this._resetActiveOption();
-
- // Move the tabindex to the currently-focused list item.
- this._keyManager.change
- .pipe(takeUntil(this._destroyed))
- .subscribe(activeItemIndex => this._setActiveOption(activeItemIndex));
-
- // If the active item is removed from the list, reset back to the first one.
- this._items.changes.pipe(takeUntil(this._destroyed)).subscribe(() => {
- const activeItem = this._keyManager.activeItem;
-
- if (!activeItem || !this._items.toArray().indexOf(activeItem)) {
- this._resetActiveOption();
- }
- });
- }
-
- /**
- * Sets an option as active.
- * @param index Index of the active option. If set to -1, no option will be active.
- */
- private _setActiveOption(index: number) {
- this._items.forEach((item, itemIndex) => item._setTabindex(itemIndex === index ? 0 : -1));
- this._keyManager.updateActiveItem(index);
- }
-
- /** Resets the active option to the first selected option. */
- private _resetActiveOption() {
- const activeItem =
- this._items.find(item => item.selected && !item.disabled) || this._items.first;
- this._setActiveOption(activeItem ? this._items.toArray().indexOf(activeItem) : -1);
- }
-
- /** Returns whether the focus is currently within the list. */
- private _containsFocus() {
- const activeElement = _getFocusedElementPierceShadowDom();
- return activeElement && this._element.nativeElement.contains(activeElement);
- }
-}
diff --git a/src/material-experimental/mdc-list/testing/list-harness.spec.ts b/src/material-experimental/mdc-list/testing/list-harness.spec.ts
deleted file mode 100644
index 48d416c6de9c..000000000000
--- a/src/material-experimental/mdc-list/testing/list-harness.spec.ts
+++ /dev/null
@@ -1,677 +0,0 @@
-import {
- BaseHarnessFilters,
- ComponentHarness,
- ComponentHarnessConstructor,
- HarnessPredicate,
- parallel,
-} from '@angular/cdk/testing';
-import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
-import {Component, Type} from '@angular/core';
-import {ComponentFixture, TestBed} from '@angular/core/testing';
-import {MatDividerHarness} from '@angular/material/divider/testing';
-import {MatListModule} from '@angular/material-experimental/mdc-list';
-import {MatActionListHarness, MatActionListItemHarness} from './action-list-harness';
-import {MatListHarness, MatListItemHarness} from './list-harness';
-import {
- MatListItemHarnessBase,
- MatListItemSection,
- MatSubheaderHarness,
-} from './list-item-harness-base';
-import {MatNavListHarness, MatNavListItemHarness} from './nav-list-harness';
-import {MatListOptionHarness, MatSelectionListHarness} from './selection-list-harness';
-import {MatListHarnessBase} from './list-harness-base';
-import {BaseListItemHarnessFilters} from './list-harness-filters';
-
-/** Tests that apply to all types of the MDC-based mat-list. */
-function runBaseListFunctionalityTests<
- L extends MatListHarnessBase<
- ComponentHarnessConstructor & {with: (x?: any) => HarnessPredicate},
- I,
- BaseListItemHarnessFilters
- >,
- I extends MatListItemHarnessBase,
- F extends {disableThirdItem: boolean},
->(
- testComponentFn: () => Type,
- listHarness: ComponentHarnessConstructor & {
- with: (config?: BaseHarnessFilters) => HarnessPredicate;
- },
- listItemHarnessBase: ComponentHarnessConstructor,
-) {
- describe('base list functionality', () => {
- let simpleListHarness: L;
- let emptyListHarness: L;
- let fixture: ComponentFixture;
-
- beforeEach(async () => {
- const testComponent = testComponentFn();
- await TestBed.configureTestingModule({
- imports: [MatListModule],
- declarations: [testComponent],
- }).compileComponents();
-
- fixture = TestBed.createComponent(testComponent);
- fixture.detectChanges();
- const loader = TestbedHarnessEnvironment.loader(fixture);
- simpleListHarness = await loader.getHarness(
- listHarness.with({selector: '.test-base-list-functionality'}),
- );
- emptyListHarness = await loader.getHarness(listHarness.with({selector: '.test-empty'}));
- });
-
- it('should get all items', async () => {
- const items = await simpleListHarness.getItems();
- expect(await parallel(() => items.map(i => i.getFullText()))).toEqual([
- 'Item 1',
- 'Item 2',
- 'Item 3',
- jasmine.stringMatching(/^Title/),
- jasmine.stringMatching(/^Item 5/),
- ]);
- });
-
- it('should get all items matching full text (deprecated filter)', async () => {
- const items = await simpleListHarness.getItems({text: /[13]/});
- expect(await parallel(() => items.map(i => i.getText()))).toEqual(['Item 1', 'Item 3']);
- });
-
- it('should get all items matching full text', async () => {
- const items = await simpleListHarness.getItems({fullText: /[13]/});
- expect(await parallel(() => items.map(i => i.getFullText()))).toEqual(['Item 1', 'Item 3']);
- });
-
- it('should get all items matching title', async () => {
- const items = await simpleListHarness.getItems({title: 'Title'});
- expect(await parallel(() => items.map(i => i.getTitle()))).toEqual(['Title']);
- });
-
- it('should get all items matching secondary text', async () => {
- const items = await simpleListHarness.getItems({secondaryText: 'Secondary Text'});
- expect(await parallel(() => items.map(i => i.getSecondaryText()))).toEqual([
- 'Secondary Text',
- ]);
- const itemsWithoutSecondaryText = await simpleListHarness.getItems({secondaryText: null});
- expect(await parallel(() => itemsWithoutSecondaryText.map(i => i.getFullText()))).toEqual([
- 'Item 2',
- 'Item 3',
- ]);
- });
-
- it('should get all items matching tertiary text', async () => {
- const items = await simpleListHarness.getItems({tertiaryText: 'Tertiary Text'});
- expect(await parallel(() => items.map(i => i.getTertiaryText()))).toEqual(['Tertiary Text']);
- const itemsWithoutTertiaryText = await simpleListHarness.getItems({tertiaryText: null});
- expect(await parallel(() => itemsWithoutTertiaryText.map(i => i.getFullText()))).toEqual([
- 'Item 1',
- 'Item 2',
- 'Item 3',
- 'TitleText that will wrap into the third line.',
- ]);
- });
-
- it('should get all items of empty list', async () => {
- expect((await emptyListHarness.getItems()).length).toBe(0);
- });
-
- it('should get items by subheader', async () => {
- const sections = await simpleListHarness.getItemsGroupedBySubheader();
- expect(sections.length).toBe(4);
- expect(sections[0].heading).toBeUndefined();
- expect(await parallel(() => sections[0].items.map(i => i.getFullText()))).toEqual(['Item 1']);
- expect(sections[1].heading).toBe('Section 1');
- expect(await parallel(() => sections[1].items.map(i => i.getFullText()))).toEqual([
- 'Item 2',
- 'Item 3',
- ]);
- expect(sections[2].heading).toBe('Section 2');
- expect(await parallel(() => sections[2].items.map(i => i.getFullText()))).toEqual([
- jasmine.stringMatching(/^Title/),
- jasmine.stringMatching(/^Item 5/),
- ]);
- expect(sections[3].heading).toBe('Section 3');
- expect(sections[3].items.length).toBe(0);
- });
-
- it('should get items by subheader for an empty list', async () => {
- const sections = await emptyListHarness.getItemsGroupedBySubheader();
- expect(sections.length).toBe(1);
- expect(sections[0].heading).toBeUndefined();
- expect(sections[0].items.length).toBe(0);
- });
-
- it('should get items grouped by divider', async () => {
- const sections = await simpleListHarness.getItemsGroupedByDividers();
- expect(sections.length).toBe(3);
- expect(await parallel(() => sections[0].map(i => i.getFullText()))).toEqual(['Item 1']);
- expect(await parallel(() => sections[1].map(i => i.getFullText()))).toEqual([
- 'Item 2',
- 'Item 3',
- ]);
- expect(sections[2].length).toBe(2);
- });
-
- it('should get items grouped by divider for an empty list', async () => {
- const sections = await emptyListHarness.getItemsGroupedByDividers();
- expect(sections.length).toBe(1);
- expect(sections[0].length).toBe(0);
- });
-
- it('should get all items, subheaders, and dividers', async () => {
- const itemsSubheadersAndDividers =
- await simpleListHarness.getItemsWithSubheadersAndDividers();
- expect(itemsSubheadersAndDividers.length).toBe(10);
- expect(itemsSubheadersAndDividers[0] instanceof listItemHarnessBase).toBe(true);
- expect(await (itemsSubheadersAndDividers[0] as MatListItemHarnessBase).getFullText()).toBe(
- 'Item 1',
- );
- expect(itemsSubheadersAndDividers[1] instanceof MatSubheaderHarness).toBe(true);
- expect(await (itemsSubheadersAndDividers[1] as MatSubheaderHarness).getText()).toBe(
- 'Section 1',
- );
- expect(itemsSubheadersAndDividers[2] instanceof MatDividerHarness).toBe(true);
- expect(itemsSubheadersAndDividers[3] instanceof listItemHarnessBase).toBe(true);
- expect(await (itemsSubheadersAndDividers[3] as MatListItemHarnessBase).getFullText()).toBe(
- 'Item 2',
- );
- expect(itemsSubheadersAndDividers[4] instanceof listItemHarnessBase).toBe(true);
- expect(await (itemsSubheadersAndDividers[4] as MatListItemHarnessBase).getFullText()).toBe(
- 'Item 3',
- );
- expect(itemsSubheadersAndDividers[5] instanceof MatSubheaderHarness).toBe(true);
- expect(await (itemsSubheadersAndDividers[5] as MatSubheaderHarness).getText()).toBe(
- 'Section 2',
- );
- expect(itemsSubheadersAndDividers[6] instanceof MatDividerHarness).toBe(true);
- expect(await (itemsSubheadersAndDividers[9] as MatSubheaderHarness).getText()).toBe(
- 'Section 3',
- );
- });
-
- it('should get all items, subheaders, and dividers excluding some harness types', async () => {
- const items = await simpleListHarness.getItemsWithSubheadersAndDividers({
- subheader: false,
- divider: false,
- });
- const subheaders = await simpleListHarness.getItemsWithSubheadersAndDividers({
- item: false,
- divider: false,
- });
- const dividers = await simpleListHarness.getItemsWithSubheadersAndDividers({
- item: false,
- subheader: false,
- });
- expect(await parallel(() => items.map(i => i.getFullText()))).toEqual([
- 'Item 1',
- 'Item 2',
- 'Item 3',
- 'TitleText that will wrap into the third line.',
- 'Item 5Secondary TextTertiary Text',
- ]);
- expect(await parallel(() => subheaders.map(s => s.getText()))).toEqual([
- 'Section 1',
- 'Section 2',
- 'Section 3',
- ]);
- expect(await parallel(() => dividers.map(d => d.getOrientation()))).toEqual([
- 'horizontal',
- 'horizontal',
- ]);
- });
-
- it('should get all items, subheaders, and dividers with filters', async () => {
- const itemsSubheadersAndDividers = await simpleListHarness.getItemsWithSubheadersAndDividers({
- item: {fullText: /1/},
- subheader: {text: /2/},
- });
- expect(itemsSubheadersAndDividers.length).toBe(4);
- expect(itemsSubheadersAndDividers[0] instanceof listItemHarnessBase).toBe(true);
- expect(await (itemsSubheadersAndDividers[0] as MatListItemHarnessBase).getFullText()).toBe(
- 'Item 1',
- );
- expect(itemsSubheadersAndDividers[1] instanceof MatDividerHarness).toBe(true);
- expect(itemsSubheadersAndDividers[2] instanceof MatSubheaderHarness).toBe(true);
- expect(await (itemsSubheadersAndDividers[2] as MatSubheaderHarness).getText()).toBe(
- 'Section 2',
- );
- expect(itemsSubheadersAndDividers[3] instanceof MatDividerHarness).toBe(true);
- });
-
- it('should get list item text, title, secondary and tertiary text', async () => {
- const items = await simpleListHarness.getItems();
- expect(items.length).toBe(5);
- expect(await items[0].getFullText()).toBe('Item 1');
- expect(await items[0].getTitle()).toBe('Item');
- expect(await items[0].getSecondaryText()).toBe('1');
- expect(await items[0].getTertiaryText()).toBe(null);
-
- expect(await items[1].getFullText()).toBe('Item 2');
- expect(await items[1].getTitle()).toBe('Item 2');
- expect(await items[1].getSecondaryText()).toBe(null);
- expect(await items[1].getTertiaryText()).toBe(null);
-
- expect(await items[2].getFullText()).toBe('Item 3');
- expect(await items[2].getTitle()).toBe('Item 3');
- expect(await items[2].getSecondaryText()).toBe(null);
- expect(await items[2].getTertiaryText()).toBe(null);
-
- expect(await items[3].getFullText()).toBe('TitleText that will wrap into the third line.');
- expect(await items[3].getTitle()).toBe('Title');
- expect(await items[3].getSecondaryText()).toBe('Text that will wrap into the third line.');
- expect(await items[3].getTertiaryText()).toBe(null);
-
- expect(await items[4].getFullText()).toBe('Item 5Secondary TextTertiary Text');
- expect(await items[4].getTitle()).toBe('Item 5');
- expect(await items[4].getSecondaryText()).toBe('Secondary Text');
- expect(await items[4].getTertiaryText()).toBe('Tertiary Text');
- });
-
- it('should check list item icons and avatars', async () => {
- const items = await simpleListHarness.getItems();
- expect(items.length).toBe(5);
- expect(await items[0].hasIcon()).toBe(true);
- expect(await items[0].hasAvatar()).toBe(true);
- expect(await items[1].hasIcon()).toBe(false);
- expect(await items[1].hasAvatar()).toBe(false);
- expect(await items[2].hasIcon()).toBe(false);
- expect(await items[2].hasAvatar()).toBe(false);
- expect(await items[3].hasIcon()).toBe(false);
- expect(await items[3].hasAvatar()).toBe(false);
- expect(await items[4].hasIcon()).toBe(false);
- expect(await items[4].hasAvatar()).toBe(false);
- });
-
- it('should get harness loader for list item content', async () => {
- const items = await simpleListHarness.getItems();
- expect(items.length).toBe(5);
- const childHarness = await items[1].getHarness(TestItemContentHarness);
- expect(childHarness).not.toBeNull();
- });
-
- it('should be able to get content harness loader of list item', async () => {
- const items = await simpleListHarness.getItems();
- expect(items.length).toBe(5);
- const loader = await items[1].getChildLoader(MatListItemSection.CONTENT);
- await expectAsync(loader.getHarness(TestItemContentHarness)).toBeResolved();
- });
-
- it('should check disabled state of items', async () => {
- fixture.componentInstance.disableThirdItem = true;
- const items = await simpleListHarness.getItems();
- expect(items.length).toBe(5);
- expect(await items[0].isDisabled()).toBe(false);
- expect(await items[2].isDisabled()).toBe(true);
- });
- });
-}
-
-describe('MatListHarness', () => {
- runBaseListFunctionalityTests(() => ListHarnessTest, MatListHarness, MatListItemHarness);
-});
-
-describe('MatActionListHarness', () => {
- runBaseListFunctionalityTests(
- () => ActionListHarnessTest,
- MatActionListHarness,
- MatActionListItemHarness,
- );
-
- describe('additional functionality', () => {
- let harness: MatActionListHarness;
- let fixture: ComponentFixture;
-
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- imports: [MatListModule],
- declarations: [ActionListHarnessTest],
- }).compileComponents();
-
- fixture = TestBed.createComponent(ActionListHarnessTest);
- fixture.detectChanges();
- const loader = TestbedHarnessEnvironment.loader(fixture);
- harness = await loader.getHarness(
- MatActionListHarness.with({selector: '.test-base-list-functionality'}),
- );
- });
-
- it('should click items', async () => {
- const items = await harness.getItems();
- expect(items.length).toBe(5);
- await items[0].click();
- expect(fixture.componentInstance.lastClicked).toBe('Item 1');
- await items[1].click();
- expect(fixture.componentInstance.lastClicked).toBe('Item 2');
- await items[2].click();
- expect(fixture.componentInstance.lastClicked).toBe('Item 3');
- });
- });
-});
-
-describe('MatNavListHarness', () => {
- runBaseListFunctionalityTests(() => NavListHarnessTest, MatNavListHarness, MatNavListItemHarness);
-
- describe('additional functionality', () => {
- let harness: MatNavListHarness;
- let fixture: ComponentFixture;
-
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- imports: [MatListModule],
- declarations: [NavListHarnessTest],
- }).compileComponents();
-
- fixture = TestBed.createComponent(NavListHarnessTest);
- fixture.detectChanges();
- const loader = TestbedHarnessEnvironment.loader(fixture);
- harness = await loader.getHarness(
- MatNavListHarness.with({selector: '.test-base-list-functionality'}),
- );
- });
-
- it('should click items', async () => {
- const items = await harness.getItems();
- expect(items.length).toBe(5);
- await items[0].click();
- expect(fixture.componentInstance.lastClicked).toBe('Item 1');
- await items[1].click();
- expect(fixture.componentInstance.lastClicked).toBe('Item 2');
- await items[2].click();
- expect(fixture.componentInstance.lastClicked).toBe('Item 3');
- });
-
- it('should get href', async () => {
- const items = await harness.getItems();
- expect(await parallel(() => items.map(i => i.getHref()))).toEqual([
- null,
- '',
- '/somestuff',
- null,
- null,
- ]);
- });
-
- it('should get item harness by href', async () => {
- const items = await harness.getItems({href: /stuff/});
- expect(items.length).toBe(1);
- expect(await items[0].getHref()).toBe('/somestuff');
- });
-
- it('should get activated state', async () => {
- const items = await harness.getItems();
- const activated = await parallel(() => items.map(item => item.isActivated()));
- expect(activated).toEqual([false, true, false, false, false]);
- });
-
- it('should get item harness by activated state', async () => {
- const activatedItems = await harness.getItems({activated: true});
- const activatedItemsText = await parallel(() =>
- activatedItems.map(item => item.getFullText()),
- );
- expect(activatedItemsText).toEqual(['Item 2']);
- });
- });
-});
-
-describe('MatSelectionListHarness', () => {
- runBaseListFunctionalityTests(
- () => SelectionListHarnessTest,
- MatSelectionListHarness,
- MatListOptionHarness,
- );
-
- describe('additional functionality', () => {
- let harness: MatSelectionListHarness;
- let emptyHarness: MatSelectionListHarness;
- let fixture: ComponentFixture;
-
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- imports: [MatListModule],
- declarations: [SelectionListHarnessTest],
- }).compileComponents();
-
- fixture = TestBed.createComponent(SelectionListHarnessTest);
- fixture.detectChanges();
- const loader = TestbedHarnessEnvironment.loader(fixture);
- harness = await loader.getHarness(
- MatSelectionListHarness.with({selector: '.test-base-list-functionality'}),
- );
- emptyHarness = await loader.getHarness(
- MatSelectionListHarness.with({selector: '.test-empty'}),
- );
- });
-
- it('should check disabled state of list', async () => {
- expect(await harness.isDisabled()).toBe(false);
- expect(await emptyHarness.isDisabled()).toBe(true);
- });
-
- it('should get all selected options', async () => {
- expect((await harness.getItems({selected: true})).length).toBe(0);
- const items = await harness.getItems();
- await parallel(() => items.map(item => item.select()));
- expect((await harness.getItems({selected: true})).length).toBe(5);
- });
-
- it('should check multiple options', async () => {
- expect((await harness.getItems({selected: true})).length).toBe(0);
- await harness.selectItems({fullText: /1/}, {fullText: /3/});
- const selected = await harness.getItems({selected: true});
- expect(await parallel(() => selected.map(item => item.getFullText()))).toEqual([
- 'Item 1',
- 'Item 3',
- ]);
- });
-
- it('should uncheck multiple options', async () => {
- await harness.selectItems();
- expect((await harness.getItems({selected: true})).length).toBe(5);
- await harness.deselectItems({fullText: /1/}, {fullText: /3/});
- const selected = await harness.getItems({selected: true});
- expect(await parallel(() => selected.map(item => item.getFullText()))).toEqual([
- 'Item 2',
- jasmine.stringMatching(/^Title/),
- jasmine.stringMatching(/^Item 5/),
- ]);
- });
-
- it('should get option checkbox position', async () => {
- const items = await harness.getItems();
- expect(items.length).toBe(5);
- expect(await items[0].getCheckboxPosition()).toBe('before');
- expect(await items[1].getCheckboxPosition()).toBe('after');
- });
-
- it('should toggle option selection', async () => {
- const items = await harness.getItems();
- expect(items.length).toBe(5);
- expect(await items[0].isSelected()).toBe(false);
- await items[0].toggle();
- expect(await items[0].isSelected()).toBe(true);
- await items[0].toggle();
- expect(await items[0].isSelected()).toBe(false);
- });
-
- it('should check single option', async () => {
- const items = await harness.getItems();
- expect(items.length).toBe(5);
- expect(await items[0].isSelected()).toBe(false);
- await items[0].select();
- expect(await items[0].isSelected()).toBe(true);
- await items[0].select();
- expect(await items[0].isSelected()).toBe(true);
- });
-
- it('should uncheck single option', async () => {
- const items = await harness.getItems();
- expect(items.length).toBe(5);
- await items[0].select();
- expect(await items[0].isSelected()).toBe(true);
- await items[0].deselect();
- expect(await items[0].isSelected()).toBe(false);
- await items[0].deselect();
- expect(await items[0].isSelected()).toBe(false);
- });
- });
-});
-
-@Component({
- template: `
-
-
- Item
- 1
- icon
- Avatar
-
- Section 1
-
-
- Item 2
-
- Item 3
- Section 2
-
-
- Title
- Text that will wrap into the third line.
-
-
- Item 5
- Secondary Text
- Tertiary Text
-
- Section 3
-
-
-
- `,
-})
-class ListHarnessTest {
- disableThirdItem = false;
-}
-
-@Component({
- template: `
-
-
- Item
- 1
- icon
- Avatar
-
- Section 1
-
-
- Item 2
-
- Item 3
- Section 2
-
-
- Title
- Text that will wrap into the third line.
-
-
- Item 5
- Secondary Text
- Tertiary Text
-
- Section 3
-
-
-
- `,
-})
-class ActionListHarnessTest {
- lastClicked: string;
- disableThirdItem = false;
-}
-
-@Component({
- template: `
-
-
- Item
- 1
- icon
- Avatar
-
- Section 1
-
-
- Item 2
-
- Item 3
- Section 2
-
-
- Title
- Text that will wrap into the third line.
-
-
- Item 5
- Secondary Text
- Tertiary Text
-
- Section 3
-
-
-
- `,
-})
-class NavListHarnessTest {
- lastClicked: string;
- disableThirdItem = false;
-
- onClick(event: Event, value: string) {
- event.preventDefault();
- this.lastClicked = value;
- }
-}
-
-@Component({
- template: `
-
-
- Item
- 1
- icon
- Avatar
-
- Section 1
-
-
- Item 2
-
- Item 3
- Section 2
-
-
- Title
- Text that will wrap into the third line.
-
-
- Item 5
- Secondary Text
- Tertiary Text
-
- Section 3
-
-
-
- `,
-})
-class SelectionListHarnessTest {
- disableThirdItem = false;
-}
-
-class TestItemContentHarness extends ComponentHarness {
- static hostSelector = '.test-item-content';
-}
diff --git a/src/material-experimental/mdc-list/testing/list-harness.ts b/src/material-experimental/mdc-list/testing/list-harness.ts
deleted file mode 100644
index db5062286300..000000000000
--- a/src/material-experimental/mdc-list/testing/list-harness.ts
+++ /dev/null
@@ -1,54 +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 {ComponentHarnessConstructor, HarnessPredicate} from '@angular/cdk/testing';
-import {MatListHarnessBase} from './list-harness-base';
-import {ListHarnessFilters, ListItemHarnessFilters} from './list-harness-filters';
-import {getListItemPredicate, MatListItemHarnessBase} from './list-item-harness-base';
-
-/** Harness for interacting with a MDC-based list in tests. */
-export class MatListHarness extends MatListHarnessBase<
- typeof MatListItemHarness,
- MatListItemHarness,
- ListItemHarnessFilters
-> {
- /** The selector for the host element of a `MatList` instance. */
- static hostSelector = '.mat-mdc-list';
-
- /**
- * Gets a `HarnessPredicate` that can be used to search for a list with specific attributes.
- * @param options Options for filtering which list instances are considered a match.
- * @return a `HarnessPredicate` configured with the given options.
- */
- static with(
- this: ComponentHarnessConstructor,
- options: ListHarnessFilters = {},
- ): HarnessPredicate {
- return new HarnessPredicate(this, options);
- }
-
- override _itemHarness = MatListItemHarness;
-}
-
-/** Harness for interacting with a list item. */
-export class MatListItemHarness extends MatListItemHarnessBase {
- /** The selector for the host element of a `MatListItem` instance. */
- static hostSelector = `${MatListHarness.hostSelector} .mat-mdc-list-item`;
-
- /**
- * Gets a `HarnessPredicate` that can be used to search for a list item with specific attributes.
- * @param options Options for filtering which list item instances are considered a match.
- * @return a `HarnessPredicate` configured with the given options.
- */
- static with(
- this: ComponentHarnessConstructor,
- options: ListItemHarnessFilters = {},
- ): HarnessPredicate {
- return getListItemPredicate(this, options);
- }
-}
diff --git a/src/material-experimental/mdc-list/testing/list-item-harness-base.ts b/src/material-experimental/mdc-list/testing/list-item-harness-base.ts
deleted file mode 100644
index 03f9457b0df8..000000000000
--- a/src/material-experimental/mdc-list/testing/list-item-harness-base.ts
+++ /dev/null
@@ -1,198 +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 {
- ComponentHarness,
- ComponentHarnessConstructor,
- ContentContainerComponentHarness,
- HarnessPredicate,
- parallel,
-} from '@angular/cdk/testing';
-import {BaseListItemHarnessFilters, SubheaderHarnessFilters} from './list-harness-filters';
-
-const iconSelector = '.mat-mdc-list-item-icon';
-const avatarSelector = '.mat-mdc-list-item-avatar';
-
-/**
- * Gets a `HarnessPredicate` that applies the given `BaseListItemHarnessFilters` to the given
- * list item harness.
- * @template H The type of list item harness to create a predicate for.
- * @param harnessType A constructor for a list item harness.
- * @param options An instance of `BaseListItemHarnessFilters` to apply.
- * @return A `HarnessPredicate` for the given harness type with the given options applied.
- */
-export function getListItemPredicate(
- harnessType: ComponentHarnessConstructor,
- options: BaseListItemHarnessFilters,
-): HarnessPredicate {
- return new HarnessPredicate(harnessType, options)
- .addOption('text', options.text, (harness, text) =>
- HarnessPredicate.stringMatches(harness.getText(), text),
- )
- .addOption('fullText', options.fullText, (harness, fullText) =>
- HarnessPredicate.stringMatches(harness.getFullText(), fullText),
- )
- .addOption('title', options.title, (harness, title) =>
- HarnessPredicate.stringMatches(harness.getTitle(), title),
- )
- .addOption('secondaryText', options.secondaryText, (harness, secondaryText) =>
- HarnessPredicate.stringMatches(harness.getSecondaryText(), secondaryText),
- )
- .addOption('tertiaryText', options.tertiaryText, (harness, tertiaryText) =>
- HarnessPredicate.stringMatches(harness.getTertiaryText(), tertiaryText),
- );
-}
-
-/** Harness for interacting with a MDC-based list subheader. */
-export class MatSubheaderHarness extends ComponentHarness {
- static hostSelector = '.mat-mdc-subheader';
-
- static with(options: SubheaderHarnessFilters = {}): HarnessPredicate {
- return new HarnessPredicate(MatSubheaderHarness, options).addOption(
- 'text',
- options.text,
- (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text),
- );
- }
-
- /** Gets the full text content of the list item (including text from any font icons). */
- async getText(): Promise {
- return (await this.host()).text();
- }
-}
-
-/** Selectors for the various list item sections that may contain user content. */
-export const enum MatListItemSection {
- CONTENT = '.mdc-list-item__content',
-}
-
-/** Enum describing the possible variants of a list item. */
-export const enum MatListItemType {
- ONE_LINE_ITEM,
- TWO_LINE_ITEM,
- THREE_LINE_ITEM,
-}
-
-/**
- * Shared behavior among the harnesses for the various `MatListItem` flavors.
- * @docs-private
- */
-export abstract class MatListItemHarnessBase extends ContentContainerComponentHarness {
- private _lines = this.locatorForAll('.mat-mdc-list-item-line');
- private _primaryText = this.locatorFor('.mdc-list-item__primary-text');
- private _avatar = this.locatorForOptional('.mat-mdc-list-item-avatar');
- private _icon = this.locatorForOptional('.mat-mdc-list-item-icon');
- private _unscopedTextContent = this.locatorFor('.mat-mdc-list-item-unscoped-content');
-
- /** Gets the type of the list item, currently describing how many lines there are. */
- async getType(): Promise {
- const host = await this.host();
- const [isOneLine, isTwoLine] = await parallel(() => [
- host.hasClass('mdc-list-item--with-one-line'),
- host.hasClass('mdc-list-item--with-two-lines'),
- ]);
- if (isOneLine) {
- return MatListItemType.ONE_LINE_ITEM;
- } else if (isTwoLine) {
- return MatListItemType.TWO_LINE_ITEM;
- } else {
- return MatListItemType.THREE_LINE_ITEM;
- }
- }
-
- /**
- * Gets the full text content of the list item, excluding text
- * from icons and avatars.
- *
- * @deprecated Use the `getFullText` method instead.
- * @breaking-change 16.0.0
- */
- async getText(): Promise {
- return this.getFullText();
- }
-
- /**
- * Gets the full text content of the list item, excluding text
- * from icons and avatars.
- */
- async getFullText(): Promise {
- return (await this.host()).text({exclude: `${iconSelector}, ${avatarSelector}`});
- }
-
- /** Gets the title of the list item. */
- async getTitle(): Promise {
- return (await this._primaryText()).text();
- }
-
- /** Whether the list item is disabled. */
- async isDisabled(): Promise {
- return (await this.host()).hasClass('mdc-list-item--disabled');
- }
-
- /**
- * Gets the secondary line text of the list item. Null if the list item
- * does not have a secondary line.
- */
- async getSecondaryText(): Promise {
- const type = await this.getType();
- if (type === MatListItemType.ONE_LINE_ITEM) {
- return null;
- }
-
- const [lines, unscopedTextContent] = await parallel(() => [
- this._lines(),
- this._unscopedTextContent(),
- ]);
-
- // If there is no explicit line for the secondary text, the unscoped text content
- // is rendered as the secondary text (with potential text wrapping enabled).
- if (lines.length >= 1) {
- return lines[0].text();
- } else {
- return unscopedTextContent.text();
- }
- }
-
- /**
- * Gets the tertiary line text of the list item. Null if the list item
- * does not have a tertiary line.
- */
- async getTertiaryText(): Promise {
- const type = await this.getType();
- if (type !== MatListItemType.THREE_LINE_ITEM) {
- return null;
- }
-
- const [lines, unscopedTextContent] = await parallel(() => [
- this._lines(),
- this._unscopedTextContent(),
- ]);
-
- // First we check if there is an explicit line for the tertiary text. If so, we return it.
- // If there is at least an explicit secondary line though, then we know that the unscoped
- // text content corresponds to the tertiary line. If there are no explicit lines at all,
- // we know that the unscoped text content from the secondary text just wraps into the third
- // line, but there *no* actual dedicated tertiary text.
- if (lines.length === 2) {
- return lines[1].text();
- } else if (lines.length === 1) {
- return unscopedTextContent.text();
- }
- return null;
- }
-
- /** Whether this list item has an avatar. */
- async hasAvatar(): Promise {
- return !!(await this._avatar());
- }
-
- /** Whether this list item has an icon. */
- async hasIcon(): Promise {
- return !!(await this._icon());
- }
-}
diff --git a/src/material-experimental/mdc-list/testing/nav-list-harness.ts b/src/material-experimental/mdc-list/testing/nav-list-harness.ts
deleted file mode 100644
index 7d51e51a789f..000000000000
--- a/src/material-experimental/mdc-list/testing/nav-list-harness.ts
+++ /dev/null
@@ -1,94 +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 {ComponentHarnessConstructor, HarnessPredicate} from '@angular/cdk/testing';
-import {MatListHarnessBase} from './list-harness-base';
-import {NavListHarnessFilters, NavListItemHarnessFilters} from './list-harness-filters';
-import {getListItemPredicate, MatListItemHarnessBase} from './list-item-harness-base';
-
-/** Harness for interacting with a MDC-based mat-nav-list in tests. */
-export class MatNavListHarness extends MatListHarnessBase<
- typeof MatNavListItemHarness,
- MatNavListItemHarness,
- NavListItemHarnessFilters
-> {
- /** The selector for the host element of a `MatNavList` instance. */
- static hostSelector = '.mat-mdc-nav-list';
-
- /**
- * Gets a `HarnessPredicate` that can be used to search for a nav list with specific
- * attributes.
- * @param options Options for filtering which nav list instances are considered a match.
- * @return a `HarnessPredicate` configured with the given options.
- */
- static with(
- this: ComponentHarnessConstructor,
- options: NavListHarnessFilters = {},
- ): HarnessPredicate {
- return new HarnessPredicate(this, options);
- }
-
- override _itemHarness = MatNavListItemHarness;
-}
-
-/** Harness for interacting with a MDC-based nav-list item. */
-export class MatNavListItemHarness extends MatListItemHarnessBase {
- /** The selector for the host element of a `MatListItem` instance. */
- static hostSelector = `${MatNavListHarness.hostSelector} .mat-mdc-list-item`;
-
- /**
- * Gets a `HarnessPredicate` that can be used to search for a nav list item with specific
- * attributes.
- * @param options Options for filtering which nav list item instances are considered a match.
- * @return a `HarnessPredicate` configured with the given options.
- */
- static with(
- this: ComponentHarnessConstructor,
- options: NavListItemHarnessFilters = {},
- ): HarnessPredicate {
- return getListItemPredicate(this, options)
- .addOption('href', options.href, async (harness, href) =>
- HarnessPredicate.stringMatches(harness.getHref(), href),
- )
- .addOption(
- 'activated',
- options.activated,
- async (harness, activated) => (await harness.isActivated()) === activated,
- );
- }
-
- /** Gets the href for this nav list item. */
- async getHref(): Promise {
- return (await this.host()).getAttribute('href');
- }
-
- /** Clicks on the nav list item. */
- async click(): Promise {
- return (await this.host()).click();
- }
-
- /** Focuses the nav list item. */
- async focus(): Promise {
- return (await this.host()).focus();
- }
-
- /** Blurs the nav list item. */
- async blur(): Promise {
- return (await this.host()).blur();
- }
-
- /** Whether the nav list item is focused. */
- async isFocused(): Promise {
- return (await this.host()).isFocused();
- }
-
- /** Whether the list item is activated. Should only be used for nav list items. */
- async isActivated(): Promise {
- return (await this.host()).hasClass('mdc-list-item--activated');
- }
-}
diff --git a/src/material/_index.scss b/src/material/_index.scss
index 08cd8d17397a..dc7c8529f8ce 100644
--- a/src/material/_index.scss
+++ b/src/material/_index.scss
@@ -120,6 +120,8 @@
@forward './input/input-theme' as input-* show input-theme, input-color, input-typography,
input-density;
@forward './list/list-theme' as list-* show list-theme, list-color, list-typography;
+@forward './legacy-list/list-theme' as legacy-list-* show legacy-list-theme, legacy-list-color,
+ legacy-list-typography;
@forward './menu/menu-theme' as menu-* show menu-theme, menu-color, menu-typography;
@forward './legacy-menu/menu-theme' as legacy-menu-* show legacy-menu-theme, legacy-menu-color,
legacy-menu-typography;
diff --git a/src/material/_theming.scss b/src/material/_theming.scss
index 9c760ba6789c..cc9dfefd29ca 100644
--- a/src/material/_theming.scss
+++ b/src/material/_theming.scss
@@ -23,7 +23,7 @@
@forward './grid-list/grid-list-legacy-index';
@forward './icon/icon-legacy-index';
@forward './legacy-input/input-legacy-index';
-@forward './list/list-legacy-index';
+@forward './legacy-list/list-legacy-index';
@forward './legacy-menu/menu-legacy-index';
@forward './paginator/paginator-legacy-index';
@forward './legacy-progress-bar/progress-bar-legacy-index';
diff --git a/src/material/config.bzl b/src/material/config.bzl
index da266876b3b7..f8861907aa54 100644
--- a/src/material/config.bzl
+++ b/src/material/config.bzl
@@ -52,6 +52,8 @@ entryPoints = [
"legacy-input/testing",
"list",
"list/testing",
+ "legacy-list",
+ "legacy-list/testing",
"menu",
"menu/testing",
"legacy-menu",
diff --git a/src/material/core/density/private/_all-density.scss b/src/material/core/density/private/_all-density.scss
index 1d4f6f88463b..ed2778e56081 100644
--- a/src/material/core/density/private/_all-density.scss
+++ b/src/material/core/density/private/_all-density.scss
@@ -21,6 +21,7 @@
@use '../../../radio/radio-theme';
@use '../../../slider/slider-theme';
@use '../../../menu/menu-theme';
+@use '../../../list/list-theme';
@mixin private-all-unmigrated-component-densities($config) {
@include expansion-theme.density($config);
@@ -63,6 +64,7 @@
@include radio-theme.density($config);
@include slider-theme.density($config);
@include menu-theme.density($config);
+ @include list-theme.density($config);
@include private-all-unmigrated-component-densities($config);
}
diff --git a/src/material/core/theming/_all-theme.scss b/src/material/core/theming/_all-theme.scss
index 87dba055fafc..04ebc2d7eead 100644
--- a/src/material/core/theming/_all-theme.scss
+++ b/src/material/core/theming/_all-theme.scss
@@ -48,7 +48,6 @@
@include expansion-theme.theme($theme-or-color-config);
@include grid-list-theme.theme($theme-or-color-config);
@include icon-theme.theme($theme-or-color-config);
- @include list-theme.theme($theme-or-color-config);
@include paginator-theme.theme($theme-or-color-config);
@include progress-spinner-theme.theme($theme-or-color-config);
@include sidenav-theme.theme($theme-or-color-config);
@@ -78,6 +77,7 @@
@include radio-theme.theme($theme-or-color-config);
@include slider-theme.theme($theme-or-color-config);
@include menu-theme.theme($theme-or-color-config);
+ @include list-theme.theme($theme-or-color-config);
@include private-all-unmigrated-component-themes($theme-or-color-config);
}
}
diff --git a/src/material/core/theming/tests/test-css-variables-theme.scss b/src/material/core/theming/tests/test-css-variables-theme.scss
index f448ecad2509..52389581fb6c 100644
--- a/src/material/core/theming/tests/test-css-variables-theme.scss
+++ b/src/material/core/theming/tests/test-css-variables-theme.scss
@@ -13,7 +13,6 @@
@use '../../../expansion/expansion-theme';
@use '../../../grid-list/grid-list-theme';
@use '../../../icon/icon-theme';
-@use '../../../list/list-theme';
@use '../../../menu/menu-theme';
@use '../../../paginator/paginator-theme';
@use '../../../progress-spinner/progress-spinner-theme';
@@ -63,7 +62,6 @@
@include expansion-theme.theme($css-var-theme);
@include grid-list-theme.theme($css-var-theme);
@include icon-theme.theme($css-var-theme);
- @include list-theme.theme($css-var-theme);
@include menu-theme.theme($css-var-theme);
@include paginator-theme.theme($css-var-theme);
@include progress-spinner-theme.theme($css-var-theme);
diff --git a/src/material/core/typography/_all-typography.scss b/src/material/core/typography/_all-typography.scss
index d5e3972ee22b..3975346e2bd0 100644
--- a/src/material/core/typography/_all-typography.scss
+++ b/src/material/core/typography/_all-typography.scss
@@ -57,7 +57,6 @@
@include sort-theme.typography($config);
@include tabs-theme.typography($config);
@include toolbar-theme.typography($config);
- @include list-theme.typography($config);
@include snack-bar-theme.typography($config);
@include tree-theme.typography($config);
}
@@ -94,6 +93,7 @@
@include radio-theme.typography($config);
@include slider-theme.typography($config);
@include menu-theme.typography($config);
+ @include list-theme.typography($config);
}
// @deprecated Use `all-component-typographies`.
diff --git a/src/material/legacy-core/theming/_all-theme.scss b/src/material/legacy-core/theming/_all-theme.scss
index 68624e744f0f..b2eb6d0a350b 100644
--- a/src/material/legacy-core/theming/_all-theme.scss
+++ b/src/material/legacy-core/theming/_all-theme.scss
@@ -15,6 +15,7 @@
@use '../../legacy-radio/radio-theme';
@use '../../legacy-slider/slider-theme';
@use '../../legacy-menu/menu-theme';
+@use '../../legacy-list/list-theme';
// Create a theme.
@mixin all-legacy-component-themes($theme-or-color-config) {
@@ -35,6 +36,7 @@
@include radio-theme.theme($theme-or-color-config);
@include slider-theme.theme($theme-or-color-config);
@include menu-theme.theme($theme-or-color-config);
+ @include list-theme.theme($theme-or-color-config);
@include all-theme.private-all-unmigrated-component-themes($theme-or-color-config);
}
}
diff --git a/src/material/legacy-core/typography/_all-typography.scss b/src/material/legacy-core/typography/_all-typography.scss
index bf8d4c4a8302..1d4d8d923051 100644
--- a/src/material/legacy-core/typography/_all-typography.scss
+++ b/src/material/legacy-core/typography/_all-typography.scss
@@ -17,6 +17,7 @@
@use '../../legacy-radio/radio-theme';
@use '../../legacy-slider/slider-theme';
@use '../../legacy-menu/menu-theme';
+@use '../../legacy-list/list-theme';
// Includes all of the typographic styles.
@mixin all-legacy-component-typographies($config-or-theme: null) {
@@ -51,6 +52,7 @@
@include radio-theme.typography($config);
@include slider-theme.typography($config);
@include menu-theme.typography($config);
+ @include list-theme.typography($config);
}
// @deprecated Use `all-legacy-component-typographies`.
diff --git a/src/material-experimental/mdc-list/BUILD.bazel b/src/material/legacy-list/BUILD.bazel
similarity index 63%
rename from src/material-experimental/mdc-list/BUILD.bazel
rename to src/material/legacy-list/BUILD.bazel
index 88a8c438eb30..73e84f9e9a21 100644
--- a/src/material-experimental/mdc-list/BUILD.bazel
+++ b/src/material/legacy-list/BUILD.bazel
@@ -1,6 +1,7 @@
load("//src/e2e-app:test_suite.bzl", "e2e_test_suite")
load(
"//tools:defaults.bzl",
+ "markdown_to_html",
"ng_e2e_test_library",
"ng_module",
"ng_test_library",
@@ -12,74 +13,55 @@ load(
package(default_visibility = ["//visibility:public"])
ng_module(
- name = "mdc-list",
+ name = "legacy-list",
srcs = glob(
["**/*.ts"],
- exclude = [
- "**/*.spec.ts",
- ],
+ exclude = ["**/*.spec.ts"],
),
- assets = [
- ":list_scss",
- ":list_option_scss",
- ] + glob(["**/*.html"]),
+ assets = [":list.css"] + glob(["**/*.html"]),
deps = [
"//src:dev_mode_types",
+ "//src/cdk/a11y",
"//src/cdk/coercion",
"//src/cdk/collections",
- "//src/cdk/observers",
+ "//src/cdk/keycodes",
"//src/material/core",
"//src/material/divider",
"//src/material/list",
+ "@npm//@angular/common",
"@npm//@angular/core",
"@npm//@angular/forms",
+ "@npm//rxjs",
],
)
sass_library(
- name = "mdc_list_scss_lib",
+ name = "legacy_list_scss_lib",
srcs = glob(["**/_*.scss"]),
- deps = [
- "//:mdc_sass_lib",
- "//src/material:sass_lib",
- "//src/material/checkbox:checkbox_scss_lib",
- "//src/material/core:core_scss_lib",
- ],
+ deps = ["//src/material/core:core_scss_lib"],
)
sass_binary(
name = "list_scss",
src = "list.scss",
deps = [
- "//:mdc_sass_lib",
- "//src/material:sass_lib",
- "//src/material/core:core_scss_lib",
- ],
-)
-
-sass_binary(
- name = "list_option_scss",
- src = "list-option.scss",
- deps = [
- ":mdc_list_scss_lib",
- "//:mdc_sass_lib",
"//src/cdk:sass_lib",
- "//src/material:sass_lib",
"//src/material/core:core_scss_lib",
+ "//src/material/divider:divider_scss_lib",
],
)
ng_test_library(
- name = "list_tests_lib",
+ name = "unit_test_sources",
srcs = glob(
["**/*.spec.ts"],
exclude = ["**/*.e2e.spec.ts"],
),
deps = [
- ":mdc-list",
+ ":legacy-list",
+ "//src/cdk/a11y",
"//src/cdk/keycodes",
"//src/cdk/testing/private",
- "//src/cdk/testing/testbed",
"//src/material/core",
"@npm//@angular/forms",
"@npm//@angular/platform-browser",
@@ -88,9 +70,7 @@ ng_test_library(
ng_web_test_suite(
name = "unit_tests",
- deps = [
- ":list_tests_lib",
- ],
+ deps = [":unit_test_sources"],
)
ng_e2e_test_library(
@@ -108,3 +88,13 @@ e2e_test_suite(
"//src/cdk/testing/private/e2e",
],
)
+
+markdown_to_html(
+ name = "overview",
+ srcs = [":list.md"],
+)
+
+filegroup(
+ name = "source-files",
+ srcs = glob(["**/*.ts"]),
+)
diff --git a/src/material/legacy-list/README.md b/src/material/legacy-list/README.md
new file mode 100644
index 000000000000..5ebc8cf16008
--- /dev/null
+++ b/src/material/legacy-list/README.md
@@ -0,0 +1 @@
+Please see the official documentation at https://material.angular.io/components/component/list
\ No newline at end of file
diff --git a/src/material/legacy-list/_list-legacy-index.scss b/src/material/legacy-list/_list-legacy-index.scss
new file mode 100644
index 000000000000..e3fbcb69042a
--- /dev/null
+++ b/src/material/legacy-list/_list-legacy-index.scss
@@ -0,0 +1,2 @@
+@forward 'list-theme' hide color, theme, typography;
+@forward 'list-theme' as mat-legacy-list-* hide mat-legacy-list-density;
diff --git a/src/material/legacy-list/_list-theme.import.scss b/src/material/legacy-list/_list-theme.import.scss
new file mode 100644
index 000000000000..7d9dba1a9e42
--- /dev/null
+++ b/src/material/legacy-list/_list-theme.import.scss
@@ -0,0 +1,10 @@
+@forward '../core/theming/theming.import';
+@forward '../core/typography/typography-utils.import';
+@forward '../core/style/list-common.import';
+@forward 'list-theme' hide color, theme, typography;
+@forward 'list-theme' as mat-list-* hide mat-list-density;
+
+@import '../core/theming/palette';
+@import '../core/theming/theming';
+@import '../core/typography/typography-utils';
+@import '../core/style/list-common';
diff --git a/src/material/legacy-list/_list-theme.scss b/src/material/legacy-list/_list-theme.scss
new file mode 100644
index 000000000000..65847228d701
--- /dev/null
+++ b/src/material/legacy-list/_list-theme.scss
@@ -0,0 +1,118 @@
+@use 'sass:map';
+@use '../core/theming/theming';
+@use '../core/typography/typography';
+@use '../core/typography/typography-utils';
+@use '../core/style/list-common';
+
+
+@mixin color($config-or-theme) {
+ $config: theming.get-color-config($config-or-theme);
+ $background: map.get($config, background);
+ $foreground: map.get($config, foreground);
+
+ .mat-list-base {
+ .mat-list-item {
+ color: theming.get-color-from-palette($foreground, text);
+ }
+
+ .mat-list-option {
+ color: theming.get-color-from-palette($foreground, text);
+ }
+
+ .mat-subheader {
+ color: theming.get-color-from-palette($foreground, secondary-text);
+ }
+
+ .mat-list-item-disabled {
+ background-color: theming.get-color-from-palette($background, disabled-list-option);
+ color: theming.get-color-from-palette($foreground, disabled-text);
+ }
+ }
+
+ .mat-list-option,
+ .mat-nav-list .mat-list-item,
+ .mat-action-list .mat-list-item {
+ &:hover, &:focus {
+ background: theming.get-color-from-palette($background, 'hover');
+ }
+ }
+
+ .mat-list-single-selected-option {
+ &, &:hover, &:focus {
+ background: theming.get-color-from-palette($background, hover, 0.12);
+ }
+ }
+}
+
+@mixin typography($config-or-theme) {
+ $config: typography.private-typography-to-2014-config(
+ theming.get-typography-config($config-or-theme));
+ $font-family: typography-utils.font-family($config);
+
+ .mat-list-item {
+ font-family: $font-family;
+ }
+
+ .mat-list-option {
+ font-family: $font-family;
+ }
+
+ // Default list
+ .mat-list-base {
+ .mat-list-item {
+ font-size: typography-utils.font-size($config, subheading-2);
+ @include list-common.base(typography-utils.font-size($config, body-1));
+ }
+
+ .mat-list-option {
+ font-size: typography-utils.font-size($config, subheading-2);
+ @include list-common.base(typography-utils.font-size($config, body-1));
+ }
+
+ .mat-subheader {
+ font-family: typography-utils.font-family($config, body-2);
+ font-size: typography-utils.font-size($config, body-2);
+ font-weight: typography-utils.font-weight($config, body-2);
+ }
+ }
+
+ // Dense list
+ .mat-list-base[dense] {
+ .mat-list-item {
+ font-size: typography-utils.font-size($config, caption);
+ @include list-common.base(typography-utils.font-size($config, caption));
+ }
+
+ .mat-list-option {
+ font-size: typography-utils.font-size($config, caption);
+ @include list-common.base(typography-utils.font-size($config, caption));
+ }
+
+ .mat-subheader {
+ font-family: $font-family;
+ font-size: typography-utils.font-size($config, caption);
+ font-weight: typography-utils.font-weight($config, body-2);
+ }
+ }
+}
+
+@mixin _density($config-or-theme) {}
+
+@mixin theme($theme-or-color-config) {
+ $theme: theming.private-legacy-get-theme($theme-or-color-config);
+ @include theming.private-check-duplicate-theme-styles($theme, 'mat-legacy-list') {
+ $color: theming.get-color-config($theme);
+ $density: theming.get-density-config($theme);
+ $typography: theming.get-typography-config($theme);
+
+ @if $color != null {
+ @include color($color);
+ }
+ @if $density != null {
+ @include _density($density);
+ }
+ @if $typography != null {
+ @include typography($typography);
+ }
+ }
+}
diff --git a/src/material-experimental/mdc-list/index.ts b/src/material/legacy-list/index.ts
similarity index 100%
rename from src/material-experimental/mdc-list/index.ts
rename to src/material/legacy-list/index.ts
diff --git a/src/material/legacy-list/list-item.html b/src/material/legacy-list/list-item.html
new file mode 100644
index 000000000000..03e57de4ac13
--- /dev/null
+++ b/src/material/legacy-list/list-item.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/material/legacy-list/list-module.ts b/src/material/legacy-list/list-module.ts
new file mode 100644
index 000000000000..aa86366bb6fa
--- /dev/null
+++ b/src/material/legacy-list/list-module.ts
@@ -0,0 +1,55 @@
+/**
+ * @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 {CommonModule} from '@angular/common';
+import {NgModule} from '@angular/core';
+import {
+ MatCommonModule,
+ MatLineModule,
+ MatPseudoCheckboxModule,
+ MatRippleModule,
+} from '@angular/material/core';
+import {
+ MatLegacyList,
+ MatLegacyNavList,
+ MatLegacyListAvatarCssMatStyler,
+ MatLegacyListIconCssMatStyler,
+ MatLegacyListItem,
+ MatLegacyListSubheaderCssMatStyler,
+} from './list';
+import {MatLegacyListOption, MatLegacySelectionList} from './selection-list';
+import {MatDividerModule} from '@angular/material/divider';
+
+@NgModule({
+ imports: [MatLineModule, MatRippleModule, MatCommonModule, MatPseudoCheckboxModule, CommonModule],
+ exports: [
+ MatLegacyList,
+ MatLegacyNavList,
+ MatLegacyListItem,
+ MatLegacyListAvatarCssMatStyler,
+ MatLineModule,
+ MatCommonModule,
+ MatLegacyListIconCssMatStyler,
+ MatLegacyListSubheaderCssMatStyler,
+ MatPseudoCheckboxModule,
+ MatLegacySelectionList,
+ MatLegacyListOption,
+ MatDividerModule,
+ ],
+ declarations: [
+ MatLegacyList,
+ MatLegacyNavList,
+ MatLegacyListItem,
+ MatLegacyListAvatarCssMatStyler,
+ MatLegacyListIconCssMatStyler,
+ MatLegacyListSubheaderCssMatStyler,
+ MatLegacySelectionList,
+ MatLegacyListOption,
+ ],
+})
+export class MatLegacyListModule {}
diff --git a/src/material/legacy-list/list-option.html b/src/material/legacy-list/list-option.html
new file mode 100644
index 000000000000..d8a34a308cbf
--- /dev/null
+++ b/src/material/legacy-list/list-option.html
@@ -0,0 +1,19 @@
+
diff --git a/src/material/legacy-list/list.e2e.spec.ts b/src/material/legacy-list/list.e2e.spec.ts
new file mode 100644
index 000000000000..346337ec4fe5
--- /dev/null
+++ b/src/material/legacy-list/list.e2e.spec.ts
@@ -0,0 +1,14 @@
+import {browser} from 'protractor';
+import {expectToExist} from '../../cdk/testing/private/e2e';
+
+describe('list', () => {
+ beforeEach(async () => await browser.get('/list'));
+
+ it('should render a list container', async () => {
+ await expectToExist('mat-list');
+ });
+
+ it('should render list items inside the list container', async () => {
+ await expectToExist('mat-list mat-list-item');
+ });
+});
diff --git a/src/material/list/list.html b/src/material/legacy-list/list.html
similarity index 100%
rename from src/material/list/list.html
rename to src/material/legacy-list/list.html
diff --git a/src/material/legacy-list/list.md b/src/material/legacy-list/list.md
new file mode 100644
index 000000000000..16f07995215d
--- /dev/null
+++ b/src/material/legacy-list/list.md
@@ -0,0 +1,201 @@
+`` is a container component that wraps and formats a series of line items. As the base
+list component, it provides Material Design styling, but no behavior of its own.
+
+
+
+
+### Simple lists
+
+An `` element contains a number of `` elements.
+
+```html
+
+ Pepper
+ Salt
+ Paprika
+
+```
+
+### Navigation lists
+
+Use `mat-nav-list` tags for navigation lists (i.e. lists that have anchor tags).
+
+Simple navigation lists can use the `mat-list-item` attribute on anchor tag elements directly:
+
+```html
+
+ {{ link }}
+
+```
+
+For more complex navigation lists (e.g. with more than one target per item), wrap the anchor
+element in an ``.
+
+```html
+
+
+ {{ link }}
+
+ info
+
+
+
+```
+
+### Action lists
+
+Use the `` element when each item in the list performs some _action_. Each item
+in an action list is a `` element.
+
+Simple action lists can use the `mat-list-item` attribute on button tag elements directly:
+
+```html
+
+ Save
+ Undo
+
+```
+
+### Selection lists
+A selection list provides an interface for selecting values, where each list item is an option.
+
+
+
+The options within a selection-list should not contain further interactive controls, such
+as buttons and anchors.
+
+
+### Multi-line lists
+For lists that require multiple lines per item, annotate each line with an `matLine` attribute.
+Whichever heading tag is appropriate for your DOM hierarchy should be used (not necessarily ``
+as shown in the example).
+
+```html
+
+
+
+ {{message.from}}
+
+ {{message.subject}}
+ -- {{message.content}}
+
+
+
+
+
+
+
+ {{message.from}}
+ {{message.subject}}
+ {{message.content}}
+
+
+```
+
+### Lists with icons
+
+To add an icon to your list item, use the `matListIcon` attribute.
+
+
+```html
+
+
+ folder
+ {{message.from}}
+
+ {{message.subject}}
+ -- {{message.content}}
+
+
+
+```
+
+### Lists with avatars
+
+To include an avatar image, add an image tag with an `matListAvatar` attribute.
+
+```html
+
+
+
+ {{message.from}}
+
+ {{message.subject}}
+ -- {{message.content}}
+
+
+
+```
+
+### Dense lists
+Lists are also available in "dense layout" mode, which shrinks the font size and height of the list
+to suit UIs that may need to display more information. To enable this mode, add a `dense` attribute
+to the main `mat-list` tag.
+
+
+```html
+
+ Pepper
+ Salt
+ Paprika
+
+```
+
+
+### Lists with multiple sections
+
+Subheader can be added to a list by annotating a heading tag with an `matSubheader` attribute.
+To add a divider, use ``.
+
+```html
+
+ Folders
+
+ folder
+ {{folder.name}}
+ {{folder.updated}}
+
+
+ Notes
+
+ note
+ {{note.name}}
+ {{note.updated}}
+
+
+```
+
+### Accessibility
+
+Angular Material offers multiple varieties of list so that you can choose the type that best applies
+to your use-case.
+
+#### Navigation
+
+You should use `MatNavList` when every item in the list is an anchor that navigate to another URL.
+The root `` element sets `role="navigation"` and should contain only anchor elements
+with the `mat-list-item` attribute. You should not nest any interactive elements inside these
+anchors, including buttons and checkboxes.
+
+Always provide an accessible label for the `` element via `aria-label` or
+`aria-labelledby`.
+
+#### Selection
+
+You should use `MatSelectionList` and `MatListOption` for lists that allow the user to select one
+or more values. This list variant uses the `role="listbox"` interaction pattern, handling all
+associated keyboard input and focus management. You should not nest any interactive elements inside
+these options, including buttons and anchors.
+
+Always provide an accessible label for the `` element via `aria-label` or
+`aria-labelledby` that describes the selection being made.
+
+#### Custom scenarios
+
+By default, the list assumes that it will be used in a purely decorative fashion and thus sets no
+roles, ARIA attributes, or keyboard shortcuts. This is equivalent to having a sequence of ``
+elements on the page. Any interactive content within the list should be given an appropriate
+accessibility treatment based on the specific workflow of your application.
+
+If the list is used to present a list of non-interactive content items, then the list element should
+be given `role="list"` and each list item should be given `role="listitem"`.
diff --git a/src/material/legacy-list/list.scss b/src/material/legacy-list/list.scss
new file mode 100644
index 000000000000..2da7b4fd2fb5
--- /dev/null
+++ b/src/material/legacy-list/list.scss
@@ -0,0 +1,358 @@
+@use '@angular/cdk';
+
+@use '../core/style/list-common';
+@use '../core/style/layout-common';
+@use '../divider/divider-offset';
+
+$side-padding: 16px;
+$icon-padding: 4px;
+$avatar-size: 40px;
+
+// Normal list variables
+$top-padding: 8px;
+// height for single-line lists
+$base-height: 48px;
+// height for single-line lists with avatars
+$avatar-height: 56px;
+// spec requires two- and three-line lists be taller
+$two-line-height: 72px;
+$three-line-height: 88px;
+$multi-line-padding: 16px;
+$icon-size: 24px;
+
+// Dense list variables
+$dense-top-padding: 4px;
+$dense-base-height: 40px;
+$dense-avatar-height: 48px;
+$dense-two-line-height: 60px;
+$dense-three-line-height: 76px;
+$dense-multi-line-padding: 16px;
+$dense-list-icon-size: 20px;
+$dense-avatar-size: 36px;
+
+$item-inset-divider-offset: 72px;
+
+// This mixin provides all list-item styles, changing font size and height
+// based on whether the list is in dense mode.
+@mixin item-base($base-height, $height-with-avatar, $two-line-height,
+ $three-line-height, $multi-line-padding, $icon-size, $avatar-size) {
+
+ // Prevents the wrapper `mat-list-item-content` from collapsing due to it
+ // being `inline` by default.
+ display: block;
+ height: $base-height;
+ -webkit-tap-highlight-color: transparent;
+
+ // Override the user agent styling if the list item is a button.
+ width: 100%;
+ padding: 0;
+
+ .mat-list-item-content {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ box-sizing: border-box;
+ padding: 0 $side-padding;
+ position: relative;
+ height: inherit;
+ }
+
+ .mat-list-item-content-reverse {
+ display: flex;
+ align-items: center;
+ padding: 0 $side-padding;
+ flex-direction: row-reverse;
+ justify-content: space-around;
+ }
+
+ .mat-list-item-ripple {
+ display: block;
+ @include layout-common.fill;
+
+ // Disable pointer events for the ripple container because the container will overlay the
+ // user content and we don't want to disable mouse events on the user content.
+ // Pointer events can be safely disabled because the ripple trigger element is the host element.
+ pointer-events: none;
+ }
+
+ &.mat-list-item-with-avatar {
+ height: $height-with-avatar;
+ }
+
+ &.mat-2-line {
+ height: $two-line-height;
+ }
+
+
+ &.mat-3-line {
+ height: $three-line-height;
+ }
+
+ // list items with more than 3 lines should expand to match
+ // the height of its contained text
+ &.mat-multi-line {
+ height: auto;
+
+ .mat-list-item-content {
+ padding-top: $multi-line-padding;
+ padding-bottom: $multi-line-padding;
+ }
+ }
+
+ .mat-list-text {
+ @include list-common.wrapper-base();
+
+ // By default, there will be no padding for the list item text because the padding is already
+ // set on the `mat-list-item-content` element. Later, if the list-item detects that there are
+ // secondary items (avatar, checkbox), a padding on the proper side will be added.
+ padding: 0;
+ }
+
+ &.mat-list-item-with-avatar,
+ &.mat-list-option {
+ .mat-list-item-content .mat-list-text {
+ padding-right: 0;
+ padding-left: $side-padding;
+
+ [dir='rtl'] & {
+ padding-right: $side-padding;
+ padding-left: 0;
+ }
+ }
+
+ // Reversed content is mainly used by the MatSelectionList for displaying the checkbox at the
+ // end of the list option. Since there is a secondary item (checkbox) at the end of the
+ // option, there needs to be a padding for the mat-list-text on the end-side.
+ .mat-list-item-content-reverse .mat-list-text {
+ padding-left: 0;
+ padding-right: $side-padding;
+
+ [dir='rtl'] & {
+ padding-right: 0;
+ padding-left: $side-padding;
+ }
+ }
+ }
+
+ &.mat-list-item-with-avatar.mat-list-option {
+ .mat-list-item-content-reverse .mat-list-text,
+ .mat-list-item-content .mat-list-text {
+ padding-right: $side-padding;
+ padding-left: $side-padding;
+ }
+ }
+
+ .mat-list-avatar {
+ flex-shrink: 0;
+ width: $avatar-size;
+ height: $avatar-size;
+ border-radius: 50%;
+
+ // Not supported in IE11, but we're using this as a
+ // progressive enhancement to get better image scaling.
+ object-fit: cover;
+
+ ~ .mat-divider-inset {
+ @include divider-offset.inset-divider-offset($avatar-size, $side-padding);
+ }
+ }
+
+ .mat-list-icon {
+ flex-shrink: 0;
+ width: $icon-size;
+ height: $icon-size;
+ font-size: $icon-size;
+ box-sizing: content-box;
+ border-radius: 50%;
+ padding: $icon-padding;
+
+ ~ .mat-divider-inset {
+ @include divider-offset.inset-divider-offset($icon-size + (2 * $icon-padding),
+ $side-padding);
+ }
+ }
+
+ .mat-divider {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ margin: 0;
+
+ [dir='rtl'] & {
+ margin-left: auto;
+ margin-right: 0;
+ }
+
+ &.mat-divider-inset {
+ position: absolute; // necessary to override card styles
+ }
+ }
+}
+
+.mat-subheader {
+ display: flex;
+ box-sizing: border-box;
+ padding: $side-padding;
+ align-items: center;
+
+ // This needs slightly more specificity, because it
+ // can be overwritten by the typography styles.
+ .mat-list-base & {
+ margin: 0;
+ }
+}
+
+// We need some extra resets when the list items are buttons.
+button.mat-list-item, button.mat-list-option {
+ padding: 0;
+ width: 100%;
+ background: none;
+ color: inherit;
+ border: none;
+ outline: inherit;
+ -webkit-tap-highlight-color: transparent;
+ text-align: left;
+
+ [dir='rtl'] & {
+ text-align: right;
+ }
+
+ &::-moz-focus-inner {
+ border: 0;
+ }
+}
+
+// This mixin adjusts the heights and padding based on whether the list is in dense mode.
+@mixin subheader-spacing($top-padding, $base-height) {
+ height: $base-height;
+ line-height: $base-height - $side-padding * 2;
+
+ &:first-child {
+ margin-top: -$top-padding;
+ }
+}
+
+.mat-list-base {
+ padding-top: $top-padding;
+ display: block;
+ -webkit-tap-highlight-color: transparent;
+
+ .mat-subheader {
+ @include subheader-spacing($top-padding, $base-height);
+ }
+
+ .mat-list-item, .mat-list-option {
+ @include item-base(
+ $base-height,
+ $avatar-height,
+ $two-line-height,
+ $three-line-height,
+ $multi-line-padding,
+ $icon-size,
+ $avatar-size
+ );
+ }
+}
+
+
+.mat-list-base[dense] {
+ padding-top: $dense-top-padding;
+ display: block;
+
+ .mat-subheader {
+ @include subheader-spacing($dense-top-padding, $dense-base-height);
+ }
+
+ .mat-list-item, .mat-list-option {
+ @include item-base(
+ $dense-base-height,
+ $dense-avatar-height,
+ $dense-two-line-height,
+ $dense-three-line-height,
+ $dense-multi-line-padding,
+ $dense-list-icon-size,
+ $dense-avatar-size
+ );
+ }
+}
+
+.mat-nav-list {
+ a {
+ text-decoration: none;
+ color: inherit;
+ }
+
+ .mat-list-item {
+ cursor: pointer;
+ outline: none;
+ }
+}
+
+mat-action-list {
+ .mat-list-item {
+ cursor: pointer;
+ outline: inherit;
+ }
+}
+
+.mat-list-option:not(.mat-list-item-disabled) {
+ cursor: pointer;
+ outline: none;
+}
+
+.mat-list-item-disabled {
+ pointer-events: none;
+
+ // Since we can't use a color to indicate that the list
+ // item is disabled, we have to use opacity instead.
+ @include cdk.high-contrast {
+ opacity: 0.5;
+ }
+}
+
+@include cdk.high-contrast(active, off) {
+ .mat-list-option,
+ .mat-nav-list .mat-list-item,
+ mat-action-list .mat-list-item {
+ &:hover {
+ outline: dotted 1px;
+ z-index: 1;
+ }
+ }
+
+ // In single selection mode, the selected option is indicated by changing its
+ // background color, but that doesn't work in high contrast mode. We add an
+ // alternate indication by rendering out a circle.
+ .mat-list-single-selected-option::after {
+ $size: 10px;
+ content: '';
+ position: absolute;
+ top: 50%;
+ right: $side-padding;
+ transform: translateY(-50%);
+ width: $size;
+ height: 0;
+ border-bottom: solid $size;
+ border-radius: $size;
+ }
+
+ [dir='rtl'] .mat-list-single-selected-option::after {
+ right: auto;
+ left: $side-padding;
+ }
+}
+
+
+// Disable the hover styles on non-hover devices. Since this is more of a progressive
+// enhancement and not all desktop browsers support this kind of media query, we can't
+// use something like `@media (hover)`.
+@media (hover: none) {
+ .mat-list-option:not(.mat-list-single-selected-option),
+ .mat-nav-list .mat-list-item,
+ .mat-action-list .mat-list-item {
+ &:not(.mat-list-item-disabled):hover {
+ background: none;
+ }
+ }
+}
diff --git a/src/material-experimental/mdc-list/list.spec.ts b/src/material/legacy-list/list.spec.ts
similarity index 77%
rename from src/material-experimental/mdc-list/list.spec.ts
rename to src/material/legacy-list/list.spec.ts
index 61b5e5509cf9..59159607c28b 100644
--- a/src/material-experimental/mdc-list/list.spec.ts
+++ b/src/material/legacy-list/list.spec.ts
@@ -2,12 +2,12 @@ import {fakeAsync, TestBed, waitForAsync} from '@angular/core/testing';
import {dispatchFakeEvent, dispatchMouseEvent} from '@angular/cdk/testing/private';
import {Component, QueryList, ViewChildren} from '@angular/core';
import {By} from '@angular/platform-browser';
-import {MatListItem, MatListModule} from './index';
+import {MatLegacyListItem, MatLegacyListModule} from './index';
-describe('MDC-based MatList', () => {
+describe('MatList', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
- imports: [MatListModule],
+ imports: [MatLegacyListModule],
declarations: [
ListWithOneAnchorItem,
ListWithOneItem,
@@ -28,13 +28,15 @@ describe('MDC-based MatList', () => {
TestBed.compileComponents();
}));
- it('should apply an additional class to lists without lines', () => {
+ it('should not apply any additional class to a list without lines', () => {
const fixture = TestBed.createComponent(ListWithOneItem);
const listItem = fixture.debugElement.query(By.css('mat-list-item'))!;
fixture.detectChanges();
- expect(listItem.nativeElement.classList).toContain('mat-mdc-list-item');
- expect(listItem.nativeElement.classList).toContain('mdc-list-item');
- expect(listItem.nativeElement.classList).toContain('mdc-list-item--with-one-line');
+ expect(listItem.nativeElement.classList.length).toBe(2);
+ expect(listItem.nativeElement.classList).toContain('mat-list-item');
+
+ // This spec also ensures the focus indicator is present.
+ expect(listItem.nativeElement.classList).toContain('mat-focus-indicator');
});
it('should apply a particular class to lists with two lines', () => {
@@ -42,7 +44,8 @@ describe('MDC-based MatList', () => {
fixture.detectChanges();
const listItems = fixture.debugElement.children[0].queryAll(By.css('mat-list-item'));
- expect(listItems[0].nativeElement.className).toContain('mdc-list-item--with-two-lines');
+ expect(listItems[0].nativeElement.className).toContain('mat-2-line');
+ expect(listItems[1].nativeElement.className).toContain('mat-2-line');
});
it('should apply a particular class to lists with three lines', () => {
@@ -50,30 +53,26 @@ describe('MDC-based MatList', () => {
fixture.detectChanges();
const listItems = fixture.debugElement.children[0].queryAll(By.css('mat-list-item'));
- expect(listItems[0].nativeElement.className).toContain('mdc-list-item--with-three-lines');
+ expect(listItems[0].nativeElement.className).toContain('mat-3-line');
+ expect(listItems[1].nativeElement.className).toContain('mat-3-line');
});
- it('should apply a class to list items with avatars', () => {
- const fixture = TestBed.createComponent(ListWithAvatar);
+ 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('mdc-list-item--with-leading-avatar');
- expect(listItems[1].nativeElement.className).not.toContain(
- 'mdc-list-item--with-leading-avatar',
- );
+ expect(listItems[0].nativeElement.className).toContain('mat-multi-line');
+ expect(listItems[1].nativeElement.className).toContain('mat-multi-line');
});
- it('should have a strong focus indicator configured for all list-items', () => {
- const fixture = TestBed.createComponent(ListWithManyLines);
+ it('should apply a class to list items with avatars', () => {
+ const fixture = TestBed.createComponent(ListWithAvatar);
fixture.detectChanges();
- const listItems = fixture.debugElement.children[0]
- .queryAll(By.css('mat-list-item'))
- .map(debugEl => debugEl.nativeElement as HTMLElement);
- expect(listItems.every(i => i.querySelector('.mat-mdc-focus-indicator') !== null))
- .withContext('Expected all list items to have a strong focus indicator element.')
- .toBe(true);
+ const listItems = fixture.debugElement.children[0].queryAll(By.css('mat-list-item'));
+ expect(listItems[0].nativeElement.className).toContain('mat-list-item-with-avatar');
+ expect(listItems[1].nativeElement.className).not.toContain('mat-list-item-with-avatar');
});
it('should not clear custom classes provided by user', () => {
@@ -90,13 +89,14 @@ describe('MDC-based MatList', () => {
fixture.detectChanges();
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-list-item');
- expect(listItem.nativeElement.classList).toContain('mdc-list-item');
+ expect(listItem.nativeElement.classList.length).toBe(3);
+ expect(listItem.nativeElement.classList).toContain('mat-2-line');
+ expect(listItem.nativeElement.classList).toContain('mat-list-item');
+ expect(listItem.nativeElement.classList).toContain('mat-focus-indicator');
fixture.debugElement.componentInstance.showThirdLine = true;
fixture.detectChanges();
- expect(listItem.nativeElement.className).toContain('mdc-list-item--with-three-lines');
+ expect(listItem.nativeElement.className).toContain('mat-3-line');
});
it('should not apply aria roles to mat-list', () => {
@@ -133,18 +133,14 @@ describe('MDC-based MatList', () => {
.toBe('group');
});
- it('should not show ripples for non-nav lists', fakeAsync(() => {
+ it('should not show ripples for non-nav lists', () => {
const fixture = TestBed.createComponent(ListWithOneAnchorItem);
fixture.detectChanges();
- const items: QueryList
= fixture.debugElement.componentInstance.listItems;
+ const items: QueryList = fixture.debugElement.componentInstance.listItems;
expect(items.length).toBeGreaterThan(0);
-
- items.forEach(item => {
- dispatchMouseEvent(item._hostElement, 'mousedown');
- expect(fixture.nativeElement.querySelector('.mat-ripple-element')).toBe(null);
- });
- }));
+ items.forEach(item => expect(item._isRippleDisabled()).toBe(true));
+ });
it('should allow disabling ripples for specific nav-list items', () => {
const fixture = TestBed.createComponent(NavListWithOneAnchorItem);
@@ -154,12 +150,12 @@ describe('MDC-based MatList', () => {
expect(items.length).toBeGreaterThan(0);
// Ripples should be enabled by default, and can be disabled with a binding.
- items.forEach(item => expect(item.rippleDisabled).toBe(false));
+ items.forEach(item => expect(item._isRippleDisabled()).toBe(false));
fixture.componentInstance.disableItemRipple = true;
fixture.detectChanges();
- items.forEach(item => expect(item.rippleDisabled).toBe(true));
+ items.forEach(item => expect(item._isRippleDisabled()).toBe(true));
});
it('should create an action list', () => {
@@ -175,7 +171,7 @@ describe('MDC-based MatList', () => {
fixture.detectChanges();
const host = fixture.nativeElement.querySelector('mat-action-list');
- expect(host.classList).toContain('mat-mdc-action-list');
+ expect(host.classList).toContain('mat-action-list');
});
it('should enable ripples for action lists by default', () => {
@@ -183,7 +179,7 @@ describe('MDC-based MatList', () => {
fixture.detectChanges();
const items = fixture.componentInstance.listItems;
- expect(items.toArray().every(item => !item.rippleDisabled)).toBe(true);
+ expect(items.toArray().every(item => !item._isRippleDisabled())).toBe(true);
});
it('should allow disabling ripples for specific action list items', () => {
@@ -193,19 +189,19 @@ describe('MDC-based MatList', () => {
const items = fixture.componentInstance.listItems.toArray();
expect(items.length).toBeGreaterThan(0);
- expect(items.every(item => !item.rippleDisabled)).toBe(true);
+ expect(items.every(item => !item._isRippleDisabled())).toBe(true);
fixture.componentInstance.disableItemRipple = true;
fixture.detectChanges();
- expect(items.every(item => item.rippleDisabled)).toBe(true);
+ expect(items.every(item => item._isRippleDisabled())).toBe(true);
});
it('should set default type attribute to button for action list', () => {
const fixture = TestBed.createComponent(ActionListWithoutType);
fixture.detectChanges();
- const listItemEl = fixture.debugElement.query(By.css('.mat-mdc-list-item'))!;
+ const listItemEl = fixture.debugElement.query(By.css('.mat-list-item'))!;
expect(listItemEl.nativeElement.getAttribute('type')).toBe('button');
});
@@ -213,7 +209,7 @@ describe('MDC-based MatList', () => {
const fixture = TestBed.createComponent(ActionListWithType);
fixture.detectChanges();
- const listItemEl = fixture.debugElement.query(By.css('.mat-mdc-list-item'))!;
+ const listItemEl = fixture.debugElement.query(By.css('.mat-list-item'))!;
expect(listItemEl.nativeElement.getAttribute('type')).toBe('submit');
});
@@ -225,12 +221,12 @@ describe('MDC-based MatList', () => {
expect(items.length).toBeGreaterThan(0);
// Ripples should be enabled by default, and can be disabled with a binding.
- items.forEach(item => expect(item.rippleDisabled).toBe(false));
+ items.forEach(item => expect(item._isRippleDisabled()).toBe(false));
fixture.componentInstance.disableListRipple = true;
fixture.detectChanges();
- items.forEach(item => expect(item.rippleDisabled).toBe(true));
+ items.forEach(item => expect(item._isRippleDisabled()).toBe(true));
});
it('should allow disabling ripples for the entire action list', () => {
@@ -240,19 +236,19 @@ describe('MDC-based MatList', () => {
const items = fixture.componentInstance.listItems.toArray();
expect(items.length).toBeGreaterThan(0);
- expect(items.every(item => !item.rippleDisabled)).toBe(true);
+ expect(items.every(item => !item._isRippleDisabled())).toBe(true);
fixture.componentInstance.disableListRipple = true;
fixture.detectChanges();
- expect(items.every(item => item.rippleDisabled)).toBe(true);
+ expect(items.every(item => item._isRippleDisabled())).toBe(true);
});
it('should disable item ripples when list ripples are disabled via the input in nav list', fakeAsync(() => {
const fixture = TestBed.createComponent(NavListWithOneAnchorItem);
fixture.detectChanges();
- const rippleTarget = fixture.nativeElement.querySelector('.mat-mdc-list-item');
+ const rippleTarget = fixture.nativeElement.querySelector('.mat-list-item-content');
dispatchMouseEvent(rippleTarget, 'mousedown');
dispatchMouseEvent(rippleTarget, 'mouseup');
@@ -286,7 +282,7 @@ describe('MDC-based MatList', () => {
const fixture = TestBed.createComponent(ActionListWithoutType);
fixture.detectChanges();
- const rippleTarget = fixture.nativeElement.querySelector('.mat-mdc-list-item');
+ const rippleTarget = fixture.nativeElement.querySelector('.mat-list-item-content');
dispatchMouseEvent(rippleTarget, 'mousedown');
dispatchMouseEvent(rippleTarget, 'mouseup');
@@ -325,7 +321,7 @@ describe('MDC-based MatList', () => {
expect(
listItems.map(item => {
- return item.classList.contains('mdc-list-item--disabled');
+ return item.classList.contains('mat-list-item-disabled');
}),
).toEqual([false, false, false]);
@@ -334,7 +330,7 @@ describe('MDC-based MatList', () => {
expect(
listItems.map(item => {
- return item.classList.contains('mdc-list-item--disabled');
+ return item.classList.contains('mat-list-item-disabled');
}),
).toEqual([true, false, false]);
});
@@ -346,12 +342,12 @@ describe('MDC-based MatList', () => {
);
fixture.detectChanges();
- expect(listItems.every(item => item.classList.contains('mdc-list-item--disabled'))).toBe(false);
+ expect(listItems.every(item => item.classList.contains('mat-list-item-disabled'))).toBe(false);
fixture.componentInstance.listDisabled = true;
fixture.detectChanges();
- expect(listItems.every(item => item.classList.contains('mdc-list-item--disabled'))).toBe(true);
+ expect(listItems.every(item => item.classList.contains('mat-list-item-disabled'))).toBe(true);
});
});
@@ -375,7 +371,7 @@ class BaseTestList {
class ListWithOneAnchorItem extends BaseTestList {
// This needs to be declared directly on the class; if declared on the BaseTestList superclass,
// it doesn't get populated.
- @ViewChildren(MatListItem) listItems: QueryList;
+ @ViewChildren(MatLegacyListItem) listItems: QueryList;
}
@Component({
@@ -387,7 +383,7 @@ class ListWithOneAnchorItem extends BaseTestList {
`,
})
class NavListWithOneAnchorItem extends BaseTestList {
- @ViewChildren(MatListItem) listItems: QueryList;
+ @ViewChildren(MatLegacyListItem) listItems: QueryList;
disableItemRipple: boolean = false;
disableListRipple: boolean = false;
}
@@ -401,7 +397,7 @@ class NavListWithOneAnchorItem extends BaseTestList {
`,
})
class ActionListWithoutType extends BaseTestList {
- @ViewChildren(MatListItem) listItems: QueryList;
+ @ViewChildren(MatLegacyListItem) listItems: QueryList;
disableListRipple = false;
disableItemRipple = false;
}
@@ -415,7 +411,7 @@ class ActionListWithoutType extends BaseTestList {
`,
})
class ActionListWithType extends BaseTestList {
- @ViewChildren(MatListItem) listItems: QueryList;
+ @ViewChildren(MatLegacyListItem) listItems: QueryList;
}
@Component({
@@ -433,8 +429,8 @@ class ListWithOneItem extends BaseTestList {}
- {{item.name}}
- {{item.description}}
+ {{item.name}}
+ {{item.description}}
`,
})
@@ -444,9 +440,9 @@ class ListWithTwoLineItem extends BaseTestList {}
template: `
- {{item.name}}
- {{item.description}}
- Some other text
+ {{item.name}}
+ {{item.description}}
+ Some other text
`,
})
@@ -456,10 +452,10 @@ class ListWithThreeLineItem extends BaseTestList {}
template: `
- Line 1
- Line 2
- Line 3
- Line 4
+ Line 1
+ Line 2
+ Line 3
+ Line 4
`,
})
@@ -469,7 +465,7 @@ class ListWithManyLines extends BaseTestList {}
template: `
-
+
Paprika
@@ -483,8 +479,8 @@ class ListWithAvatar extends BaseTestList {}
template: `
- {{item.name}}
- {{item.description}}
+ {{item.name}}
+ {{item.description}}
`,
})
@@ -494,9 +490,9 @@ class ListWithItemWithCssClass extends BaseTestList {}
template: `
- {{item.name}}
- {{item.description}}
- Some other text
+ {{item.name}}
+ {{item.description}}
+ Some other text
`,
})
diff --git a/src/material/legacy-list/list.ts b/src/material/legacy-list/list.ts
new file mode 100644
index 000000000000..64b5c108ad23
--- /dev/null
+++ b/src/material/legacy-list/list.ts
@@ -0,0 +1,242 @@
+/**
+ * @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 {coerceBooleanProperty, BooleanInput} from '@angular/cdk/coercion';
+import {
+ AfterContentInit,
+ ChangeDetectionStrategy,
+ Component,
+ ContentChild,
+ ContentChildren,
+ Directive,
+ ElementRef,
+ Optional,
+ QueryList,
+ ViewEncapsulation,
+ OnChanges,
+ OnDestroy,
+ ChangeDetectorRef,
+ Input,
+ Inject,
+} from '@angular/core';
+import {
+ CanDisable,
+ CanDisableRipple,
+ MatLine,
+ setLines,
+ mixinDisableRipple,
+ mixinDisabled,
+} from '@angular/material/core';
+import {MAT_LIST, MAT_NAV_LIST} from '@angular/material/list';
+import {Subject} from 'rxjs';
+import {takeUntil} from 'rxjs/operators';
+
+// Boilerplate for applying mixins to MatList.
+/** @docs-private */
+const _MatListBase = mixinDisabled(mixinDisableRipple(class {}));
+
+// Boilerplate for applying mixins to MatListItem.
+/** @docs-private */
+const _MatListItemMixinBase = mixinDisableRipple(class {});
+
+@Component({
+ selector: 'mat-nav-list',
+ exportAs: 'matNavList',
+ host: {
+ 'role': 'navigation',
+ 'class': 'mat-nav-list mat-list-base',
+ },
+ templateUrl: 'list.html',
+ styleUrls: ['list.css'],
+ inputs: ['disableRipple', 'disabled'],
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [{provide: MAT_NAV_LIST, useExisting: MatLegacyNavList}],
+})
+export class MatLegacyNavList
+ extends _MatListBase
+ implements CanDisable, CanDisableRipple, OnChanges, OnDestroy
+{
+ /** Emits when the state of the list changes. */
+ readonly _stateChanges = new Subject();
+
+ ngOnChanges() {
+ this._stateChanges.next();
+ }
+
+ ngOnDestroy() {
+ this._stateChanges.complete();
+ }
+}
+
+@Component({
+ selector: 'mat-list, mat-action-list',
+ exportAs: 'matList',
+ templateUrl: 'list.html',
+ host: {
+ 'class': 'mat-list mat-list-base',
+ },
+ styleUrls: ['list.css'],
+ inputs: ['disableRipple', 'disabled'],
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [{provide: MAT_LIST, useExisting: MatLegacyList}],
+})
+export class MatLegacyList
+ extends _MatListBase
+ implements CanDisable, CanDisableRipple, OnChanges, OnDestroy
+{
+ /** Emits when the state of the list changes. */
+ readonly _stateChanges = new Subject();
+
+ constructor(private _elementRef: ElementRef) {
+ super();
+
+ if (this._getListType() === 'action-list') {
+ _elementRef.nativeElement.classList.add('mat-action-list');
+ _elementRef.nativeElement.setAttribute('role', 'group');
+ }
+ }
+
+ _getListType(): 'list' | 'action-list' | null {
+ const nodeName = this._elementRef.nativeElement.nodeName.toLowerCase();
+
+ if (nodeName === 'mat-list') {
+ return 'list';
+ }
+
+ if (nodeName === 'mat-action-list') {
+ return 'action-list';
+ }
+
+ return null;
+ }
+
+ ngOnChanges() {
+ this._stateChanges.next();
+ }
+
+ ngOnDestroy() {
+ this._stateChanges.complete();
+ }
+}
+
+/**
+ * Directive whose purpose is to add the mat- CSS styling to this selector.
+ * @docs-private
+ */
+@Directive({
+ selector: '[mat-list-avatar], [matListAvatar]',
+ host: {'class': 'mat-list-avatar'},
+})
+export class MatLegacyListAvatarCssMatStyler {}
+
+/**
+ * Directive whose purpose is to add the mat- CSS styling to this selector.
+ * @docs-private
+ */
+@Directive({
+ selector: '[mat-list-icon], [matListIcon]',
+ host: {'class': 'mat-list-icon'},
+})
+export class MatLegacyListIconCssMatStyler {}
+
+/**
+ * Directive whose purpose is to add the mat- CSS styling to this selector.
+ * @docs-private
+ */
+@Directive({
+ selector: '[mat-subheader], [matSubheader]',
+ host: {'class': 'mat-subheader'},
+})
+export class MatLegacyListSubheaderCssMatStyler {}
+
+/** An item within a Material Design list. */
+@Component({
+ selector: 'mat-list-item, a[mat-list-item], button[mat-list-item]',
+ exportAs: 'matListItem',
+ host: {
+ 'class': 'mat-list-item mat-focus-indicator',
+ '[class.mat-list-item-disabled]': 'disabled',
+ '[class.mat-list-item-with-avatar]': '_avatar || _icon',
+ },
+ inputs: ['disableRipple'],
+ templateUrl: 'list-item.html',
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class MatLegacyListItem
+ extends _MatListItemMixinBase
+ implements AfterContentInit, CanDisableRipple, OnDestroy
+{
+ private _isInteractiveList: boolean = false;
+ private _list?: MatLegacyNavList | MatLegacyList;
+ private readonly _destroyed = new Subject();
+
+ @ContentChildren(MatLine, {descendants: true}) _lines: QueryList;
+ @ContentChild(MatLegacyListAvatarCssMatStyler) _avatar: MatLegacyListAvatarCssMatStyler;
+ @ContentChild(MatLegacyListIconCssMatStyler) _icon: MatLegacyListIconCssMatStyler;
+
+ constructor(
+ private _element: ElementRef,
+ _changeDetectorRef: ChangeDetectorRef,
+ @Optional() @Inject(MAT_NAV_LIST) navList?: MatLegacyNavList,
+ @Optional() @Inject(MAT_LIST) list?: MatLegacyList,
+ ) {
+ super();
+ this._isInteractiveList = !!(navList || (list && list._getListType() === 'action-list'));
+ this._list = navList || list;
+
+ // If no type attribute is specified for , set it to "button".
+ // If a type attribute is already specified, do nothing.
+ const element = this._getHostElement();
+
+ if (element.nodeName.toLowerCase() === 'button' && !element.hasAttribute('type')) {
+ element.setAttribute('type', 'button');
+ }
+
+ if (this._list) {
+ // React to changes in the state of the parent list since
+ // some of the item's properties depend on it (e.g. `disableRipple`).
+ this._list._stateChanges.pipe(takeUntil(this._destroyed)).subscribe(() => {
+ _changeDetectorRef.markForCheck();
+ });
+ }
+ }
+
+ /** Whether the option is disabled. */
+ @Input()
+ get disabled(): boolean {
+ return this._disabled || !!(this._list && this._list.disabled);
+ }
+ set disabled(value: BooleanInput) {
+ this._disabled = coerceBooleanProperty(value);
+ }
+ private _disabled = false;
+
+ ngAfterContentInit() {
+ setLines(this._lines, this._element);
+ }
+
+ ngOnDestroy() {
+ this._destroyed.next();
+ this._destroyed.complete();
+ }
+
+ /** Whether this list item should show a ripple effect when clicked. */
+ _isRippleDisabled() {
+ return (
+ !this._isInteractiveList || this.disableRipple || !!(this._list && this._list.disableRipple)
+ );
+ }
+
+ /** Retrieves the DOM element of the component host. */
+ _getHostElement(): HTMLElement {
+ return this._element.nativeElement;
+ }
+}
diff --git a/src/material/legacy-list/public-api.ts b/src/material/legacy-list/public-api.ts
new file mode 100644
index 000000000000..fffd9d01fc9e
--- /dev/null
+++ b/src/material/legacy-list/public-api.ts
@@ -0,0 +1,13 @@
+/**
+ * @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
+ */
+
+export * from './list-module';
+export * from './list';
+export * from './selection-list';
+
+export {MAT_LIST, MAT_NAV_LIST} from '@angular/material/list';
diff --git a/src/material-experimental/mdc-list/selection-list.spec.ts b/src/material/legacy-list/selection-list.spec.ts
similarity index 68%
rename from src/material-experimental/mdc-list/selection-list.spec.ts
rename to src/material/legacy-list/selection-list.spec.ts
index 9e063a85dde6..5288edadafce 100644
--- a/src/material-experimental/mdc-list/selection-list.spec.ts
+++ b/src/material/legacy-list/selection-list.spec.ts
@@ -1,4 +1,5 @@
-import {A, D, DOWN_ARROW, END, ENTER, HOME, SPACE, UP_ARROW} from '@angular/cdk/keycodes';
+import {FocusMonitor} from '@angular/cdk/a11y';
+import {A, D, DOWN_ARROW, END, ENTER, HOME, SPACE, TAB, UP_ARROW} from '@angular/cdk/keycodes';
import {
createKeyboardEvent,
dispatchEvent,
@@ -17,24 +18,22 @@ import {
ComponentFixture,
fakeAsync,
flush,
+ inject,
TestBed,
tick,
waitForAsync,
} from '@angular/core/testing';
import {FormControl, FormsModule, NgModel, ReactiveFormsModule} from '@angular/forms';
-import {ThemePalette} from '@angular/material/core';
+import {MatRipple, ThemePalette} from '@angular/material/core';
import {By} from '@angular/platform-browser';
import {
- MatListModule,
- MatListOption,
- MatListOptionCheckboxPosition,
- MatSelectionList,
- MatSelectionListChange,
+ MatLegacyListModule,
+ MatLegacyListOption,
+ MatLegacySelectionList,
+ MatLegacySelectionListChange,
} from './index';
-describe('MDC-based MatSelectionList without forms', () => {
- const typeaheadInterval = 200;
-
+describe('MatSelectionList without forms', () => {
describe('with list option', () => {
let fixture: ComponentFixture;
let listOptions: DebugElement[];
@@ -42,7 +41,7 @@ describe('MDC-based MatSelectionList without forms', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
- imports: [MatListModule],
+ imports: [MatLegacyListModule],
declarations: [
SelectionListWithListOptions,
SelectionListWithCheckboxPositionAfter,
@@ -60,14 +59,10 @@ describe('MDC-based MatSelectionList without forms', () => {
fixture = TestBed.createComponent(SelectionListWithListOptions);
fixture.detectChanges();
- listOptions = fixture.debugElement.queryAll(By.directive(MatListOption));
- selectionList = fixture.debugElement.query(By.directive(MatSelectionList))!;
+ listOptions = fixture.debugElement.queryAll(By.directive(MatLegacyListOption));
+ selectionList = fixture.debugElement.query(By.directive(MatLegacySelectionList))!;
}));
- function getFocusIndex() {
- return listOptions.findIndex(o => document.activeElement === o.nativeElement);
- }
-
it('should be able to set a value on a list option', () => {
const optionValues = ['inbox', 'starred', 'sent-mail', 'archive', 'drafts'];
@@ -92,16 +87,16 @@ describe('MDC-based MatSelectionList without forms', () => {
expect(fixture.componentInstance.onSelectionChange).toHaveBeenCalledTimes(0);
- dispatchMouseEvent(listOptions[2].nativeElement, 'click');
+ dispatchFakeEvent(listOptions[2].nativeElement, 'click');
fixture.detectChanges();
expect(fixture.componentInstance.onSelectionChange).toHaveBeenCalledTimes(1);
});
it('should be able to dispatch one selected item', () => {
- let testListItem = listOptions[2].injector.get(MatListOption);
+ let testListItem = listOptions[2].injector.get(MatLegacyListOption);
let selectList =
- selectionList.injector.get(MatSelectionList).selectedOptions;
+ selectionList.injector.get(MatLegacySelectionList).selectedOptions;
expect(selectList.selected.length).toBe(0);
expect(listOptions[2].nativeElement.getAttribute('aria-selected')).toBe('false');
@@ -115,10 +110,10 @@ describe('MDC-based MatSelectionList without forms', () => {
});
it('should be able to dispatch multiple selected items', () => {
- let testListItem = listOptions[2].injector.get(MatListOption);
- let testListItem2 = listOptions[1].injector.get(MatListOption);
+ let testListItem = listOptions[2].injector.get(MatLegacyListOption);
+ let testListItem2 = listOptions[1].injector.get(MatLegacyListOption);
let selectList =
- selectionList.injector.get(MatSelectionList).selectedOptions;
+ selectionList.injector.get(MatLegacySelectionList).selectedOptions;
expect(selectList.selected.length).toBe(0);
expect(listOptions[2].nativeElement.getAttribute('aria-selected')).toBe('false');
@@ -162,7 +157,7 @@ describe('MDC-based MatSelectionList without forms', () => {
fixture.componentInstance.firstOptionColor = 'primary';
fixture.detectChanges();
- expect(optionNativeElements[0].classList).not.toContain('mat-accent');
+ expect(optionNativeElements[0].classList).toContain('mat-primary');
expect(optionNativeElements[0].classList).not.toContain('mat-warn');
expect(
optionNativeElements.slice(1).every(option => option.classList.contains('mat-warn')),
@@ -175,6 +170,7 @@ describe('MDC-based MatSelectionList without forms', () => {
fixture.componentInstance.firstOptionColor = 'primary';
fixture.detectChanges();
+ expect(classList).toContain('mat-primary');
expect(classList).not.toContain('mat-accent');
expect(classList).not.toContain('mat-warn');
@@ -187,9 +183,9 @@ describe('MDC-based MatSelectionList without forms', () => {
});
it('should be able to deselect an option', () => {
- let testListItem = listOptions[2].injector.get(MatListOption);
+ let testListItem = listOptions[2].injector.get(MatLegacyListOption);
let selectList =
- selectionList.injector.get(MatSelectionList).selectedOptions;
+ selectionList.injector.get(MatLegacySelectionList).selectedOptions;
expect(selectList.selected.length).toBe(0);
@@ -204,33 +200,33 @@ describe('MDC-based MatSelectionList without forms', () => {
expect(selectList.selected.length).toBe(0);
});
- it('should not add the mdc-list-item--selected class (in multiple mode)', () => {
- let testListItem = listOptions[2].injector.get(MatListOption);
+ it('should not add the mat-list-single-selected-option class (in multiple mode)', () => {
+ let testListItem = listOptions[2].injector.get(MatLegacyListOption);
- dispatchMouseEvent(testListItem._hostElement, 'click');
+ testListItem._handleClick();
fixture.detectChanges();
- expect(listOptions[2].nativeElement.classList.contains('mdc-list-item--selected')).toBe(
- false,
- );
+ expect(
+ listOptions[2].nativeElement.classList.contains('mat-list-single-selected-option'),
+ ).toBe(false);
});
it('should not allow selection of disabled items', () => {
- let testListItem = listOptions[0].injector.get(MatListOption);
+ let testListItem = listOptions[0].injector.get(MatLegacyListOption);
let selectList =
- selectionList.injector.get(MatSelectionList).selectedOptions;
+ selectionList.injector.get(MatLegacySelectionList).selectedOptions;
- expect(selectList.selected.length).withContext('before click').toBe(0);
+ expect(selectList.selected.length).toBe(0);
expect(listOptions[0].nativeElement.getAttribute('aria-disabled')).toBe('true');
- dispatchMouseEvent(testListItem._hostElement, 'click');
+ testListItem._handleClick();
fixture.detectChanges();
- expect(selectList.selected.length).withContext('after click').toBe(0);
+ expect(selectList.selected.length).toBe(0);
});
it('should be able to un-disable disabled items', () => {
- let testListItem = listOptions[0].injector.get(MatListOption);
+ let testListItem = listOptions[0].injector.get(MatLegacyListOption);
expect(listOptions[0].nativeElement.getAttribute('aria-disabled')).toBe('true');
@@ -242,34 +238,53 @@ describe('MDC-based MatSelectionList without forms', () => {
it('should be able to use keyboard select with SPACE', () => {
const testListItem = listOptions[1].nativeElement as HTMLElement;
+ const SPACE_EVENT = createKeyboardEvent('keydown', SPACE);
const selectList =
- selectionList.injector.get(MatSelectionList).selectedOptions;
+ selectionList.injector.get(MatLegacySelectionList).selectedOptions;
expect(selectList.selected.length).toBe(0);
- testListItem.focus();
- expect(getFocusIndex()).toBe(1);
+ dispatchFakeEvent(testListItem, 'focus');
+ selectionList.componentInstance._keydown(SPACE_EVENT);
- const event = dispatchKeyboardEvent(testListItem, 'keydown', SPACE);
fixture.detectChanges();
expect(selectList.selected.length).toBe(1);
- expect(event.defaultPrevented).toBe(true);
+ expect(SPACE_EVENT.defaultPrevented).toBe(true);
});
it('should be able to select an item using ENTER', () => {
const testListItem = listOptions[1].nativeElement as HTMLElement;
+ const ENTER_EVENT = createKeyboardEvent('keydown', ENTER);
const selectList =
- selectionList.injector.get(MatSelectionList).selectedOptions;
+ selectionList.injector.get(MatLegacySelectionList).selectedOptions;
expect(selectList.selected.length).toBe(0);
- testListItem.focus();
- expect(getFocusIndex()).toBe(1);
+ dispatchFakeEvent(testListItem, 'focus');
+ selectionList.componentInstance._keydown(ENTER_EVENT);
- const event = dispatchKeyboardEvent(testListItem, 'keydown', ENTER);
fixture.detectChanges();
expect(selectList.selected.length).toBe(1);
- expect(event.defaultPrevented).toBe(true);
+ expect(ENTER_EVENT.defaultPrevented).toBe(true);
+ });
+
+ it('should not be able to toggle an item when pressing a modifier key', () => {
+ const testListItem = listOptions[1].nativeElement as HTMLElement;
+ const selectList =
+ selectionList.injector.get(MatLegacySelectionList).selectedOptions;
+
+ expect(selectList.selected.length).toBe(0);
+
+ [ENTER, SPACE].forEach(key => {
+ const event = createKeyboardEvent('keydown', key, undefined, {control: true});
+
+ dispatchFakeEvent(testListItem, 'focus');
+ selectionList.componentInstance._keydown(event);
+ fixture.detectChanges();
+ expect(event.defaultPrevented).toBe(false);
+ });
+
+ expect(selectList.selected.length).toBe(0);
});
it('should not be able to toggle a disabled option using SPACE', () => {
@@ -279,98 +294,244 @@ describe('MDC-based MatSelectionList without forms', () => {
expect(selectionModel.selected.length).toBe(0);
listOptions[1].componentInstance.disabled = true;
- fixture.detectChanges();
-
- testListItem.focus();
- expect(getFocusIndex()).toBe(1);
- dispatchKeyboardEvent(testListItem, 'keydown', SPACE);
+ dispatchFakeEvent(testListItem, 'focus');
+ selectionList.componentInstance._keydown(createKeyboardEvent('keydown', SPACE));
fixture.detectChanges();
expect(selectionModel.selected.length).toBe(0);
});
it('should focus the first option when the list takes focus for the first time', () => {
- expect(listOptions[0].nativeElement.tabIndex).toBe(0);
- expect(listOptions.slice(1).every(o => o.nativeElement.tabIndex === -1)).toBe(true);
+ spyOn(listOptions[0].componentInstance, 'focus').and.callThrough();
+
+ const manager = selectionList.componentInstance._keyManager;
+ expect(manager.activeItemIndex).toBe(-1);
+
+ dispatchFakeEvent(selectionList.nativeElement, 'focus');
+ fixture.detectChanges();
+
+ expect(manager.activeItemIndex).toBe(0);
+ expect(listOptions[0].componentInstance.focus).toHaveBeenCalled();
});
- it('should focus the first selected option when list receives focus', fakeAsync(() => {
+ it('should not move focus to the first item if focus originated from a mouse interaction', fakeAsync(
+ inject([FocusMonitor], (focusMonitor: FocusMonitor) => {
+ spyOn(listOptions[0].componentInstance, 'focus').and.callThrough();
+
+ const manager = selectionList.componentInstance._keyManager;
+ expect(manager.activeItemIndex).toBe(-1);
+
+ focusMonitor.focusVia(selectionList.nativeElement, 'mouse');
+ fixture.detectChanges();
+ flush();
+
+ expect(manager.activeItemIndex).toBe(-1);
+ expect(listOptions[0].componentInstance.focus).not.toHaveBeenCalled();
+ }),
+ ));
+
+ it('should focus the first selected option when list receives focus', () => {
+ spyOn(listOptions[2].componentInstance, 'focus').and.callThrough();
+
+ const manager = selectionList.componentInstance._keyManager;
+ expect(manager.activeItemIndex).toBe(-1);
+
dispatchMouseEvent(listOptions[2].nativeElement, 'click');
fixture.detectChanges();
- expect(listOptions.map(o => o.nativeElement.tabIndex)).toEqual([-1, -1, 0, -1, -1]);
+ dispatchMouseEvent(listOptions[3].nativeElement, 'click');
+ fixture.detectChanges();
- dispatchMouseEvent(listOptions[1].nativeElement, 'click');
+ dispatchFakeEvent(selectionList.nativeElement, 'focus');
fixture.detectChanges();
- expect(listOptions.map(o => o.nativeElement.tabIndex)).toEqual([-1, 0, -1, -1, -1]);
+ expect(manager.activeItemIndex).toBe(2);
+ expect(listOptions[2].componentInstance.focus).toHaveBeenCalled();
+ });
- // De-select both options to ensure that the first item in the list-item
- // becomes the designated option for focus.
- dispatchMouseEvent(listOptions[1].nativeElement, 'click');
- dispatchMouseEvent(listOptions[2].nativeElement, 'click');
- fixture.detectChanges();
+ it('should allow focus to escape when tabbing away', fakeAsync(() => {
+ selectionList.componentInstance._keyManager.onKeydown(createKeyboardEvent('keydown', TAB));
+
+ expect(selectionList.componentInstance._tabIndex)
+ .withContext('Expected tabIndex to be set to -1 temporarily.')
+ .toBe(-1);
- expect(listOptions.map(o => o.nativeElement.tabIndex)).toEqual([0, -1, -1, -1, -1]);
+ tick();
+
+ expect(selectionList.componentInstance._tabIndex)
+ .withContext('Expected tabIndex to be reset back to 0')
+ .toBe(0);
}));
+ it('should restore focus if active option is destroyed', () => {
+ const manager = selectionList.componentInstance._keyManager;
+
+ spyOn(listOptions[3].componentInstance, 'focus').and.callThrough();
+ listOptions[4].componentInstance._handleFocus();
+
+ expect(manager.activeItemIndex).toBe(4);
+
+ fixture.componentInstance.showLastOption = false;
+ fixture.detectChanges();
+
+ expect(manager.activeItemIndex).toBe(3);
+ expect(listOptions[3].componentInstance.focus).toHaveBeenCalled();
+ });
+
+ it('should not attempt to focus the next option when the destroyed option was not focused', () => {
+ const manager = selectionList.componentInstance._keyManager;
+
+ // Focus and blur the option to move the active item index.
+ listOptions[4].componentInstance._handleFocus();
+ listOptions[4].componentInstance._handleBlur();
+
+ spyOn(listOptions[3].componentInstance, 'focus').and.callThrough();
+
+ expect(manager.activeItemIndex).toBe(4);
+
+ fixture.componentInstance.showLastOption = false;
+ fixture.detectChanges();
+
+ expect(manager.activeItemIndex).toBe(3);
+ expect(listOptions[3].componentInstance.focus).not.toHaveBeenCalled();
+ });
+
it('should focus previous item when press UP ARROW', () => {
- listOptions[2].nativeElement.focus();
- expect(getFocusIndex()).toEqual(2);
+ let UP_EVENT = createKeyboardEvent('keydown', UP_ARROW);
+ let manager = selectionList.componentInstance._keyManager;
+
+ dispatchFakeEvent(listOptions[2].nativeElement, 'focus');
+ expect(manager.activeItemIndex).toEqual(2);
+
+ selectionList.componentInstance._keydown(UP_EVENT);
- dispatchKeyboardEvent(listOptions[2].nativeElement, 'keydown', UP_ARROW);
fixture.detectChanges();
- expect(getFocusIndex()).toEqual(1);
+ expect(manager.activeItemIndex).toEqual(1);
+ });
+
+ it('should focus and toggle the next item when pressing SHIFT + UP_ARROW', () => {
+ const manager = selectionList.componentInstance._keyManager;
+ const upKeyEvent = createKeyboardEvent('keydown', UP_ARROW, undefined, {shift: true});
+
+ dispatchFakeEvent(listOptions[3].nativeElement, 'focus');
+ expect(manager.activeItemIndex).toBe(3);
+
+ expect(listOptions[1].componentInstance.selected).toBe(false);
+ expect(listOptions[2].componentInstance.selected).toBe(false);
+
+ selectionList.componentInstance._keydown(upKeyEvent);
+ fixture.detectChanges();
+
+ expect(listOptions[1].componentInstance.selected).toBe(false);
+ expect(listOptions[2].componentInstance.selected).toBe(true);
+
+ selectionList.componentInstance._keydown(upKeyEvent);
+ fixture.detectChanges();
+
+ expect(listOptions[1].componentInstance.selected).toBe(true);
+ expect(listOptions[2].componentInstance.selected).toBe(true);
});
it('should focus next item when press DOWN ARROW', () => {
- listOptions[2].nativeElement.focus();
- expect(getFocusIndex()).toEqual(2);
+ const manager = selectionList.componentInstance._keyManager;
+
+ dispatchFakeEvent(listOptions[2].nativeElement, 'focus');
+ expect(manager.activeItemIndex).toEqual(2);
+
+ selectionList.componentInstance._keydown(createKeyboardEvent('keydown', DOWN_ARROW));
+ fixture.detectChanges();
+
+ expect(manager.activeItemIndex).toEqual(3);
+ });
+
+ it('should focus and toggle the next item when pressing SHIFT + DOWN_ARROW', () => {
+ const manager = selectionList.componentInstance._keyManager;
+ const downKeyEvent = createKeyboardEvent('keydown', DOWN_ARROW, undefined, {shift: true});
+
+ dispatchFakeEvent(listOptions[0].nativeElement, 'focus');
+ expect(manager.activeItemIndex).toBe(0);
- dispatchKeyboardEvent(listOptions[2].nativeElement, 'keydown', DOWN_ARROW);
+ expect(listOptions[1].componentInstance.selected).toBe(false);
+ expect(listOptions[2].componentInstance.selected).toBe(false);
+
+ selectionList.componentInstance._keydown(downKeyEvent);
+ fixture.detectChanges();
+
+ expect(listOptions[1].componentInstance.selected).toBe(true);
+ expect(listOptions[2].componentInstance.selected).toBe(false);
+
+ selectionList.componentInstance._keydown(downKeyEvent);
fixture.detectChanges();
- expect(getFocusIndex()).toEqual(3);
+ expect(listOptions[1].componentInstance.selected).toBe(true);
+ expect(listOptions[2].componentInstance.selected).toBe(true);
});
it('should be able to focus the first item when pressing HOME', () => {
- listOptions[2].nativeElement.focus();
- expect(getFocusIndex()).toBe(2);
+ const manager = selectionList.componentInstance._keyManager;
+ expect(manager.activeItemIndex).toBe(-1);
- const event = dispatchKeyboardEvent(listOptions[2].nativeElement, 'keydown', HOME);
+ const event = dispatchKeyboardEvent(selectionList.nativeElement, 'keydown', HOME);
fixture.detectChanges();
- expect(getFocusIndex()).toBe(0);
+ expect(manager.activeItemIndex).toBe(0);
expect(event.defaultPrevented).toBe(true);
});
+ it('should not change focus when pressing HOME with a modifier key', () => {
+ const manager = selectionList.componentInstance._keyManager;
+ expect(manager.activeItemIndex).toBe(-1);
+
+ const event = createKeyboardEvent('keydown', HOME, undefined, {alt: true});
+
+ dispatchEvent(selectionList.nativeElement, event);
+ fixture.detectChanges();
+
+ expect(manager.activeItemIndex).toBe(-1);
+ expect(event.defaultPrevented).toBe(false);
+ });
+
it('should focus the last item when pressing END', () => {
- listOptions[2].nativeElement.focus();
- expect(getFocusIndex()).toBe(2);
+ const manager = selectionList.componentInstance._keyManager;
+ expect(manager.activeItemIndex).toBe(-1);
- const event = dispatchKeyboardEvent(listOptions[2].nativeElement, 'keydown', END);
+ const event = dispatchKeyboardEvent(selectionList.nativeElement, 'keydown', END);
fixture.detectChanges();
- expect(getFocusIndex()).toBe(4);
+ expect(manager.activeItemIndex).toBe(4);
expect(event.defaultPrevented).toBe(true);
});
+ it('should not change focus when pressing END with a modifier key', () => {
+ const manager = selectionList.componentInstance._keyManager;
+ expect(manager.activeItemIndex).toBe(-1);
+
+ const event = createKeyboardEvent('keydown', END, undefined, {alt: true});
+
+ dispatchEvent(selectionList.nativeElement, event);
+ fixture.detectChanges();
+
+ expect(manager.activeItemIndex).toBe(-1);
+ expect(event.defaultPrevented).toBe(false);
+ });
+
it('should select all items using ctrl + a', () => {
listOptions.forEach(option => (option.componentInstance.disabled = false));
- fixture.detectChanges();
+ const event = createKeyboardEvent('keydown', A, undefined, {control: true});
expect(listOptions.some(option => option.componentInstance.selected)).toBe(false);
- listOptions[2].nativeElement.focus();
- dispatchKeyboardEvent(listOptions[2].nativeElement, 'keydown', A, 'A', {control: true});
+ dispatchEvent(selectionList.nativeElement, event);
fixture.detectChanges();
expect(listOptions.every(option => option.componentInstance.selected)).toBe(true);
});
it('should not select disabled items when pressing ctrl + a', () => {
+ const event = createKeyboardEvent('keydown', A, undefined, {control: true});
+
listOptions.slice(0, 2).forEach(option => (option.componentInstance.disabled = true));
fixture.detectChanges();
@@ -382,8 +543,7 @@ describe('MDC-based MatSelectionList without forms', () => {
false,
]);
- listOptions[3].nativeElement.focus();
- dispatchKeyboardEvent(listOptions[3].nativeElement, 'keydown', A, 'A', {control: true});
+ dispatchEvent(selectionList.nativeElement, event);
fixture.detectChanges();
expect(listOptions.map(option => option.componentInstance.selected)).toEqual([
@@ -396,26 +556,28 @@ describe('MDC-based MatSelectionList without forms', () => {
});
it('should select all items using ctrl + a if some items are selected', () => {
+ const event = createKeyboardEvent('keydown', A, undefined, {control: true});
+
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});
+ dispatchEvent(selectionList.nativeElement, event);
fixture.detectChanges();
expect(listOptions.every(option => option.componentInstance.selected)).toBe(true);
});
it('should deselect all with ctrl + a if all options are selected', () => {
+ const event = createKeyboardEvent('keydown', A, undefined, {control: true});
+
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});
+ dispatchEvent(selectionList.nativeElement, event);
fixture.detectChanges();
expect(listOptions.every(option => option.componentInstance.selected)).toBe(false);
@@ -424,10 +586,9 @@ describe('MDC-based MatSelectionList without forms', () => {
it('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();
+ const event = createKeyboardEvent('keydown', A, undefined, {control: true});
- listOptions[2].nativeElement.focus();
- dispatchKeyboardEvent(listOptions[2].nativeElement, 'keydown', A, 'A', {control: true});
+ dispatchEvent(selectionList.nativeElement, event);
fixture.detectChanges();
expect(spy).toHaveBeenCalledTimes(1);
@@ -439,75 +600,77 @@ describe('MDC-based MatSelectionList without forms', () => {
});
it('should be able to jump focus down to an item by typing', fakeAsync(() => {
- const firstOption = listOptions[0].nativeElement;
+ const listEl = selectionList.nativeElement;
+ const manager = selectionList.componentInstance._keyManager;
- firstOption.focus();
- expect(getFocusIndex()).toBe(0);
+ expect(manager.activeItemIndex).toBe(-1);
- dispatchEvent(firstOption, createKeyboardEvent('keydown', 83, 's'));
+ dispatchEvent(listEl, createKeyboardEvent('keydown', 83, 's'));
fixture.detectChanges();
- tick(typeaheadInterval);
+ tick(200);
- expect(getFocusIndex()).toBe(1);
+ expect(manager.activeItemIndex).toBe(1);
- dispatchEvent(firstOption, createKeyboardEvent('keydown', 68, 'd'));
+ dispatchEvent(listEl, createKeyboardEvent('keydown', 68, 'd'));
fixture.detectChanges();
- tick(typeaheadInterval);
+ tick(200);
- expect(getFocusIndex()).toBe(4);
+ expect(manager.activeItemIndex).toBe(4);
}));
it('should be able to skip to an item by typing', fakeAsync(() => {
- listOptions[0].nativeElement.focus();
- expect(getFocusIndex()).toBe(0);
+ const manager = selectionList.componentInstance._keyManager;
+
+ expect(manager.activeItemIndex).not.toBe(4);
- dispatchKeyboardEvent(listOptions[0].nativeElement, 'keydown', D, 'd');
+ const event = createKeyboardEvent('keydown', D, 'd');
+ selectionList.componentInstance._keydown(event);
fixture.detectChanges();
- tick(typeaheadInterval);
+ tick(200);
- expect(getFocusIndex()).toBe(4);
+ expect(manager.activeItemIndex).toBe(4);
}));
- // Test for "A" specifically, because it's a special case that can be used
- // to select all values.
+ // Test for "A" specifically, because it's a special case that can be used to select all values.
it('should be able to skip to an item by typing the letter "A"', fakeAsync(() => {
- listOptions[0].nativeElement.focus();
- expect(getFocusIndex()).toBe(0);
+ const manager = selectionList.componentInstance._keyManager;
- dispatchKeyboardEvent(listOptions[0].nativeElement, 'keydown', A, 'a');
+ expect(manager.activeItemIndex).not.toBe(3);
+
+ const event = createKeyboardEvent('keydown', A, 'a');
+ selectionList.componentInstance._keydown(event);
fixture.detectChanges();
- tick(typeaheadInterval);
+ tick(200);
- expect(getFocusIndex()).toBe(3);
+ expect(manager.activeItemIndex).toBe(3);
}));
it('should not select items while using the typeahead', fakeAsync(() => {
+ const manager = selectionList.componentInstance._keyManager;
const testListItem = listOptions[1].nativeElement as HTMLElement;
- const model = selectionList.injector.get(MatSelectionList).selectedOptions;
+ const model =
+ selectionList.injector.get(MatLegacySelectionList).selectedOptions;
- testListItem.focus();
dispatchFakeEvent(testListItem, 'focus');
fixture.detectChanges();
- expect(getFocusIndex()).toBe(1);
+ expect(manager.activeItemIndex).toBe(1);
expect(model.isEmpty()).toBe(true);
- dispatchKeyboardEvent(testListItem, 'keydown', D, 'd');
+ selectionList.componentInstance._keydown(createKeyboardEvent('keydown', D, 'd'));
fixture.detectChanges();
- tick(typeaheadInterval / 2); // Tick only half the typeahead timeout.
+ tick(100); // Tick only half the typeahead timeout.
- dispatchKeyboardEvent(testListItem, 'keydown', SPACE);
+ selectionList.componentInstance._keydown(createKeyboardEvent('keydown', SPACE));
fixture.detectChanges();
- // Tick the buffer timeout again as a new key has been pressed that resets
- // the buffer timeout.
- tick(typeaheadInterval);
+ tick(100); // Tick the rest of the timeout.
- expect(getFocusIndex()).toBe(4);
+ expect(manager.activeItemIndex).toBe(4);
expect(model.isEmpty()).toBe(true);
}));
it('should be able to select all options', () => {
- const list: MatSelectionList = selectionList.componentInstance;
+ const list: MatLegacySelectionList = selectionList.componentInstance;
expect(list.options.toArray().every(option => option.selected)).toBe(false);
@@ -519,7 +682,7 @@ describe('MDC-based MatSelectionList without forms', () => {
});
it('should be able to select all options, even if they are disabled', () => {
- const list: MatSelectionList = selectionList.componentInstance;
+ const list: MatLegacySelectionList = selectionList.componentInstance;
list.options.forEach(option => (option.disabled = true));
fixture.detectChanges();
@@ -533,7 +696,7 @@ describe('MDC-based MatSelectionList without forms', () => {
});
it('should be able to deselect all options', () => {
- const list: MatSelectionList = selectionList.componentInstance;
+ const list: MatLegacySelectionList = selectionList.componentInstance;
list.options.forEach(option => option.toggle());
expect(list.options.toArray().every(option => option.selected)).toBe(true);
@@ -546,7 +709,7 @@ describe('MDC-based MatSelectionList without forms', () => {
});
it('should be able to deselect all options, even if they are disabled', () => {
- const list: MatSelectionList = selectionList.componentInstance;
+ const list: MatLegacySelectionList = selectionList.componentInstance;
list.options.forEach(option => option.toggle());
expect(list.options.toArray().every(option => option.selected)).toBe(true);
@@ -561,7 +724,7 @@ describe('MDC-based MatSelectionList without forms', () => {
});
it('should update the list value when an item is selected programmatically', () => {
- const list: MatSelectionList = selectionList.componentInstance;
+ const list: MatLegacySelectionList = selectionList.componentInstance;
expect(list.selectedOptions.isEmpty()).toBe(true);
@@ -575,8 +738,8 @@ describe('MDC-based MatSelectionList without forms', () => {
});
it('should update the item selected state when it is selected via the model', () => {
- const list: MatSelectionList = selectionList.componentInstance;
- const item: MatListOption = listOptions[0].componentInstance;
+ const list: MatLegacySelectionList = selectionList.componentInstance;
+ const item: MatLegacyListOption = listOptions[0].componentInstance;
expect(item.selected).toBe(false);
@@ -593,9 +756,9 @@ describe('MDC-based MatSelectionList without forms', () => {
it('should be able to reach list options that are indirect descendants', () => {
const descendatsFixture = TestBed.createComponent(SelectionListWithIndirectChildOptions);
descendatsFixture.detectChanges();
- listOptions = descendatsFixture.debugElement.queryAll(By.directive(MatListOption));
- selectionList = descendatsFixture.debugElement.query(By.directive(MatSelectionList))!;
- const list: MatSelectionList = selectionList.componentInstance;
+ listOptions = descendatsFixture.debugElement.queryAll(By.directive(MatLegacyListOption));
+ selectionList = descendatsFixture.debugElement.query(By.directive(MatLegacySelectionList))!;
+ const list: MatLegacySelectionList = selectionList.componentInstance;
expect(list.options.toArray().every(option => option.selected)).toBe(false);
@@ -607,8 +770,9 @@ describe('MDC-based MatSelectionList without forms', () => {
it('should disable list item ripples when the ripples on the list have been disabled', fakeAsync(() => {
const rippleTarget = fixture.nativeElement.querySelector(
- '.mat-mdc-list-option:not(.mdc-list-item--disabled)',
+ '.mat-list-option:not(.mat-list-item-disabled) .mat-list-item-content',
);
+
dispatchMouseEvent(rippleTarget, 'mousedown');
dispatchMouseEvent(rippleTarget, 'mouseup');
@@ -631,6 +795,7 @@ describe('MDC-based MatSelectionList without forms', () => {
dispatchMouseEvent(rippleTarget, 'mousedown');
dispatchMouseEvent(rippleTarget, 'mouseup');
+ flush();
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length)
.withContext('Expected no ripples after list ripples are disabled.')
@@ -640,37 +805,28 @@ describe('MDC-based MatSelectionList without forms', () => {
it('can bind both selected and value at the same time', () => {
const componentFixture = TestBed.createComponent(SelectionListWithSelectedOptionAndValue);
componentFixture.detectChanges();
- const listItemEl = componentFixture.debugElement.query(By.directive(MatListOption))!;
+ const listItemEl = componentFixture.debugElement.query(By.directive(MatLegacyListOption))!;
expect(listItemEl.componentInstance.selected).toBe(true);
expect(listItemEl.componentInstance.value).toBe(componentFixture.componentInstance.itemValue);
});
it('should have a focus indicator', () => {
- const optionNativeElements = listOptions.map(option => option.nativeElement as HTMLElement);
+ const optionNativeElements = listOptions.map(option => option.nativeElement);
expect(
- optionNativeElements.every(
- element => element.querySelector('.mat-mdc-focus-indicator') !== null,
- ),
+ optionNativeElements.every(element => element.classList.contains('mat-focus-indicator')),
).toBe(true);
});
-
- it('should hide the internal SVG', () => {
- listOptions.forEach(option => {
- const svg = option.nativeElement.querySelector('.mdc-checkbox svg');
- expect(svg.getAttribute('aria-hidden')).toBe('true');
- });
- });
});
describe('with list option selected', () => {
let fixture: ComponentFixture;
- let listOptionElements: DebugElement[];
+ let listItemEl: DebugElement;
let selectionList: DebugElement;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
- imports: [MatListModule],
+ imports: [MatLegacyListModule],
declarations: [SelectionListWithSelectedOption],
});
@@ -679,38 +835,61 @@ describe('MDC-based MatSelectionList without forms', () => {
beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(SelectionListWithSelectedOption);
- listOptionElements = fixture.debugElement.queryAll(By.directive(MatListOption))!;
- selectionList = fixture.debugElement.query(By.directive(MatSelectionList))!;
+ listItemEl = fixture.debugElement.query(By.directive(MatLegacyListOption))!;
+ selectionList = fixture.debugElement.query(By.directive(MatLegacySelectionList))!;
fixture.detectChanges();
}));
it('should set its initial selected state in the selectedOptions', () => {
- let options = listOptionElements.map(optionEl =>
- optionEl.injector.get(MatListOption),
- );
+ let optionEl = listItemEl.injector.get(MatLegacyListOption);
let selectedOptions = selectionList.componentInstance.selectedOptions;
- expect(selectedOptions.isSelected(options[0])).toBeFalse();
- expect(selectedOptions.isSelected(options[1])).toBeTrue();
- expect(selectedOptions.isSelected(options[2])).toBeTrue();
- expect(selectedOptions.isSelected(options[3])).toBeFalse();
+ expect(selectedOptions.isSelected(optionEl)).toBeTruthy();
});
+ });
- it('should focus the first selected option on first focus if an item is pre-selected', fakeAsync(() => {
- // MDC manages the focus through setting a `tabindex` on the designated list item. We
- // assert that the proper tabindex is set on the pre-selected option at index 1, and
- // ensure that other options are not reachable through tab.
- expect(listOptionElements.map(el => el.nativeElement.tabIndex)).toEqual([-1, 0, -1, -1]);
+ describe('with changing option value', () => {
+ let fixture: ComponentFixture;
+ let selectionList: MatLegacySelectionList;
+ let listOption: MatLegacyListOption;
+
+ beforeEach(waitForAsync(() => {
+ TestBed.configureTestingModule({
+ imports: [MatLegacyListModule],
+ declarations: [SelectionListWithChangingOptionValue],
+ });
+
+ TestBed.compileComponents();
}));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SelectionListWithChangingOptionValue);
+ fixture.detectChanges();
+
+ selectionList = fixture.debugElement.query(
+ By.directive(MatLegacySelectionList),
+ )!.componentInstance;
+ listOption = fixture.debugElement.query(By.directive(MatLegacyListOption))!.componentInstance;
+ });
+
+ it('should use `compareWith` function when updating option selection state', () => {
+ expect(selectionList.selectedOptions.isSelected(listOption)).toBeTrue();
+ fixture.componentInstance.value = {id: 1};
+ fixture.detectChanges();
+ expect(selectionList.selectedOptions.isSelected(listOption)).toBeTrue();
+ fixture.componentInstance.value = {id: 2};
+ fixture.detectChanges();
+ expect(selectionList.selectedOptions.isSelected(listOption)).toBeFalse();
+ });
});
describe('with option disabled', () => {
let fixture: ComponentFixture;
let listOptionEl: HTMLElement;
- let listOption: MatListOption;
+ let listOption: MatLegacyListOption;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
- imports: [MatListModule],
+ imports: [MatLegacyListModule],
declarations: [SelectionListWithDisabledOption],
});
@@ -720,7 +899,7 @@ describe('MDC-based MatSelectionList without forms', () => {
beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(SelectionListWithDisabledOption);
- const listOptionDebug = fixture.debugElement.query(By.directive(MatListOption))!;
+ const listOptionDebug = fixture.debugElement.query(By.directive(MatLegacyListOption))!;
listOption = listOptionDebug.componentInstance;
listOptionEl = listOptionDebug.nativeElement;
@@ -729,25 +908,25 @@ describe('MDC-based MatSelectionList without forms', () => {
}));
it('should disable ripples for disabled option', () => {
- expect(listOption.rippleDisabled)
+ expect(listOption._isRippleDisabled())
.withContext('Expected ripples to be enabled by default')
.toBe(false);
fixture.componentInstance.disableItem = true;
fixture.detectChanges();
- expect(listOption.rippleDisabled)
+ expect(listOption._isRippleDisabled())
.withContext('Expected ripples to be disabled if option is disabled')
.toBe(true);
});
it('should apply the "mat-list-item-disabled" class properly', () => {
- expect(listOptionEl.classList).not.toContain('mdc-list-item--disabled');
+ expect(listOptionEl.classList).not.toContain('mat-list-item-disabled');
fixture.componentInstance.disableItem = true;
fixture.detectChanges();
- expect(listOptionEl.classList).toContain('mdc-list-item--disabled');
+ expect(listOptionEl.classList).toContain('mat-list-item-disabled');
});
});
@@ -758,7 +937,7 @@ describe('MDC-based MatSelectionList without forms', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
- imports: [MatListModule],
+ imports: [MatLegacyListModule],
declarations: [
SelectionListWithListOptions,
SelectionListWithCheckboxPositionAfter,
@@ -772,35 +951,41 @@ describe('MDC-based MatSelectionList without forms', () => {
beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(SelectionListWithListDisabled);
- listOption = fixture.debugElement.queryAll(By.directive(MatListOption));
- selectionList = fixture.debugElement.query(By.directive(MatSelectionList))!;
+ listOption = fixture.debugElement.queryAll(By.directive(MatLegacyListOption));
+ selectionList = fixture.debugElement.query(By.directive(MatLegacySelectionList))!;
fixture.detectChanges();
}));
it('should not allow selection on disabled selection-list', () => {
- let testListItem = listOption[2].injector.get(MatListOption);
+ let testListItem = listOption[2].injector.get(MatLegacyListOption);
let selectList =
- selectionList.injector.get(MatSelectionList).selectedOptions;
+ selectionList.injector.get(MatLegacySelectionList).selectedOptions;
expect(selectList.selected.length).toBe(0);
- dispatchMouseEvent(testListItem._hostElement, 'click');
+ testListItem._handleClick();
fixture.detectChanges();
expect(selectList.selected.length).toBe(0);
});
it('should update state of options if list state has changed', () => {
- const testOption = listOption[2].componentInstance as MatListOption;
-
- expect(testOption.rippleDisabled)
+ // To verify that the template of the list options has been re-rendered after the disabled
+ // property of the selection list has been updated, the ripple directive can be used.
+ // Inspecting the host classes of the options doesn't work because those update as part
+ // of the parent template (of the selection-list).
+ const listOptionRipple = listOption[2]
+ .query(By.directive(MatRipple))!
+ .injector.get(MatRipple);
+
+ expect(listOptionRipple.disabled)
.withContext('Expected ripples of list option to be disabled')
.toBe(true);
fixture.componentInstance.disabled = false;
fixture.detectChanges();
- expect(testOption.rippleDisabled)
+ expect(listOptionRipple.disabled)
.withContext('Expected ripples of list option to be enabled')
.toBe(false);
});
@@ -811,7 +996,7 @@ describe('MDC-based MatSelectionList without forms', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
- imports: [MatListModule],
+ imports: [MatLegacyListModule],
declarations: [
SelectionListWithListOptions,
SelectionListWithCheckboxPositionAfter,
@@ -829,137 +1014,33 @@ describe('MDC-based MatSelectionList without forms', () => {
}));
it('should be able to customize checkbox position', () => {
- expect(fixture.nativeElement.querySelector('.mdc-list-item__end .mdc-checkbox'))
- .withContext('Expected checkbox to show up after content.')
- .toBeTruthy();
- expect(fixture.nativeElement.querySelector('.mdc-list-item__start .mdc-checkbox'))
- .withContext('Expected no checkbox to show up before content.')
- .toBeFalsy();
+ let listItemContent = fixture.debugElement.query(By.css('.mat-list-item-content'))!;
+ expect(listItemContent.nativeElement.classList).toContain('mat-list-item-content-reverse');
});
});
describe('with list item elements', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
- imports: [MatListModule],
+ imports: [MatLegacyListModule],
declarations: [SelectionListWithAvatar, SelectionListWithIcon],
}).compileComponents();
}));
- function expectCheckboxAtPosition(
- listItemElement: HTMLElement,
- position: MatListOptionCheckboxPosition,
- ) {
- const containerSelector =
- position === 'before' ? '.mdc-list-item__start' : 'mdc-list-item__end';
- const checkboxPositionClass =
- position === 'before'
- ? 'mdc-list-item--with-leading-checkbox'
- : 'mdc-list-item--with-trailing-checkbox';
- expect(listItemElement.querySelector(`${containerSelector} .mdc-checkbox`))
- .withContext(`Expected checkbox to be aligned ${position}`)
- .toBeDefined();
- expect(listItemElement.classList).toContain(checkboxPositionClass);
- }
-
- /**
- * Expects an icon to be shown at the given position. Also
- * ensures no avatar is shown at the specified position.
- */
- function expectIconAt(item: HTMLElement, position: 'before' | 'after') {
- 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');
-
- if (position === 'before') {
- expect(icon.classList).toContain('mdc-list-item__start');
- expect(item.classList).toContain('mdc-list-item--with-leading-icon');
- expect(item.classList).not.toContain('mdc-list-item--with-trailing-icon');
- } else {
- expect(icon.classList).toContain('mdc-list-item__end');
- expect(item.classList).toContain('mdc-list-item--with-trailing-icon');
- expect(item.classList).not.toContain('mdc-list-item--with-leading-icon');
- }
- }
-
- /**
- * Expects an avatar to be shown at the given position. Also
- * ensures that no icon is shown at the specified position.
- */
- function expectAvatarAt(item: HTMLElement, position: 'before' | 'after') {
- 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');
-
- if (position === 'before') {
- expect(avatar.classList).toContain('mdc-list-item__start');
- expect(item.classList).toContain('mdc-list-item--with-leading-avatar');
- expect(item.classList).not.toContain('mat-mdc-list-option-with-trailing-avatar');
- } else {
- expect(avatar.classList).toContain('mdc-list-item__end');
- expect(item.classList).toContain('mat-mdc-list-option-with-trailing-avatar');
- expect(item.classList).not.toContain('mdc-list-item--with-leading-avatar');
- }
- }
-
it('should add a class to reflect that it has an avatar', () => {
- const fixture = TestBed.createComponent(SelectionListWithAvatar);
+ const fixture = TestBed.createComponent(SelectionListWithIcon);
fixture.detectChanges();
- const listOption = fixture.nativeElement.querySelector('.mat-mdc-list-option');
- expect(listOption.classList).toContain('mdc-list-item--with-leading-avatar');
+ const listOption = fixture.nativeElement.querySelector('.mat-list-option');
+ expect(listOption.classList).toContain('mat-list-item-with-avatar');
});
it('should add a class to reflect that it has an icon', () => {
const fixture = TestBed.createComponent(SelectionListWithIcon);
fixture.detectChanges();
- const listOption = fixture.nativeElement.querySelector('.mat-mdc-list-option');
- expect(listOption.classList).toContain('mdc-list-item--with-leading-icon');
- });
-
- it('should align icons properly together with checkbox', () => {
- const fixture = TestBed.createComponent(SelectionListWithIcon);
- fixture.detectChanges();
- const listOption = fixture.nativeElement.querySelector(
- '.mat-mdc-list-option',
- )! as HTMLElement;
-
- expectCheckboxAtPosition(listOption, 'after');
- expectIconAt(listOption, 'before');
-
- fixture.componentInstance.checkboxPosition = 'before';
- fixture.detectChanges();
- expectCheckboxAtPosition(listOption, 'before');
- expectIconAt(listOption, 'after');
-
- fixture.componentInstance.checkboxPosition = 'after';
- fixture.detectChanges();
- expectCheckboxAtPosition(listOption, 'after');
- expectIconAt(listOption, 'before');
- });
-
- it('should align avatars properly together with checkbox', () => {
- const fixture = TestBed.createComponent(SelectionListWithAvatar);
- fixture.detectChanges();
- const listOption = fixture.nativeElement.querySelector(
- '.mat-mdc-list-option',
- )! as HTMLElement;
-
- expectCheckboxAtPosition(listOption, 'after');
- expectAvatarAt(listOption, 'before');
-
- fixture.componentInstance.checkboxPosition = 'before';
- fixture.detectChanges();
- expectCheckboxAtPosition(listOption, 'before');
- expectAvatarAt(listOption, 'after');
-
- fixture.componentInstance.checkboxPosition = 'after';
- fixture.detectChanges();
- expectCheckboxAtPosition(listOption, 'after');
- expectAvatarAt(listOption, 'before');
+ const listOption = fixture.nativeElement.querySelector('.mat-list-option');
+ expect(listOption.classList).toContain('mat-list-item-with-avatar');
});
});
@@ -970,43 +1051,43 @@ describe('MDC-based MatSelectionList without forms', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
- imports: [MatListModule],
+ imports: [MatLegacyListModule],
declarations: [SelectionListWithListOptions],
}).compileComponents();
fixture = TestBed.createComponent(SelectionListWithListOptions);
fixture.componentInstance.multiple = false;
- listOptions = fixture.debugElement.queryAll(By.directive(MatListOption));
- selectionList = fixture.debugElement.query(By.directive(MatSelectionList))!;
+ listOptions = fixture.debugElement.queryAll(By.directive(MatLegacyListOption));
+ selectionList = fixture.debugElement.query(By.directive(MatLegacySelectionList))!;
fixture.detectChanges();
}));
- function getFocusIndex() {
- return listOptions.findIndex(o => document.activeElement === o.nativeElement);
- }
-
it('should select one option at a time', () => {
- const testListItem1 = listOptions[1].injector.get(MatListOption);
- const testListItem2 = listOptions[2].injector.get(MatListOption);
+ const testListItem1 = listOptions[1].injector.get(MatLegacyListOption);
+ const testListItem2 = listOptions[2].injector.get(MatLegacyListOption);
const selectList =
- selectionList.injector.get(MatSelectionList).selectedOptions;
+ selectionList.injector.get(MatLegacySelectionList).selectedOptions;
expect(selectList.selected.length).toBe(0);
- dispatchMouseEvent(testListItem1._hostElement, 'click');
+ dispatchFakeEvent(testListItem1._getHostElement(), 'click');
fixture.detectChanges();
expect(selectList.selected).toEqual([testListItem1]);
- expect(listOptions[1].nativeElement.classList.contains('mdc-list-item--selected')).toBe(true);
+ expect(
+ listOptions[1].nativeElement.classList.contains('mat-list-single-selected-option'),
+ ).toBe(true);
- dispatchMouseEvent(testListItem2._hostElement, 'click');
+ dispatchFakeEvent(testListItem2._getHostElement(), 'click');
fixture.detectChanges();
expect(selectList.selected).toEqual([testListItem2]);
- expect(listOptions[1].nativeElement.classList.contains('mdc-list-item--selected')).toBe(
- false,
- );
- expect(listOptions[2].nativeElement.classList.contains('mdc-list-item--selected')).toBe(true);
+ expect(
+ listOptions[1].nativeElement.classList.contains('mat-list-single-selected-option'),
+ ).toBe(false);
+ expect(
+ listOptions[2].nativeElement.classList.contains('mat-list-single-selected-option'),
+ ).toBe(true);
});
it('should not show check boxes', () => {
@@ -1014,18 +1095,18 @@ describe('MDC-based MatSelectionList without forms', () => {
});
it('should not deselect the target option on click', () => {
- const testListItem1 = listOptions[1].injector.get(MatListOption);
+ const testListItem1 = listOptions[1].injector.get(MatLegacyListOption);
const selectList =
- selectionList.injector.get(MatSelectionList).selectedOptions;
+ selectionList.injector.get(MatLegacySelectionList).selectedOptions;
expect(selectList.selected.length).toBe(0);
- dispatchMouseEvent(testListItem1._hostElement, 'click');
+ dispatchFakeEvent(testListItem1._getHostElement(), 'click');
fixture.detectChanges();
expect(selectList.selected).toEqual([testListItem1]);
- dispatchMouseEvent(testListItem1._hostElement, 'click');
+ dispatchFakeEvent(testListItem1._getHostElement(), 'click');
fixture.detectChanges();
expect(selectList.selected).toEqual([testListItem1]);
@@ -1053,15 +1134,16 @@ describe('MDC-based MatSelectionList without forms', () => {
'should focus, but not toggle, the next item when pressing SHIFT + UP_ARROW in single ' +
'selection mode',
() => {
- listOptions[3].nativeElement.focus();
- expect(getFocusIndex()).toBe(3);
+ const manager = selectionList.componentInstance._keyManager;
+ const upKeyEvent = createKeyboardEvent('keydown', UP_ARROW, undefined, {shift: true});
+
+ dispatchFakeEvent(listOptions[3].nativeElement, 'focus');
+ expect(manager.activeItemIndex).toBe(3);
expect(listOptions[1].componentInstance.selected).toBe(false);
expect(listOptions[2].componentInstance.selected).toBe(false);
- dispatchKeyboardEvent(listOptions[3].nativeElement, 'keydown', UP_ARROW, undefined, {
- shift: true,
- });
+ selectionList.componentInstance._keydown(upKeyEvent);
fixture.detectChanges();
expect(listOptions[1].componentInstance.selected).toBe(false);
@@ -1073,15 +1155,16 @@ describe('MDC-based MatSelectionList without forms', () => {
'should focus, but not toggle, the next item when pressing SHIFT + DOWN_ARROW ' +
'in single selection mode',
() => {
- listOptions[3].nativeElement.focus();
- expect(getFocusIndex()).toBe(3);
+ const manager = selectionList.componentInstance._keyManager;
+ const downKeyEvent = createKeyboardEvent('keydown', DOWN_ARROW, undefined, {shift: true});
+
+ dispatchFakeEvent(listOptions[0].nativeElement, 'focus');
+ expect(manager.activeItemIndex).toBe(0);
expect(listOptions[1].componentInstance.selected).toBe(false);
expect(listOptions[2].componentInstance.selected).toBe(false);
- dispatchKeyboardEvent(listOptions[3].nativeElement, 'keydown', DOWN_ARROW, undefined, {
- shift: true,
- });
+ selectionList.componentInstance._keydown(downKeyEvent);
fixture.detectChanges();
expect(listOptions[1].componentInstance.selected).toBe(false);
@@ -1093,17 +1176,17 @@ describe('MDC-based MatSelectionList without forms', () => {
describe('with single selection', () => {
let fixture: ComponentFixture;
let optionElement: HTMLElement;
- let option: MatListOption;
+ let option: MatLegacyListOption;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
- imports: [MatListModule],
+ imports: [MatLegacyListModule],
declarations: [ListOptionWithTwoWayBinding],
}).compileComponents();
fixture = TestBed.createComponent(ListOptionWithTwoWayBinding);
fixture.detectChanges();
- const optionDebug = fixture.debugElement.query(By.directive(MatListOption));
+ const optionDebug = fixture.debugElement.query(By.directive(MatLegacyListOption));
option = optionDebug.componentInstance;
optionElement = optionDebug.nativeElement;
}));
@@ -1128,10 +1211,10 @@ describe('MDC-based MatSelectionList without forms', () => {
});
});
-describe('MDC-based MatSelectionList with forms', () => {
+describe('MatSelectionList with forms', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
- imports: [MatListModule, FormsModule, ReactiveFormsModule],
+ imports: [MatLegacyListModule, FormsModule, ReactiveFormsModule],
declarations: [
SelectionListWithModel,
SelectionListWithFormControl,
@@ -1148,17 +1231,17 @@ describe('MDC-based MatSelectionList with forms', () => {
describe('and ngModel', () => {
let fixture: ComponentFixture;
let selectionListDebug: DebugElement;
- let listOptions: MatListOption[];
+ let listOptions: MatLegacyListOption[];
let ngModel: NgModel;
beforeEach(() => {
fixture = TestBed.createComponent(SelectionListWithModel);
fixture.detectChanges();
- selectionListDebug = fixture.debugElement.query(By.directive(MatSelectionList))!;
+ selectionListDebug = fixture.debugElement.query(By.directive(MatLegacySelectionList))!;
ngModel = selectionListDebug.injector.get(NgModel);
listOptions = fixture.debugElement
- .queryAll(By.directive(MatListOption))
+ .queryAll(By.directive(MatLegacyListOption))
.map(optionDebugEl => optionDebugEl.componentInstance);
});
@@ -1182,7 +1265,7 @@ describe('MDC-based MatSelectionList with forms', () => {
.withContext('Expected no options to be selected by default')
.toBe(0);
- dispatchMouseEvent(listOptions[0]._hostElement, 'click');
+ dispatchFakeEvent(listOptions[0]._getHostElement(), 'click');
fixture.detectChanges();
tick();
@@ -1226,7 +1309,7 @@ describe('MDC-based MatSelectionList with forms', () => {
.withContext('Expected the selection-list to be untouched by default.')
.toBe(false);
- dispatchFakeEvent(fixture.nativeElement.querySelector('.mat-mdc-list-option'), 'blur');
+ dispatchFakeEvent(fixture.nativeElement.querySelector('.mat-list-option'), 'blur');
fixture.detectChanges();
tick();
@@ -1241,10 +1324,10 @@ describe('MDC-based MatSelectionList with forms', () => {
fixture.detectChanges();
ngModel = fixture.debugElement
- .query(By.directive(MatSelectionList))!
+ .query(By.directive(MatLegacySelectionList))!
.injector.get(NgModel);
listOptions = fixture.debugElement
- .queryAll(By.directive(MatListOption))
+ .queryAll(By.directive(MatLegacyListOption))
.map(optionDebugEl => optionDebugEl.componentInstance);
// Flush the initial tick to ensure that every action from the ControlValueAccessor
@@ -1319,7 +1402,7 @@ describe('MDC-based MatSelectionList with forms', () => {
tick();
listOptions = fixture.debugElement
- .queryAll(By.directive(MatListOption))
+ .queryAll(By.directive(MatLegacyListOption))
.map(optionDebugEl => optionDebugEl.componentInstance);
fixture.componentInstance.selectedOptions = ['one', 'two', 'two'];
@@ -1329,6 +1412,16 @@ describe('MDC-based MatSelectionList with forms', () => {
expect(listOptions.map(option => option.selected)).toEqual([true, true, true, false, false]);
}));
+ it('should only be in the tab order if it has options', () => {
+ expect(selectionListDebug.componentInstance.options.length > 0).toBe(true);
+ expect(selectionListDebug.nativeElement.tabIndex).toBe(0);
+
+ fixture.componentInstance.options = [];
+ fixture.detectChanges();
+
+ expect(selectionListDebug.nativeElement.tabIndex).toBe(-1);
+ });
+
it('should dispatch one change event per change when updating a single-selection list', fakeAsync(() => {
fixture.destroy();
fixture = TestBed.createComponent(SelectionListWithModel);
@@ -1336,7 +1429,7 @@ describe('MDC-based MatSelectionList with forms', () => {
fixture.componentInstance.selectedOptions = ['opt3'];
fixture.detectChanges();
const options = fixture.debugElement
- .queryAll(By.directive(MatListOption))
+ .queryAll(By.directive(MatLegacyListOption))
.map(optionDebugEl => optionDebugEl.nativeElement);
expect(fixture.componentInstance.modelChangeSpy).not.toHaveBeenCalled();
@@ -1359,16 +1452,18 @@ describe('MDC-based MatSelectionList with forms', () => {
describe('and formControl', () => {
let fixture: ComponentFixture;
- let listOptions: MatListOption[];
- let selectionList: MatSelectionList;
+ let listOptions: MatLegacyListOption[];
+ let selectionList: MatLegacySelectionList;
beforeEach(() => {
fixture = TestBed.createComponent(SelectionListWithFormControl);
fixture.detectChanges();
- selectionList = fixture.debugElement.query(By.directive(MatSelectionList))!.componentInstance;
+ selectionList = fixture.debugElement.query(
+ By.directive(MatLegacySelectionList),
+ )!.componentInstance;
listOptions = fixture.debugElement
- .queryAll(By.directive(MatListOption))
+ .queryAll(By.directive(MatLegacyListOption))
.map(optionDebugEl => optionDebugEl.componentInstance);
});
@@ -1461,7 +1556,7 @@ describe('MDC-based MatSelectionList with forms', () => {
fixture.detectChanges();
listOptions = fixture.debugElement
- .queryAll(By.directive(MatListOption))
+ .queryAll(By.directive(MatLegacyListOption))
.map(optionDebugEl => optionDebugEl.componentInstance);
expect(listOptions[1].selected)
@@ -1496,7 +1591,7 @@ describe('MDC-based MatSelectionList with forms', () => {
fixture.detectChanges();
listOptions = fixture.debugElement
- .queryAll(By.directive(MatListOption))
+ .queryAll(By.directive(MatLegacyListOption))
.map(optionDebugEl => optionDebugEl.componentInstance);
expect(listOptions.length).toBe(4);
@@ -1508,7 +1603,7 @@ describe('MDC-based MatSelectionList with forms', () => {
it('should add preselected options to the model value', fakeAsync(() => {
const fixture = TestBed.createComponent(SelectionListWithPreselectedOption);
const listOptions = fixture.debugElement
- .queryAll(By.directive(MatListOption))
+ .queryAll(By.directive(MatLegacyListOption))
.map(optionDebugEl => optionDebugEl.componentInstance);
fixture.detectChanges();
@@ -1521,7 +1616,7 @@ describe('MDC-based MatSelectionList with forms', () => {
it('should handle preselected option both through the model and the view', fakeAsync(() => {
const fixture = TestBed.createComponent(SelectionListWithPreselectedOptionAndModel);
const listOptions = fixture.debugElement
- .queryAll(By.directive(MatListOption))
+ .queryAll(By.directive(MatLegacyListOption))
.map(optionDebugEl => optionDebugEl.componentInstance);
fixture.detectChanges();
@@ -1536,17 +1631,14 @@ describe('MDC-based MatSelectionList with forms', () => {
const fixture = TestBed.createComponent(SelectionListWithPreselectedFormControlOnPush);
fixture.detectChanges();
- const option = fixture.debugElement.queryAll(By.directive(MatListOption))[1];
- const checkbox = option.nativeElement.querySelector(
- '.mdc-checkbox__native-control',
- ) as HTMLInputElement;
+ const option = fixture.debugElement.queryAll(By.directive(MatLegacyListOption))[1];
fixture.detectChanges();
flush();
fixture.detectChanges();
expect(option.componentInstance.selected).toBe(true);
- expect(checkbox.checked).toBe(true);
+ expect(option.nativeElement.querySelector('.mat-pseudo-checkbox-checked')).toBeTruthy();
}));
});
@@ -1605,7 +1697,7 @@ class SelectionListWithListOptions {
selectionListColor: ThemePalette;
firstOptionColor: ThemePalette;
- onSelectionChange(_change: MatSelectionListChange) {}
+ onSelectionChange(_change: MatLegacySelectionListChange) {}
}
@Component({
@@ -1662,10 +1754,7 @@ class SelectionListWithDisabledOption {
@Component({
template: `
- Not selected - Item #1
- Pre-selected - Item #2
- Pre-selected - Item #3
- Not selected - Item #4
+ Item
`,
})
class SelectionListWithSelectedOption {}
@@ -1766,7 +1855,7 @@ class SelectionListWithPreselectedFormControlOnPush {
`,
})
class SelectionListWithCustomComparator {
- @ViewChildren(MatListOption) optionInstances: QueryList;
+ @ViewChildren(MatLegacyListOption) optionInstances: QueryList;
selectedOptions: {id: number; label: string}[] = [];
compareWith?: (o1: any, o2: any) => boolean;
options = [
@@ -1776,33 +1865,42 @@ class SelectionListWithCustomComparator {
];
}
+@Component({
+ template: `
+
+
+ One
+
+ `,
+})
+class SelectionListWithChangingOptionValue {
+ compareWith = (o1: any, o2: any) => o1 && o2 && o1.id === o2.id;
+ value = {id: 1};
+}
+
@Component({
template: `
-
- I
+
+ I
Inbox
`,
})
-class SelectionListWithAvatar {
- checkboxPosition: MatListOptionCheckboxPosition | undefined;
-}
+class SelectionListWithAvatar {}
@Component({
template: `
-
- I
+
+ I
Inbox
`,
})
-class SelectionListWithIcon {
- checkboxPosition: MatListOptionCheckboxPosition | undefined;
-}
+class SelectionListWithIcon {}
@Component({
// Note the blank `ngSwitch` which we need in order to hit the bug that we're testing.
@@ -1815,7 +1913,7 @@ class SelectionListWithIcon {
`,
})
class SelectionListWithIndirectChildOptions {
- @ViewChildren(MatListOption) optionInstances: QueryList;
+ @ViewChildren(MatLegacyListOption) optionInstances: QueryList;
}
@Component({
diff --git a/src/material/legacy-list/selection-list.ts b/src/material/legacy-list/selection-list.ts
new file mode 100644
index 000000000000..bbfd4d0f565c
--- /dev/null
+++ b/src/material/legacy-list/selection-list.ts
@@ -0,0 +1,747 @@
+/**
+ * @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 {FocusableOption, FocusKeyManager, FocusMonitor} from '@angular/cdk/a11y';
+import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
+import {SelectionModel} from '@angular/cdk/collections';
+import {A, DOWN_ARROW, ENTER, hasModifierKey, SPACE, UP_ARROW} from '@angular/cdk/keycodes';
+import {
+ AfterContentInit,
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ ContentChild,
+ ContentChildren,
+ ElementRef,
+ EventEmitter,
+ forwardRef,
+ Inject,
+ Input,
+ OnChanges,
+ OnDestroy,
+ OnInit,
+ Output,
+ QueryList,
+ SimpleChanges,
+ ViewChild,
+ ViewEncapsulation,
+} from '@angular/core';
+import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
+import {
+ CanDisableRipple,
+ MatLine,
+ mixinDisableRipple,
+ setLines,
+ ThemePalette,
+} from '@angular/material/core';
+import {Subject} from 'rxjs';
+import {startWith, takeUntil} from 'rxjs/operators';
+import {MatLegacyListAvatarCssMatStyler, MatLegacyListIconCssMatStyler} from './list';
+
+const _MatSelectionListBase = mixinDisableRipple(class {});
+const _MatListOptionBase = mixinDisableRipple(class {});
+
+/** @docs-private */
+export const MAT_SELECTION_LIST_VALUE_ACCESSOR: any = {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => MatLegacySelectionList),
+ multi: true,
+};
+
+/** Change event that is being fired whenever the selected state of an option changes. */
+export class MatLegacySelectionListChange {
+ constructor(
+ /** Reference to the selection list that emitted the event. */
+ public source: MatLegacySelectionList,
+ /** Reference to the options that have been changed. */
+ public options: MatLegacyListOption[],
+ ) {}
+}
+
+/**
+ * Type describing possible positions of a checkbox in a list option
+ * with respect to the list item's text.
+ */
+export type MatLegacyListOptionCheckboxPosition = 'before' | 'after';
+
+/**
+ * Component for list-options of selection-list. Each list-option can automatically
+ * generate a checkbox and can put current item into the selectionModel of selection-list
+ * if the current item is selected.
+ */
+@Component({
+ selector: 'mat-list-option',
+ exportAs: 'matListOption',
+ inputs: ['disableRipple'],
+ host: {
+ 'role': 'option',
+ 'class': 'mat-list-item mat-list-option mat-focus-indicator',
+ '(focus)': '_handleFocus()',
+ '(blur)': '_handleBlur()',
+ '(click)': '_handleClick()',
+ '[class.mat-list-item-disabled]': 'disabled',
+ '[class.mat-list-item-with-avatar]': '_avatar || _icon',
+ // Manually set the "primary" or "warn" class if the color has been explicitly
+ // set to "primary" or "warn". The pseudo checkbox picks up these classes for
+ // its theme.
+ '[class.mat-primary]': 'color === "primary"',
+ // Even though accent is the default, we need to set this class anyway, because the list might
+ // be placed inside a parent that has one of the other colors with a higher specificity.
+ '[class.mat-accent]': 'color !== "primary" && color !== "warn"',
+ '[class.mat-warn]': 'color === "warn"',
+ '[class.mat-list-single-selected-option]': 'selected && !selectionList.multiple',
+ '[attr.aria-selected]': 'selected',
+ '[attr.aria-disabled]': 'disabled',
+ '[attr.tabindex]': '-1',
+ },
+ templateUrl: 'list-option.html',
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class MatLegacyListOption
+ extends _MatListOptionBase
+ implements AfterContentInit, OnDestroy, OnInit, FocusableOption, CanDisableRipple
+{
+ private _selected = false;
+ private _disabled = false;
+ private _hasFocus = false;
+
+ @ContentChild(MatLegacyListAvatarCssMatStyler) _avatar: MatLegacyListAvatarCssMatStyler;
+ @ContentChild(MatLegacyListIconCssMatStyler) _icon: MatLegacyListIconCssMatStyler;
+ @ContentChildren(MatLine, {descendants: true}) _lines: QueryList;
+
+ /**
+ * Emits when the selected state of the option has changed.
+ * Use to facilitate two-data binding to the `selected` property.
+ * @docs-private
+ */
+ @Output()
+ readonly selectedChange: EventEmitter = new EventEmitter();
+
+ /** DOM element containing the item's text. */
+ @ViewChild('text') _text: ElementRef;
+
+ /** Whether the label should appear before or after the checkbox. Defaults to 'after' */
+ @Input() checkboxPosition: MatLegacyListOptionCheckboxPosition = 'after';
+
+ /** Theme color of the list option. This sets the color of the checkbox. */
+ @Input()
+ get color(): ThemePalette {
+ return this._color || this.selectionList.color;
+ }
+ set color(newValue: ThemePalette) {
+ this._color = newValue;
+ }
+ private _color: ThemePalette;
+
+ /**
+ * 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;
+ /** Value of the option */
+ @Input()
+ get value(): any {
+ return this._value;
+ }
+ set value(newValue: any) {
+ if (
+ this.selected &&
+ !this.selectionList.compareWith(newValue, this.value) &&
+ this._inputsInitialized
+ ) {
+ this.selected = false;
+ }
+
+ this._value = newValue;
+ }
+ private _value: any;
+
+ /** Whether the option is disabled. */
+ @Input()
+ get disabled(): boolean {
+ return this._disabled || (this.selectionList && this.selectionList.disabled);
+ }
+ set disabled(value: BooleanInput) {
+ const newValue = coerceBooleanProperty(value);
+
+ if (newValue !== this._disabled) {
+ this._disabled = newValue;
+ this._changeDetector.markForCheck();
+ }
+ }
+
+ /** Whether the option is selected. */
+ @Input()
+ get selected(): boolean {
+ return this.selectionList.selectedOptions.isSelected(this);
+ }
+ set selected(value: BooleanInput) {
+ const isSelected = coerceBooleanProperty(value);
+
+ if (isSelected !== this._selected) {
+ this._setSelected(isSelected);
+
+ if (isSelected || this.selectionList.multiple) {
+ this.selectionList._reportValueChange();
+ }
+ }
+ }
+
+ constructor(
+ private _element: ElementRef,
+ private _changeDetector: ChangeDetectorRef,
+ /** @docs-private */
+ @Inject(forwardRef(() => MatLegacySelectionList)) public selectionList: MatLegacySelectionList,
+ ) {
+ super();
+ }
+
+ ngOnInit() {
+ const list = this.selectionList;
+
+ if (list._value && list._value.some(value => list.compareWith(this._value, value))) {
+ this._setSelected(true);
+ }
+
+ const wasSelected = this._selected;
+
+ // List options that are selected at initialization can't be reported properly to the form
+ // control. This is because it takes some time until the selection-list knows about all
+ // available options. Also it can happen that the ControlValueAccessor has an initial value
+ // that should be used instead. Deferring the value change report to the next tick ensures
+ // that the form control value is not being overwritten.
+ Promise.resolve().then(() => {
+ if (this._selected || wasSelected) {
+ this.selected = true;
+ this._changeDetector.markForCheck();
+ }
+ });
+ this._inputsInitialized = true;
+ }
+
+ ngAfterContentInit() {
+ setLines(this._lines, this._element);
+ }
+
+ ngOnDestroy(): void {
+ if (this.selected) {
+ // We have to delay this until the next tick in order
+ // to avoid changed after checked errors.
+ Promise.resolve().then(() => {
+ this.selected = false;
+ });
+ }
+
+ const hadFocus = this._hasFocus;
+ const newActiveItem = this.selectionList._removeOptionFromList(this);
+
+ // Only move focus if this option was focused at the time it was destroyed.
+ if (hadFocus && newActiveItem) {
+ newActiveItem.focus();
+ }
+ }
+
+ /** Toggles the selection state of the option. */
+ toggle(): void {
+ this.selected = !this.selected;
+ }
+
+ /** Allows for programmatic focusing of the option. */
+ focus(): void {
+ this._element.nativeElement.focus();
+ }
+
+ /**
+ * Returns the list item's text label. Implemented as a part of the FocusKeyManager.
+ * @docs-private
+ */
+ getLabel() {
+ return this._text ? this._text.nativeElement.textContent || '' : '';
+ }
+
+ /** Whether this list item should show a ripple effect when clicked. */
+ _isRippleDisabled() {
+ return this.disabled || this.disableRipple || this.selectionList.disableRipple;
+ }
+
+ _handleClick() {
+ if (!this.disabled && (this.selectionList.multiple || !this.selected)) {
+ this.toggle();
+
+ // Emit a change event if the selected state of the option changed through user interaction.
+ this.selectionList._emitChangeEvent([this]);
+ }
+ }
+
+ _handleFocus() {
+ this.selectionList._setFocusedOption(this);
+ this._hasFocus = true;
+ }
+
+ _handleBlur() {
+ this.selectionList._onTouched();
+ this._hasFocus = false;
+ }
+
+ /** Retrieves the DOM element of the component host. */
+ _getHostElement(): HTMLElement {
+ return this._element.nativeElement;
+ }
+
+ /** Sets the selected state of the option. Returns whether the value has changed. */
+ _setSelected(selected: boolean): boolean {
+ if (selected === this._selected) {
+ return false;
+ }
+
+ this._selected = selected;
+
+ if (selected) {
+ this.selectionList.selectedOptions.select(this);
+ } else {
+ this.selectionList.selectedOptions.deselect(this);
+ }
+
+ this.selectedChange.emit(selected);
+ this._changeDetector.markForCheck();
+ return true;
+ }
+
+ /**
+ * Notifies Angular that the option needs to be checked in the next change detection run. Mainly
+ * used to trigger an update of the list option if the disabled state of the selection list
+ * changed.
+ */
+ _markForCheck() {
+ this._changeDetector.markForCheck();
+ }
+}
+
+/**
+ * Material Design list component where each item is a selectable option. Behaves as a listbox.
+ */
+@Component({
+ selector: 'mat-selection-list',
+ exportAs: 'matSelectionList',
+ inputs: ['disableRipple'],
+ host: {
+ 'role': 'listbox',
+ 'class': 'mat-selection-list mat-list-base',
+ '(keydown)': '_keydown($event)',
+ '[attr.aria-multiselectable]': 'multiple',
+ '[attr.aria-disabled]': 'disabled.toString()',
+ '[attr.tabindex]': '_tabIndex',
+ },
+ template: ' ',
+ styleUrls: ['list.css'],
+ encapsulation: ViewEncapsulation.None,
+ providers: [MAT_SELECTION_LIST_VALUE_ACCESSOR],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class MatLegacySelectionList
+ extends _MatSelectionListBase
+ implements CanDisableRipple, AfterContentInit, ControlValueAccessor, OnDestroy, OnChanges
+{
+ private _multiple = true;
+ private _contentInitialized = false;
+
+ /** The FocusKeyManager which handles focus. */
+ _keyManager: FocusKeyManager;
+
+ /** The option components contained within this selection-list. */
+ @ContentChildren(MatLegacyListOption, {descendants: true})
+ options: QueryList;
+
+ /** Emits a change event whenever the selected state of an option changes. */
+ @Output() readonly selectionChange: EventEmitter =
+ new EventEmitter();
+
+ /** Theme color of the selection list. This sets the checkbox color for all list options. */
+ @Input() color: ThemePalette = 'accent';
+
+ /**
+ * Function used for comparing an option against the selected value when determining which
+ * options should appear as selected. The first argument is the value of an options. The second
+ * one is a value from the selected value. A boolean must be returned.
+ */
+ @Input() compareWith: (o1: any, o2: any) => boolean = (a1, a2) => a1 === a2;
+
+ /** Whether the selection list is disabled. */
+ @Input()
+ get disabled(): boolean {
+ return this._disabled;
+ }
+ set disabled(value: BooleanInput) {
+ this._disabled = coerceBooleanProperty(value);
+
+ // The `MatSelectionList` and `MatListOption` are using the `OnPush` change detection
+ // strategy. Therefore the options will not check for any changes if the `MatSelectionList`
+ // changed its state. Since we know that a change to `disabled` property of the list affects
+ // the state of the options, we manually mark each option for check.
+ this._markOptionsForCheck();
+ }
+ private _disabled: boolean = false;
+
+ /** Whether selection is limited to one or multiple items (default multiple). */
+ @Input()
+ get multiple(): boolean {
+ return this._multiple;
+ }
+ set multiple(value: BooleanInput) {
+ const newValue = coerceBooleanProperty(value);
+
+ if (newValue !== this._multiple) {
+ if (this._contentInitialized && (typeof ngDevMode === 'undefined' || ngDevMode)) {
+ throw new Error(
+ 'Cannot change `multiple` mode of mat-selection-list after initialization.',
+ );
+ }
+
+ this._multiple = newValue;
+ this.selectedOptions = new SelectionModel(this._multiple, this.selectedOptions.selected);
+ }
+ }
+
+ /** The currently selected options. */
+ selectedOptions = new SelectionModel(this._multiple);
+
+ /** The tabindex of the selection list. */
+ _tabIndex = -1;
+
+ /** View to model callback that should be called whenever the selected options change. */
+ private _onChange: (value: any) => void = (_: any) => {};
+
+ /** Keeps track of the currently-selected value. */
+ _value: string[] | null;
+
+ /** Emits when the list has been destroyed. */
+ private readonly _destroyed = new Subject();
+
+ /** View to model callback that should be called if the list or its options lost focus. */
+ _onTouched: () => void = () => {};
+
+ /** Whether the list has been destroyed. */
+ private _isDestroyed: boolean;
+
+ constructor(
+ private _element: ElementRef,
+ private _changeDetector: ChangeDetectorRef,
+ private _focusMonitor: FocusMonitor,
+ ) {
+ super();
+ }
+
+ ngAfterContentInit(): void {
+ this._contentInitialized = true;
+
+ this._keyManager = new FocusKeyManager(this.options)
+ .withWrap()
+ .withTypeAhead()
+ .withHomeAndEnd()
+ // Allow disabled items to be focusable. For accessibility reasons, there must be a way for
+ // screen reader users, that allows reading the different options of the list.
+ .skipPredicate(() => false)
+ .withAllowedModifierKeys(['shiftKey']);
+
+ if (this._value) {
+ this._setOptionsFromValues(this._value);
+ }
+
+ // If the user attempts to tab out of the selection list, allow focus to escape.
+ this._keyManager.tabOut.pipe(takeUntil(this._destroyed)).subscribe(() => {
+ this._allowFocusEscape();
+ });
+
+ // When the number of options change, update the tabindex of the selection list.
+ this.options.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => {
+ this._updateTabIndex();
+ });
+
+ // Sync external changes to the model back to the options.
+ this.selectedOptions.changed.pipe(takeUntil(this._destroyed)).subscribe(event => {
+ if (event.added) {
+ for (let item of event.added) {
+ item.selected = true;
+ }
+ }
+
+ if (event.removed) {
+ for (let item of event.removed) {
+ item.selected = false;
+ }
+ }
+ });
+
+ this._focusMonitor
+ .monitor(this._element)
+ .pipe(takeUntil(this._destroyed))
+ .subscribe(origin => {
+ if (origin === 'keyboard' || origin === 'program') {
+ let toFocus = 0;
+ for (let i = 0; i < this.options.length; i++) {
+ if (this.options.get(i)?.selected) {
+ toFocus = i;
+ break;
+ }
+ }
+ this._keyManager.setActiveItem(toFocus);
+ }
+ });
+ }
+
+ ngOnChanges(changes: SimpleChanges) {
+ const disableRippleChanges = changes['disableRipple'];
+ const colorChanges = changes['color'];
+
+ if (
+ (disableRippleChanges && !disableRippleChanges.firstChange) ||
+ (colorChanges && !colorChanges.firstChange)
+ ) {
+ this._markOptionsForCheck();
+ }
+ }
+
+ ngOnDestroy() {
+ this._focusMonitor.stopMonitoring(this._element);
+ this._destroyed.next();
+ this._destroyed.complete();
+ this._isDestroyed = true;
+ }
+
+ /** Focuses the selection list. */
+ focus(options?: FocusOptions) {
+ this._element.nativeElement.focus(options);
+ }
+
+ /** Selects all of the options. Returns the options that changed as a result. */
+ selectAll(): MatLegacyListOption[] {
+ return this._setAllOptionsSelected(true);
+ }
+
+ /** Deselects all of the options. Returns the options that changed as a result. */
+ deselectAll(): MatLegacyListOption[] {
+ return this._setAllOptionsSelected(false);
+ }
+
+ /** Sets the focused option of the selection-list. */
+ _setFocusedOption(option: MatLegacyListOption) {
+ this._keyManager.updateActiveItem(option);
+ }
+
+ /**
+ * Removes an option from the selection list and updates the active item.
+ * @returns Currently-active item.
+ */
+ _removeOptionFromList(option: MatLegacyListOption): MatLegacyListOption | null {
+ const optionIndex = this._getOptionIndex(option);
+
+ if (optionIndex > -1 && this._keyManager.activeItemIndex === optionIndex) {
+ // Check whether the option is the last item
+ if (optionIndex > 0) {
+ this._keyManager.updateActiveItem(optionIndex - 1);
+ } else if (optionIndex === 0 && this.options.length > 1) {
+ this._keyManager.updateActiveItem(Math.min(optionIndex + 1, this.options.length - 1));
+ }
+ }
+
+ return this._keyManager.activeItem;
+ }
+
+ /** Passes relevant key presses to our key manager. */
+ _keydown(event: KeyboardEvent) {
+ const keyCode = event.keyCode;
+ const manager = this._keyManager;
+ const previousFocusIndex = manager.activeItemIndex;
+ const hasModifier = hasModifierKey(event);
+
+ switch (keyCode) {
+ case SPACE:
+ case ENTER:
+ if (!hasModifier && !manager.isTyping()) {
+ this._toggleFocusedOption();
+ // Always prevent space from scrolling the page since the list has focus
+ event.preventDefault();
+ }
+ break;
+ default:
+ // The "A" key gets special treatment, because it's used for the "select all" functionality.
+ if (
+ keyCode === A &&
+ this.multiple &&
+ hasModifierKey(event, 'ctrlKey') &&
+ !manager.isTyping()
+ ) {
+ const shouldSelect = this.options.some(option => !option.disabled && !option.selected);
+ this._setAllOptionsSelected(shouldSelect, true, true);
+ event.preventDefault();
+ } else {
+ manager.onKeydown(event);
+ }
+ }
+
+ if (
+ this.multiple &&
+ (keyCode === UP_ARROW || keyCode === DOWN_ARROW) &&
+ event.shiftKey &&
+ manager.activeItemIndex !== previousFocusIndex
+ ) {
+ this._toggleFocusedOption();
+ }
+ }
+
+ /** Reports a value change to the ControlValueAccessor */
+ _reportValueChange() {
+ // Stop reporting value changes after the list has been destroyed. This avoids
+ // cases where the list might wrongly reset its value once it is removed, but
+ // the form control is still live.
+ if (this.options && !this._isDestroyed) {
+ const value = this._getSelectedOptionValues();
+ this._onChange(value);
+ this._value = value;
+ }
+ }
+
+ /** Emits a change event if the selected state of an option changed. */
+ _emitChangeEvent(options: MatLegacyListOption[]) {
+ this.selectionChange.emit(new MatLegacySelectionListChange(this, options));
+ }
+
+ /** Implemented as part of ControlValueAccessor. */
+ writeValue(values: string[]): void {
+ this._value = values;
+
+ if (this.options) {
+ this._setOptionsFromValues(values || []);
+ }
+ }
+
+ /** Implemented as a part of ControlValueAccessor. */
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ }
+
+ /** Implemented as part of ControlValueAccessor. */
+ registerOnChange(fn: (value: any) => void): void {
+ this._onChange = fn;
+ }
+
+ /** Implemented as part of ControlValueAccessor. */
+ registerOnTouched(fn: () => void): void {
+ this._onTouched = fn;
+ }
+
+ /** Sets the selected options based on the specified values. */
+ private _setOptionsFromValues(values: string[]) {
+ this.options.forEach(option => option._setSelected(false));
+
+ values.forEach(value => {
+ const correspondingOption = this.options.find(option => {
+ // Skip options that are already in the model. This allows us to handle cases
+ // where the same primitive value is selected multiple times.
+ return option.selected ? false : this.compareWith(option.value, value);
+ });
+
+ if (correspondingOption) {
+ correspondingOption._setSelected(true);
+ }
+ });
+ }
+
+ /** Returns the values of the selected options. */
+ private _getSelectedOptionValues(): string[] {
+ return this.options.filter(option => option.selected).map(option => option.value);
+ }
+
+ /** Toggles the state of the currently focused option if enabled. */
+ private _toggleFocusedOption(): void {
+ let focusedIndex = this._keyManager.activeItemIndex;
+
+ if (focusedIndex != null && this._isValidIndex(focusedIndex)) {
+ let focusedOption: MatLegacyListOption = this.options.toArray()[focusedIndex];
+
+ if (focusedOption && !focusedOption.disabled && (this._multiple || !focusedOption.selected)) {
+ focusedOption.toggle();
+
+ // Emit a change event because the focused option changed its state through user
+ // interaction.
+ this._emitChangeEvent([focusedOption]);
+ }
+ }
+ }
+
+ /**
+ * Sets the selected state on all of the options
+ * and emits an event if anything changed.
+ */
+ private _setAllOptionsSelected(
+ isSelected: boolean,
+ skipDisabled?: boolean,
+ isUserInput?: boolean,
+ ): MatLegacyListOption[] {
+ // Keep track of whether anything changed, because we only want to
+ // emit the changed event when something actually changed.
+ const changedOptions: MatLegacyListOption[] = [];
+
+ this.options.forEach(option => {
+ if ((!skipDisabled || !option.disabled) && option._setSelected(isSelected)) {
+ changedOptions.push(option);
+ }
+ });
+
+ if (changedOptions.length) {
+ this._reportValueChange();
+
+ if (isUserInput) {
+ this._emitChangeEvent(changedOptions);
+ }
+ }
+
+ return changedOptions;
+ }
+
+ /**
+ * Utility to ensure all indexes are valid.
+ * @param index The index to be checked.
+ * @returns True if the index is valid for our list of options.
+ */
+ private _isValidIndex(index: number): boolean {
+ return index >= 0 && index < this.options.length;
+ }
+
+ /** Returns the index of the specified list option. */
+ private _getOptionIndex(option: MatLegacyListOption): number {
+ return this.options.toArray().indexOf(option);
+ }
+
+ /** Marks all the options to be checked in the next change detection run. */
+ private _markOptionsForCheck() {
+ if (this.options) {
+ this.options.forEach(option => option._markForCheck());
+ }
+ }
+
+ /**
+ * Removes the tabindex from the selection list and resets it back afterwards, allowing the user
+ * to tab out of it. This prevents the list from capturing focus and redirecting it back within
+ * the list, creating a focus trap if it user tries to tab away.
+ */
+ private _allowFocusEscape() {
+ this._tabIndex = -1;
+
+ setTimeout(() => {
+ this._tabIndex = 0;
+ this._changeDetector.markForCheck();
+ });
+ }
+
+ /** Updates the tabindex based upon if the selection list is empty. */
+ private _updateTabIndex(): void {
+ this._tabIndex = this.options.length === 0 ? -1 : 0;
+ }
+}
diff --git a/src/material-experimental/mdc-list/testing/BUILD.bazel b/src/material/legacy-list/testing/BUILD.bazel
similarity index 68%
rename from src/material-experimental/mdc-list/testing/BUILD.bazel
rename to src/material/legacy-list/testing/BUILD.bazel
index 9d1b04af2b94..4078e5798c90 100644
--- a/src/material-experimental/mdc-list/testing/BUILD.bazel
+++ b/src/material/legacy-list/testing/BUILD.bazel
@@ -11,8 +11,8 @@ ts_library(
deps = [
"//src/cdk/coercion",
"//src/cdk/testing",
- "//src/material-experimental/mdc-list",
"//src/material/divider/testing",
+ "//src/material/legacy-list",
],
)
@@ -21,6 +21,19 @@ filegroup(
srcs = glob(["**/*.ts"]),
)
+ng_test_library(
+ name = "harness_tests_lib",
+ srcs = ["shared.spec.ts"],
+ deps = [
+ ":testing",
+ "//src/cdk/testing",
+ "//src/cdk/testing/testbed",
+ "//src/material/divider/testing",
+ "//src/material/legacy-list",
+ "@npm//@angular/platform-browser",
+ ],
+)
+
ng_test_library(
name = "unit_tests_lib",
srcs = glob(
@@ -28,17 +41,14 @@ ng_test_library(
exclude = ["shared.spec.ts"],
),
deps = [
+ ":harness_tests_lib",
":testing",
- "//src/cdk/testing",
- "//src/cdk/testing/testbed",
- "//src/material-experimental/mdc-list",
"//src/material/divider/testing",
+ "//src/material/legacy-list",
],
)
ng_web_test_suite(
name = "unit_tests",
- deps = [
- ":unit_tests_lib",
- ],
+ deps = [":unit_tests_lib"],
)
diff --git a/src/material-experimental/mdc-list/testing/action-list-harness.ts b/src/material/legacy-list/testing/action-list-harness.ts
similarity index 56%
rename from src/material-experimental/mdc-list/testing/action-list-harness.ts
rename to src/material/legacy-list/testing/action-list-harness.ts
index 92f65c512647..8a7a2517a1bc 100644
--- a/src/material-experimental/mdc-list/testing/action-list-harness.ts
+++ b/src/material/legacy-list/testing/action-list-harness.ts
@@ -6,52 +6,50 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {ComponentHarnessConstructor, HarnessPredicate} from '@angular/cdk/testing';
-import {MatListHarnessBase} from './list-harness-base';
+import {HarnessPredicate} from '@angular/cdk/testing';
+import {MatLegacyListHarnessBase} from './list-harness-base';
import {ActionListHarnessFilters, ActionListItemHarnessFilters} from './list-harness-filters';
-import {getListItemPredicate, MatListItemHarnessBase} from './list-item-harness-base';
+import {getListItemPredicate, MatLegacyListItemHarnessBase} from './list-item-harness-base';
-/** Harness for interacting with a MDC-based action-list in tests. */
-export class MatActionListHarness extends MatListHarnessBase<
- typeof MatActionListItemHarness,
- MatActionListItemHarness,
+/** Harness for interacting with a standard mat-action-list in tests. */
+export class MatLegacyActionListHarness extends MatLegacyListHarnessBase<
+ typeof MatLegacyActionListItemHarness,
+ MatLegacyActionListItemHarness,
ActionListItemHarnessFilters
> {
/** The selector for the host element of a `MatActionList` instance. */
- static hostSelector = '.mat-mdc-action-list';
+ static hostSelector = 'mat-action-list.mat-list';
/**
- * Gets a `HarnessPredicate` that can be used to search for an action list with specific
- * attributes.
+ * Gets a `HarnessPredicate` that can be used to search for a `MatActionListHarness` that meets
+ * certain criteria.
* @param options Options for filtering which action list instances are considered a match.
* @return a `HarnessPredicate` configured with the given options.
*/
- static with(
- this: ComponentHarnessConstructor,
+ static with(
options: ActionListHarnessFilters = {},
- ): HarnessPredicate {
- return new HarnessPredicate(this, options);
+ ): HarnessPredicate {
+ return new HarnessPredicate(MatLegacyActionListHarness, options);
}
- override _itemHarness = MatActionListItemHarness;
+ override _itemHarness = MatLegacyActionListItemHarness;
}
/** Harness for interacting with an action list item. */
-export class MatActionListItemHarness extends MatListItemHarnessBase {
+export class MatLegacyActionListItemHarness extends MatLegacyListItemHarnessBase {
/** The selector for the host element of a `MatListItem` instance. */
- static hostSelector = `${MatActionListHarness.hostSelector} .mat-mdc-list-item`;
+ static hostSelector = `${MatLegacyActionListHarness.hostSelector} .mat-list-item`;
/**
- * Gets a `HarnessPredicate` that can be used to search for a list item with specific
- * attributes.
+ * Gets a `HarnessPredicate` that can be used to search for a `MatActionListItemHarness` that
+ * meets certain criteria.
* @param options Options for filtering which action list item instances are considered a match.
* @return a `HarnessPredicate` configured with the given options.
*/
- static with(
- this: ComponentHarnessConstructor,
+ static with(
options: ActionListItemHarnessFilters = {},
- ): HarnessPredicate {
- return getListItemPredicate(this, options);
+ ): HarnessPredicate {
+ return getListItemPredicate(MatLegacyActionListItemHarness, options);
}
/** Clicks on the action list item. */
diff --git a/src/material-experimental/mdc-list/testing/index.ts b/src/material/legacy-list/testing/index.ts
similarity index 100%
rename from src/material-experimental/mdc-list/testing/index.ts
rename to src/material/legacy-list/testing/index.ts
diff --git a/src/material-experimental/mdc-list/testing/list-harness-base.ts b/src/material/legacy-list/testing/list-harness-base.ts
similarity index 91%
rename from src/material-experimental/mdc-list/testing/list-harness-base.ts
rename to src/material/legacy-list/testing/list-harness-base.ts
index f1e22de1302a..2a8f41266bbf 100644
--- a/src/material-experimental/mdc-list/testing/list-harness-base.ts
+++ b/src/material/legacy-list/testing/list-harness-base.ts
@@ -14,7 +14,7 @@ import {
} from '@angular/cdk/testing';
import {DividerHarnessFilters, MatDividerHarness} from '@angular/material/divider/testing';
import {BaseListItemHarnessFilters, SubheaderHarnessFilters} from './list-harness-filters';
-import {MatSubheaderHarness} from './list-item-harness-base';
+import {MatLegacySubheaderHarness} from './list-item-harness-base';
/** Represents a section of a list falling under a specific header. */
export interface ListSection {
@@ -32,7 +32,7 @@ export interface ListSection {
* @template F The filter type used filter list item harness of type `C`.
* @docs-private
*/
-export abstract class MatListHarnessBase<
+export abstract class MatLegacyListHarnessBase<
T extends ComponentHarnessConstructor & {with: (options?: F) => HarnessPredicate},
C extends ComponentHarness,
F extends BaseListItemHarnessFilters,
@@ -62,9 +62,8 @@ export abstract class MatListHarnessBase<
item: filters,
divider: false,
});
-
for (const itemOrSubheader of itemsAndSubheaders) {
- if (itemOrSubheader instanceof MatSubheaderHarness) {
+ if (itemOrSubheader instanceof MatLegacySubheaderHarness) {
if (currentSection.heading !== undefined || currentSection.items.length) {
listSections.push(currentSection);
}
@@ -133,7 +132,7 @@ export abstract class MatListHarnessBase<
item: false;
subheader?: SubheaderHarnessFilters | false;
divider: false;
- }): Promise;
+ }): Promise;
getItemsWithSubheadersAndDividers(filters: {
item: false;
subheader: false;
@@ -143,7 +142,7 @@ export abstract class MatListHarnessBase<
item?: F | false;
subheader?: SubheaderHarnessFilters | false;
divider: false;
- }): Promise<(C | MatSubheaderHarness)[]>;
+ }): Promise<(C | MatLegacySubheaderHarness)[]>;
getItemsWithSubheadersAndDividers(filters: {
item?: F | false;
subheader: false;
@@ -153,25 +152,25 @@ export abstract class MatListHarnessBase<
item: false;
subheader?: false | SubheaderHarnessFilters;
divider?: false | DividerHarnessFilters;
- }): Promise<(MatSubheaderHarness | MatDividerHarness)[]>;
+ }): Promise<(MatLegacySubheaderHarness | MatDividerHarness)[]>;
getItemsWithSubheadersAndDividers(filters?: {
item?: F | false;
subheader?: SubheaderHarnessFilters | false;
divider?: DividerHarnessFilters | false;
- }): Promise<(C | MatSubheaderHarness | MatDividerHarness)[]>;
+ }): Promise<(C | MatLegacySubheaderHarness | MatDividerHarness)[]>;
async getItemsWithSubheadersAndDividers(
filters: {
item?: F | false;
subheader?: SubheaderHarnessFilters | false;
divider?: DividerHarnessFilters | false;
} = {},
- ): Promise<(C | MatSubheaderHarness | MatDividerHarness)[]> {
+ ): Promise<(C | MatLegacySubheaderHarness | MatDividerHarness)[]> {
const query = [];
if (filters.item !== false) {
query.push(this._itemHarness.with(filters.item || ({} as F)));
}
if (filters.subheader !== false) {
- query.push(MatSubheaderHarness.with(filters.subheader));
+ query.push(MatLegacySubheaderHarness.with(filters.subheader));
}
if (filters.divider !== false) {
query.push(MatDividerHarness.with(filters.divider));
diff --git a/src/material-experimental/mdc-list/testing/list-harness-filters.ts b/src/material/legacy-list/testing/list-harness-filters.ts
similarity index 81%
rename from src/material-experimental/mdc-list/testing/list-harness-filters.ts
rename to src/material/legacy-list/testing/list-harness-filters.ts
index e3d32bc08a88..bd2111e319ae 100644
--- a/src/material-experimental/mdc-list/testing/list-harness-filters.ts
+++ b/src/material/legacy-list/testing/list-harness-filters.ts
@@ -17,14 +17,6 @@ export interface NavListHarnessFilters extends BaseHarnessFilters {}
export interface SelectionListHarnessFilters extends BaseHarnessFilters {}
export interface BaseListItemHarnessFilters extends BaseHarnessFilters {
- title?: string | RegExp;
- secondaryText?: string | RegExp | null;
- tertiaryText?: string | RegExp | null;
- fullText?: string | RegExp;
- /**
- * @deprecated Use the `fullText` filter instead.
- * @breaking-change 16.0.0
- */
text?: string | RegExp;
}
@@ -34,7 +26,6 @@ export interface ActionListItemHarnessFilters extends BaseListItemHarnessFilters
export interface NavListItemHarnessFilters extends BaseListItemHarnessFilters {
href?: string | RegExp | null;
- activated?: boolean;
}
export interface ListOptionHarnessFilters extends BaseListItemHarnessFilters {
diff --git a/src/material/legacy-list/testing/list-harness.spec.ts b/src/material/legacy-list/testing/list-harness.spec.ts
new file mode 100644
index 000000000000..7331b6e4c8bf
--- /dev/null
+++ b/src/material/legacy-list/testing/list-harness.spec.ts
@@ -0,0 +1,26 @@
+import {MatDividerHarness} from '@angular/material/divider/testing';
+import {MatLegacyListModule} from '@angular/material/legacy-list';
+import {MatLegacyActionListHarness} from './action-list-harness';
+import {MatLegacyListHarness} from './list-harness';
+import {MatLegacyNavListHarness} from './nav-list-harness';
+import {MatLegacySelectionListHarness} from './selection-list-harness';
+import {
+ MatLegacyListItemHarnessBase,
+ MatLegacyListItemSection,
+ MatLegacySubheaderHarness,
+} from './list-item-harness-base';
+import {runHarnessTests} from './shared.spec';
+
+describe('Non-MDC-based list harnesses', () => {
+ runHarnessTests(
+ MatLegacyListModule,
+ MatLegacyListHarness,
+ MatLegacyActionListHarness,
+ MatLegacyNavListHarness,
+ MatLegacySelectionListHarness,
+ MatLegacyListItemHarnessBase,
+ MatLegacySubheaderHarness,
+ MatDividerHarness,
+ {content: MatLegacyListItemSection.CONTENT},
+ );
+});
diff --git a/src/material/legacy-list/testing/list-harness.ts b/src/material/legacy-list/testing/list-harness.ts
new file mode 100644
index 000000000000..7d415a92071f
--- /dev/null
+++ b/src/material/legacy-list/testing/list-harness.ts
@@ -0,0 +1,50 @@
+/**
+ * @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 {HarnessPredicate} from '@angular/cdk/testing';
+import {MatLegacyListHarnessBase} from './list-harness-base';
+import {ListHarnessFilters, ListItemHarnessFilters} from './list-harness-filters';
+import {getListItemPredicate, MatLegacyListItemHarnessBase} from './list-item-harness-base';
+
+/** Harness for interacting with a standard mat-list in tests. */
+export class MatLegacyListHarness extends MatLegacyListHarnessBase<
+ typeof MatLegacyListItemHarness,
+ MatLegacyListItemHarness,
+ ListItemHarnessFilters
+> {
+ /** The selector for the host element of a `MatList` instance. */
+ static hostSelector = '.mat-list:not(mat-action-list)';
+
+ /**
+ * Gets a `HarnessPredicate` that can be used to search for a `MatListHarness` that meets certain
+ * criteria.
+ * @param options Options for filtering which list instances are considered a match.
+ * @return a `HarnessPredicate` configured with the given options.
+ */
+ static with(options: ListHarnessFilters = {}): HarnessPredicate {
+ return new HarnessPredicate(MatLegacyListHarness, options);
+ }
+
+ override _itemHarness = MatLegacyListItemHarness;
+}
+
+/** Harness for interacting with a list item. */
+export class MatLegacyListItemHarness extends MatLegacyListItemHarnessBase {
+ /** The selector for the host element of a `MatListItem` instance. */
+ static hostSelector = `${MatLegacyListHarness.hostSelector} .mat-list-item`;
+
+ /**
+ * Gets a `HarnessPredicate` that can be used to search for a `MatListItemHarness` that meets
+ * certain criteria.
+ * @param options Options for filtering which list item instances are considered a match.
+ * @return a `HarnessPredicate` configured with the given options.
+ */
+ static with(options: ListItemHarnessFilters = {}): HarnessPredicate {
+ return getListItemPredicate(MatLegacyListItemHarness, options);
+ }
+}
diff --git a/src/material/legacy-list/testing/list-item-harness-base.ts b/src/material/legacy-list/testing/list-item-harness-base.ts
new file mode 100644
index 000000000000..8c973b748491
--- /dev/null
+++ b/src/material/legacy-list/testing/list-item-harness-base.ts
@@ -0,0 +1,98 @@
+/**
+ * @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 {
+ ComponentHarness,
+ ComponentHarnessConstructor,
+ HarnessPredicate,
+ ContentContainerComponentHarness,
+ parallel,
+} from '@angular/cdk/testing';
+import {BaseListItemHarnessFilters, SubheaderHarnessFilters} from './list-harness-filters';
+
+const iconSelector = '.mat-list-icon';
+const avatarSelector = '.mat-list-avatar';
+
+/**
+ * Gets a `HarnessPredicate` that applies the given `BaseListItemHarnessFilters` to the given
+ * list item harness.
+ * @template H The type of list item harness to create a predicate for.
+ * @param harnessType A constructor for a list item harness.
+ * @param options An instance of `BaseListItemHarnessFilters` to apply.
+ * @return A `HarnessPredicate` for the given harness type with the given options applied.
+ */
+export function getListItemPredicate(
+ harnessType: ComponentHarnessConstructor,
+ options: BaseListItemHarnessFilters,
+): HarnessPredicate {
+ return new HarnessPredicate(harnessType, options).addOption(
+ 'text',
+ options.text,
+ (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text),
+ );
+}
+
+/** Harness for interacting with a list subheader. */
+export class MatLegacySubheaderHarness extends ComponentHarness {
+ static hostSelector = '.mat-subheader';
+
+ static with(options: SubheaderHarnessFilters = {}): HarnessPredicate {
+ return new HarnessPredicate(MatLegacySubheaderHarness, options).addOption(
+ 'text',
+ options.text,
+ (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text),
+ );
+ }
+
+ /** Gets the full text content of the list item (including text from any font icons). */
+ async getText(): Promise {
+ return (await this.host()).text();
+ }
+}
+
+/** Selectors for the various list item sections that may contain user content. */
+export const enum MatLegacyListItemSection {
+ CONTENT = '.mat-list-item-content',
+ // TODO(mmalerba): consider adding sections for leading/trailing icons.
+}
+
+/**
+ * Shared behavior among the harnesses for the various `MatListItem` flavors.
+ * @docs-private
+ */
+export abstract class MatLegacyListItemHarnessBase extends ContentContainerComponentHarness {
+ private _lines = this.locatorForAll('.mat-line');
+ private _avatar = this.locatorForOptional(avatarSelector);
+ private _icon = this.locatorForOptional(iconSelector);
+
+ /** Gets the full text content of the list item. */
+ async getText(): Promise {
+ return (await this.host()).text({exclude: `${iconSelector}, ${avatarSelector}`});
+ }
+
+ /** Gets the lines of text (`mat-line` elements) in this nav list item. */
+ async getLinesText(): Promise {
+ const lines = await this._lines();
+ return parallel(() => lines.map(l => l.text()));
+ }
+
+ /** Whether this list item has an avatar. */
+ async hasAvatar(): Promise {
+ return !!(await this._avatar());
+ }
+
+ /** Whether this list item has an icon. */
+ async hasIcon(): Promise {
+ return !!(await this._icon());
+ }
+
+ /** Whether this list option is disabled. */
+ async isDisabled(): Promise {
+ return (await this.host()).hasClass('mat-list-item-disabled');
+ }
+}
diff --git a/src/material/legacy-list/testing/nav-list-harness.ts b/src/material/legacy-list/testing/nav-list-harness.ts
new file mode 100644
index 000000000000..40d5aa6d8628
--- /dev/null
+++ b/src/material/legacy-list/testing/nav-list-harness.ts
@@ -0,0 +1,81 @@
+/**
+ * @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 {HarnessPredicate} from '@angular/cdk/testing';
+import {MatLegacyListHarnessBase} from './list-harness-base';
+import {NavListHarnessFilters, NavListItemHarnessFilters} from './list-harness-filters';
+import {getListItemPredicate, MatLegacyListItemHarnessBase} from './list-item-harness-base';
+
+/** Harness for interacting with a standard mat-nav-list in tests. */
+export class MatLegacyNavListHarness extends MatLegacyListHarnessBase<
+ typeof MatLegacyNavListItemHarness,
+ MatLegacyNavListItemHarness,
+ NavListItemHarnessFilters
+> {
+ /** The selector for the host element of a `MatNavList` instance. */
+ static hostSelector = '.mat-nav-list';
+
+ /**
+ * Gets a `HarnessPredicate` that can be used to search for a `MatNavListHarness` that meets
+ * certain criteria.
+ * @param options Options for filtering which nav list instances are considered a match.
+ * @return a `HarnessPredicate` configured with the given options.
+ */
+ static with(options: NavListHarnessFilters = {}): HarnessPredicate {
+ return new HarnessPredicate(MatLegacyNavListHarness, options);
+ }
+
+ override _itemHarness = MatLegacyNavListItemHarness;
+}
+
+/** Harness for interacting with a nav list item. */
+export class MatLegacyNavListItemHarness extends MatLegacyListItemHarnessBase {
+ /** The selector for the host element of a `MatListItem` instance. */
+ static hostSelector = `${MatLegacyNavListHarness.hostSelector} .mat-list-item`;
+
+ /**
+ * Gets a `HarnessPredicate` that can be used to search for a `MatNavListItemHarness` that
+ * meets certain criteria.
+ * @param options Options for filtering which nav list item instances are considered a match.
+ * @return a `HarnessPredicate` configured with the given options.
+ */
+ static with(
+ options: NavListItemHarnessFilters = {},
+ ): HarnessPredicate {
+ return getListItemPredicate(MatLegacyNavListItemHarness, options).addOption(
+ 'href',
+ options.href,
+ async (harness, href) => HarnessPredicate.stringMatches(harness.getHref(), href),
+ );
+ }
+
+ /** Gets the href for this nav list item. */
+ async getHref(): Promise {
+ return (await this.host()).getAttribute('href');
+ }
+
+ /** Clicks on the nav list item. */
+ async click(): Promise {
+ return (await this.host()).click();
+ }
+
+ /** Focuses the nav list item. */
+ async focus(): Promise {
+ return (await this.host()).focus();
+ }
+
+ /** Blurs the nav list item. */
+ async blur(): Promise {
+ return (await this.host()).blur();
+ }
+
+ /** Whether the nav list item is focused. */
+ async isFocused(): Promise {
+ return (await this.host()).isFocused();
+ }
+}
diff --git a/src/material-experimental/mdc-list/testing/public-api.ts b/src/material/legacy-list/testing/public-api.ts
similarity index 79%
rename from src/material-experimental/mdc-list/testing/public-api.ts
rename to src/material/legacy-list/testing/public-api.ts
index fcf12ca761e3..10dcba2f6ef3 100644
--- a/src/material-experimental/mdc-list/testing/public-api.ts
+++ b/src/material/legacy-list/testing/public-api.ts
@@ -11,4 +11,4 @@ export * from './list-harness';
export * from './list-harness-filters';
export * from './nav-list-harness';
export * from './selection-list-harness';
-export {MatListItemSection, MatSubheaderHarness, MatListItemType} from './list-item-harness-base';
+export {MatLegacyListItemSection} from './list-item-harness-base';
diff --git a/src/material-experimental/mdc-list/testing/selection-list-harness.ts b/src/material/legacy-list/testing/selection-list-harness.ts
similarity index 64%
rename from src/material-experimental/mdc-list/testing/selection-list-harness.ts
rename to src/material/legacy-list/testing/selection-list-harness.ts
index 77055c05b02c..0304ae46c501 100644
--- a/src/material-experimental/mdc-list/testing/selection-list-harness.ts
+++ b/src/material/legacy-list/testing/selection-list-harness.ts
@@ -6,39 +6,38 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {ComponentHarnessConstructor, HarnessPredicate, parallel} from '@angular/cdk/testing';
-import {MatListOptionCheckboxPosition} from '@angular/material-experimental/mdc-list';
-import {MatListHarnessBase} from './list-harness-base';
+import {HarnessPredicate, parallel} from '@angular/cdk/testing';
+import {MatLegacyListOptionCheckboxPosition} from '@angular/material/legacy-list';
+import {MatLegacyListHarnessBase} from './list-harness-base';
import {
ListItemHarnessFilters,
ListOptionHarnessFilters,
SelectionListHarnessFilters,
} from './list-harness-filters';
-import {getListItemPredicate, MatListItemHarnessBase} from './list-item-harness-base';
+import {getListItemPredicate, MatLegacyListItemHarnessBase} from './list-item-harness-base';
-/** Harness for interacting with a MDC_based selection-list in tests. */
-export class MatSelectionListHarness extends MatListHarnessBase<
- typeof MatListOptionHarness,
- MatListOptionHarness,
+/** Harness for interacting with a standard mat-selection-list in tests. */
+export class MatLegacySelectionListHarness extends MatLegacyListHarnessBase<
+ typeof MatLegacyListOptionHarness,
+ MatLegacyListOptionHarness,
ListOptionHarnessFilters
> {
/** The selector for the host element of a `MatSelectionList` instance. */
- static hostSelector = '.mat-mdc-selection-list';
+ static hostSelector = '.mat-selection-list';
/**
- * Gets a `HarnessPredicate` that can be used to search for a selection list with specific
- * attributes.
+ * Gets a `HarnessPredicate` that can be used to search for a `MatSelectionListHarness` that meets
+ * certain criteria.
* @param options Options for filtering which selection list instances are considered a match.
* @return a `HarnessPredicate` configured with the given options.
*/
- static with(
- this: ComponentHarnessConstructor,
+ static with(
options: SelectionListHarnessFilters = {},
- ): HarnessPredicate {
- return new HarnessPredicate(this, options);
+ ): HarnessPredicate {
+ return new HarnessPredicate(MatLegacySelectionListHarness, options);
}
- override _itemHarness = MatListOptionHarness;
+ override _itemHarness = MatLegacyListOptionHarness;
/** Whether the selection list is disabled. */
async isDisabled(): Promise {
@@ -64,44 +63,47 @@ export class MatSelectionListHarness extends MatListHarnessBase<
}
/** Gets all items matching the given list of filters. */
- private async _getItems(filters: ListOptionHarnessFilters[]): Promise {
+ private async _getItems(
+ filters: ListOptionHarnessFilters[],
+ ): Promise {
if (!filters.length) {
return this.getItems();
}
- const matches = await parallel(() =>
- filters.map(filter => this.locatorForAll(MatListOptionHarness.with(filter))()),
- );
+ const matches = await parallel(() => {
+ return filters.map(filter => this.locatorForAll(MatLegacyListOptionHarness.with(filter))());
+ });
return matches.reduce((result, current) => [...result, ...current], []);
}
}
-/** Harness for interacting with a MDC-based list option. */
-export class MatListOptionHarness extends MatListItemHarnessBase {
+/** Harness for interacting with a list option. */
+export class MatLegacyListOptionHarness extends MatLegacyListItemHarnessBase {
/** The selector for the host element of a `MatListOption` instance. */
- static hostSelector = '.mat-mdc-list-option';
+ static hostSelector = '.mat-list-option';
/**
- * Gets a `HarnessPredicate` that can be used to search for a list option with specific
- * attributes.
+ * Gets a `HarnessPredicate` that can be used to search for a `MatListOptionHarness` that
+ * meets certain criteria.
* @param options Options for filtering which list option instances are considered a match.
* @return a `HarnessPredicate` configured with the given options.
*/
- static with(
- this: ComponentHarnessConstructor,
+ static with(
options: ListOptionHarnessFilters = {},
- ): HarnessPredicate {
- return getListItemPredicate(this, options).addOption(
+ ): HarnessPredicate {
+ return getListItemPredicate(MatLegacyListOptionHarness, options).addOption(
'is selected',
options.selected,
async (harness, selected) => (await harness.isSelected()) === selected,
);
}
- private _beforeCheckbox = this.locatorForOptional('.mdc-list-item__start .mdc-checkbox');
+ private _itemContent = this.locatorFor('.mat-list-item-content');
/** Gets the position of the checkbox relative to the list option content. */
- async getCheckboxPosition(): Promise {
- return (await this._beforeCheckbox()) !== null ? 'before' : 'after';
+ async getCheckboxPosition(): Promise {
+ return (await (await this._itemContent()).hasClass('mat-list-item-content-reverse'))
+ ? 'after'
+ : 'before';
}
/** Whether the list option is selected. */
@@ -130,8 +132,8 @@ export class MatListOptionHarness extends MatListItemHarnessBase {
}
/**
- * Puts the list option in a checked state by toggling it if it is currently
- * unchecked, or doing nothing if it is already checked.
+ * Puts the list option in a checked state by toggling it if it is currently unchecked, or doing
+ * nothing if it is already checked.
*/
async select() {
if (!(await this.isSelected())) {
@@ -140,8 +142,8 @@ export class MatListOptionHarness extends MatListItemHarnessBase {
}
/**
- * Puts the list option in an unchecked state by toggling it if it is currently
- * checked, or doing nothing if it is already unchecked.
+ * Puts the list option in an unchecked state by toggling it if it is currently checked, or doing
+ * nothing if it is already unchecked.
*/
async deselect() {
if (await this.isSelected()) {
diff --git a/src/material/list/testing/shared.spec.ts b/src/material/legacy-list/testing/shared.spec.ts
similarity index 88%
rename from src/material/list/testing/shared.spec.ts
rename to src/material/legacy-list/testing/shared.spec.ts
index e366d07fe040..366565f5ee03 100644
--- a/src/material/list/testing/shared.spec.ts
+++ b/src/material/legacy-list/testing/shared.spec.ts
@@ -9,36 +9,36 @@ import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {Component, Type} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {MatDividerHarness} from '@angular/material/divider/testing';
-import {MatListModule} from '@angular/material/list';
-import {MatActionListHarness, MatActionListItemHarness} from './action-list-harness';
-import {MatListHarness, MatListItemHarness} from './list-harness';
-import {MatListHarnessBase} from './list-harness-base';
+import {MatLegacyListModule} from '@angular/material/legacy-list';
+import {MatLegacyActionListHarness, MatLegacyActionListItemHarness} from './action-list-harness';
+import {MatLegacyListHarness, MatLegacyListItemHarness} from './list-harness';
+import {MatLegacyListHarnessBase} from './list-harness-base';
import {BaseListItemHarnessFilters} from './list-harness-filters';
import {
- MatListItemHarnessBase,
- MatListItemSection,
- MatSubheaderHarness,
+ MatLegacyListItemHarnessBase,
+ MatLegacyListItemSection,
+ MatLegacySubheaderHarness,
} from './list-item-harness-base';
-import {MatNavListHarness, MatNavListItemHarness} from './nav-list-harness';
-import {MatListOptionHarness, MatSelectionListHarness} from './selection-list-harness';
+import {MatLegacyNavListHarness, MatLegacyNavListItemHarness} from './nav-list-harness';
+import {MatLegacyListOptionHarness, MatLegacySelectionListHarness} from './selection-list-harness';
/** Tests that apply to all types of mat-list. */
function runBaseListFunctionalityTests<
- L extends MatListHarnessBase<
+ L extends MatLegacyListHarnessBase<
ComponentHarnessConstructor & {with: (x?: any) => HarnessPredicate},
I,
BaseListItemHarnessFilters
>,
- I extends MatListItemHarnessBase,
+ I extends MatLegacyListItemHarnessBase,
F extends {disableThirdItem: boolean},
>(
testComponent: Type,
- listModule: typeof MatListModule,
+ listModule: typeof MatLegacyListModule,
listHarness: ComponentHarnessConstructor & {
with: (config?: BaseHarnessFilters) => HarnessPredicate;
},
- listItemHarnessBase: typeof MatListItemHarnessBase,
- subheaderHarness: typeof MatSubheaderHarness,
+ listItemHarnessBase: typeof MatLegacyListItemHarnessBase,
+ subheaderHarness: typeof MatLegacySubheaderHarness,
dividerHarness: typeof MatDividerHarness,
selectors: {content: string},
) {
@@ -120,24 +120,24 @@ function runBaseListFunctionalityTests<
await simpleListHarness.getItemsWithSubheadersAndDividers();
expect(itemsSubheadersAndDividers.length).toBe(7);
expect(itemsSubheadersAndDividers[0] instanceof listItemHarnessBase).toBe(true);
- expect(await (itemsSubheadersAndDividers[0] as MatListItemHarnessBase).getText()).toBe(
+ expect(await (itemsSubheadersAndDividers[0] as MatLegacyListItemHarnessBase).getText()).toBe(
'Item 1',
);
expect(itemsSubheadersAndDividers[1] instanceof subheaderHarness).toBe(true);
- expect(await (itemsSubheadersAndDividers[1] as MatSubheaderHarness).getText()).toBe(
+ expect(await (itemsSubheadersAndDividers[1] as MatLegacySubheaderHarness).getText()).toBe(
'Section 1',
);
expect(itemsSubheadersAndDividers[2] instanceof dividerHarness).toBe(true);
expect(itemsSubheadersAndDividers[3] instanceof listItemHarnessBase).toBe(true);
- expect(await (itemsSubheadersAndDividers[3] as MatListItemHarnessBase).getText()).toBe(
+ expect(await (itemsSubheadersAndDividers[3] as MatLegacyListItemHarnessBase).getText()).toBe(
'Item 2',
);
expect(itemsSubheadersAndDividers[4] instanceof listItemHarnessBase).toBe(true);
- expect(await (itemsSubheadersAndDividers[4] as MatListItemHarnessBase).getText()).toBe(
+ expect(await (itemsSubheadersAndDividers[4] as MatLegacyListItemHarnessBase).getText()).toBe(
'Item 3',
);
expect(itemsSubheadersAndDividers[5] instanceof subheaderHarness).toBe(true);
- expect(await (itemsSubheadersAndDividers[5] as MatSubheaderHarness).getText()).toBe(
+ expect(await (itemsSubheadersAndDividers[5] as MatLegacySubheaderHarness).getText()).toBe(
'Section 2',
);
expect(itemsSubheadersAndDividers[6] instanceof dividerHarness).toBe(true);
@@ -178,12 +178,12 @@ function runBaseListFunctionalityTests<
});
expect(itemsSubheadersAndDividers.length).toBe(4);
expect(itemsSubheadersAndDividers[0] instanceof listItemHarnessBase).toBe(true);
- expect(await (itemsSubheadersAndDividers[0] as MatListItemHarnessBase).getText()).toBe(
+ expect(await (itemsSubheadersAndDividers[0] as MatLegacyListItemHarnessBase).getText()).toBe(
'Item 1',
);
expect(itemsSubheadersAndDividers[1] instanceof dividerHarness).toBe(true);
expect(itemsSubheadersAndDividers[2] instanceof subheaderHarness).toBe(true);
- expect(await (itemsSubheadersAndDividers[2] as MatSubheaderHarness).getText()).toBe(
+ expect(await (itemsSubheadersAndDividers[2] as MatLegacySubheaderHarness).getText()).toBe(
'Section 2',
);
expect(itemsSubheadersAndDividers[3] instanceof dividerHarness).toBe(true);
@@ -221,7 +221,7 @@ function runBaseListFunctionalityTests<
it('should be able to get content harness loader of list item', async () => {
const items = await simpleListHarness.getItems();
expect(items.length).toBe(3);
- const loader = await items[1].getChildLoader(selectors.content as MatListItemSection);
+ const loader = await items[1].getChildLoader(selectors.content as MatLegacyListItemSection);
await expectAsync(loader.getHarness(TestItemContentHarness)).toBeResolved();
});
@@ -240,18 +240,18 @@ function runBaseListFunctionalityTests<
* harness.
*/
export function runHarnessTests(
- listModule: typeof MatListModule,
- listHarness: typeof MatListHarness,
- actionListHarness: typeof MatActionListHarness,
- navListHarness: typeof MatNavListHarness,
- selectionListHarness: typeof MatSelectionListHarness,
- listItemHarnessBase: typeof MatListItemHarnessBase,
- subheaderHarness: typeof MatSubheaderHarness,
+ listModule: typeof MatLegacyListModule,
+ listHarness: typeof MatLegacyListHarness,
+ actionListHarness: typeof MatLegacyActionListHarness,
+ navListHarness: typeof MatLegacyNavListHarness,
+ selectionListHarness: typeof MatLegacySelectionListHarness,
+ listItemHarnessBase: typeof MatLegacyListItemHarnessBase,
+ subheaderHarness: typeof MatLegacySubheaderHarness,
dividerHarness: typeof MatDividerHarness,
selectors: {content: string},
) {
describe('MatListHarness', () => {
- runBaseListFunctionalityTests(
+ runBaseListFunctionalityTests(
ListHarnessTest,
listModule,
listHarness,
@@ -264,8 +264,8 @@ export function runHarnessTests(
describe('MatActionListHarness', () => {
runBaseListFunctionalityTests<
- MatActionListHarness,
- MatActionListItemHarness,
+ MatLegacyActionListHarness,
+ MatLegacyActionListItemHarness,
ActionListHarnessTest
>(
ActionListHarnessTest,
@@ -278,7 +278,7 @@ export function runHarnessTests(
);
describe('additional functionality', () => {
- let harness: MatActionListHarness;
+ let harness: MatLegacyActionListHarness;
let fixture: ComponentFixture;
beforeEach(async () => {
@@ -309,7 +309,11 @@ export function runHarnessTests(
});
describe('MatNavListHarness', () => {
- runBaseListFunctionalityTests(
+ runBaseListFunctionalityTests<
+ MatLegacyNavListHarness,
+ MatLegacyNavListItemHarness,
+ NavListHarnessTest
+ >(
NavListHarnessTest,
listModule,
navListHarness,
@@ -320,7 +324,7 @@ export function runHarnessTests(
);
describe('additional functionality', () => {
- let harness: MatNavListHarness;
+ let harness: MatLegacyNavListHarness;
let fixture: ComponentFixture;
beforeEach(async () => {
@@ -363,8 +367,8 @@ export function runHarnessTests(
describe('MatSelectionListHarness', () => {
runBaseListFunctionalityTests<
- MatSelectionListHarness,
- MatListOptionHarness,
+ MatLegacySelectionListHarness,
+ MatLegacyListOptionHarness,
SelectionListHarnessTest
>(
SelectionListHarnessTest,
@@ -377,8 +381,8 @@ export function runHarnessTests(
);
describe('additional functionality', () => {
- let harness: MatSelectionListHarness;
- let emptyHarness: MatSelectionListHarness;
+ let harness: MatLegacySelectionListHarness;
+ let emptyHarness: MatLegacySelectionListHarness;
let fixture: ComponentFixture;
beforeEach(async () => {
diff --git a/src/material/list/BUILD.bazel b/src/material/list/BUILD.bazel
index aa55f801798a..d7cc769101d0 100644
--- a/src/material/list/BUILD.bazel
+++ b/src/material/list/BUILD.bazel
@@ -16,51 +16,67 @@ ng_module(
name = "list",
srcs = glob(
["**/*.ts"],
- exclude = ["**/*.spec.ts"],
+ exclude = [
+ "**/*.spec.ts",
+ ],
),
- assets = [":list.css"] + glob(["**/*.html"]),
+ assets = [
+ ":list_scss",
+ ":list_option_scss",
+ ] + glob(["**/*.html"]),
deps = [
"//src:dev_mode_types",
- "//src/cdk/a11y",
"//src/cdk/coercion",
"//src/cdk/collections",
- "//src/cdk/keycodes",
+ "//src/cdk/observers",
"//src/material/core",
"//src/material/divider",
- "@npm//@angular/common",
"@npm//@angular/core",
"@npm//@angular/forms",
- "@npm//rxjs",
],
)
sass_library(
name = "list_scss_lib",
srcs = glob(["**/_*.scss"]),
- deps = ["//src/material/core:core_scss_lib"],
+ deps = [
+ "//:mdc_sass_lib",
+ "//src/material/checkbox:checkbox_scss_lib",
+ "//src/material/core:core_scss_lib",
+ ],
)
sass_binary(
name = "list_scss",
src = "list.scss",
deps = [
+ "//:mdc_sass_lib",
+ "//src/material/core:core_scss_lib",
+ ],
+)
+
+sass_binary(
+ name = "list_option_scss",
+ src = "list-option.scss",
+ deps = [
+ ":list_scss_lib",
+ "//:mdc_sass_lib",
"//src/cdk:sass_lib",
"//src/material/core:core_scss_lib",
- "//src/material/divider:divider_scss_lib",
],
)
ng_test_library(
- name = "unit_test_sources",
+ name = "list_tests_lib",
srcs = glob(
["**/*.spec.ts"],
exclude = ["**/*.e2e.spec.ts"],
),
deps = [
":list",
- "//src/cdk/a11y",
"//src/cdk/keycodes",
"//src/cdk/testing/private",
+ "//src/cdk/testing/testbed",
"//src/material/core",
"@npm//@angular/forms",
"@npm//@angular/platform-browser",
@@ -69,7 +85,9 @@ ng_test_library(
ng_web_test_suite(
name = "unit_tests",
- deps = [":unit_test_sources"],
+ deps = [
+ ":list_tests_lib",
+ ],
)
ng_e2e_test_library(
diff --git a/src/material/list/README.md b/src/material/list/README.md
index 5ebc8cf16008..dae27c0261d4 100644
--- a/src/material/list/README.md
+++ b/src/material/list/README.md
@@ -1 +1 @@
-Please see the official documentation at https://material.angular.io/components/component/list
\ No newline at end of file
+Please see the official documentation at https://material.angular.io/components/component/list
diff --git a/src/material-experimental/mdc-list/_interactive-list-theme.import.scss b/src/material/list/_interactive-list-theme.import.scss
similarity index 100%
rename from src/material-experimental/mdc-list/_interactive-list-theme.import.scss
rename to src/material/list/_interactive-list-theme.import.scss
diff --git a/src/material-experimental/mdc-list/_interactive-list-theme.scss b/src/material/list/_interactive-list-theme.scss
similarity index 92%
rename from src/material-experimental/mdc-list/_interactive-list-theme.scss
rename to src/material/list/_interactive-list-theme.scss
index 53e0d6e20f26..c42a5a0305d0 100644
--- a/src/material-experimental/mdc-list/_interactive-list-theme.scss
+++ b/src/material/list/_interactive-list-theme.scss
@@ -1,13 +1,13 @@
-@use '@angular/material' as mat;
-@use '@material/ripple' as mdc-ripple;
@use 'sass:map';
+@use '@material/ripple' as mdc-ripple;
+@use '../core/theming/theming';
// Mixin that provides colors for the various states of an interactive list-item. MDC
// has integrated styles for these states but relies on their complex ripples for it.
@mixin private-interactive-list-item-state-colors($config) {
$is-dark-theme: map.get($config, is-dark);
$active-base-color: if($is-dark-theme, white, black);
- $selected-color: mat.get-color-from-palette(map.get($config, primary));
+ $selected-color: theming.get-color-from-palette(map.get($config, primary));
.mat-mdc-list-item-interactive {
&::before {
diff --git a/src/material/list/_list-legacy-index.scss b/src/material/list/_list-legacy-index.scss
deleted file mode 100644
index 271fe4dc8537..000000000000
--- a/src/material/list/_list-legacy-index.scss
+++ /dev/null
@@ -1,2 +0,0 @@
-@forward 'list-theme' hide color, theme, typography;
-@forward 'list-theme' as mat-list-* hide mat-list-density;
diff --git a/src/material-experimental/mdc-list/_list-option-theme.import.scss b/src/material/list/_list-option-theme.import.scss
similarity index 100%
rename from src/material-experimental/mdc-list/_list-option-theme.import.scss
rename to src/material/list/_list-option-theme.import.scss
diff --git a/src/material-experimental/mdc-list/_list-option-theme.scss b/src/material/list/_list-option-theme.scss
similarity index 73%
rename from src/material-experimental/mdc-list/_list-option-theme.scss
rename to src/material/list/_list-option-theme.scss
index 6e9fba7043ad..021053ee259b 100644
--- a/src/material-experimental/mdc-list/_list-option-theme.scss
+++ b/src/material/list/_list-option-theme.scss
@@ -1,5 +1,6 @@
-@use '@angular/material' as mat;
@use '@material/checkbox' as mdc-checkbox;
+@use '../core/mdc-helpers/mdc-helpers';
+@use '../checkbox/checkbox-private';
@use './list-option-trailing-avatar-compat';
// Mixin that overrides the selected item and checkbox colors for list options. By
@@ -7,7 +8,7 @@
// inside list options by default uses the `primary` color too.
@mixin private-list-option-color-override($color-config, $color, $mdc-color) {
& .mdc-list-item__start, & .mdc-list-item__end {
- @include mat.private-checkbox-styles-with-color($color-config, $color, $mdc-color);
+ @include checkbox-private.private-checkbox-styles-with-color($color-config, $color, $mdc-color);
}
}
@@ -17,11 +18,11 @@
@mixin private-list-option-typography-styles() {
@include list-option-trailing-avatar-compat.core-styles(
- $query: mat.$private-mdc-typography-styles-query);
+ $query: mdc-helpers.$mdc-typography-styles-query);
.mat-mdc-list-option {
.mdc-list-item__start, .mdc-list-item__end {
- @include mdc-checkbox.without-ripple($query: mat.$private-mdc-typography-styles-query);
+ @include mdc-checkbox.without-ripple($query: mdc-helpers.$mdc-typography-styles-query);
}
}
}
diff --git a/src/material-experimental/mdc-list/_list-option-trailing-avatar-compat.scss b/src/material/list/_list-option-trailing-avatar-compat.scss
similarity index 93%
rename from src/material-experimental/mdc-list/_list-option-trailing-avatar-compat.scss
rename to src/material/list/_list-option-trailing-avatar-compat.scss
index 8d81f0b917b1..051427222b8b 100644
--- a/src/material-experimental/mdc-list/_list-option-trailing-avatar-compat.scss
+++ b/src/material/list/_list-option-trailing-avatar-compat.scss
@@ -1,9 +1,9 @@
-@use '@angular/material' as mat;
@use '@material/typography/typography';
@use '@material/feature-targeting/feature-targeting';
@use '@material/density/functions' as density-functions;
@use '@material/list/evolution-mixins' as mdc-list;
@use '@material/list/evolution-variables' as mdc-list-variables;
+@use '../core/mdc-helpers/mdc-helpers';
// For compatibility with the non-MDC selection list, we support avatars that are
// shown at the end of the list option. This is not supported by the MDC list as the
@@ -16,7 +16,7 @@
@mixin core-styles($query) {
$feat-structure: feature-targeting.create-target($query, structure);
- @include mat.private-disable-mdc-fallback-declarations {
+ @include mdc-helpers.disable-mdc-fallback-declarations {
.mat-mdc-list-option-with-trailing-avatar {
@include mdc-list.item-end-spacing(16px, $query: $query);
@include mdc-list.item-end-size(40px, $query: $query);
@@ -49,7 +49,7 @@
$property-name: height,
);
- @include mat.private-disable-mdc-fallback-declarations {
+ @include mdc-helpers.disable-mdc-fallback-declarations {
.mat-mdc-list-option-with-trailing-avatar {
@include mdc-list.one-line-item-height($one-line-tall-height);
@include mdc-list.two-line-item-height($two-line-tall-height);
diff --git a/src/material/list/_list-theme.import.scss b/src/material/list/_list-theme.import.scss
index 7d9dba1a9e42..0e0a4a5684aa 100644
--- a/src/material/list/_list-theme.import.scss
+++ b/src/material/list/_list-theme.import.scss
@@ -1,10 +1,6 @@
-@forward '../core/theming/theming.import';
-@forward '../core/typography/typography-utils.import';
-@forward '../core/style/list-common.import';
-@forward 'list-theme' hide color, theme, typography;
-@forward 'list-theme' as mat-list-* hide mat-list-density;
+@forward 'interactive-list-theme' as mat-mdc-*;
+@forward 'list-option-theme' as mat-mdc-*;
+@forward 'list-theme' as mat-mdc-list-*;
-@import '../core/theming/palette';
-@import '../core/theming/theming';
-@import '../core/typography/typography-utils';
-@import '../core/style/list-common';
+@import './interactive-list-theme';
+@import './list-option-theme';
diff --git a/src/material/list/_list-theme.scss b/src/material/list/_list-theme.scss
index c3721145362b..48ce6b14b4c6 100644
--- a/src/material/list/_list-theme.scss
+++ b/src/material/list/_list-theme.scss
@@ -1,103 +1,68 @@
@use 'sass:map';
+@use '@material/list/evolution-mixins' as mdc-list;
+
@use '../core/theming/theming';
@use '../core/typography/typography';
@use '../core/typography/typography-utils';
-@use '../core/style/list-common';
-
+@use '../core/mdc-helpers/mdc-helpers';
+@use './interactive-list-theme';
+@use './list-option-theme';
@mixin color($config-or-theme) {
$config: theming.get-color-config($config-or-theme);
- $background: map.get($config, background);
- $foreground: map.get($config, foreground);
-
- .mat-list-base {
- .mat-list-item {
- color: theming.get-color-from-palette($foreground, text);
+ $primary: theming.get-color-from-palette(map.get($config, primary));
+ $accent: theming.get-color-from-palette(map.get($config, accent));
+ $warn: theming.get-color-from-palette(map.get($config, warn));
+
+ @include mdc-helpers.using-mdc-theme($config) {
+ // MDC's state styles are tied in with their ripple. Since we don't use the MDC
+ // ripple, we need to add the hover, focus and selected states manually.
+ @include interactive-list-theme.private-interactive-list-item-state-colors($config);
+ @include mdc-list.without-ripple($query: mdc-helpers.$mdc-theme-styles-query);
+
+ .mat-mdc-list-option {
+ @include list-option-theme.private-list-option-color-override($config, $primary, primary);
}
-
- .mat-list-option {
- color: theming.get-color-from-palette($foreground, text);
+ .mat-mdc-list-option.mat-accent {
+ @include list-option-theme.private-list-option-color-override($config, $accent, secondary);
}
-
- .mat-subheader {
- color: theming.get-color-from-palette($foreground, secondary-text);
- }
-
- .mat-list-item-disabled {
- background-color: theming.get-color-from-palette($background, disabled-list-option);
- color: theming.get-color-from-palette($foreground, disabled-text);
+ .mat-mdc-list-option.mat-warn {
+ @include list-option-theme.private-list-option-color-override($config, $warn, error);
}
}
+}
- .mat-list-option,
- .mat-nav-list .mat-list-item,
- .mat-action-list .mat-list-item {
- &:hover, &:focus {
- background: theming.get-color-from-palette($background, 'hover');
- }
- }
+@mixin density($config-or-theme) {
+ $density-scale: theming.get-density-config($config-or-theme);
- .mat-list-single-selected-option {
- &, &:hover, &:focus {
- background: theming.get-color-from-palette($background, hover, 0.12);
+ @include mdc-helpers.disable-mdc-fallback-declarations {
+ .mat-mdc-list-item {
+ @include mdc-list.one-line-item-density($density-scale);
+ @include mdc-list.two-line-item-density($density-scale);
+ @include mdc-list.three-line-item-density($density-scale);
}
+
+ @include list-option-theme.private-list-option-density-styles($density-scale);
}
}
@mixin typography($config-or-theme) {
- $config: typography.private-typography-to-2014-config(
+ $config: typography.private-typography-to-2018-config(
theming.get-typography-config($config-or-theme));
- $font-family: typography-utils.font-family($config);
-
- .mat-list-item {
- font-family: $font-family;
+ @include mdc-helpers.using-mdc-typography($config) {
+ @include mdc-list.without-ripple($query: mdc-helpers.$mdc-typography-styles-query);
+ @include list-option-theme.private-list-option-typography-styles();
}
- .mat-list-option {
- font-family: $font-family;
- }
-
- // Default list
- .mat-list-base {
- .mat-list-item {
- font-size: typography-utils.font-size($config, subheading-2);
- @include list-common.base(typography-utils.font-size($config, body-1));
- }
-
- .mat-list-option {
- font-size: typography-utils.font-size($config, subheading-2);
- @include list-common.base(typography-utils.font-size($config, body-1));
- }
-
- .mat-subheader {
- font-family: typography-utils.font-family($config, body-2);
- font-size: typography-utils.font-size($config, body-2);
- font-weight: typography-utils.font-weight($config, body-2);
- }
- }
-
- // Dense list
- .mat-list-base[dense] {
- .mat-list-item {
- font-size: typography-utils.font-size($config, caption);
- @include list-common.base(typography-utils.font-size($config, caption));
- }
-
- .mat-list-option {
- font-size: typography-utils.font-size($config, caption);
- @include list-common.base(typography-utils.font-size($config, caption));
- }
-
- .mat-subheader {
- font-family: $font-family;
- font-size: typography-utils.font-size($config, caption);
- font-weight: typography-utils.font-weight($config, body-2);
- }
+ // According to the public spec this should be subtitle-1.
+ // However, body-1 and subtitle-1 are nearly identical in the public spec,
+ // and the Google-specific spec states that it should be body-1.
+ // For consistency, we use body-1 for both public and Google internal.
+ .mat-mdc-list-item .mdc-list-item__primary-text {
+ @include typography-utils.typography-level($config, body-1);
}
}
-@mixin _density($config-or-theme) {}
-
@mixin theme($theme-or-color-config) {
$theme: theming.private-legacy-get-theme($theme-or-color-config);
@include theming.private-check-duplicate-theme-styles($theme, 'mat-list') {
@@ -109,7 +74,7 @@
@include color($color);
}
@if $density != null {
- @include _density($density);
+ @include density($density);
}
@if $typography != null {
@include typography($typography);
diff --git a/src/material-experimental/mdc-list/action-list.ts b/src/material/list/action-list.ts
similarity index 100%
rename from src/material-experimental/mdc-list/action-list.ts
rename to src/material/list/action-list.ts
diff --git a/src/material-experimental/mdc-list/list-base.ts b/src/material/list/list-base.ts
similarity index 100%
rename from src/material-experimental/mdc-list/list-base.ts
rename to src/material/list/list-base.ts
diff --git a/src/material-experimental/mdc-list/list-item-sections.ts b/src/material/list/list-item-sections.ts
similarity index 100%
rename from src/material-experimental/mdc-list/list-item-sections.ts
rename to src/material/list/list-item-sections.ts
diff --git a/src/material/list/list-item.html b/src/material/list/list-item.html
index 03e57de4ac13..e52a1e9491c4 100644
--- a/src/material/list/list-item.html
+++ b/src/material/list/list-item.html
@@ -1,13 +1,20 @@
-
-
+
+
+
+
+
+
+
+
-
-
+
-
+
-
-
+
+
diff --git a/src/material/list/list-module.ts b/src/material/list/list-module.ts
index c4ca757670ae..8b1ec7903d19 100644
--- a/src/material/list/list-module.ts
+++ b/src/material/list/list-module.ts
@@ -8,48 +8,59 @@
import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
-import {
- MatCommonModule,
- MatLineModule,
- MatPseudoCheckboxModule,
- MatRippleModule,
-} from '@angular/material/core';
-import {
- MatList,
- MatNavList,
- MatListAvatarCssMatStyler,
- MatListIconCssMatStyler,
- MatListItem,
- MatListSubheaderCssMatStyler,
-} from './list';
-import {MatListOption, MatSelectionList} from './selection-list';
+import {MatPseudoCheckboxModule, MatRippleModule, MatCommonModule} from '@angular/material/core';
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 {
+ 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: [MatLineModule, MatRippleModule, MatCommonModule, MatPseudoCheckboxModule, CommonModule],
+ imports: [
+ ObserversModule,
+ CommonModule,
+ MatCommonModule,
+ MatRippleModule,
+ MatPseudoCheckboxModule,
+ ],
exports: [
MatList,
+ MatActionList,
MatNavList,
- MatListItem,
- MatListAvatarCssMatStyler,
- MatLineModule,
- MatCommonModule,
- MatListIconCssMatStyler,
- MatListSubheaderCssMatStyler,
- MatPseudoCheckboxModule,
MatSelectionList,
+ MatListItem,
MatListOption,
+ MatListItemAvatar,
+ MatListItemIcon,
+ MatListSubheaderCssMatStyler,
MatDividerModule,
+ MatListItemLine,
+ MatListItemTitle,
+ MatListItemMeta,
],
declarations: [
MatList,
+ MatActionList,
MatNavList,
- MatListItem,
- MatListAvatarCssMatStyler,
- MatListIconCssMatStyler,
- MatListSubheaderCssMatStyler,
MatSelectionList,
+ MatListItem,
MatListOption,
+ MatListSubheaderCssMatStyler,
+ MatListItemAvatar,
+ MatListItemIcon,
+ MatListItemLine,
+ MatListItemTitle,
+ MatListItemMeta,
],
})
export class MatListModule {}
diff --git a/src/material-experimental/mdc-list/list-option-types.ts b/src/material/list/list-option-types.ts
similarity index 100%
rename from src/material-experimental/mdc-list/list-option-types.ts
rename to src/material/list/list-option-types.ts
diff --git a/src/material/list/list-option.html b/src/material/list/list-option.html
index d8a34a308cbf..3dd89050aa65 100644
--- a/src/material/list/list-option.html
+++ b/src/material/list/list-option.html
@@ -1,19 +1,64 @@
-
+
+
+
+
+
-
+
+
+
-
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
-
+
+
diff --git a/src/material-experimental/mdc-list/list-option.scss b/src/material/list/list-option.scss
similarity index 90%
rename from src/material-experimental/mdc-list/list-option.scss
rename to src/material/list/list-option.scss
index e1835a4ed5c8..0c905d4af8bc 100644
--- a/src/material-experimental/mdc-list/list-option.scss
+++ b/src/material/list/list-option.scss
@@ -1,22 +1,23 @@
@use 'sass:map';
-@use '@angular/material' as mat;
@use '@angular/cdk';
@use '@material/checkbox/checkbox' as mdc-checkbox;
@use '@material/list/evolution-variables' as mdc-list-variables;
@use '@material/checkbox/checkbox-theme' as mdc-checkbox-theme;
+@use '../core/mdc-helpers/mdc-helpers';
+@use '../checkbox/checkbox-private';
@use './list-option-trailing-avatar-compat';
// For compatibility with the non-MDC list, we support avatars that are shown at the end
// of the list option. We create a class similar to MDC's `--trailing-icon` one.
-@include list-option-trailing-avatar-compat.core-styles($query: mat.$private-mdc-base-styles-query);
+@include list-option-trailing-avatar-compat.core-styles($query: mdc-helpers.$mdc-base-styles-query);
.mat-mdc-list-option {
// The MDC-based list-option uses the MDC checkbox for the selection indicators.
// We need to ensure that the checkbox styles are not included for the list-option.
- @include mat.private-disable-mdc-fallback-declarations {
+ @include mdc-helpers.disable-mdc-fallback-declarations {
@include mdc-checkbox.static-styles(
- $query: mat.$private-mdc-base-styles-without-animation-query);
+ $query: mdc-helpers.$mdc-base-styles-without-animation-query);
&:not(._mat-animation-noopable) {
@include mdc-checkbox.static-styles($query: animation);
@@ -26,7 +27,7 @@
// We can't use the MDC checkbox here directly, because this checkbox is purely
// decorative and including the MDC one will bring in unnecessary JS.
.mdc-checkbox {
- $config: map.merge(mat.$private-checkbox-theme-config, (
+ $config: map.merge(checkbox-private.$private-checkbox-theme-config, (
// Since this checkbox isn't interactive, we can exclude the focus/hover/press styles.
selected-focus-icon-color: null,
selected-hover-icon-color: null,
diff --git a/src/material-experimental/mdc-list/list-option.ts b/src/material/list/list-option.ts
similarity index 100%
rename from src/material-experimental/mdc-list/list-option.ts
rename to src/material/list/list-option.ts
diff --git a/src/material/list/list.e2e.spec.ts b/src/material/list/list.e2e.spec.ts
index 346337ec4fe5..da97d573c380 100644
--- a/src/material/list/list.e2e.spec.ts
+++ b/src/material/list/list.e2e.spec.ts
@@ -1,14 +1,3 @@
-import {browser} from 'protractor';
-import {expectToExist} from '../../cdk/testing/private/e2e';
-
-describe('list', () => {
- beforeEach(async () => await browser.get('/list'));
-
- it('should render a list container', async () => {
- await expectToExist('mat-list');
- });
-
- it('should render list items inside the list container', async () => {
- await expectToExist('mat-list mat-list-item');
- });
+it('should have e2e tests', () => {
+ // TODO: Implement.
});
diff --git a/src/material/list/list.scss b/src/material/list/list.scss
index 2da7b4fd2fb5..702db34036f2 100644
--- a/src/material/list/list.scss
+++ b/src/material/list/list.scss
@@ -1,215 +1,104 @@
-@use '@angular/cdk';
-
-@use '../core/style/list-common';
+@use '@material/list/evolution-mixins' as mdc-list;
@use '../core/style/layout-common';
-@use '../divider/divider-offset';
-
-$side-padding: 16px;
-$icon-padding: 4px;
-$avatar-size: 40px;
-
-// Normal list variables
-$top-padding: 8px;
-// height for single-line lists
-$base-height: 48px;
-// height for single-line lists with avatars
-$avatar-height: 56px;
-// spec requires two- and three-line lists be taller
-$two-line-height: 72px;
-$three-line-height: 88px;
-$multi-line-padding: 16px;
-$icon-size: 24px;
-
-// Dense list variables
-$dense-top-padding: 4px;
-$dense-base-height: 40px;
-$dense-avatar-height: 48px;
-$dense-two-line-height: 60px;
-$dense-three-line-height: 76px;
-$dense-multi-line-padding: 16px;
-$dense-list-icon-size: 20px;
-$dense-avatar-size: 36px;
+@use '../core/mdc-helpers/mdc-helpers';
-$item-inset-divider-offset: 72px;
-
-// This mixin provides all list-item styles, changing font size and height
-// based on whether the list is in dense mode.
-@mixin item-base($base-height, $height-with-avatar, $two-line-height,
- $three-line-height, $multi-line-padding, $icon-size, $avatar-size) {
+@include mdc-helpers.disable-mdc-fallback-declarations {
+ @include mdc-list.without-ripple($query: mdc-helpers.$mdc-base-styles-query);
+}
- // Prevents the wrapper `mat-list-item-content` from collapsing due to it
- // being `inline` by default.
+// MDC expects the list element to be a ``, since we use `` instead we need to
+// explicitly set `display: block`
+.mat-mdc-list-base {
display: block;
- height: $base-height;
- -webkit-tap-highlight-color: transparent;
+}
- // Override the user agent styling if the list item is a button.
+// MDC expects that the list items are always ``, since we actually use `` in some
+// cases, we need to make sure it expands to fill the available width.
+.mat-mdc-list-item,
+.mat-mdc-list-option {
width: 100%;
- padding: 0;
-
- .mat-list-item-content {
- display: flex;
- flex-direction: row;
- align-items: center;
- box-sizing: border-box;
- padding: 0 $side-padding;
- position: relative;
- height: inherit;
- }
-
- .mat-list-item-content-reverse {
- display: flex;
- align-items: center;
- padding: 0 $side-padding;
- flex-direction: row-reverse;
- justify-content: space-around;
- }
-
- .mat-list-item-ripple {
- display: block;
- @include layout-common.fill;
-
- // Disable pointer events for the ripple container because the container will overlay the
- // user content and we don't want to disable mouse events on the user content.
- // Pointer events can be safely disabled because the ripple trigger element is the host element.
- pointer-events: none;
- }
-
- &.mat-list-item-with-avatar {
- height: $height-with-avatar;
- }
-
- &.mat-2-line {
- height: $two-line-height;
- }
-
-
- &.mat-3-line {
- height: $three-line-height;
- }
-
- // list items with more than 3 lines should expand to match
- // the height of its contained text
- &.mat-multi-line {
- height: auto;
-
- .mat-list-item-content {
- padding-top: $multi-line-padding;
- padding-bottom: $multi-line-padding;
- }
- }
-
- .mat-list-text {
- @include list-common.wrapper-base();
+ box-sizing: border-box;
+}
- // By default, there will be no padding for the list item text because the padding is already
- // set on the `mat-list-item-content` element. Later, if the list-item detects that there are
- // secondary items (avatar, checkbox), a padding on the proper side will be added.
- padding: 0;
+// MDC doesn't have list dividers, so we use mat-divider and style appropriately.
+// TODO(devversion): check if we can use the MDC dividers.
+.mat-mdc-list-item,
+.mat-mdc-list-option {
+ .mat-divider-inset {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
}
- &.mat-list-item-with-avatar,
- &.mat-list-option {
- .mat-list-item-content .mat-list-text {
- padding-right: 0;
- padding-left: $side-padding;
-
- [dir='rtl'] & {
- padding-right: $side-padding;
- padding-left: 0;
- }
- }
-
- // Reversed content is mainly used by the MatSelectionList for displaying the checkbox at the
- // end of the list option. Since there is a secondary item (checkbox) at the end of the
- // option, there needs to be a padding for the mat-list-text on the end-side.
- .mat-list-item-content-reverse .mat-list-text {
- padding-left: 0;
- padding-right: $side-padding;
-
- [dir='rtl'] & {
- padding-right: 0;
- padding-left: $side-padding;
- }
- }
- }
+ .mat-mdc-list-item-avatar ~ .mat-divider-inset {
+ margin-left: 72px;
- &.mat-list-item-with-avatar.mat-list-option {
- .mat-list-item-content-reverse .mat-list-text,
- .mat-list-item-content .mat-list-text {
- padding-right: $side-padding;
- padding-left: $side-padding;
+ [dir='rtl'] & {
+ margin-right: 72px;
}
}
+}
- .mat-list-avatar {
- flex-shrink: 0;
- width: $avatar-size;
- height: $avatar-size;
- border-radius: 50%;
-
- // Not supported in IE11, but we're using this as a
- // progressive enhancement to get better image scaling.
- object-fit: cover;
-
- ~ .mat-divider-inset {
- @include divider-offset.inset-divider-offset($avatar-size, $side-padding);
- }
- }
+// MDC's hover and focus state styles are included with their ripple which we don't use.
+// Instead we add the focus, hover and selected styles ourselves using this pseudo-element
+.mat-mdc-list-item-interactive::before {
+ @include layout-common.fill();
+ content: '';
+ opacity: 0;
+}
- .mat-list-icon {
- flex-shrink: 0;
- width: $icon-size;
- height: $icon-size;
- font-size: $icon-size;
- box-sizing: content-box;
- border-radius: 50%;
- padding: $icon-padding;
+// MDC always sets the cursor to `pointer`. We do not want to show this for non-interactive
+// lists. See: https://github.com/material-components/material-components-web/issues/6443
+.mat-mdc-list-non-interactive .mdc-list-item {
+ cursor: default;
+}
- ~ .mat-divider-inset {
- @include divider-offset.inset-divider-offset($icon-size + (2 * $icon-padding),
- $side-padding);
- }
+// The MDC-based list items already use the `::before` pseudo element for the standard
+// focus/selected/hover state. Hence, we need to have a separate list-item spanning
+// element that can be used for strong focus indicators.
+.mat-mdc-list-item {
+ > .mat-mdc-focus-indicator {
+ @include layout-common.fill();
+ pointer-events: none;
}
- .mat-divider {
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- margin: 0;
-
- [dir='rtl'] & {
- margin-left: auto;
- margin-right: 0;
- }
-
- &.mat-divider-inset {
- position: absolute; // necessary to override card styles
- }
+ // For list items, render the focus indicator when the parent
+ // listem item is focused.
+ &:focus > .mat-mdc-focus-indicator::before {
+ content: '';
}
}
-.mat-subheader {
- display: flex;
- box-sizing: border-box;
- padding: $side-padding;
- align-items: center;
-
- // This needs slightly more specificity, because it
- // can be overwritten by the typography styles.
- .mat-list-base & {
- margin: 0;
+.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;
+ }
+
+ // 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;
}
}
-// We need some extra resets when the list items are buttons.
-button.mat-list-item, button.mat-list-option {
- padding: 0;
- width: 100%;
+// MDC doesn't account for button being used as a list item. We override some of
+// the default button styles here so that they look right when used as a list
+// item.
+mat-action-list button {
background: none;
color: inherit;
border: none;
+ font: inherit;
outline: inherit;
-webkit-tap-highlight-color: transparent;
text-align: left;
@@ -222,137 +111,3 @@ button.mat-list-item, button.mat-list-option {
border: 0;
}
}
-
-// This mixin adjusts the heights and padding based on whether the list is in dense mode.
-@mixin subheader-spacing($top-padding, $base-height) {
- height: $base-height;
- line-height: $base-height - $side-padding * 2;
-
- &:first-child {
- margin-top: -$top-padding;
- }
-}
-
-.mat-list-base {
- padding-top: $top-padding;
- display: block;
- -webkit-tap-highlight-color: transparent;
-
- .mat-subheader {
- @include subheader-spacing($top-padding, $base-height);
- }
-
- .mat-list-item, .mat-list-option {
- @include item-base(
- $base-height,
- $avatar-height,
- $two-line-height,
- $three-line-height,
- $multi-line-padding,
- $icon-size,
- $avatar-size
- );
- }
-}
-
-
-.mat-list-base[dense] {
- padding-top: $dense-top-padding;
- display: block;
-
- .mat-subheader {
- @include subheader-spacing($dense-top-padding, $dense-base-height);
- }
-
- .mat-list-item, .mat-list-option {
- @include item-base(
- $dense-base-height,
- $dense-avatar-height,
- $dense-two-line-height,
- $dense-three-line-height,
- $dense-multi-line-padding,
- $dense-list-icon-size,
- $dense-avatar-size
- );
- }
-}
-
-.mat-nav-list {
- a {
- text-decoration: none;
- color: inherit;
- }
-
- .mat-list-item {
- cursor: pointer;
- outline: none;
- }
-}
-
-mat-action-list {
- .mat-list-item {
- cursor: pointer;
- outline: inherit;
- }
-}
-
-.mat-list-option:not(.mat-list-item-disabled) {
- cursor: pointer;
- outline: none;
-}
-
-.mat-list-item-disabled {
- pointer-events: none;
-
- // Since we can't use a color to indicate that the list
- // item is disabled, we have to use opacity instead.
- @include cdk.high-contrast {
- opacity: 0.5;
- }
-}
-
-@include cdk.high-contrast(active, off) {
- .mat-list-option,
- .mat-nav-list .mat-list-item,
- mat-action-list .mat-list-item {
- &:hover {
- outline: dotted 1px;
- z-index: 1;
- }
- }
-
- // In single selection mode, the selected option is indicated by changing its
- // background color, but that doesn't work in high contrast mode. We add an
- // alternate indication by rendering out a circle.
- .mat-list-single-selected-option::after {
- $size: 10px;
- content: '';
- position: absolute;
- top: 50%;
- right: $side-padding;
- transform: translateY(-50%);
- width: $size;
- height: 0;
- border-bottom: solid $size;
- border-radius: $size;
- }
-
- [dir='rtl'] .mat-list-single-selected-option::after {
- right: auto;
- left: $side-padding;
- }
-}
-
-
-// Disable the hover styles on non-hover devices. Since this is more of a progressive
-// enhancement and not all desktop browsers support this kind of media query, we can't
-// use something like `@media (hover)`.
-@media (hover: none) {
- .mat-list-option:not(.mat-list-single-selected-option),
- .mat-nav-list .mat-list-item,
- .mat-action-list .mat-list-item {
- &:not(.mat-list-item-disabled):hover {
- background: none;
- }
- }
-}
diff --git a/src/material/list/list.spec.ts b/src/material/list/list.spec.ts
index 3213631ed2fd..61b5e5509cf9 100644
--- a/src/material/list/list.spec.ts
+++ b/src/material/list/list.spec.ts
@@ -4,7 +4,7 @@ import {Component, QueryList, ViewChildren} from '@angular/core';
import {By} from '@angular/platform-browser';
import {MatListItem, MatListModule} from './index';
-describe('MatList', () => {
+describe('MDC-based MatList', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [MatListModule],
@@ -28,15 +28,13 @@ describe('MatList', () => {
TestBed.compileComponents();
}));
- it('should not apply any additional class to a list without lines', () => {
+ it('should apply an additional class to lists without lines', () => {
const fixture = TestBed.createComponent(ListWithOneItem);
const listItem = fixture.debugElement.query(By.css('mat-list-item'))!;
fixture.detectChanges();
- expect(listItem.nativeElement.classList.length).toBe(2);
- expect(listItem.nativeElement.classList).toContain('mat-list-item');
-
- // This spec also ensures the focus indicator is present.
- expect(listItem.nativeElement.classList).toContain('mat-focus-indicator');
+ expect(listItem.nativeElement.classList).toContain('mat-mdc-list-item');
+ expect(listItem.nativeElement.classList).toContain('mdc-list-item');
+ expect(listItem.nativeElement.classList).toContain('mdc-list-item--with-one-line');
});
it('should apply a particular class to lists with two lines', () => {
@@ -44,8 +42,7 @@ describe('MatList', () => {
fixture.detectChanges();
const listItems = fixture.debugElement.children[0].queryAll(By.css('mat-list-item'));
- expect(listItems[0].nativeElement.className).toContain('mat-2-line');
- expect(listItems[1].nativeElement.className).toContain('mat-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', () => {
@@ -53,26 +50,30 @@ describe('MatList', () => {
fixture.detectChanges();
const listItems = fixture.debugElement.children[0].queryAll(By.css('mat-list-item'));
- expect(listItems[0].nativeElement.className).toContain('mat-3-line');
- expect(listItems[1].nativeElement.className).toContain('mat-3-line');
+ expect(listItems[0].nativeElement.className).toContain('mdc-list-item--with-three-lines');
});
- it('should apply a particular class to lists with more than 3 lines', () => {
- const fixture = TestBed.createComponent(ListWithManyLines);
+ it('should apply a class to list items with avatars', () => {
+ const fixture = TestBed.createComponent(ListWithAvatar);
fixture.detectChanges();
const listItems = fixture.debugElement.children[0].queryAll(By.css('mat-list-item'));
- expect(listItems[0].nativeElement.className).toContain('mat-multi-line');
- expect(listItems[1].nativeElement.className).toContain('mat-multi-line');
+ expect(listItems[0].nativeElement.className).toContain('mdc-list-item--with-leading-avatar');
+ expect(listItems[1].nativeElement.className).not.toContain(
+ 'mdc-list-item--with-leading-avatar',
+ );
});
- it('should apply a class to list items with avatars', () => {
- const fixture = TestBed.createComponent(ListWithAvatar);
+ it('should have a strong focus indicator configured for all list-items', () => {
+ const fixture = TestBed.createComponent(ListWithManyLines);
fixture.detectChanges();
+ const listItems = fixture.debugElement.children[0]
+ .queryAll(By.css('mat-list-item'))
+ .map(debugEl => debugEl.nativeElement as HTMLElement);
- const listItems = fixture.debugElement.children[0].queryAll(By.css('mat-list-item'));
- expect(listItems[0].nativeElement.className).toContain('mat-list-item-with-avatar');
- expect(listItems[1].nativeElement.className).not.toContain('mat-list-item-with-avatar');
+ expect(listItems.every(i => i.querySelector('.mat-mdc-focus-indicator') !== null))
+ .withContext('Expected all list items to have a strong focus indicator element.')
+ .toBe(true);
});
it('should not clear custom classes provided by user', () => {
@@ -89,14 +90,13 @@ describe('MatList', () => {
fixture.detectChanges();
const listItem = fixture.debugElement.children[0].query(By.css('mat-list-item'))!;
- expect(listItem.nativeElement.classList.length).toBe(3);
- expect(listItem.nativeElement.classList).toContain('mat-2-line');
- expect(listItem.nativeElement.classList).toContain('mat-list-item');
- expect(listItem.nativeElement.classList).toContain('mat-focus-indicator');
+ expect(listItem.nativeElement.classList).toContain('mdc-list-item--with-two-lines');
+ 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-3-line');
+ expect(listItem.nativeElement.className).toContain('mdc-list-item--with-three-lines');
});
it('should not apply aria roles to mat-list', () => {
@@ -133,14 +133,18 @@ describe('MatList', () => {
.toBe('group');
});
- it('should not show ripples for non-nav lists', () => {
+ it('should not show ripples for non-nav lists', fakeAsync(() => {
const fixture = TestBed.createComponent(ListWithOneAnchorItem);
fixture.detectChanges();
const items: QueryList = fixture.debugElement.componentInstance.listItems;
expect(items.length).toBeGreaterThan(0);
- items.forEach(item => expect(item._isRippleDisabled()).toBe(true));
- });
+
+ items.forEach(item => {
+ dispatchMouseEvent(item._hostElement, 'mousedown');
+ expect(fixture.nativeElement.querySelector('.mat-ripple-element')).toBe(null);
+ });
+ }));
it('should allow disabling ripples for specific nav-list items', () => {
const fixture = TestBed.createComponent(NavListWithOneAnchorItem);
@@ -150,12 +154,12 @@ describe('MatList', () => {
expect(items.length).toBeGreaterThan(0);
// Ripples should be enabled by default, and can be disabled with a binding.
- items.forEach(item => expect(item._isRippleDisabled()).toBe(false));
+ items.forEach(item => expect(item.rippleDisabled).toBe(false));
fixture.componentInstance.disableItemRipple = true;
fixture.detectChanges();
- items.forEach(item => expect(item._isRippleDisabled()).toBe(true));
+ items.forEach(item => expect(item.rippleDisabled).toBe(true));
});
it('should create an action list', () => {
@@ -171,7 +175,7 @@ describe('MatList', () => {
fixture.detectChanges();
const host = fixture.nativeElement.querySelector('mat-action-list');
- expect(host.classList).toContain('mat-action-list');
+ expect(host.classList).toContain('mat-mdc-action-list');
});
it('should enable ripples for action lists by default', () => {
@@ -179,7 +183,7 @@ describe('MatList', () => {
fixture.detectChanges();
const items = fixture.componentInstance.listItems;
- expect(items.toArray().every(item => !item._isRippleDisabled())).toBe(true);
+ expect(items.toArray().every(item => !item.rippleDisabled)).toBe(true);
});
it('should allow disabling ripples for specific action list items', () => {
@@ -189,19 +193,19 @@ describe('MatList', () => {
const items = fixture.componentInstance.listItems.toArray();
expect(items.length).toBeGreaterThan(0);
- expect(items.every(item => !item._isRippleDisabled())).toBe(true);
+ expect(items.every(item => !item.rippleDisabled)).toBe(true);
fixture.componentInstance.disableItemRipple = true;
fixture.detectChanges();
- expect(items.every(item => item._isRippleDisabled())).toBe(true);
+ expect(items.every(item => item.rippleDisabled)).toBe(true);
});
it('should set default type attribute to button for action list', () => {
const fixture = TestBed.createComponent(ActionListWithoutType);
fixture.detectChanges();
- const listItemEl = fixture.debugElement.query(By.css('.mat-list-item'))!;
+ const listItemEl = fixture.debugElement.query(By.css('.mat-mdc-list-item'))!;
expect(listItemEl.nativeElement.getAttribute('type')).toBe('button');
});
@@ -209,7 +213,7 @@ describe('MatList', () => {
const fixture = TestBed.createComponent(ActionListWithType);
fixture.detectChanges();
- const listItemEl = fixture.debugElement.query(By.css('.mat-list-item'))!;
+ const listItemEl = fixture.debugElement.query(By.css('.mat-mdc-list-item'))!;
expect(listItemEl.nativeElement.getAttribute('type')).toBe('submit');
});
@@ -221,12 +225,12 @@ describe('MatList', () => {
expect(items.length).toBeGreaterThan(0);
// Ripples should be enabled by default, and can be disabled with a binding.
- items.forEach(item => expect(item._isRippleDisabled()).toBe(false));
+ items.forEach(item => expect(item.rippleDisabled).toBe(false));
fixture.componentInstance.disableListRipple = true;
fixture.detectChanges();
- items.forEach(item => expect(item._isRippleDisabled()).toBe(true));
+ items.forEach(item => expect(item.rippleDisabled).toBe(true));
});
it('should allow disabling ripples for the entire action list', () => {
@@ -236,19 +240,19 @@ describe('MatList', () => {
const items = fixture.componentInstance.listItems.toArray();
expect(items.length).toBeGreaterThan(0);
- expect(items.every(item => !item._isRippleDisabled())).toBe(true);
+ expect(items.every(item => !item.rippleDisabled)).toBe(true);
fixture.componentInstance.disableListRipple = true;
fixture.detectChanges();
- expect(items.every(item => item._isRippleDisabled())).toBe(true);
+ expect(items.every(item => item.rippleDisabled)).toBe(true);
});
it('should disable item ripples when list ripples are disabled via the input in nav list', fakeAsync(() => {
const fixture = TestBed.createComponent(NavListWithOneAnchorItem);
fixture.detectChanges();
- const rippleTarget = fixture.nativeElement.querySelector('.mat-list-item-content');
+ const rippleTarget = fixture.nativeElement.querySelector('.mat-mdc-list-item');
dispatchMouseEvent(rippleTarget, 'mousedown');
dispatchMouseEvent(rippleTarget, 'mouseup');
@@ -282,7 +286,7 @@ describe('MatList', () => {
const fixture = TestBed.createComponent(ActionListWithoutType);
fixture.detectChanges();
- const rippleTarget = fixture.nativeElement.querySelector('.mat-list-item-content');
+ const rippleTarget = fixture.nativeElement.querySelector('.mat-mdc-list-item');
dispatchMouseEvent(rippleTarget, 'mousedown');
dispatchMouseEvent(rippleTarget, 'mouseup');
@@ -321,7 +325,7 @@ describe('MatList', () => {
expect(
listItems.map(item => {
- return item.classList.contains('mat-list-item-disabled');
+ return item.classList.contains('mdc-list-item--disabled');
}),
).toEqual([false, false, false]);
@@ -330,7 +334,7 @@ describe('MatList', () => {
expect(
listItems.map(item => {
- return item.classList.contains('mat-list-item-disabled');
+ return item.classList.contains('mdc-list-item--disabled');
}),
).toEqual([true, false, false]);
});
@@ -342,12 +346,12 @@ describe('MatList', () => {
);
fixture.detectChanges();
- expect(listItems.every(item => item.classList.contains('mat-list-item-disabled'))).toBe(false);
+ expect(listItems.every(item => item.classList.contains('mdc-list-item--disabled'))).toBe(false);
fixture.componentInstance.listDisabled = true;
fixture.detectChanges();
- expect(listItems.every(item => item.classList.contains('mat-list-item-disabled'))).toBe(true);
+ expect(listItems.every(item => item.classList.contains('mdc-list-item--disabled'))).toBe(true);
});
});
@@ -429,8 +433,8 @@ class ListWithOneItem extends BaseTestList {}
- {{item.name}}
- {{item.description}}
+ {{item.name}}
+ {{item.description}}
`,
})
@@ -440,9 +444,9 @@ class ListWithTwoLineItem extends BaseTestList {}
template: `
- {{item.name}}
- {{item.description}}
- Some other text
+ {{item.name}}
+ {{item.description}}
+ Some other text
`,
})
@@ -452,10 +456,10 @@ class ListWithThreeLineItem extends BaseTestList {}
template: `
- Line 1
- Line 2
- Line 3
- Line 4
+ Line 1
+ Line 2
+ Line 3
+ Line 4
`,
})
@@ -465,7 +469,7 @@ class ListWithManyLines extends BaseTestList {}
template: `
-
+
Paprika
@@ -479,8 +483,8 @@ class ListWithAvatar extends BaseTestList {}
template: `
- {{item.name}}
- {{item.description}}
+ {{item.name}}
+ {{item.description}}
`,
})
@@ -490,9 +494,9 @@ class ListWithItemWithCssClass extends BaseTestList {}
template: `
- {{item.name}}
- {{item.description}}
- Some other text
+ {{item.name}}
+ {{item.description}}
+ Some other text
`,
})
diff --git a/src/material/list/list.ts b/src/material/list/list.ts
index 2973fb06bbcf..cfd38450f3e7 100644
--- a/src/material/list/list.ts
+++ b/src/material/list/list.ts
@@ -6,43 +6,26 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {coerceBooleanProperty, BooleanInput} from '@angular/cdk/coercion';
+import {Platform} from '@angular/cdk/platform';
import {
- AfterContentInit,
ChangeDetectionStrategy,
Component,
- ContentChild,
+ Input,
ContentChildren,
- Directive,
ElementRef,
+ Inject,
+ NgZone,
Optional,
QueryList,
+ ViewChild,
ViewEncapsulation,
- OnChanges,
- OnDestroy,
- ChangeDetectorRef,
- Input,
InjectionToken,
- Inject,
} from '@angular/core';
-import {
- CanDisable,
- CanDisableRipple,
- MatLine,
- setLines,
- mixinDisableRipple,
- mixinDisabled,
-} from '@angular/material/core';
-import {Subject} from 'rxjs';
-import {takeUntil} from 'rxjs/operators';
-
-// Boilerplate for applying mixins to MatList.
-/** @docs-private */
-const _MatListBase = mixinDisabled(mixinDisableRipple(class {}));
-
-// Boilerplate for applying mixins to MatListItem.
-/** @docs-private */
-const _MatListItemMixinBase = mixinDisableRipple(class {});
+import {MAT_RIPPLE_GLOBAL_OPTIONS, RippleGlobalOptions} from '@angular/material/core';
+import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
+import {MatListBase, MatListItemBase} from './list-base';
+import {MatListItemLine, MatListItemMeta, MatListItemTitle} from './list-item-sections';
+import {coerceBooleanProperty} from '@angular/cdk/coercion';
/**
* Injection token that can be used to inject instances of `MatList`. It serves as
@@ -51,206 +34,60 @@ const _MatListItemMixinBase = mixinDisableRipple(class {});
*/
export const MAT_LIST = new InjectionToken('MatList');
-/**
- * Injection token that can be used to inject instances of `MatNavList`. It serves as
- * alternative token to the actual `MatNavList` class which could cause unnecessary
- * retention of the class and its component metadata.
- */
-export const MAT_NAV_LIST = new InjectionToken('MatNavList');
-
@Component({
- selector: 'mat-nav-list',
- exportAs: 'matNavList',
- host: {
- 'role': 'navigation',
- 'class': 'mat-nav-list mat-list-base',
- },
- templateUrl: 'list.html',
- styleUrls: ['list.css'],
- inputs: ['disableRipple', 'disabled'],
- encapsulation: ViewEncapsulation.None,
- changeDetection: ChangeDetectionStrategy.OnPush,
- providers: [{provide: MAT_NAV_LIST, useExisting: MatNavList}],
-})
-export class MatNavList
- extends _MatListBase
- implements CanDisable, CanDisableRipple, OnChanges, OnDestroy
-{
- /** Emits when the state of the list changes. */
- readonly _stateChanges = new Subject();
-
- ngOnChanges() {
- this._stateChanges.next();
- }
-
- ngOnDestroy() {
- this._stateChanges.complete();
- }
-}
-
-@Component({
- selector: 'mat-list, mat-action-list',
+ selector: 'mat-list',
exportAs: 'matList',
- templateUrl: 'list.html',
+ template: ' ',
host: {
- 'class': 'mat-list mat-list-base',
+ 'class': 'mat-mdc-list mat-mdc-list-base mdc-list',
},
styleUrls: ['list.css'],
- inputs: ['disableRipple', 'disabled'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
- providers: [{provide: MAT_LIST, useExisting: MatList}],
-})
-export class MatList
- extends _MatListBase
- implements CanDisable, CanDisableRipple, OnChanges, OnDestroy
-{
- /** Emits when the state of the list changes. */
- readonly _stateChanges = new Subject();
-
- constructor(private _elementRef: ElementRef) {
- super();
-
- if (this._getListType() === 'action-list') {
- _elementRef.nativeElement.classList.add('mat-action-list');
- _elementRef.nativeElement.setAttribute('role', 'group');
- }
- }
-
- _getListType(): 'list' | 'action-list' | null {
- const nodeName = this._elementRef.nativeElement.nodeName.toLowerCase();
-
- if (nodeName === 'mat-list') {
- return 'list';
- }
-
- if (nodeName === 'mat-action-list') {
- return 'action-list';
- }
-
- return null;
- }
-
- ngOnChanges() {
- this._stateChanges.next();
- }
-
- ngOnDestroy() {
- this._stateChanges.complete();
- }
-}
-
-/**
- * Directive whose purpose is to add the mat- CSS styling to this selector.
- * @docs-private
- */
-@Directive({
- selector: '[mat-list-avatar], [matListAvatar]',
- host: {'class': 'mat-list-avatar'},
+ providers: [{provide: MatListBase, useExisting: MatList}],
})
-export class MatListAvatarCssMatStyler {}
+export class MatList extends MatListBase {}
-/**
- * Directive whose purpose is to add the mat- CSS styling to this selector.
- * @docs-private
- */
-@Directive({
- selector: '[mat-list-icon], [matListIcon]',
- host: {'class': 'mat-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]',
- host: {'class': 'mat-subheader'},
-})
-export class MatListSubheaderCssMatStyler {}
-
-/** An item within a Material Design list. */
@Component({
selector: 'mat-list-item, a[mat-list-item], button[mat-list-item]',
exportAs: 'matListItem',
host: {
- 'class': 'mat-list-item mat-focus-indicator',
- '[class.mat-list-item-disabled]': 'disabled',
- '[class.mat-list-item-with-avatar]': '_avatar || _icon',
+ 'class': 'mat-mdc-list-item mdc-list-item',
+ '[class.mdc-list-item--activated]': 'activated',
+ '[class.mdc-list-item--with-leading-avatar]': '_avatars.length !== 0',
+ '[class.mdc-list-item--with-leading-icon]': '_icons.length !== 0',
+ '[class.mdc-list-item--with-trailing-meta]': '_meta.length !== 0',
+ '[class._mat-animation-noopable]': '_noopAnimations',
},
- inputs: ['disableRipple'],
templateUrl: 'list-item.html',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class MatListItem
- extends _MatListItemMixinBase
- implements AfterContentInit, CanDisableRipple, OnDestroy
-{
- private _isInteractiveList: boolean = false;
- private _list?: MatNavList | MatList;
- private readonly _destroyed = new Subject();
-
- @ContentChildren(MatLine, {descendants: true}) _lines: QueryList;
- @ContentChild(MatListAvatarCssMatStyler) _avatar: MatListAvatarCssMatStyler;
- @ContentChild(MatListIconCssMatStyler) _icon: MatListIconCssMatStyler;
-
- constructor(
- private _element: ElementRef,
- _changeDetectorRef: ChangeDetectorRef,
- @Optional() @Inject(MAT_NAV_LIST) navList?: MatNavList,
- @Optional() @Inject(MAT_LIST) list?: MatList,
- ) {
- super();
- this._isInteractiveList = !!(navList || (list && list._getListType() === 'action-list'));
- this._list = navList || list;
-
- // If no type attribute is specified for , set it to "button".
- // If a type attribute is already specified, do nothing.
- const element = this._getHostElement();
-
- if (element.nodeName.toLowerCase() === 'button' && !element.hasAttribute('type')) {
- element.setAttribute('type', 'button');
- }
-
- if (this._list) {
- // React to changes in the state of the parent list since
- // some of the item's properties depend on it (e.g. `disableRipple`).
- this._list._stateChanges.pipe(takeUntil(this._destroyed)).subscribe(() => {
- _changeDetectorRef.markForCheck();
- });
- }
- }
-
- /** Whether the option is disabled. */
+export class MatListItem extends MatListItemBase {
+ @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;
+
+ /** Indicates whether an item in a `` is the currently active page. */
@Input()
- get disabled(): boolean {
- return this._disabled || !!(this._list && this._list.disabled);
+ get activated() {
+ return this._activated;
}
- set disabled(value: BooleanInput) {
- this._disabled = coerceBooleanProperty(value);
+ set activated(activated) {
+ this._activated = coerceBooleanProperty(activated);
}
- private _disabled = false;
+ _activated = false;
- ngAfterContentInit() {
- setLines(this._lines, this._element);
- }
-
- ngOnDestroy() {
- this._destroyed.next();
- this._destroyed.complete();
- }
-
- /** Whether this list item should show a ripple effect when clicked. */
- _isRippleDisabled() {
- return (
- !this._isInteractiveList || this.disableRipple || !!(this._list && this._list.disableRipple)
- );
- }
-
- /** Retrieves the DOM element of the component host. */
- _getHostElement(): HTMLElement {
- return this._element.nativeElement;
+ constructor(
+ element: ElementRef,
+ ngZone: NgZone,
+ listBase: MatListBase,
+ platform: Platform,
+ @Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) globalRippleOptions?: RippleGlobalOptions,
+ @Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string,
+ ) {
+ super(element, ngZone, listBase, platform, globalRippleOptions, animationMode);
}
}
diff --git a/src/material-experimental/mdc-list/nav-list.ts b/src/material/list/nav-list.ts
similarity index 74%
rename from src/material-experimental/mdc-list/nav-list.ts
rename to src/material/list/nav-list.ts
index cbbfca60bda6..75525e6ebb98 100644
--- a/src/material-experimental/mdc-list/nav-list.ts
+++ b/src/material/list/nav-list.ts
@@ -6,9 +6,16 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core';
+import {ChangeDetectionStrategy, Component, InjectionToken, ViewEncapsulation} from '@angular/core';
import {MatListBase} from './list-base';
+/**
+ * Injection token that can be used to inject instances of `MatNavList`. It serves as
+ * alternative token to the actual `MatNavList` class which could cause unnecessary
+ * retention of the class and its component metadata.
+ */
+export const MAT_NAV_LIST = new InjectionToken('MatNavList');
+
@Component({
selector: 'mat-nav-list',
exportAs: 'matNavList',
diff --git a/src/material/list/public-api.ts b/src/material/list/public-api.ts
index 48e73ad07f08..7e8b6b3e6ebe 100644
--- a/src/material/list/public-api.ts
+++ b/src/material/list/public-api.ts
@@ -6,6 +6,14 @@
* found in the LICENSE file at https://angular.io/license
*/
-export * from './list-module';
+export * from './action-list';
export * from './list';
+export * from './list-module';
+export * from './nav-list';
export * from './selection-list';
+export * from './list-option';
+export * from './subheader';
+export * from './list-item-sections';
+
+export {MatListOptionCheckboxPosition} from './list-option-types';
+export {MatListOption} from './list-option';
diff --git a/src/material/list/selection-list.spec.ts b/src/material/list/selection-list.spec.ts
index 843ccd4e4338..9e063a85dde6 100644
--- a/src/material/list/selection-list.spec.ts
+++ b/src/material/list/selection-list.spec.ts
@@ -1,5 +1,4 @@
-import {FocusMonitor} from '@angular/cdk/a11y';
-import {A, D, DOWN_ARROW, END, ENTER, HOME, SPACE, TAB, UP_ARROW} from '@angular/cdk/keycodes';
+import {A, D, DOWN_ARROW, END, ENTER, HOME, SPACE, UP_ARROW} from '@angular/cdk/keycodes';
import {
createKeyboardEvent,
dispatchEvent,
@@ -18,17 +17,24 @@ import {
ComponentFixture,
fakeAsync,
flush,
- inject,
TestBed,
tick,
waitForAsync,
} from '@angular/core/testing';
import {FormControl, FormsModule, NgModel, ReactiveFormsModule} from '@angular/forms';
-import {MatRipple, ThemePalette} from '@angular/material/core';
+import {ThemePalette} from '@angular/material/core';
import {By} from '@angular/platform-browser';
-import {MatListModule, MatListOption, MatSelectionList, MatSelectionListChange} from './index';
+import {
+ MatListModule,
+ MatListOption,
+ MatListOptionCheckboxPosition,
+ MatSelectionList,
+ MatSelectionListChange,
+} from './index';
+
+describe('MDC-based MatSelectionList without forms', () => {
+ const typeaheadInterval = 200;
-describe('MatSelectionList without forms', () => {
describe('with list option', () => {
let fixture: ComponentFixture;
let listOptions: DebugElement[];
@@ -58,6 +64,10 @@ describe('MatSelectionList without forms', () => {
selectionList = fixture.debugElement.query(By.directive(MatSelectionList))!;
}));
+ function getFocusIndex() {
+ return listOptions.findIndex(o => document.activeElement === o.nativeElement);
+ }
+
it('should be able to set a value on a list option', () => {
const optionValues = ['inbox', 'starred', 'sent-mail', 'archive', 'drafts'];
@@ -82,7 +92,7 @@ describe('MatSelectionList without forms', () => {
expect(fixture.componentInstance.onSelectionChange).toHaveBeenCalledTimes(0);
- dispatchFakeEvent(listOptions[2].nativeElement, 'click');
+ dispatchMouseEvent(listOptions[2].nativeElement, 'click');
fixture.detectChanges();
expect(fixture.componentInstance.onSelectionChange).toHaveBeenCalledTimes(1);
@@ -152,7 +162,7 @@ describe('MatSelectionList without forms', () => {
fixture.componentInstance.firstOptionColor = 'primary';
fixture.detectChanges();
- expect(optionNativeElements[0].classList).toContain('mat-primary');
+ expect(optionNativeElements[0].classList).not.toContain('mat-accent');
expect(optionNativeElements[0].classList).not.toContain('mat-warn');
expect(
optionNativeElements.slice(1).every(option => option.classList.contains('mat-warn')),
@@ -165,7 +175,6 @@ describe('MatSelectionList without forms', () => {
fixture.componentInstance.firstOptionColor = 'primary';
fixture.detectChanges();
- expect(classList).toContain('mat-primary');
expect(classList).not.toContain('mat-accent');
expect(classList).not.toContain('mat-warn');
@@ -195,15 +204,15 @@ describe('MatSelectionList without forms', () => {
expect(selectList.selected.length).toBe(0);
});
- it('should not add the mat-list-single-selected-option class (in multiple mode)', () => {
+ it('should not add the mdc-list-item--selected class (in multiple mode)', () => {
let testListItem = listOptions[2].injector.get(MatListOption);
- testListItem._handleClick();
+ dispatchMouseEvent(testListItem._hostElement, 'click');
fixture.detectChanges();
- expect(
- listOptions[2].nativeElement.classList.contains('mat-list-single-selected-option'),
- ).toBe(false);
+ expect(listOptions[2].nativeElement.classList.contains('mdc-list-item--selected')).toBe(
+ false,
+ );
});
it('should not allow selection of disabled items', () => {
@@ -211,13 +220,13 @@ describe('MatSelectionList without forms', () => {
let selectList =
selectionList.injector.get(MatSelectionList).selectedOptions;
- expect(selectList.selected.length).toBe(0);
+ expect(selectList.selected.length).withContext('before click').toBe(0);
expect(listOptions[0].nativeElement.getAttribute('aria-disabled')).toBe('true');
- testListItem._handleClick();
+ dispatchMouseEvent(testListItem._hostElement, 'click');
fixture.detectChanges();
- expect(selectList.selected.length).toBe(0);
+ expect(selectList.selected.length).withContext('after click').toBe(0);
});
it('should be able to un-disable disabled items', () => {
@@ -233,53 +242,34 @@ describe('MatSelectionList without forms', () => {
it('should be able to use keyboard select with SPACE', () => {
const testListItem = listOptions[1].nativeElement as HTMLElement;
- const SPACE_EVENT = createKeyboardEvent('keydown', SPACE);
const selectList =
selectionList.injector.get(MatSelectionList).selectedOptions;
expect(selectList.selected.length).toBe(0);
- dispatchFakeEvent(testListItem, 'focus');
- selectionList.componentInstance._keydown(SPACE_EVENT);
+ testListItem.focus();
+ expect(getFocusIndex()).toBe(1);
+ const event = dispatchKeyboardEvent(testListItem, 'keydown', SPACE);
fixture.detectChanges();
expect(selectList.selected.length).toBe(1);
- expect(SPACE_EVENT.defaultPrevented).toBe(true);
+ expect(event.defaultPrevented).toBe(true);
});
it('should be able to select an item using ENTER', () => {
const testListItem = listOptions[1].nativeElement as HTMLElement;
- const ENTER_EVENT = createKeyboardEvent('keydown', ENTER);
const selectList =
selectionList.injector.get(MatSelectionList).selectedOptions;
expect(selectList.selected.length).toBe(0);
- dispatchFakeEvent(testListItem, 'focus');
- selectionList.componentInstance._keydown(ENTER_EVENT);
+ testListItem.focus();
+ expect(getFocusIndex()).toBe(1);
+ const event = dispatchKeyboardEvent(testListItem, 'keydown', ENTER);
fixture.detectChanges();
expect(selectList.selected.length).toBe(1);
- expect(ENTER_EVENT.defaultPrevented).toBe(true);
- });
-
- it('should not be able to toggle an item when pressing a modifier key', () => {
- const testListItem = listOptions[1].nativeElement as HTMLElement;
- const selectList =
- selectionList.injector.get(MatSelectionList).selectedOptions;
-
- expect(selectList.selected.length).toBe(0);
-
- [ENTER, SPACE].forEach(key => {
- const event = createKeyboardEvent('keydown', key, undefined, {control: true});
-
- dispatchFakeEvent(testListItem, 'focus');
- selectionList.componentInstance._keydown(event);
- fixture.detectChanges();
- expect(event.defaultPrevented).toBe(false);
- });
-
- expect(selectList.selected.length).toBe(0);
+ expect(event.defaultPrevented).toBe(true);
});
it('should not be able to toggle a disabled option using SPACE', () => {
@@ -289,244 +279,98 @@ describe('MatSelectionList without forms', () => {
expect(selectionModel.selected.length).toBe(0);
listOptions[1].componentInstance.disabled = true;
+ fixture.detectChanges();
- dispatchFakeEvent(testListItem, 'focus');
- selectionList.componentInstance._keydown(createKeyboardEvent('keydown', SPACE));
+ testListItem.focus();
+ expect(getFocusIndex()).toBe(1);
+
+ dispatchKeyboardEvent(testListItem, 'keydown', SPACE);
fixture.detectChanges();
expect(selectionModel.selected.length).toBe(0);
});
it('should focus the first option when the list takes focus for the first time', () => {
- spyOn(listOptions[0].componentInstance, 'focus').and.callThrough();
-
- const manager = selectionList.componentInstance._keyManager;
- expect(manager.activeItemIndex).toBe(-1);
-
- dispatchFakeEvent(selectionList.nativeElement, 'focus');
- fixture.detectChanges();
-
- expect(manager.activeItemIndex).toBe(0);
- expect(listOptions[0].componentInstance.focus).toHaveBeenCalled();
+ expect(listOptions[0].nativeElement.tabIndex).toBe(0);
+ expect(listOptions.slice(1).every(o => o.nativeElement.tabIndex === -1)).toBe(true);
});
- it('should not move focus to the first item if focus originated from a mouse interaction', fakeAsync(
- inject([FocusMonitor], (focusMonitor: FocusMonitor) => {
- spyOn(listOptions[0].componentInstance, 'focus').and.callThrough();
-
- const manager = selectionList.componentInstance._keyManager;
- expect(manager.activeItemIndex).toBe(-1);
-
- focusMonitor.focusVia(selectionList.nativeElement, 'mouse');
- fixture.detectChanges();
- flush();
-
- expect(manager.activeItemIndex).toBe(-1);
- expect(listOptions[0].componentInstance.focus).not.toHaveBeenCalled();
- }),
- ));
-
- it('should focus the first selected option when list receives focus', () => {
- spyOn(listOptions[2].componentInstance, 'focus').and.callThrough();
-
- const manager = selectionList.componentInstance._keyManager;
- expect(manager.activeItemIndex).toBe(-1);
-
+ it('should focus the first selected option when list receives focus', fakeAsync(() => {
dispatchMouseEvent(listOptions[2].nativeElement, 'click');
fixture.detectChanges();
- dispatchMouseEvent(listOptions[3].nativeElement, 'click');
- fixture.detectChanges();
-
- dispatchFakeEvent(selectionList.nativeElement, 'focus');
- fixture.detectChanges();
-
- expect(manager.activeItemIndex).toBe(2);
- expect(listOptions[2].componentInstance.focus).toHaveBeenCalled();
- });
-
- it('should allow focus to escape when tabbing away', fakeAsync(() => {
- selectionList.componentInstance._keyManager.onKeydown(createKeyboardEvent('keydown', TAB));
-
- expect(selectionList.componentInstance._tabIndex)
- .withContext('Expected tabIndex to be set to -1 temporarily.')
- .toBe(-1);
+ expect(listOptions.map(o => o.nativeElement.tabIndex)).toEqual([-1, -1, 0, -1, -1]);
- tick();
-
- expect(selectionList.componentInstance._tabIndex)
- .withContext('Expected tabIndex to be reset back to 0')
- .toBe(0);
- }));
-
- it('should restore focus if active option is destroyed', () => {
- const manager = selectionList.componentInstance._keyManager;
-
- spyOn(listOptions[3].componentInstance, 'focus').and.callThrough();
- listOptions[4].componentInstance._handleFocus();
-
- expect(manager.activeItemIndex).toBe(4);
-
- fixture.componentInstance.showLastOption = false;
+ dispatchMouseEvent(listOptions[1].nativeElement, 'click');
fixture.detectChanges();
- expect(manager.activeItemIndex).toBe(3);
- expect(listOptions[3].componentInstance.focus).toHaveBeenCalled();
- });
-
- it('should not attempt to focus the next option when the destroyed option was not focused', () => {
- const manager = selectionList.componentInstance._keyManager;
-
- // Focus and blur the option to move the active item index.
- listOptions[4].componentInstance._handleFocus();
- listOptions[4].componentInstance._handleBlur();
-
- spyOn(listOptions[3].componentInstance, 'focus').and.callThrough();
+ expect(listOptions.map(o => o.nativeElement.tabIndex)).toEqual([-1, 0, -1, -1, -1]);
- expect(manager.activeItemIndex).toBe(4);
-
- fixture.componentInstance.showLastOption = false;
+ // De-select both options to ensure that the first item in the list-item
+ // becomes the designated option for focus.
+ dispatchMouseEvent(listOptions[1].nativeElement, 'click');
+ dispatchMouseEvent(listOptions[2].nativeElement, 'click');
fixture.detectChanges();
- expect(manager.activeItemIndex).toBe(3);
- expect(listOptions[3].componentInstance.focus).not.toHaveBeenCalled();
- });
+ expect(listOptions.map(o => o.nativeElement.tabIndex)).toEqual([0, -1, -1, -1, -1]);
+ }));
it('should focus previous item when press UP ARROW', () => {
- let UP_EVENT = createKeyboardEvent('keydown', UP_ARROW);
- let manager = selectionList.componentInstance._keyManager;
-
- dispatchFakeEvent(listOptions[2].nativeElement, 'focus');
- expect(manager.activeItemIndex).toEqual(2);
-
- selectionList.componentInstance._keydown(UP_EVENT);
+ listOptions[2].nativeElement.focus();
+ expect(getFocusIndex()).toEqual(2);
+ dispatchKeyboardEvent(listOptions[2].nativeElement, 'keydown', UP_ARROW);
fixture.detectChanges();
- expect(manager.activeItemIndex).toEqual(1);
- });
-
- it('should focus and toggle the next item when pressing SHIFT + UP_ARROW', () => {
- const manager = selectionList.componentInstance._keyManager;
- const upKeyEvent = createKeyboardEvent('keydown', UP_ARROW, undefined, {shift: true});
-
- dispatchFakeEvent(listOptions[3].nativeElement, 'focus');
- expect(manager.activeItemIndex).toBe(3);
-
- expect(listOptions[1].componentInstance.selected).toBe(false);
- expect(listOptions[2].componentInstance.selected).toBe(false);
-
- selectionList.componentInstance._keydown(upKeyEvent);
- fixture.detectChanges();
-
- expect(listOptions[1].componentInstance.selected).toBe(false);
- expect(listOptions[2].componentInstance.selected).toBe(true);
-
- selectionList.componentInstance._keydown(upKeyEvent);
- fixture.detectChanges();
-
- expect(listOptions[1].componentInstance.selected).toBe(true);
- expect(listOptions[2].componentInstance.selected).toBe(true);
+ expect(getFocusIndex()).toEqual(1);
});
it('should focus next item when press DOWN ARROW', () => {
- const manager = selectionList.componentInstance._keyManager;
-
- dispatchFakeEvent(listOptions[2].nativeElement, 'focus');
- expect(manager.activeItemIndex).toEqual(2);
+ listOptions[2].nativeElement.focus();
+ expect(getFocusIndex()).toEqual(2);
- selectionList.componentInstance._keydown(createKeyboardEvent('keydown', DOWN_ARROW));
+ dispatchKeyboardEvent(listOptions[2].nativeElement, 'keydown', DOWN_ARROW);
fixture.detectChanges();
- expect(manager.activeItemIndex).toEqual(3);
- });
-
- it('should focus and toggle the next item when pressing SHIFT + DOWN_ARROW', () => {
- const manager = selectionList.componentInstance._keyManager;
- const downKeyEvent = createKeyboardEvent('keydown', DOWN_ARROW, undefined, {shift: true});
-
- dispatchFakeEvent(listOptions[0].nativeElement, 'focus');
- expect(manager.activeItemIndex).toBe(0);
-
- expect(listOptions[1].componentInstance.selected).toBe(false);
- expect(listOptions[2].componentInstance.selected).toBe(false);
-
- selectionList.componentInstance._keydown(downKeyEvent);
- fixture.detectChanges();
-
- expect(listOptions[1].componentInstance.selected).toBe(true);
- expect(listOptions[2].componentInstance.selected).toBe(false);
-
- selectionList.componentInstance._keydown(downKeyEvent);
- fixture.detectChanges();
-
- expect(listOptions[1].componentInstance.selected).toBe(true);
- expect(listOptions[2].componentInstance.selected).toBe(true);
+ expect(getFocusIndex()).toEqual(3);
});
it('should be able to focus the first item when pressing HOME', () => {
- const manager = selectionList.componentInstance._keyManager;
- expect(manager.activeItemIndex).toBe(-1);
+ listOptions[2].nativeElement.focus();
+ expect(getFocusIndex()).toBe(2);
- const event = dispatchKeyboardEvent(selectionList.nativeElement, 'keydown', HOME);
+ const event = dispatchKeyboardEvent(listOptions[2].nativeElement, 'keydown', HOME);
fixture.detectChanges();
- expect(manager.activeItemIndex).toBe(0);
+ expect(getFocusIndex()).toBe(0);
expect(event.defaultPrevented).toBe(true);
});
- it('should not change focus when pressing HOME with a modifier key', () => {
- const manager = selectionList.componentInstance._keyManager;
- expect(manager.activeItemIndex).toBe(-1);
-
- const event = createKeyboardEvent('keydown', HOME, undefined, {alt: true});
-
- dispatchEvent(selectionList.nativeElement, event);
- fixture.detectChanges();
-
- expect(manager.activeItemIndex).toBe(-1);
- expect(event.defaultPrevented).toBe(false);
- });
-
it('should focus the last item when pressing END', () => {
- const manager = selectionList.componentInstance._keyManager;
- expect(manager.activeItemIndex).toBe(-1);
+ listOptions[2].nativeElement.focus();
+ expect(getFocusIndex()).toBe(2);
- const event = dispatchKeyboardEvent(selectionList.nativeElement, 'keydown', END);
+ const event = dispatchKeyboardEvent(listOptions[2].nativeElement, 'keydown', END);
fixture.detectChanges();
- expect(manager.activeItemIndex).toBe(4);
+ expect(getFocusIndex()).toBe(4);
expect(event.defaultPrevented).toBe(true);
});
- it('should not change focus when pressing END with a modifier key', () => {
- const manager = selectionList.componentInstance._keyManager;
- expect(manager.activeItemIndex).toBe(-1);
-
- const event = createKeyboardEvent('keydown', END, undefined, {alt: true});
-
- dispatchEvent(selectionList.nativeElement, event);
- fixture.detectChanges();
-
- expect(manager.activeItemIndex).toBe(-1);
- expect(event.defaultPrevented).toBe(false);
- });
-
it('should select all items using ctrl + a', () => {
listOptions.forEach(option => (option.componentInstance.disabled = false));
- const event = createKeyboardEvent('keydown', A, undefined, {control: true});
+ fixture.detectChanges();
expect(listOptions.some(option => option.componentInstance.selected)).toBe(false);
- dispatchEvent(selectionList.nativeElement, event);
+ 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', () => {
- const event = createKeyboardEvent('keydown', A, undefined, {control: true});
-
listOptions.slice(0, 2).forEach(option => (option.componentInstance.disabled = true));
fixture.detectChanges();
@@ -538,7 +382,8 @@ describe('MatSelectionList without forms', () => {
false,
]);
- dispatchEvent(selectionList.nativeElement, event);
+ listOptions[3].nativeElement.focus();
+ dispatchKeyboardEvent(listOptions[3].nativeElement, 'keydown', A, 'A', {control: true});
fixture.detectChanges();
expect(listOptions.map(option => option.componentInstance.selected)).toEqual([
@@ -551,28 +396,26 @@ describe('MatSelectionList without forms', () => {
});
it('should select all items using ctrl + a if some items are selected', () => {
- const event = createKeyboardEvent('keydown', A, undefined, {control: true});
-
listOptions.slice(0, 2).forEach(option => (option.componentInstance.selected = true));
fixture.detectChanges();
expect(listOptions.some(option => option.componentInstance.selected)).toBe(true);
- dispatchEvent(selectionList.nativeElement, event);
+ 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', () => {
- const event = createKeyboardEvent('keydown', A, undefined, {control: true});
-
listOptions.forEach(option => (option.componentInstance.selected = true));
fixture.detectChanges();
expect(listOptions.every(option => option.componentInstance.selected)).toBe(true);
- dispatchEvent(selectionList.nativeElement, event);
+ listOptions[2].nativeElement.focus();
+ dispatchKeyboardEvent(listOptions[2].nativeElement, 'keydown', A, 'A', {control: true});
fixture.detectChanges();
expect(listOptions.every(option => option.componentInstance.selected)).toBe(false);
@@ -581,9 +424,10 @@ describe('MatSelectionList without forms', () => {
it('should dispatch the selectionChange event when selecting via ctrl + a', () => {
const spy = spyOn(fixture.componentInstance, 'onSelectionChange');
listOptions.forEach(option => (option.componentInstance.disabled = false));
- const event = createKeyboardEvent('keydown', A, undefined, {control: true});
+ fixture.detectChanges();
- dispatchEvent(selectionList.nativeElement, event);
+ listOptions[2].nativeElement.focus();
+ dispatchKeyboardEvent(listOptions[2].nativeElement, 'keydown', A, 'A', {control: true});
fixture.detectChanges();
expect(spy).toHaveBeenCalledTimes(1);
@@ -595,71 +439,70 @@ describe('MatSelectionList without forms', () => {
});
it('should be able to jump focus down to an item by typing', fakeAsync(() => {
- const listEl = selectionList.nativeElement;
- const manager = selectionList.componentInstance._keyManager;
+ const firstOption = listOptions[0].nativeElement;
- expect(manager.activeItemIndex).toBe(-1);
+ firstOption.focus();
+ expect(getFocusIndex()).toBe(0);
- dispatchEvent(listEl, createKeyboardEvent('keydown', 83, 's'));
+ dispatchEvent(firstOption, createKeyboardEvent('keydown', 83, 's'));
fixture.detectChanges();
- tick(200);
+ tick(typeaheadInterval);
- expect(manager.activeItemIndex).toBe(1);
+ expect(getFocusIndex()).toBe(1);
- dispatchEvent(listEl, createKeyboardEvent('keydown', 68, 'd'));
+ dispatchEvent(firstOption, createKeyboardEvent('keydown', 68, 'd'));
fixture.detectChanges();
- tick(200);
+ tick(typeaheadInterval);
- expect(manager.activeItemIndex).toBe(4);
+ expect(getFocusIndex()).toBe(4);
}));
it('should be able to skip to an item by typing', fakeAsync(() => {
- const manager = selectionList.componentInstance._keyManager;
+ listOptions[0].nativeElement.focus();
+ expect(getFocusIndex()).toBe(0);
- expect(manager.activeItemIndex).not.toBe(4);
-
- const event = createKeyboardEvent('keydown', D, 'd');
- selectionList.componentInstance._keydown(event);
+ dispatchKeyboardEvent(listOptions[0].nativeElement, 'keydown', D, 'd');
fixture.detectChanges();
- tick(200);
+ tick(typeaheadInterval);
- expect(manager.activeItemIndex).toBe(4);
+ expect(getFocusIndex()).toBe(4);
}));
- // Test for "A" specifically, because it's a special case that can be used to select all values.
+ // Test for "A" specifically, because it's a special case that can be used
+ // to select all values.
it('should be able to skip to an item by typing the letter "A"', fakeAsync(() => {
- const manager = selectionList.componentInstance._keyManager;
-
- expect(manager.activeItemIndex).not.toBe(3);
+ listOptions[0].nativeElement.focus();
+ expect(getFocusIndex()).toBe(0);
- const event = createKeyboardEvent('keydown', A, 'a');
- selectionList.componentInstance._keydown(event);
+ dispatchKeyboardEvent(listOptions[0].nativeElement, 'keydown', A, 'a');
fixture.detectChanges();
- tick(200);
+ tick(typeaheadInterval);
- expect(manager.activeItemIndex).toBe(3);
+ expect(getFocusIndex()).toBe(3);
}));
it('should not select items while using the typeahead', fakeAsync(() => {
- const manager = selectionList.componentInstance._keyManager;
const testListItem = listOptions[1].nativeElement as HTMLElement;
const model = selectionList.injector.get(MatSelectionList).selectedOptions;
+ testListItem.focus();
dispatchFakeEvent(testListItem, 'focus');
fixture.detectChanges();
- expect(manager.activeItemIndex).toBe(1);
+ expect(getFocusIndex()).toBe(1);
expect(model.isEmpty()).toBe(true);
- selectionList.componentInstance._keydown(createKeyboardEvent('keydown', D, 'd'));
+ dispatchKeyboardEvent(testListItem, 'keydown', D, 'd');
fixture.detectChanges();
- tick(100); // Tick only half the typeahead timeout.
+ tick(typeaheadInterval / 2); // Tick only half the typeahead timeout.
- selectionList.componentInstance._keydown(createKeyboardEvent('keydown', SPACE));
+ dispatchKeyboardEvent(testListItem, 'keydown', SPACE);
fixture.detectChanges();
- tick(100); // Tick the rest of the timeout.
+ // Tick the buffer timeout again as a new key has been pressed that resets
+ // the buffer timeout.
+ tick(typeaheadInterval);
- expect(manager.activeItemIndex).toBe(4);
+ expect(getFocusIndex()).toBe(4);
expect(model.isEmpty()).toBe(true);
}));
@@ -764,9 +607,8 @@ describe('MatSelectionList without forms', () => {
it('should disable list item ripples when the ripples on the list have been disabled', fakeAsync(() => {
const rippleTarget = fixture.nativeElement.querySelector(
- '.mat-list-option:not(.mat-list-item-disabled) .mat-list-item-content',
+ '.mat-mdc-list-option:not(.mdc-list-item--disabled)',
);
-
dispatchMouseEvent(rippleTarget, 'mousedown');
dispatchMouseEvent(rippleTarget, 'mouseup');
@@ -789,7 +631,6 @@ describe('MatSelectionList without forms', () => {
dispatchMouseEvent(rippleTarget, 'mousedown');
dispatchMouseEvent(rippleTarget, 'mouseup');
- flush();
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length)
.withContext('Expected no ripples after list ripples are disabled.')
@@ -805,17 +646,26 @@ describe('MatSelectionList without forms', () => {
});
it('should have a focus indicator', () => {
- const optionNativeElements = listOptions.map(option => option.nativeElement);
+ const optionNativeElements = listOptions.map(option => option.nativeElement as HTMLElement);
expect(
- optionNativeElements.every(element => element.classList.contains('mat-focus-indicator')),
+ optionNativeElements.every(
+ element => element.querySelector('.mat-mdc-focus-indicator') !== null,
+ ),
).toBe(true);
});
+
+ it('should hide the internal SVG', () => {
+ listOptions.forEach(option => {
+ const svg = option.nativeElement.querySelector('.mdc-checkbox svg');
+ expect(svg.getAttribute('aria-hidden')).toBe('true');
+ });
+ });
});
describe('with list option selected', () => {
let fixture: ComponentFixture;
- let listItemEl: DebugElement;
+ let listOptionElements: DebugElement[];
let selectionList: DebugElement;
beforeEach(waitForAsync(() => {
@@ -829,49 +679,28 @@ describe('MatSelectionList without forms', () => {
beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(SelectionListWithSelectedOption);
- listItemEl = fixture.debugElement.query(By.directive(MatListOption))!;
+ listOptionElements = fixture.debugElement.queryAll(By.directive(MatListOption))!;
selectionList = fixture.debugElement.query(By.directive(MatSelectionList))!;
fixture.detectChanges();
}));
it('should set its initial selected state in the selectedOptions', () => {
- let optionEl = listItemEl.injector.get