-
Normal lists
@@ -16,18 +18,18 @@
Normal lists
- {{contact.name}}
- extra line
- {{contact.headline}}
+ {{contact.name}}
+ extra line
+ {{contact.headline}}
Today
-
- {{message.from}}
-
+
+
{{message.from}}
+
{{message.subject}} --
{{message.message}}
@@ -35,16 +37,16 @@
Normal lists
- {{message.from}}
- {{message.subject}}
- {{message.message}}
+ {{message.from}}
+ {{message.subject}}
+ {{message.message}}
- {{ link.name }}
-
+ {{ link.name }}
+
info
@@ -62,25 +64,25 @@ Dense lists
- {{contact.name}}
- {{contact.headline}}
+ {{contact.name}}
+ {{contact.headline}}
Today
-
- {{message.from}}
- {{message.subject}}
- {{message.message}}
+
+ {{message.from}}
+ {{message.subject}}
+ {{message.message}}
- {{ link.name }}
-
+ {{ link.name }}
+
info
@@ -89,7 +91,7 @@ Dense lists
-
- folder
- {{ link.name }}
+
+ folder
+ {{ link.name }}
@@ -134,13 +136,13 @@ Selection list
Dogs
-
- Shiba Inu
+
+ Shiba Inu
-
- Other Shiba Inu
+
+ Other Shiba Inu
@@ -183,21 +185,52 @@ Single Selection list
Selected: {{favoriteOptions | json}}
+
+
Line scenarios
+
+
+ Title
+
+ Title
+ This is unscoped text content that is supposed to not wrap. The list has only
+ acquired two lines and therefore there is no space for wrapping.
+
+
+ Title
+ This is unscoped text content that is supposed to wrap to the third line.
+ The list item acquire spaces for three lines and text should have an ellipsis in the
+ third line upon text overflow.
+
+
+ Title
+ This is unscoped text content that is supposed to not wrap. The list has only
+ acquired two lines (automatically) and therefore there is no space for wrapping.
+
+
+ Title
+ Secondary line
+ Tertiary line
+
+
+
+ Show item boxes
+
+
Line alignment
- {{ link.name }}
- Not in an matLine
+ {{ link.name }}
+ Unscoped content
First
- Second
- Not in an matLine
+ Second
+ Unscoped content
@@ -207,19 +240,19 @@
Icon alignment in selection list
- info
+ info
Bananas
- info
+ info
Oranges
- info
+ info
Cake
- info
+ info
Fries
diff --git a/src/dev-app/mdc-list/mdc-list-demo.scss b/src/dev-app/mdc-list/mdc-list-demo.scss
index 3f5fd9822baf..30bed5b5e262 100644
--- a/src/dev-app/mdc-list/mdc-list-demo.scss
+++ b/src/dev-app/mdc-list/mdc-list-demo.scss
@@ -15,6 +15,10 @@
.mat-mdc-icon-button {
color: rgba(0, 0, 0, 0.12);
}
+
+ &.demo-show-boxes .mat-mdc-list-item {
+ border: 1px solid grey;
+ }
}
.demo-secondary-text {
diff --git a/src/dev-app/mdc-list/mdc-list-demo.ts b/src/dev-app/mdc-list/mdc-list-demo.ts
index 5296c897d33f..6bd7b8dd695d 100644
--- a/src/dev-app/mdc-list/mdc-list-demo.ts
+++ b/src/dev-app/mdc-list/mdc-list-demo.ts
@@ -27,10 +27,10 @@ export class MdcListDemo {
messages: {from: string; subject: string; message: string; image: string}[] = [
{
- from: 'Nancy',
+ from: 'John',
subject: 'Brunch?',
message: 'Did you want to go on Sunday? I was thinking that might work.',
- image: 'https://angular.io/generated/images/bios/cindygreenekaplan.jpg',
+ image: 'https://angular.io/generated/images/bios/devversion.jpg',
},
{
from: 'Mary',
@@ -46,9 +46,15 @@ export class MdcListDemo {
},
];
- links: {name: string}[] = [{name: 'Inbox'}, {name: 'Outbox'}, {name: 'Spam'}, {name: 'Trash'}];
+ links: {name: string; href: string}[] = [
+ {name: 'Inbox', href: '/mdc-list#inbox'},
+ {name: 'Outbox', href: '/mdc-list#outbox'},
+ {name: 'Spam', href: '/mdc-list#spam'},
+ {name: 'Trash', href: '/mdc-list#trash'},
+ ];
thirdLine = false;
+ showBoxes = false;
infoClicked = false;
selectionListDisabled = false;
selectionListRippleDisabled = false;
@@ -71,4 +77,8 @@ export class MdcListDemo {
alertItem(msg: string) {
alert(msg);
}
+
+ isActivated(href: string) {
+ return window.location.href === new URL(href, window.location.href).toString();
+ }
}
diff --git a/src/dev-app/mdc-sidenav/BUILD.bazel b/src/dev-app/mdc-sidenav/BUILD.bazel
deleted file mode 100644
index 1f8b93d549a5..000000000000
--- a/src/dev-app/mdc-sidenav/BUILD.bazel
+++ /dev/null
@@ -1,21 +0,0 @@
-load("//tools:defaults.bzl", "ng_module", "sass_binary")
-
-package(default_visibility = ["//visibility:public"])
-
-ng_module(
- name = "mdc-sidenav",
- srcs = glob(["**/*.ts"]),
- assets = [
- "mdc-sidenav-demo.html",
- ":mdc_sidenav_demo_scss",
- ],
- deps = [
- "//src/material-experimental/mdc-sidenav",
- "@npm//@angular/router",
- ],
-)
-
-sass_binary(
- name = "mdc_sidenav_demo_scss",
- src = "mdc-sidenav-demo.scss",
-)
diff --git a/src/dev-app/mdc-sidenav/mdc-sidenav-demo-module.ts b/src/dev-app/mdc-sidenav/mdc-sidenav-demo-module.ts
deleted file mode 100644
index 59a0ab1edc9a..000000000000
--- a/src/dev-app/mdc-sidenav/mdc-sidenav-demo-module.ts
+++ /dev/null
@@ -1,18 +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 {NgModule} from '@angular/core';
-import {MatSidenavModule} from '@angular/material-experimental/mdc-sidenav';
-import {RouterModule} from '@angular/router';
-import {MdcSidenavDemo} from './mdc-sidenav-demo';
-
-@NgModule({
- imports: [MatSidenavModule, RouterModule.forChild([{path: '', component: MdcSidenavDemo}])],
- declarations: [MdcSidenavDemo],
-})
-export class MdcSidenavDemoModule {}
diff --git a/src/dev-app/mdc-sidenav/mdc-sidenav-demo.html b/src/dev-app/mdc-sidenav/mdc-sidenav-demo.html
deleted file mode 100644
index f3776973b7c3..000000000000
--- a/src/dev-app/mdc-sidenav/mdc-sidenav-demo.html
+++ /dev/null
@@ -1,2 +0,0 @@
-
-Not yet implemented.
diff --git a/src/dev-app/mdc-sidenav/mdc-sidenav-demo.scss b/src/dev-app/mdc-sidenav/mdc-sidenav-demo.scss
deleted file mode 100644
index af5dd72000aa..000000000000
--- a/src/dev-app/mdc-sidenav/mdc-sidenav-demo.scss
+++ /dev/null
@@ -1 +0,0 @@
-// TODO: copy in demo styles from existing mat-sidenav demo.
diff --git a/src/dev-app/mdc-snack-bar/mdc-snack-bar-demo.ts b/src/dev-app/mdc-snack-bar/mdc-snack-bar-demo.ts
index 51d5bc2654c7..26cc2e9ba194 100644
--- a/src/dev-app/mdc-snack-bar/mdc-snack-bar-demo.ts
+++ b/src/dev-app/mdc-snack-bar/mdc-snack-bar-demo.ts
@@ -8,12 +8,12 @@
import {Directionality} from '@angular/cdk/bidi';
import {Component, TemplateRef, ViewChild, ViewEncapsulation} from '@angular/core';
-import {MatSnackBar} from '@angular/material-experimental/mdc-snack-bar';
import {
+ MatSnackBar,
MatSnackBarConfig,
MatSnackBarHorizontalPosition,
MatSnackBarVerticalPosition,
-} from '@angular/material/snack-bar';
+} from '@angular/material-experimental/mdc-snack-bar';
@Component({
selector: 'mdc-snack-bar-demo',
diff --git a/src/dev-app/mdc-tabs/mdc-tabs-demo.html b/src/dev-app/mdc-tabs/mdc-tabs-demo.html
index 70156eec4d1c..daddf891f4f1 100644
--- a/src/dev-app/mdc-tabs/mdc-tabs-demo.html
+++ b/src/dev-app/mdc-tabs/mdc-tabs-demo.html
@@ -127,4 +127,13 @@
Tab nav bar
[active]="activeLink == link">{{link}}
Disabled Link
+
+
Tab nav bar with panel
+
+ {{link}}
+ Disabled Link
+
+
diff --git a/src/dev-app/platform/platform-demo-module.ts b/src/dev-app/platform/platform-demo-module.ts
index 0fe3b841c462..413c7ab44a06 100644
--- a/src/dev-app/platform/platform-demo-module.ts
+++ b/src/dev-app/platform/platform-demo-module.ts
@@ -6,18 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {PlatformModule} from '@angular/cdk/platform';
import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';
import {PlatformDemo} from './platform-demo';
@NgModule({
- imports: [
- CommonModule,
- PlatformModule,
- RouterModule.forChild([{path: '', component: PlatformDemo}]),
- ],
+ imports: [CommonModule, RouterModule.forChild([{path: '', component: PlatformDemo}])],
declarations: [PlatformDemo],
})
export class PlatformDemoModule {}
diff --git a/src/dev-app/routes.ts b/src/dev-app/routes.ts
index 8071764bac18..a1c2fe27d92e 100644
--- a/src/dev-app/routes.ts
+++ b/src/dev-app/routes.ts
@@ -224,11 +224,6 @@ export const DEV_APP_ROUTES: Routes = [
loadChildren: () =>
import('./mdc-select/mdc-select-demo-module').then(m => m.MdcSelectDemoModule),
},
- {
- path: 'mdc-sidenav',
- loadChildren: () =>
- import('./mdc-sidenav/mdc-sidenav-demo-module').then(m => m.MdcSidenavDemoModule),
- },
{
path: 'mdc-snack-bar',
loadChildren: () =>
diff --git a/src/dev-app/tabs/tabs-demo.html b/src/dev-app/tabs/tabs-demo.html
index 52d62976e2a8..7fd61001693f 100644
--- a/src/dev-app/tabs/tabs-demo.html
+++ b/src/dev-app/tabs/tabs-demo.html
@@ -18,5 +18,5 @@
Tab group stretched
Tab group theming
-
Tab Navigation Bar basic
+
Tab navigation bar basic
diff --git a/src/e2e-app/BUILD.bazel b/src/e2e-app/BUILD.bazel
index 8a3b9b8ce19e..a83c64265888 100644
--- a/src/e2e-app/BUILD.bazel
+++ b/src/e2e-app/BUILD.bazel
@@ -1,6 +1,4 @@
-load("//tools/dev-server:index.bzl", "dev_server")
-load("//tools:defaults.bzl", "ng_module", "sass_binary")
-load("//tools/esbuild:index.bzl", "esbuild", "esbuild_config")
+load("//tools:defaults.bzl", "devmode_esbuild", "esbuild_config", "http_server", "ng_module", "sass_binary")
load("//src/components-examples:config.bzl", "ALL_EXAMPLES")
load("//tools/angular:index.bzl", "LINKER_PROCESSED_FW_PACKAGES")
@@ -9,7 +7,6 @@ package(default_visibility = ["//visibility:public"])
# List of dependencies that are referenced in the `index.html` file.
devserverIndexHtmlDependencies = [
"@npm//zone.js",
- "@npm//core-js-bundle",
"@npm//kagekiri",
"@npm//material-components-web",
"//src/material/prebuilt-themes:indigo-pink",
@@ -73,6 +70,7 @@ ng_module(
"//src/material/progress-bar",
"//src/material/progress-spinner",
"//src/material/radio",
+ "//src/material/select",
"//src/material/sidenav",
"//src/material/slide-toggle",
"//src/material/tabs",
@@ -103,7 +101,7 @@ esbuild_config(
config_file = "esbuild.config.mjs",
)
-esbuild(
+devmode_esbuild(
name = "bundles",
testonly = True,
config = ":esbuild_config",
@@ -120,8 +118,8 @@ esbuild(
],
)
-dev_server(
- name = "devserver",
+http_server(
+ name = "server",
testonly = True,
srcs = devserverIndexHtmlDependencies,
additional_root_paths = [
diff --git a/src/e2e-app/e2e-app/e2e-app-layout.ts b/src/e2e-app/e2e-app/e2e-app-layout.ts
index 8fc83ee7824f..37e072d32883 100644
--- a/src/e2e-app/e2e-app/e2e-app-layout.ts
+++ b/src/e2e-app/e2e-app/e2e-app-layout.ts
@@ -27,6 +27,7 @@ export class E2eAppLayout {
{path: 'progress-bar', title: 'Progress bar'},
{path: 'progress-spinner', title: 'Progress Spinner'},
{path: 'radio', title: 'Radios'},
+ {path: 'select', title: 'Select'},
{path: 'sidenav', title: 'Sidenav'},
{path: 'slide-toggle', title: 'Slide Toggle'},
{path: 'stepper', title: 'Stepper'},
diff --git a/src/e2e-app/index.html b/src/e2e-app/index.html
index 81722b6f5e02..80258fa1f0a4 100644
--- a/src/e2e-app/index.html
+++ b/src/e2e-app/index.html
@@ -19,7 +19,6 @@
Loading...
I am a sibling!
-
diff --git a/src/e2e-app/main-module.ts b/src/e2e-app/main-module.ts
index 3aca19b85dd5..c7d44b05b0e9 100644
--- a/src/e2e-app/main-module.ts
+++ b/src/e2e-app/main-module.ts
@@ -1,6 +1,6 @@
import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
-import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {RouterModule} from '@angular/router';
import {BlockScrollStrategyE2eModule} from './block-scroll-strategy/block-scroll-strategy-e2e-module';
import {ButtonToggleE2eModule} from './button-toggle/button-toggle-e2e-module';
@@ -41,11 +41,14 @@ import {VirtualScrollE2eModule} from './virtual-scroll/virtual-scroll-e2e-module
import {MdcProgressBarE2eModule} from './mdc-progress-bar/mdc-progress-bar-e2e-module';
import {MdcProgressSpinnerE2eModule} from './mdc-progress-spinner/mdc-progress-spinner-module';
+/** We allow for animations to be explicitly enabled in certain e2e tests. */
+const enableAnimations = window.location.search.includes('animations=true');
+
@NgModule({
imports: [
BrowserModule,
E2eAppModule,
- NoopAnimationsModule,
+ BrowserAnimationsModule.withConfig({disableAnimations: !enableAnimations}),
RouterModule.forRoot(E2E_APP_ROUTES),
// E2E demos
diff --git a/src/e2e-app/routes.ts b/src/e2e-app/routes.ts
index 17198e3ddd1d..6b4a0bbbc6f4 100644
--- a/src/e2e-app/routes.ts
+++ b/src/e2e-app/routes.ts
@@ -36,6 +36,7 @@ import {BasicTabs} from './tabs/tabs-e2e';
import {ToolbarE2e} from './toolbar/toolbar-e2e';
import {VirtualScrollE2E} from './virtual-scroll/virtual-scroll-e2e';
import {Home} from './e2e-app/e2e-app-layout';
+import {SelectE2e} from './select/select-e2e';
export const E2E_APP_ROUTES: Routes = [
{path: '', component: Home},
@@ -70,6 +71,7 @@ export const E2E_APP_ROUTES: Routes = [
{path: 'progress-spinner', component: ProgressSpinnerE2E},
{path: 'radio', component: SimpleRadioButtons},
{path: 'sidenav', component: SidenavE2E},
+ {path: 'select', component: SelectE2e},
{path: 'slide-toggle', component: SlideToggleE2E},
{path: 'stepper', component: StepperE2e},
{path: 'tabs', component: BasicTabs},
diff --git a/src/e2e-app/select/select-e2e-module.ts b/src/e2e-app/select/select-e2e-module.ts
new file mode 100644
index 000000000000..312de216486d
--- /dev/null
+++ b/src/e2e-app/select/select-e2e-module.ts
@@ -0,0 +1,17 @@
+/**
+ * @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 {NgModule} from '@angular/core';
+import {ExampleViewerModule} from '../example-viewer/example-viewer-module';
+import {SelectE2e} from './select-e2e';
+
+@NgModule({
+ imports: [ExampleViewerModule],
+ declarations: [SelectE2e],
+})
+export class SelectE2eModule {}
diff --git a/src/dev-app/mdc-sidenav/mdc-sidenav-demo.ts b/src/e2e-app/select/select-e2e.ts
similarity index 61%
rename from src/dev-app/mdc-sidenav/mdc-sidenav-demo.ts
rename to src/e2e-app/select/select-e2e.ts
index 4e281eef08d7..3ce153a17bc3 100644
--- a/src/dev-app/mdc-sidenav/mdc-sidenav-demo.ts
+++ b/src/e2e-app/select/select-e2e.ts
@@ -9,8 +9,9 @@
import {Component} from '@angular/core';
@Component({
- selector: 'mdc-sidenav-demo',
- templateUrl: 'mdc-sidenav-demo.html',
- styleUrls: ['mdc-sidenav-demo.css'],
+ selector: 'select-demo',
+ template: `
`,
})
-export class MdcSidenavDemo {}
+export class SelectE2e {
+ examples = ['select-overview'];
+}
diff --git a/src/e2e-app/test_suite.bzl b/src/e2e-app/test_suite.bzl
index e854ca93c718..759a1cf2baf2 100644
--- a/src/e2e-app/test_suite.bzl
+++ b/src/e2e-app/test_suite.bzl
@@ -9,7 +9,7 @@ def e2e_test_suite(name, data = [], tags = ["e2e"], deps = []):
"@npm//@axe-core/webdriverjs",
] + data,
on_prepare = "//src/e2e-app:start-devserver.js",
- server = "//src/e2e-app:devserver",
+ server = "//src/e2e-app:server",
tags = tags,
deps = deps,
)
diff --git a/src/google-maps/google-map/google-map.spec.ts b/src/google-maps/google-map/google-map.spec.ts
index 0472272c217a..5eea53ede1c3 100644
--- a/src/google-maps/google-map/google-map.spec.ts
+++ b/src/google-maps/google-map/google-map.spec.ts
@@ -363,6 +363,18 @@ describe('GoogleMap', () => {
expect(mapConstructorSpy.calls.mostRecent()?.args[1].mapTypeId).toBe('satellite');
});
+ it('should emit mapInitialized event when the map is initialized', () => {
+ mapSpy = createMapSpy(DEFAULT_OPTIONS);
+ mapConstructorSpy = createMapConstructorSpy(mapSpy);
+
+ const fixture = TestBed.createComponent(TestApp);
+ fixture.detectChanges();
+
+ expect(fixture.componentInstance.mapInitializedSpy).toHaveBeenCalledOnceWith(
+ fixture.componentInstance.map.googleMap,
+ );
+ });
+
it('should emit authFailure event when window.gm_authFailure is called', () => {
mapSpy = createMapSpy(DEFAULT_OPTIONS);
mapConstructorSpy = createMapConstructorSpy(mapSpy);
@@ -397,7 +409,8 @@ describe('GoogleMap', () => {
[mapTypeId]="mapTypeId"
(mapClick)="handleClick($event)"
(centerChanged)="handleCenterChanged()"
- (mapRightclick)="handleRightclick($event)">
+ (mapRightclick)="handleRightclick($event)"
+ (mapInitialized)="mapInitializedSpy($event)">
`,
})
class TestApp {
@@ -412,4 +425,5 @@ class TestApp {
handleClick(event: google.maps.MapMouseEvent) {}
handleCenterChanged() {}
handleRightclick(event: google.maps.MapMouseEvent) {}
+ mapInitializedSpy = jasmine.createSpy('mapInitialized');
}
diff --git a/src/google-maps/google-map/google-map.ts b/src/google-maps/google-map/google-map.ts
index 5bfbf7f55020..1014fe7be4df 100644
--- a/src/google-maps/google-map/google-map.ts
+++ b/src/google-maps/google-map/google-map.ts
@@ -104,6 +104,10 @@ export class GoogleMap implements OnChanges, OnInit, OnDestroy {
}
private _options = DEFAULT_OPTIONS;
+ /** Event emitted when the map is initialized. */
+ @Output() readonly mapInitialized: EventEmitter
=
+ new EventEmitter();
+
/**
* See
* https://developers.google.com/maps/documentation/javascript/events#auth-errors
@@ -305,6 +309,7 @@ export class GoogleMap implements OnChanges, OnInit, OnDestroy {
this.googleMap = new google.maps.Map(this._mapEl, this._combineOptions());
});
this._eventManager.setTarget(this.googleMap);
+ this.mapInitialized.emit(this.googleMap);
}
}
diff --git a/src/google-maps/map-info-window/map-info-window.spec.ts b/src/google-maps/map-info-window/map-info-window.spec.ts
index c13c1741c8ca..3b14834cf611 100644
--- a/src/google-maps/map-info-window/map-info-window.spec.ts
+++ b/src/google-maps/map-info-window/map-info-window.spec.ts
@@ -129,7 +129,13 @@ describe('MapInfoWindow', () => {
expect(infoWindowSpy.close).toHaveBeenCalled();
infoWindowComponent.open(fakeMarkerComponent);
- expect(infoWindowSpy.open).toHaveBeenCalledWith(mapSpy, fakeMarker);
+ expect(infoWindowSpy.open).toHaveBeenCalledWith(
+ jasmine.objectContaining({
+ map: mapSpy,
+ anchor: fakeMarker,
+ shouldFocus: undefined,
+ }),
+ );
});
it('should not try to reopen info window multiple times for the same marker', () => {
@@ -224,6 +230,29 @@ describe('MapInfoWindow', () => {
infoWindowComponent.open();
expect(infoWindowSpy.open).toHaveBeenCalledTimes(1);
});
+
+ it('should allow for the focus behavior to be changed when opening the info window', () => {
+ const fakeMarker = {} as unknown as google.maps.Marker;
+ const fakeMarkerComponent = {
+ marker: fakeMarker,
+ getAnchor: () => fakeMarker,
+ } as unknown as MapMarker;
+ const infoWindowSpy = createInfoWindowSpy({});
+ createInfoWindowConstructorSpy(infoWindowSpy).and.callThrough();
+
+ const fixture = TestBed.createComponent(TestApp);
+ const infoWindowComponent = fixture.debugElement
+ .query(By.directive(MapInfoWindow))!
+ .injector.get(MapInfoWindow);
+ fixture.detectChanges();
+
+ infoWindowComponent.open(fakeMarkerComponent, false);
+ expect(infoWindowSpy.open).toHaveBeenCalledWith(
+ jasmine.objectContaining({
+ shouldFocus: false,
+ }),
+ );
+ });
});
@Component({
diff --git a/src/google-maps/map-info-window/map-info-window.ts b/src/google-maps/map-info-window/map-info-window.ts
index 85d014bb9c70..faf84b6e51c1 100644
--- a/src/google-maps/map-info-window/map-info-window.ts
+++ b/src/google-maps/map-info-window/map-info-window.ts
@@ -168,7 +168,7 @@ export class MapInfoWindow implements OnInit, OnDestroy {
* Opens the MapInfoWindow using the provided anchor. If the anchor is not set,
* then the position property of the options input is used instead.
*/
- open(anchor?: MapAnchorPoint) {
+ open(anchor?: MapAnchorPoint, shouldFocus?: boolean) {
this._assertInitialized();
const anchorObject = anchor ? anchor.getAnchor() : undefined;
@@ -178,7 +178,13 @@ export class MapInfoWindow implements OnInit, OnDestroy {
// case where the window doesn't have an anchor, but is placed at a particular position.
if (this.infoWindow.get('anchor') !== anchorObject || !anchorObject) {
this._elementRef.nativeElement.style.display = '';
- this.infoWindow.open(this._googleMap.googleMap, anchorObject);
+
+ // The config is cast to `any`, because the internal typings are out of date.
+ this.infoWindow.open({
+ map: this._googleMap.googleMap,
+ anchor: anchorObject,
+ shouldFocus,
+ } as any);
}
}
diff --git a/src/google-maps/testing/fake-google-map-utils.ts b/src/google-maps/testing/fake-google-map-utils.ts
index 57c8d79b507f..a1bf7bc58c45 100644
--- a/src/google-maps/testing/fake-google-map-utils.ts
+++ b/src/google-maps/testing/fake-google-map-utils.ts
@@ -148,7 +148,7 @@ export function createInfoWindowSpy(
'get',
]);
infoWindowSpy.addListener.and.returnValue({remove: () => {}});
- infoWindowSpy.open.and.callFake((_map: any, target: any) => (anchor = target));
+ infoWindowSpy.open.and.callFake((config: any) => (anchor = config.anchor));
infoWindowSpy.close.and.callFake(() => (anchor = null));
infoWindowSpy.get.and.callFake((key: string) => (key === 'anchor' ? anchor : null));
return infoWindowSpy;
diff --git a/src/google-maps/tsconfig.json b/src/google-maps/tsconfig.json
index d57281895299..10f13e87d57c 100644
--- a/src/google-maps/tsconfig.json
+++ b/src/google-maps/tsconfig.json
@@ -5,12 +5,7 @@
"rootDir": "..",
"baseUrl": ".",
"paths": {},
- "types": [
- "jasmine"
- ]
+ "types": ["jasmine"]
},
- "include": [
- "./**/*.ts",
- "../dev-mode-types.d.ts"
- ]
+ "include": ["./**/*.ts", "../dev-mode-types.d.ts"]
}
diff --git a/src/material-experimental/_index.scss b/src/material-experimental/_index.scss
index 3fe2fc770fed..c7571ca82471 100644
--- a/src/material-experimental/_index.scss
+++ b/src/material-experimental/_index.scss
@@ -16,7 +16,8 @@
popover-edit-typography, popover-edit-density, popover-edit-theme;
// MDC-related themes
-@forward './mdc-core/core-theme' as mdc-core-* show mdc-core-theme;
+@forward './mdc-core/core-theme' as mdc-core-* show mdc-core-theme, mdc-core-color,
+ mdc-core-density, mdc-core-typography;
@forward './mdc-helpers/focus-indicators-theme' as mdc-strong-focus-indicators-* show
mdc-strong-focus-indicators-color, mdc-strong-focus-indicators-theme;
@forward './mdc-core/option/option-theme' as mdc-option-* show mdc-option-color,
diff --git a/src/material-experimental/column-resize/_column-resize-theme.scss b/src/material-experimental/column-resize/_column-resize-theme.scss
index c88c47b1859e..10a798c2660e 100644
--- a/src/material-experimental/column-resize/_column-resize-theme.scss
+++ b/src/material-experimental/column-resize/_column-resize-theme.scss
@@ -9,8 +9,8 @@
$foreground: map.get($config, foreground);
$non-resizable-hover-divider: theming.get-color-from-palette($foreground, divider);
- $resizable-hover-divider: theming.get-color-from-palette($primary, 200);
- $resizable-active-divider: theming.get-color-from-palette($primary, 500);
+ $resizable-hover-divider: theming.get-color-from-palette($primary, 600);
+ $resizable-active-divider: theming.get-color-from-palette($primary, 600);
// TODO: these styles don't really belong in the `color` part of the theme.
// We should figure out a better place for them.
@@ -21,13 +21,16 @@
.mat-column-resize-flex {
.mat-header-cell,
- .mat-cell {
+ .mat-mdc-header-cell,
+ .mat-cell,
+ .mat-mdc-cell {
box-sizing: border-box;
min-width: 32px;
}
}
- .mat-header-cell {
+ .mat-header-cell,
+ .mat-mdc-header-cell {
position: relative;
}
@@ -36,6 +39,7 @@
}
.mat-header-cell:not(.mat-resizable)::after,
+ .mat-mdc-header-cell:not(.mat-resizable)::after,
.mat-resizable-handle {
background: transparent;
bottom: 0;
@@ -46,23 +50,26 @@
width: 1px;
}
- .mat-header-cell:not(.mat-resizable)::after {
+ .mat-header-cell:not(.mat-resizable)::after,
+ .mat-mdc-header-cell:not(.mat-resizable)::after {
content: '';
}
.mat-header-cell:not(.mat-resizable)::after,
+ .mat-mdc-header-cell:not(.mat-resizable)::after,
.mat-resizable-handle {
right: 0;
}
- [dir='rtl'] .mat-header-cell:not(.mat-resizable)::after,
- [dir='rtl'] .mat-resizable-handle {
- left: 0;
- right: auto;
- }
+ .mat-header-row.cdk-column-resize-hover-or-active,
+ .mat-mdc-header-row.cdk-column-resize-hover-or-active {
+ .mat-header-cell,
+ .mat-mdc-header-cell {
+ border-right: none;
+ }
- .mat-header-row.cdk-column-resize-hover-or-active {
- .mat-header-cell:not(.mat-resizable)::after {
+ .mat-header-cell:not(.mat-resizable)::after,
+ .mat-mdc-header-cell:not(.mat-resizable)::after {
background: $non-resizable-hover-divider;
}
@@ -71,13 +78,31 @@
}
}
+ [dir='rtl'] {
+ .mat-header-cell:not(.mat-resizable)::after,
+ .mat-mdc-header-cell:not(.mat-resizable)::after,
+ .mat-resizable-handle {
+ left: 0;
+ right: auto;
+ }
+
+ .mat-header-row.cdk-column-resize-hover-or-active,
+ .mat-mdc-header-row.cdk-column-resize-hover-or-active {
+ .mat-header-cell,
+ .mat-mdc-header-cell {
+ border-left: none;
+ }
+ }
+ }
+
.mat-resizable.cdk-resizable-overlay-thumb-active > .mat-resizable-handle {
opacity: 0;
transition: none;
}
.mat-resizable-handle:focus,
- .mat-header-row.cdk-column-resize-hover-or-active .mat-resizable-handle:focus {
+ .mat-header-row.cdk-column-resize-hover-or-active .mat-resizable-handle:focus,
+ .mat-mdc-header-row.cdk-column-resize-hover-or-active .mat-resizable-handle:focus {
background: $resizable-active-divider;
outline: none;
}
@@ -94,11 +119,22 @@
&:active {
background: linear-gradient(90deg,
transparent, transparent 7px,
- $resizable-active-divider, $resizable-active-divider 1px,
- transparent 8px, transparent);
+ $resizable-active-divider 7px, $resizable-active-divider 9px,
+ transparent 9px, transparent);
will-change: transform;
+
+ .mat-column-resize-overlay-thumb-top {
+ background: linear-gradient(90deg,
+ transparent, transparent 4px,
+ $resizable-active-divider 4px, $resizable-active-divider 12px,
+ transparent 12px, transparent);
+ }
}
}
+
+ .mat-column-resize-overlay-thumb-top {
+ width: 100%;
+ }
}
@mixin typography($config-or-theme) {}
diff --git a/src/material-experimental/column-resize/column-resize-module.ts b/src/material-experimental/column-resize/column-resize-module.ts
index 5b7a78717115..55add11dcc99 100644
--- a/src/material-experimental/column-resize/column-resize-module.ts
+++ b/src/material-experimental/column-resize/column-resize-module.ts
@@ -23,7 +23,6 @@ const ENTRY_COMMON_COMPONENTS = [MatColumnResizeOverlayHandle];
@NgModule({
declarations: ENTRY_COMMON_COMPONENTS,
exports: ENTRY_COMMON_COMPONENTS,
- entryComponents: ENTRY_COMMON_COMPONENTS,
})
export class MatColumnResizeCommonModule {}
diff --git a/src/material-experimental/column-resize/column-resize.spec.ts b/src/material-experimental/column-resize/column-resize.spec.ts
index 7640bbf6b3b6..62fc437b5fd9 100644
--- a/src/material-experimental/column-resize/column-resize.spec.ts
+++ b/src/material-experimental/column-resize/column-resize.spec.ts
@@ -159,10 +159,18 @@ abstract class BaseTestComponent {
dataSource = new ElementDataSource();
direction = 'ltr';
+ getTableHeight(): number {
+ return this.table.nativeElement.querySelector('.mat-table').offsetHeight;
+ }
+
getTableWidth(): number {
return this.table.nativeElement.querySelector('.mat-table').offsetWidth;
}
+ getHeaderRowHeight(): number {
+ return this.table.nativeElement.querySelector('.mat-header-row .mat-header-cell').offsetHeight;
+ }
+
getColumnElement(index: number): HTMLElement {
return this.table.nativeElement!.querySelectorAll('.mat-header-cell')[index] as HTMLElement;
}
@@ -189,6 +197,10 @@ abstract class BaseTestComponent {
return document.querySelectorAll('.mat-column-resize-overlay-thumb')[index] as HTMLElement;
}
+ getOverlayThumbTopElement(index: number): HTMLElement {
+ return document.querySelectorAll('.mat-column-resize-overlay-thumb-top')[index] as HTMLElement;
+ }
+
getOverlayThumbPosition(index: number): number {
const thumbPositionElement = this.getOverlayThumbElement(index)!.parentNode as HTMLElement;
const left = parseInt(thumbPositionElement.style.left!, 10);
@@ -372,6 +384,9 @@ describe('Material Popover Edit', () => {
it('shows resize handle overlays on header row hover and while a resize handle is in use', fakeAsync(() => {
expect(component.getOverlayThumbElement(0)).toBeUndefined();
+ const headerRowHeight = component.getHeaderRowHeight();
+ const tableHeight = component.getTableHeight();
+
component.triggerHoverState();
fixture.detectChanges();
@@ -382,6 +397,13 @@ describe('Material Popover Edit', () => {
component.getOverlayThumbElement(2).classList.contains('mat-column-resize-overlay-thumb'),
).toBe(true);
+ (expect(component.getOverlayThumbElement(0).offsetHeight) as any).isApproximately(
+ headerRowHeight,
+ );
+ (expect(component.getOverlayThumbElement(2).offsetHeight) as any).isApproximately(
+ headerRowHeight,
+ );
+
component.beginColumnResizeWithMouse(0);
expect(
@@ -391,6 +413,16 @@ describe('Material Popover Edit', () => {
component.getOverlayThumbElement(2).classList.contains('mat-column-resize-overlay-thumb'),
).toBe(true);
+ (expect(component.getOverlayThumbElement(0).offsetHeight) as any).isApproximately(
+ tableHeight,
+ );
+ (expect(component.getOverlayThumbTopElement(0).offsetHeight) as any).isApproximately(
+ headerRowHeight,
+ );
+ (expect(component.getOverlayThumbElement(2).offsetHeight) as any).isApproximately(
+ headerRowHeight,
+ );
+
component.completeResizeWithMouseInProgress(0);
component.endHoverState();
fixture.detectChanges();
diff --git a/src/material-experimental/column-resize/overlay-handle.ts b/src/material-experimental/column-resize/overlay-handle.ts
index 5e8996004d16..0a3fac2b55ea 100644
--- a/src/material-experimental/column-resize/overlay-handle.ts
+++ b/src/material-experimental/column-resize/overlay-handle.ts
@@ -12,6 +12,7 @@ import {
ElementRef,
Inject,
NgZone,
+ ViewChild,
ViewEncapsulation,
} from '@angular/core';
import {DOCUMENT} from '@angular/common';
@@ -39,11 +40,13 @@ import {AbstractMatColumnResize} from './column-resize-directives/common';
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
host: {'class': 'mat-column-resize-overlay-thumb'},
- template: '',
+ template: '
',
})
export class MatColumnResizeOverlayHandle extends ResizeOverlayHandle {
protected readonly document: Document;
+ @ViewChild('top', {static: true}) topElement: ElementRef;
+
constructor(
protected readonly columnDef: CdkColumnDef,
protected readonly columnResize: ColumnResize,
@@ -64,10 +67,12 @@ export class MatColumnResizeOverlayHandle extends ResizeOverlayHandle {
protected override updateResizeActive(active: boolean): void {
super.updateResizeActive(active);
+ const originHeight = this.resizeRef.origin.nativeElement.offsetHeight;
+ this.topElement.nativeElement.style.height = `${originHeight}px`;
this.resizeRef.overlayRef.updateSize({
height: active
? (this.columnResize as AbstractMatColumnResize).getTableHeight()
- : this.resizeRef.origin.nativeElement!.offsetHeight,
+ : originHeight,
});
}
}
diff --git a/src/material-experimental/config.bzl b/src/material-experimental/config.bzl
index b0750f2d83a2..2bcfe3d246c8 100644
--- a/src/material-experimental/config.bzl
+++ b/src/material-experimental/config.bzl
@@ -32,7 +32,6 @@ entryPoints = [
"mdc-radio/testing",
"mdc-select",
"mdc-select/testing",
- "mdc-sidenav",
"mdc-slide-toggle",
"mdc-slide-toggle/testing",
"mdc-slider",
diff --git a/src/material-experimental/mdc-autocomplete/autocomplete-trigger.ts b/src/material-experimental/mdc-autocomplete/autocomplete-trigger.ts
index 5a1e3c5f7c4c..4c2196b28c5b 100644
--- a/src/material-experimental/mdc-autocomplete/autocomplete-trigger.ts
+++ b/src/material-experimental/mdc-autocomplete/autocomplete-trigger.ts
@@ -34,13 +34,14 @@ export const MAT_AUTOCOMPLETE_VALUE_ACCESSOR: any = {
'[attr.aria-activedescendant]': '(panelOpen && activeOption) ? activeOption.id : null',
'[attr.aria-expanded]': 'autocompleteDisabled ? null : panelOpen.toString()',
'[attr.aria-owns]': '(autocompleteDisabled || !panelOpen) ? null : autocomplete?.id',
- '[attr.aria-haspopup]': '!autocompleteDisabled',
+ '[attr.aria-haspopup]': 'autocompleteDisabled ? null : "listbox"',
// Note: we use `focusin`, as opposed to `focus`, in order to open the panel
// a little earlier. This avoids issues where IE delays the focusing of the input.
'(focusin)': '_handleFocus()',
'(blur)': '_onTouched()',
'(input)': '_handleInput($event)',
'(keydown)': '_handleKeydown($event)',
+ '(click)': '_handleClick()',
},
exportAs: 'matAutocompleteTrigger',
providers: [MAT_AUTOCOMPLETE_VALUE_ACCESSOR],
diff --git a/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts b/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts
index 3f8771b1b52a..621e2d69855e 100644
--- a/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts
+++ b/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts
@@ -9,6 +9,7 @@ import {
dispatchEvent,
dispatchFakeEvent,
dispatchKeyboardEvent,
+ dispatchMouseEvent,
MockNgZone,
typeInElement,
} from '../../cdk/testing/private';
@@ -561,13 +562,38 @@ describe('MDC-based MatAutocomplete', () => {
});
it('should set aria-haspopup depending on whether the autocomplete is disabled', () => {
- expect(input.getAttribute('aria-haspopup')).toBe('true');
+ expect(input.getAttribute('aria-haspopup')).toBe('listbox');
fixture.componentInstance.autocompleteDisabled = true;
fixture.detectChanges();
- expect(input.getAttribute('aria-haspopup')).toBe('false');
+ expect(input.hasAttribute('aria-haspopup')).toBe(false);
});
+
+ it('should close the panel when pressing escape', fakeAsync(() => {
+ const trigger = fixture.componentInstance.trigger;
+
+ input.focus();
+ flush();
+ fixture.detectChanges();
+
+ expect(document.activeElement).withContext('Expected input to be focused.').toBe(input);
+ expect(trigger.panelOpen).withContext('Expected panel to be open.').toBe(true);
+
+ trigger.closePanel();
+ fixture.detectChanges();
+
+ expect(document.activeElement)
+ .withContext('Expected input to continue to be focused.')
+ .toBe(input);
+ expect(trigger.panelOpen).withContext('Expected panel to be closed.').toBe(false);
+
+ input.click();
+ flush();
+ fixture.detectChanges();
+
+ expect(trigger.panelOpen).withContext('Expected panel to reopen on click.').toBe(true);
+ }));
});
it('should not close the panel when clicking on the input', fakeAsync(() => {
@@ -1085,6 +1111,27 @@ describe('MDC-based MatAutocomplete', () => {
.toBe(false);
});
+ it('should not interfere with the ENTER key when pressing a modifier', fakeAsync(() => {
+ const trigger = fixture.componentInstance.trigger;
+
+ expect(input.value).withContext('Expected input to start off blank.').toBeFalsy();
+ expect(trigger.panelOpen).withContext('Expected panel to start off open.').toBe(true);
+
+ fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
+ flush();
+ fixture.detectChanges();
+
+ Object.defineProperty(ENTER_EVENT, 'altKey', {get: () => true});
+ fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT);
+ fixture.detectChanges();
+
+ expect(trigger.panelOpen).withContext('Expected panel to remain open.').toBe(true);
+ expect(input.value).withContext('Expected input to remain blank.').toBeFalsy();
+ expect(ENTER_EVENT.defaultPrevented)
+ .withContext('Expected the default ENTER action not to have been prevented.')
+ .toBe(false);
+ }));
+
it('should fill the text field, not select an option, when SPACE is entered', () => {
typeInElement(input, 'New');
fixture.detectChanges();
@@ -1374,6 +1421,23 @@ describe('MDC-based MatAutocomplete', () => {
.toBeFalsy();
}));
+ it('should not close when a click event occurs on the outside while the panel has focus', fakeAsync(() => {
+ const trigger = fixture.componentInstance.trigger;
+
+ input.focus();
+ flush();
+ fixture.detectChanges();
+
+ expect(document.activeElement).toBe(input, 'Expected input to be focused.');
+ expect(trigger.panelOpen).toBe(true, 'Expected panel to be open.');
+
+ dispatchMouseEvent(document.body, 'click');
+ fixture.detectChanges();
+
+ expect(document.activeElement).toBe(input, 'Expected input to continue to be focused.');
+ expect(trigger.panelOpen).toBe(true, 'Expected panel to stay open.');
+ }));
+
it('should reset the active option when closing with the escape key', fakeAsync(() => {
const trigger = fixture.componentInstance.trigger;
@@ -1431,6 +1495,20 @@ describe('MDC-based MatAutocomplete', () => {
expect(!!trigger.activeOption).withContext('Expected no active options.').toBe(false);
}));
+
+ it('should not prevent the default action when a modifier key is pressed', () => {
+ ['metaKey', 'ctrlKey', 'altKey', 'shiftKey'].forEach(name => {
+ const event = createKeyboardEvent('keydown', DOWN_ARROW);
+ Object.defineProperty(event, name, {get: () => true});
+
+ fixture.componentInstance.trigger._handleKeydown(event);
+ fixture.detectChanges();
+
+ expect(event.defaultPrevented)
+ .withContext(`Expected autocompete not to block ${name} key`)
+ .toBe(false);
+ });
+ });
});
describe('option groups', () => {
@@ -2234,6 +2312,22 @@ describe('MDC-based MatAutocomplete', () => {
.toBe(false);
}));
+ it('should be able to preselect the first option when the floating label is disabled', fakeAsync(() => {
+ fixture.componentInstance.floatLabel = 'never';
+ fixture.componentInstance.trigger.autocomplete.autoActiveFirstOption = true;
+ fixture.detectChanges();
+
+ fixture.componentInstance.trigger.openPanel();
+ fixture.detectChanges();
+ zone.simulateZoneExit();
+ // Note: should not have a detectChanges call here
+ // in order for the test to fail when it's supposed to.
+
+ expect(overlayContainerElement.querySelectorAll('mat-option')[0].classList)
+ .withContext('Expected first option to be highlighted.')
+ .toContain('mat-mdc-option-active');
+ }));
+
it('should be able to configure preselecting the first option globally', fakeAsync(() => {
fixture.destroy();
TestBed.resetTestingModule();
@@ -2279,6 +2373,34 @@ describe('MDC-based MatAutocomplete', () => {
subscription!.unsubscribe();
}));
+ it('should emit to `optionSelections` if the list of options changes', fakeAsync(() => {
+ const spy = jasmine.createSpy('option selection spy');
+ const subscription = fixture.componentInstance.trigger.optionSelections.subscribe(spy);
+ const openAndSelectFirstOption = () => {
+ fixture.detectChanges();
+ fixture.componentInstance.trigger.openPanel();
+ fixture.detectChanges();
+ zone.simulateZoneExit();
+ (overlayContainerElement.querySelector('mat-option') as HTMLElement).click();
+ fixture.detectChanges();
+ zone.simulateZoneExit();
+ };
+
+ fixture.componentInstance.states = [{code: 'OR', name: 'Oregon'}];
+ fixture.detectChanges();
+
+ openAndSelectFirstOption();
+ expect(spy).toHaveBeenCalledTimes(1);
+
+ fixture.componentInstance.states = [{code: 'WV', name: 'West Virginia'}];
+ fixture.detectChanges();
+
+ openAndSelectFirstOption();
+ expect(spy).toHaveBeenCalledTimes(2);
+
+ subscription!.unsubscribe();
+ }));
+
it('should reposition the panel when the amount of options changes', fakeAsync(() => {
let formField = fixture.debugElement.query(By.css('.mat-mdc-form-field'))!.nativeElement;
let inputReference = formField.querySelector('.mdc-text-field');
@@ -2625,6 +2747,217 @@ describe('MDC-based MatAutocomplete', () => {
}));
});
+ describe('automatically selecting the active option', () => {
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ fixture = createComponent(SimpleAutocomplete);
+ fixture.detectChanges();
+ fixture.componentInstance.trigger.autocomplete.autoSelectActiveOption = true;
+ });
+
+ it(
+ 'should update the input value as the user is navigating, without changing the model ' +
+ 'value or closing the panel',
+ fakeAsync(() => {
+ const {trigger, stateCtrl, closedSpy} = fixture.componentInstance;
+ const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
+
+ trigger.openPanel();
+ fixture.detectChanges();
+ zone.simulateZoneExit();
+ fixture.detectChanges();
+
+ expect(stateCtrl.value).toBeFalsy();
+ expect(input.value).toBeFalsy();
+ expect(trigger.panelOpen).toBe(true);
+ expect(closedSpy).not.toHaveBeenCalled();
+
+ dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
+ fixture.detectChanges();
+
+ expect(stateCtrl.value).toBeFalsy();
+ expect(input.value).toBe('Alabama');
+ expect(trigger.panelOpen).toBe(true);
+ expect(closedSpy).not.toHaveBeenCalled();
+
+ dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
+ fixture.detectChanges();
+
+ expect(stateCtrl.value).toBeFalsy();
+ expect(input.value).toBe('California');
+ expect(trigger.panelOpen).toBe(true);
+ expect(closedSpy).not.toHaveBeenCalled();
+ }),
+ );
+
+ it('should revert back to the last typed value if the user presses escape', fakeAsync(() => {
+ const {trigger, stateCtrl, closedSpy} = fixture.componentInstance;
+ const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
+
+ trigger.openPanel();
+ fixture.detectChanges();
+ zone.simulateZoneExit();
+ fixture.detectChanges();
+ typeInElement(input, 'al');
+ fixture.detectChanges();
+ tick();
+
+ expect(stateCtrl.value).toBe('al');
+ expect(input.value).toBe('al');
+ expect(trigger.panelOpen).toBe(true);
+ expect(closedSpy).not.toHaveBeenCalled();
+
+ dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
+ fixture.detectChanges();
+
+ expect(stateCtrl.value).toBe('al');
+ expect(input.value).toBe('Alabama');
+ expect(trigger.panelOpen).toBe(true);
+ expect(closedSpy).not.toHaveBeenCalled();
+
+ dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
+ fixture.detectChanges();
+
+ expect(stateCtrl.value).toBe('al');
+ expect(input.value).toBe('al');
+ expect(trigger.panelOpen).toBe(false);
+ expect(closedSpy).toHaveBeenCalledTimes(1);
+ }));
+
+ it(
+ 'should clear the input if the user presses escape while there was a pending ' +
+ 'auto selection and there is no previous value',
+ fakeAsync(() => {
+ const {trigger, stateCtrl} = fixture.componentInstance;
+ const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
+
+ trigger.openPanel();
+ fixture.detectChanges();
+ zone.simulateZoneExit();
+ fixture.detectChanges();
+
+ expect(stateCtrl.value).toBeFalsy();
+ expect(input.value).toBeFalsy();
+
+ dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
+ fixture.detectChanges();
+
+ expect(stateCtrl.value).toBeFalsy();
+ expect(input.value).toBe('Alabama');
+
+ dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
+ fixture.detectChanges();
+
+ expect(stateCtrl.value).toBeFalsy();
+ expect(input.value).toBeFalsy();
+ }),
+ );
+
+ it('should propagate the auto-selected value if the user clicks away', fakeAsync(() => {
+ const {trigger, stateCtrl} = fixture.componentInstance;
+ const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
+
+ trigger.openPanel();
+ fixture.detectChanges();
+ zone.simulateZoneExit();
+ fixture.detectChanges();
+
+ expect(stateCtrl.value).toBeFalsy();
+ expect(input.value).toBeFalsy();
+
+ dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
+ fixture.detectChanges();
+
+ expect(stateCtrl.value).toBeFalsy();
+ expect(input.value).toBe('Alabama');
+
+ dispatchFakeEvent(document, 'click');
+ fixture.detectChanges();
+
+ expect(stateCtrl.value).toEqual({code: 'AL', name: 'Alabama'});
+ expect(input.value).toBe('Alabama');
+ }));
+
+ it('should propagate the auto-selected value if the user tabs away', fakeAsync(() => {
+ const {trigger, stateCtrl} = fixture.componentInstance;
+ const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
+
+ trigger.openPanel();
+ fixture.detectChanges();
+ zone.simulateZoneExit();
+ fixture.detectChanges();
+
+ expect(stateCtrl.value).toBeFalsy();
+ expect(input.value).toBeFalsy();
+
+ dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
+ fixture.detectChanges();
+
+ expect(stateCtrl.value).toBeFalsy();
+ expect(input.value).toBe('Alabama');
+
+ dispatchKeyboardEvent(input, 'keydown', TAB);
+ fixture.detectChanges();
+
+ expect(stateCtrl.value).toEqual({code: 'AL', name: 'Alabama'});
+ expect(input.value).toBe('Alabama');
+ }));
+
+ it('should propagate the auto-selected value if the user presses enter on it', fakeAsync(() => {
+ const {trigger, stateCtrl} = fixture.componentInstance;
+ const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
+
+ trigger.openPanel();
+ fixture.detectChanges();
+ zone.simulateZoneExit();
+ fixture.detectChanges();
+
+ expect(stateCtrl.value).toBeFalsy();
+ expect(input.value).toBeFalsy();
+
+ dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
+ fixture.detectChanges();
+
+ expect(stateCtrl.value).toBeFalsy();
+ expect(input.value).toBe('Alabama');
+
+ dispatchKeyboardEvent(input, 'keydown', ENTER);
+ fixture.detectChanges();
+
+ expect(stateCtrl.value).toEqual({code: 'AL', name: 'Alabama'});
+ expect(input.value).toBe('Alabama');
+ }));
+
+ it('should allow the user to click on an option different from the auto-selected one', fakeAsync(() => {
+ const {trigger, stateCtrl} = fixture.componentInstance;
+ const input: HTMLInputElement = fixture.nativeElement.querySelector('input');
+
+ trigger.openPanel();
+ fixture.detectChanges();
+ zone.simulateZoneExit();
+ fixture.detectChanges();
+
+ expect(stateCtrl.value).toBeFalsy();
+ expect(input.value).toBeFalsy();
+
+ dispatchKeyboardEvent(input, 'keydown', DOWN_ARROW);
+ fixture.detectChanges();
+
+ expect(stateCtrl.value).toBeFalsy();
+ expect(input.value).toBe('Alabama');
+
+ const options = overlayContainerElement.querySelectorAll(
+ 'mat-option',
+ ) as NodeListOf;
+ options[2].click();
+ fixture.detectChanges();
+
+ expect(stateCtrl.value).toEqual({code: 'FL', name: 'Florida'});
+ expect(input.value).toBe('Florida');
+ }));
+ });
+
it('should have correct width when opened', () => {
const widthFixture = createComponent(SimpleAutocomplete);
widthFixture.componentInstance.width = 300;
@@ -2820,6 +3153,29 @@ describe('MDC-based MatAutocomplete', () => {
expect(event.option.value).toBe('Washington');
}));
+ it('should refocus the input after the selection event is emitted', fakeAsync(() => {
+ const events: string[] = [];
+ const fixture = createComponent(AutocompleteWithSelectEvent);
+ fixture.detectChanges();
+ const input = fixture.nativeElement.querySelector('input');
+
+ fixture.componentInstance.trigger.openPanel();
+ zone.simulateZoneExit();
+ fixture.detectChanges();
+
+ const options = overlayContainerElement.querySelectorAll(
+ 'mat-option',
+ ) as NodeListOf;
+ spyOn(input, 'focus').and.callFake(() => events.push('focus'));
+ fixture.componentInstance.optionSelected.and.callFake(() => events.push('select'));
+
+ options[1].click();
+ tick();
+ fixture.detectChanges();
+
+ expect(events).toEqual(['select', 'focus']);
+ }));
+
it('should emit an event when a newly-added option is selected', fakeAsync(() => {
let fixture = createComponent(AutocompleteWithSelectEvent);
diff --git a/src/material-experimental/mdc-button/BUILD.bazel b/src/material-experimental/mdc-button/BUILD.bazel
index aca24ef76421..48fef69e3ab3 100644
--- a/src/material-experimental/mdc-button/BUILD.bazel
+++ b/src/material-experimental/mdc-button/BUILD.bazel
@@ -102,6 +102,7 @@ ng_test_library(
deps = [
":mdc-button",
"//src/cdk/platform",
+ "//src/cdk/testing/private",
"//src/material-experimental/mdc-core",
"//src/material/button",
"@npm//@angular/platform-browser",
diff --git a/src/material-experimental/mdc-button/_button-base.scss b/src/material-experimental/mdc-button/_button-base.scss
index 7c5cbfa28515..f0b0f8ee88ef 100644
--- a/src/material-experimental/mdc-button/_button-base.scss
+++ b/src/material-experimental/mdc-button/_button-base.scss
@@ -1,3 +1,4 @@
+@use 'sass:map';
@use '@material/touch-target' as mdc-touch-target;
@use '../mdc-helpers/mdc-helpers';
@use '../../material/core/style/layout-common';
@@ -8,22 +9,10 @@
// ripple and state container so that they fill the button, match the border radius, and avoid
// pointer events.
@mixin mat-private-button-interactive() {
- .mdc-button__ripple::before, .mdc-button__ripple::after,
- .mdc-fab__ripple::before, .mdc-fab__ripple::after {
- content: '';
- pointer-events: none;
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- opacity: 0;
- border-radius: inherit;
- @content;
- }
-
// The ripple container should match the bounds of the entire button.
- .mat-mdc-button-ripple, .mdc-button__ripple, .mdc-fab__ripple {
+ .mat-mdc-button-ripple,
+ .mat-mdc-button-persistent-ripple,
+ .mat-mdc-button-persistent-ripple::before {
@include layout-common.fill;
// Disable pointer events for the ripple container and state overlay because the container
@@ -38,6 +27,17 @@
border-radius: inherit;
}
+ // We use ::before so that we can reuse some of MDC's theming.
+ .mat-mdc-button-persistent-ripple::before {
+ content: '';
+ opacity: 0;
+ background-color: var(--mat-mdc-button-persistent-ripple-color);
+ }
+
+ .mat-ripple-element {
+ background-color: var(--mat-mdc-button-ripple-color);
+ }
+
// The content should appear over the state and ripple layers, otherwise they may adversely affect
// the accessibility of the text content.
.mdc-button__label {
@@ -59,6 +59,7 @@
&[disabled] {
cursor: default;
pointer-events: none;
+ @content;
}
}
@@ -75,3 +76,15 @@
$query: mdc-helpers.$mat-base-styles-query);
}
}
+
+// Changes a button theme to exclude the ripple styles.
+@function mat-private-button-remove-ripple($theme) {
+ @return map.merge($theme, (
+ focus-state-layer-color: null,
+ focus-state-layer-opacity: null,
+ hover-state-layer-color: null,
+ hover-state-layer-opacity: null,
+ pressed-state-layer-color: null,
+ pressed-state-layer-opacity: null,
+ ));
+}
diff --git a/src/material-experimental/mdc-button/_button-theme-private.scss b/src/material-experimental/mdc-button/_button-theme-private.scss
index 46b97177d996..742438150fd8 100644
--- a/src/material-experimental/mdc-button/_button-theme-private.scss
+++ b/src/material-experimental/mdc-button/_button-theme-private.scss
@@ -1,28 +1,43 @@
+@use 'sass:map';
+@use '@material/ripple/ripple-theme' as mdc-ripple-theme;
@use '@material/theme/theme-color' as mdc-theme-color;
-@use '@material/theme/theme' as mdc-theme;
@use '../../material/core/ripple/ripple-theme';
-// Selector for the element that has a background color and opacity applied to its ::before and
-// ::after for state interactions (hover, active, focus). Their API calls this their
-// "ripple target", but we do not use it as our ripple, just state color.
-$button-state-target: '.mdc-button__ripple';
-$fab-state-target: '.mdc-fab__ripple';
-
-// The MDC button's ripple ink color is based on the theme color, not on the foreground base
-// which is what the ripple mixin uses. This creates a new theme that sets the color to the
-// foreground base to appropriately color the ink.
-@mixin ripple-ink-color($mdc-color) {
- @include ripple-theme.color((
- foreground: (
- base: mdc-theme-color.prop-value($mdc-color)
- ),
- ));
+@mixin _ripple-color($color) {
+ --mat-mdc-button-persistent-ripple-color: #{$color};
+ --mat-mdc-button-ripple-color: #{rgba($color, 0.1)};
}
-// Applies the disabled theme color to the text color.
-@mixin apply-disabled-color() {
- @include mdc-theme.prop(color,
- mdc-theme-color.ink-color-for-fill_(disabled, mdc-theme-color.$background));
+@mixin ripple-theme-styles($config, $is-filled) {
+ $opacities: if(map.get($config, is-dark),
+ mdc-ripple-theme.$light-ink-opacities, mdc-ripple-theme.$dark-ink-opacities);
+
+ // Ideally these styles would be structural, but MDC bases some of the opacities on the theme.
+ &:hover .mat-mdc-button-persistent-ripple::before {
+ opacity: map.get($opacities, hover);
+ }
+
+ &:focus .mat-mdc-button-persistent-ripple::before {
+ opacity: map.get($opacities, focus);
+ }
+
+ &:active .mat-mdc-button-persistent-ripple::before {
+ opacity: map.get($opacities, press);
+ }
+
+ @include _ripple-color(mdc-theme-color.prop-value(on-surface));
+
+ &.mat-primary {
+ @include _ripple-color(mdc-theme-color.prop-value(if($is-filled, on-primary, primary)));
+ }
+
+ &.mat-accent {
+ @include _ripple-color(mdc-theme-color.prop-value(if($is-filled, on-secondary, secondary)));
+ }
+
+ &.mat-warn {
+ @include _ripple-color(mdc-theme-color.prop-value(if($is-filled, on-error, error)));
+ }
}
// Wraps the content style in a selector for the disabled state.
@@ -37,14 +52,6 @@ $fab-state-target: '.mdc-fab__ripple';
}
}
-// Applies the disabled theme background color for raised buttons. Value is taken from
-// mixin `mdc-button--filled`.
-// TODO(andrewseguin): Discuss with the MDC team about providing a variable for the 0.12 value
-// or otherwise have a mixin we can call to apply this style for both button and anchors.
-@mixin apply-disabled-background() {
- @include mdc-theme.prop(background-color, rgba(mdc-theme-color.prop-value(on-surface), 0.12));
-}
-
// Hides the touch target on lower densities.
@mixin touch-target-density($scale) {
@if ($scale == -2 or $scale == 'minimum') {
diff --git a/src/material-experimental/mdc-button/_button-theme.scss b/src/material-experimental/mdc-button/_button-theme.scss
index 4f0df23e0872..9c6f6f5fb4ad 100644
--- a/src/material-experimental/mdc-button/_button-theme.scss
+++ b/src/material-experimental/mdc-button/_button-theme.scss
@@ -1,146 +1,184 @@
+@use 'sass:map';
@use '@material/button/button' as mdc-button;
@use '@material/button/button-theme' as mdc-button-theme;
-@use '@material/ripple/ripple-theme' as mdc-ripple-theme;
+@use '@material/button/button-text-theme' as mdc-button-text-theme;
+@use '@material/button/button-filled-theme' as mdc-button-filled-theme;
+@use '@material/button/button-protected-theme' as mdc-button-protected-theme;
+@use '@material/button/button-outlined-theme' as mdc-button-outlined-theme;
@use '@material/theme/theme-color' as mdc-theme-color;
-@use '@material/theme/theme' as mdc-theme;
-@use '@material/elevation/elevation-theme' as mdc-elevation-theme;
-@use '../../material/core/ripple/ripple-theme';
@use '../../material/core/typography/typography';
@use '../mdc-helpers/mdc-helpers';
@use '../../material/core/theming/theming';
@use './button-theme-private';
+@mixin _button-variant($color) {
+ @include mdc-button-text-theme.theme((
+ label-text-color: $color,
+ ));
+}
+
+@mixin _unelevated-button-variant($foreground, $background) {
+ @include mdc-button-filled-theme.theme((
+ container-color: $background,
+ label-text-color: $foreground,
+ ));
+}
+
+@mixin _raised-button-variant($foreground, $background) {
+ @include mdc-button-protected-theme.theme((
+ container-color: $background,
+ label-text-color: $foreground,
+ ));
+}
+
+@mixin _outlined-button-variant($color) {
+ @include mdc-button-outlined-theme.theme((
+ label-text-color: $color,
+ ));
+}
+
@mixin color($config-or-theme) {
$config: theming.get-color-config($config-or-theme);
@include mdc-helpers.mat-using-mdc-theme($config) {
- // Add state interactions for hover, focus, press, active. Colors are changed based on
- // the mixin mdc-states-base-color
- .mat-mdc-button, .mat-mdc-raised-button, .mat-mdc-unelevated-button, .mat-mdc-outlined-button {
- @include mdc-ripple-theme.states(
- $query: mdc-helpers.$mat-theme-styles-query,
- $ripple-target: button-theme-private.$button-state-target);
- }
-
- .mat-mdc-button, .mat-mdc-outlined-button {
+ $is-dark: map.get($config, is-dark);
+ $on-surface: mdc-theme-color.prop-value(on-surface);
+ $surface: mdc-theme-color.prop-value(surface);
+ $disabled-ink-color: rgba($on-surface, if($is-dark, 0.5, 0.38));
+ $disabled-container-color: rgba($on-surface, 0.12);
+ $primary: mdc-theme-color.prop-value(primary);
+ $on-primary: mdc-theme-color.prop-value(on-primary);
+ $secondary: mdc-theme-color.prop-value(secondary);
+ $on-secondary: mdc-theme-color.prop-value(on-secondary);
+ $error: mdc-theme-color.prop-value(error);
+ $on-error: mdc-theme-color.prop-value(on-error);
+
+ .mat-mdc-button {
&.mat-unthemed {
- @include mdc-button-theme.ink-color(mdc-theme-color.$on-surface,
- $query: mdc-helpers.$mat-theme-styles-query);
+ @include _button-variant($on-surface);
}
&.mat-primary {
- @include mdc-button-theme.ink-color(primary, $query: mdc-helpers.$mat-theme-styles-query);
- @include mdc-ripple-theme.states-base-color(primary,
- $query: mdc-helpers.$mat-theme-styles-query,
- $ripple-target: button-theme-private.$button-state-target);
- @include button-theme-private.ripple-ink-color(primary);
+ @include _button-variant($primary);
}
&.mat-accent {
- @include mdc-button-theme.ink-color(secondary, $query: mdc-helpers.$mat-theme-styles-query);
- @include mdc-ripple-theme.states-base-color(secondary,
- $query: mdc-helpers.$mat-theme-styles-query,
- $ripple-target: button-theme-private.$button-state-target);
- @include button-theme-private.ripple-ink-color(secondary);
+ @include _button-variant($secondary);
}
&.mat-warn {
- @include mdc-button-theme.ink-color(error, $query: mdc-helpers.$mat-theme-styles-query);
- @include mdc-ripple-theme.states-base-color(error,
- $query: mdc-helpers.$mat-theme-styles-query,
- $ripple-target: button-theme-private.$button-state-target);
- @include button-theme-private.ripple-ink-color(error);
+ @include _button-variant($error);
+ }
+
+ @include button-theme-private.apply-disabled-style() {
+ @include mdc-button-text-theme.theme((
+ // We need to pass both the disabled and enabled values, because the enabled
+ // ones apply to anchors while the disabled ones are for buttons.
+ disabled-label-text-color: $disabled-ink-color,
+ label-text-color: $disabled-ink-color
+ ));
}
}
- .mat-mdc-raised-button,
.mat-mdc-unelevated-button {
&.mat-unthemed {
- @include mdc-button-theme.container-fill-color(mdc-theme-color.$surface,
- $query: mdc-helpers.$mat-theme-styles-query);
- @include mdc-button-theme.ink-color(mdc-theme-color.$on-surface,
- $query: mdc-helpers.$mat-theme-styles-query);
- @include mdc-ripple-theme.states-base-color(mdc-theme-color.$on-surface,
- $query: mdc-helpers.$mat-theme-styles-query,
- $ripple-target: button-theme-private.$button-state-target);
+ @include _unelevated-button-variant($on-surface, $surface);
}
&.mat-primary {
- @include mdc-button-theme.container-fill-color(primary,
- $query: mdc-helpers.$mat-theme-styles-query);
- @include mdc-button-theme.ink-color(on-primary,
- $query: mdc-helpers.$mat-theme-styles-query);
- @include mdc-ripple-theme.states-base-color(on-primary,
- $query: mdc-helpers.$mat-theme-styles-query,
- $ripple-target: button-theme-private.$button-state-target);
- @include button-theme-private.ripple-ink-color(on-primary);
+ @include _unelevated-button-variant($on-primary, $primary);
}
&.mat-accent {
- @include mdc-button-theme.container-fill-color(secondary,
- $query: mdc-helpers.$mat-theme-styles-query);
- @include mdc-button-theme.ink-color(on-secondary,
- $query: mdc-helpers.$mat-theme-styles-query);
- @include mdc-ripple-theme.states-base-color(on-secondary,
- $query: mdc-helpers.$mat-theme-styles-query,
- $ripple-target: button-theme-private.$button-state-target);
- @include button-theme-private.ripple-ink-color(on-secondary);
+ @include _unelevated-button-variant($on-secondary, $secondary);
}
&.mat-warn {
- @include mdc-button-theme.container-fill-color(error,
- $query: mdc-helpers.$mat-theme-styles-query);
- @include mdc-button-theme.ink-color(on-error, $query: mdc-helpers.$mat-theme-styles-query);
- @include mdc-ripple-theme.states-base-color(on-error,
- $query: mdc-helpers.$mat-theme-styles-query,
- $ripple-target: button-theme-private.$button-state-target);
- @include button-theme-private.ripple-ink-color(on-error);
+ @include _unelevated-button-variant($on-error, $error);
}
@include button-theme-private.apply-disabled-style() {
- @include button-theme-private.apply-disabled-background();
+ @include mdc-button-filled-theme.theme((
+ // We need to pass both the disabled and enabled values, because the enabled
+ // ones apply to anchors while the disabled ones are for buttons.
+ disabled-container-color: $disabled-container-color,
+ disabled-label-text-color: $disabled-ink-color,
+ container-color: $disabled-container-color,
+ label-text-color: $disabled-ink-color,
+ ));
}
}
- .mat-mdc-outlined-button {
+ .mat-mdc-raised-button {
&.mat-unthemed {
- @include mdc-button-theme.outline-color(mdc-theme-color.$on-surface,
- $query: mdc-helpers.$mat-theme-styles-query);
+ @include _raised-button-variant($on-surface, $surface);
}
&.mat-primary {
- @include mdc-button-theme.outline-color(primary,
- $query: mdc-helpers.$mat-theme-styles-query);
+ @include _raised-button-variant($on-primary, $primary);
}
&.mat-accent {
- @include mdc-button-theme.outline-color(secondary,
- $query: mdc-helpers.$mat-theme-styles-query);
+ @include _raised-button-variant($on-secondary, $secondary);
}
&.mat-warn {
- @include mdc-button-theme.outline-color(error,
- $query: mdc-helpers.$mat-theme-styles-query);
+ @include _raised-button-variant($on-error, $error);
}
@include button-theme-private.apply-disabled-style() {
- @include mdc-theme.prop(border-color,
- mdc-theme-color.ink-color-for-fill_(disabled, mdc-theme-color.$background));
+ @include mdc-button-protected-theme.theme((
+ // We need to pass both the disabled and enabled values, because the enabled
+ // ones apply to anchors while the disabled ones are for buttons.
+ disabled-container-color: $disabled-container-color,
+ disabled-label-text-color: $disabled-ink-color,
+ container-color: $disabled-container-color,
+ label-text-color: $disabled-ink-color,
+ container-elevation: 0,
+ ));
}
}
- .mat-mdc-raised-button {
- @include button-theme-private.apply-disabled-style() {
- @include mdc-elevation-theme.elevation(0, $query: mdc-helpers.$mat-theme-styles-query);
+ .mat-mdc-outlined-button {
+ @include mdc-button-outlined-theme.theme((
+ outline-color: rgba(mdc-theme-color.prop-value(on-surface), 0.12)
+ ));
+
+ &.mat-unthemed {
+ @include _outlined-button-variant($on-surface);
+ }
+
+ &.mat-primary {
+ @include _outlined-button-variant($primary);
+ }
+
+ &.mat-accent {
+ @include _outlined-button-variant($secondary);
+ }
+
+ &.mat-warn {
+ @include _outlined-button-variant($error);
}
- }
- .mat-mdc-button, .mat-mdc-raised-button, .mat-mdc-unelevated-button, .mat-mdc-outlined-button {
@include button-theme-private.apply-disabled-style() {
- @include button-theme-private.apply-disabled-color();
+ @include mdc-button-outlined-theme.theme((
+ // We need to pass both the disabled and enabled values, because the enabled
+ // ones apply to anchors while the disabled ones are for buttons.
+ label-text-color: $disabled-ink-color,
+ disabled-label-text-color: $disabled-ink-color,
+ outline-color: rgba($on-surface, 0.12),
+ disabled-outline-color: rgba($on-surface, 0.12),
+ ));
}
}
- @include mdc-button.without-ripple($query: mdc-helpers.$mat-theme-styles-query);
+ // Ripple colors
+ .mat-mdc-button, .mat-mdc-outlined-button {
+ @include button-theme-private.ripple-theme-styles($config, false);
+ }
+
+ .mat-mdc-raised-button, .mat-mdc-unelevated-button {
+ @include button-theme-private.ripple-theme-styles($config, true);
+ }
}
}
diff --git a/src/material-experimental/mdc-button/_fab-theme.scss b/src/material-experimental/mdc-button/_fab-theme.scss
index 676fa8340ce3..68075b26bb35 100644
--- a/src/material-experimental/mdc-button/_fab-theme.scss
+++ b/src/material-experimental/mdc-button/_fab-theme.scss
@@ -1,68 +1,62 @@
+@use 'sass:map';
@use '@material/fab/fab' as mdc-fab;
@use '@material/fab/fab-theme' as mdc-fab-theme;
-@use '@material/ripple/ripple-theme' as mdc-ripple-theme;
-@use '@material/elevation/elevation-theme' as mdc-elevation-theme;
@use '@material/theme/theme-color' as mdc-theme-color;
@use '../mdc-helpers/mdc-helpers';
@use '../../material/core/theming/theming';
@use '../../material/core/typography/typography';
@use './button-theme-private';
+@mixin _fab-variant($foreground, $background) {
+ @include mdc-fab-theme.theme((
+ container-color: $background,
+ icon-color: $foreground,
+ ));
+
+ --mat-mdc-fab-color: #{$foreground};
+}
+
@mixin color($config-or-theme) {
$config: theming.get-color-config($config-or-theme);
@include mdc-helpers.mat-using-mdc-theme($config) {
+ $on-surface: mdc-theme-color.prop-value(on-surface);
+ $is-dark: map.get($config, is-dark);
+
.mat-mdc-fab, .mat-mdc-mini-fab {
- @include mdc-ripple-theme.states(
- $query: mdc-helpers.$mat-theme-styles-query,
- $ripple-target: button-theme-private.$fab-state-target);
+ @include button-theme-private.ripple-theme-styles($config, true);
&.mat-unthemed {
- @include mdc-ripple-theme.states-base-color(mdc-theme-color.$on-surface,
- $query: mdc-helpers.$mat-theme-styles-query,
- $ripple-target: button-theme-private.$fab-state-target);
- @include mdc-fab-theme.container-color(mdc-theme-color.$on-surface,
- $query: mdc-helpers.$mat-theme-styles-query);
- @include mdc-fab-theme.ink-color(mdc-theme-color.$on-surface,
- $query: mdc-helpers.$mat-theme-styles-query);
+ @include _fab-variant($on-surface, mdc-theme-color.prop-value(surface));
}
&.mat-primary {
- @include mdc-ripple-theme.states-base-color(on-primary,
- $query: mdc-helpers.$mat-theme-styles-query,
- $ripple-target: button-theme-private.$fab-state-target);
- @include mdc-fab-theme.container-color(primary,
- $query: mdc-helpers.$mat-theme-styles-query);
- @include mdc-fab-theme.ink-color(on-primary, $query: mdc-helpers.$mat-theme-styles-query);
- @include button-theme-private.ripple-ink-color(on-primary);
+ @include _fab-variant(
+ mdc-theme-color.prop-value(on-primary),
+ mdc-theme-color.prop-value(primary)
+ );
}
&.mat-accent {
- @include mdc-ripple-theme.states-base-color(on-secondary,
- $query: mdc-helpers.$mat-theme-styles-query,
- $ripple-target: button-theme-private.$fab-state-target);
- @include mdc-fab-theme.container-color(secondary,
- $query: mdc-helpers.$mat-theme-styles-query);
- @include mdc-fab-theme.ink-color(on-secondary, $query: mdc-helpers.$mat-theme-styles-query);
- @include button-theme-private.ripple-ink-color(on-secondary);
+ @include _fab-variant(
+ mdc-theme-color.prop-value(on-secondary),
+ mdc-theme-color.prop-value(secondary)
+ );
}
&.mat-warn {
- @include mdc-ripple-theme.states-base-color(on-error,
- $query: mdc-helpers.$mat-theme-styles-query,
- $ripple-target: button-theme-private.$fab-state-target);
- @include mdc-fab-theme.container-color(error, $query: mdc-helpers.$mat-theme-styles-query);
- @include mdc-fab-theme.ink-color(on-error, $query: mdc-helpers.$mat-theme-styles-query);
- @include button-theme-private.ripple-ink-color(on-error);
+ @include _fab-variant(
+ mdc-theme-color.prop-value(on-error),
+ mdc-theme-color.prop-value(error)
+ );
}
@include button-theme-private.apply-disabled-style() {
- @include button-theme-private.apply-disabled-color();
- @include button-theme-private.apply-disabled-background();
- @include mdc-elevation-theme.elevation(0, $query: mdc-helpers.$mat-theme-styles-query);
+ @include _fab-variant(
+ rgba($on-surface, if(map.get($config, is-dark), 0.5, 0.38)),
+ rgba($on-surface, 0.12)
+ );
}
}
-
- @include mdc-fab.without-ripple($query: mdc-helpers.$mat-theme-styles-query);
}
}
diff --git a/src/material-experimental/mdc-button/_icon-button-theme.scss b/src/material-experimental/mdc-button/_icon-button-theme.scss
index 16196ca888db..e69cb94f0dad 100644
--- a/src/material-experimental/mdc-button/_icon-button-theme.scss
+++ b/src/material-experimental/mdc-button/_icon-button-theme.scss
@@ -1,5 +1,7 @@
+@use 'sass:map';
@use '@material/icon-button/mixins' as mdc-icon-button;
-@use '@material/ripple/ripple-theme' as mdc-ripple-theme;
+@use '@material/icon-button/icon-button-theme' as mdc-icon-button-theme;
+@use '@material/theme/theme-color' as mdc-theme-color;
@use '../mdc-helpers/mdc-helpers';
@use '../../material/core/theming/theming';
@use '../../material/core/typography/typography';
@@ -8,41 +10,32 @@
@mixin color($config-or-theme) {
$config: theming.get-color-config($config-or-theme);
@include mdc-helpers.mat-using-mdc-theme($config) {
+ $is-dark: map.get($config, is-dark);
+ $on-surface: mdc-theme-color.prop-value(on-surface);
+ $disabled-color: rgba($on-surface, if($is-dark, 0.5, 0.38));
+
.mat-mdc-icon-button {
- @include mdc-ripple-theme.states(
- $query: mdc-helpers.$mat-theme-styles-query,
- $ripple-target: button-theme-private.$button-state-target);
+ @include button-theme-private.ripple-theme-styles($config, false);
&.mat-primary {
- @include mdc-ripple-theme.states-base-color(primary,
- $query: mdc-helpers.$mat-theme-styles-query,
- $ripple-target: button-theme-private.$button-state-target);
- @include mdc-icon-button.ink-color(primary, $query: mdc-helpers.$mat-theme-styles-query);
- @include button-theme-private.ripple-ink-color(primary);
+ @include mdc-icon-button-theme.theme((icon-color: mdc-theme-color.prop-value(primary)));
}
&.mat-accent {
- @include mdc-ripple-theme.states-base-color(secondary,
- $query: mdc-helpers.$mat-theme-styles-query,
- $ripple-target: button-theme-private.$button-state-target);
- @include mdc-icon-button.ink-color(secondary, $query: mdc-helpers.$mat-theme-styles-query);
- @include button-theme-private.ripple-ink-color(secondary);
+ @include mdc-icon-button-theme.theme((icon-color: mdc-theme-color.prop-value(secondary)));
}
&.mat-warn {
- @include mdc-ripple-theme.states-base-color(error,
- $query: mdc-helpers.$mat-theme-styles-query,
- $ripple-target: button-theme-private.$button-state-target);
- @include mdc-icon-button.ink-color(error, $query: mdc-helpers.$mat-theme-styles-query);
- @include button-theme-private.ripple-ink-color(error);
+ @include mdc-icon-button-theme.theme((icon-color: (mdc-theme-color.prop-value(error))));
}
@include button-theme-private.apply-disabled-style() {
- @include button-theme-private.apply-disabled-color();
+ @include mdc-icon-button-theme.theme((
+ icon-color: $disabled-color,
+ disabled-icon-color: $disabled-color,
+ ));
}
}
-
- @include mdc-icon-button.without-ripple($query: mdc-helpers.$mat-theme-styles-query);
}
}
diff --git a/src/material-experimental/mdc-button/button-base.ts b/src/material-experimental/mdc-button/button-base.ts
index f20cd6bec6dc..9d6806a1de71 100644
--- a/src/material-experimental/mdc-button/button-base.ts
+++ b/src/material-experimental/mdc-button/button-base.ts
@@ -6,9 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {BooleanInput} from '@angular/cdk/coercion';
import {Platform} from '@angular/cdk/platform';
-import {Directive, ElementRef, HostListener, NgZone, ViewChild} from '@angular/core';
+import {Directive, ElementRef, NgZone, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {
CanColor,
CanDisable,
@@ -86,12 +85,12 @@ export class MatButtonBase
extends _MatButtonMixin
implements CanDisable, CanColor, CanDisableRipple
{
- /** Whether the ripple is centered on the button. */
- _isRippleCentered = false;
-
/** Whether this button is a FAB. Used to apply the correct class on the ripple. */
_isFab = false;
+ /** Whether this button is an icon button. Used to apply the correct class on the ripple. */
+ _isIconButton = false;
+
/** Reference to the MatRipple instance of the button. */
@ViewChild(MatRipple) ripple: MatRipple;
@@ -129,9 +128,6 @@ export class MatButtonBase
_isRippleDisabled() {
return this.disableRipple || this.disabled;
}
-
- static ngAcceptInputType_disabled: BooleanInput;
- static ngAcceptInputType_disableRipple: BooleanInput;
}
/** Shared inputs by buttons using the `` tag */
@@ -160,24 +156,28 @@ export const MAT_ANCHOR_HOST = {
* Anchor button base.
*/
@Directive()
-export class MatAnchorBase extends MatButtonBase {
+export class MatAnchorBase extends MatButtonBase implements OnInit, OnDestroy {
tabIndex: number;
constructor(elementRef: ElementRef, platform: Platform, ngZone: NgZone, animationMode?: string) {
super(elementRef, platform, ngZone, animationMode);
}
- // We have to use a `HostListener` here in order to support both Ivy and ViewEngine.
- // In Ivy the `host` bindings will be merged when this class is extended, whereas in
- // ViewEngine they're overwritten.
- // TODO(mmalerba): we move this back into `host` once Ivy is turned on by default.
- // tslint:disable-next-line:no-host-decorator-in-concrete
- @HostListener('click', ['$event'])
- _haltDisabledEvents(event: Event) {
+ ngOnInit(): void {
+ this._ngZone.runOutsideAngular(() => {
+ this._elementRef.nativeElement.addEventListener('click', this._haltDisabledEvents);
+ });
+ }
+
+ ngOnDestroy(): void {
+ this._elementRef.nativeElement.removeEventListener('click', this._haltDisabledEvents);
+ }
+
+ _haltDisabledEvents = (event: Event): void => {
// A disabled button shouldn't apply any actions
if (this.disabled) {
event.preventDefault();
event.stopImmediatePropagation();
}
- }
+ };
}
diff --git a/src/material-experimental/mdc-button/button.html b/src/material-experimental/mdc-button/button.html
index acca8bffd4c9..d8644898e680 100644
--- a/src/material-experimental/mdc-button/button.html
+++ b/src/material-experimental/mdc-button/button.html
@@ -1,6 +1,8 @@
+ class="mat-mdc-button-persistent-ripple"
+ [class.mdc-button__ripple]="!_isFab && !_isIconButton"
+ [class.mdc-fab__ripple]="_isFab"
+ [class.mdc-icon-button__ripple]="_isIconButton">
@@ -18,7 +20,7 @@
diff --git a/src/material-experimental/mdc-button/button.scss b/src/material-experimental/mdc-button/button.scss
index 72c072e85aa0..04972576ee79 100644
--- a/src/material-experimental/mdc-button/button.scss
+++ b/src/material-experimental/mdc-button/button.scss
@@ -1,10 +1,55 @@
+@use 'sass:map';
@use '@material/button/button' as mdc-button;
+@use '@material/button/button-base' as mdc-button-base;
@use '@material/button/variables' as mdc-button-variables;
+@use '@material/button/button-text-theme' as mdc-button-text-theme;
+@use '@material/button/button-filled-theme' as mdc-button-filled-theme;
+@use '@material/button/button-protected-theme' as mdc-button-protected-theme;
+@use '@material/button/button-outlined-theme' as mdc-button-outlined-theme;
@use '../../material/core/style/private';
@use '../mdc-helpers/mdc-helpers';
@use 'button-base';
-@include mdc-button.without-ripple($query: mdc-helpers.$mat-base-styles-query);
+@include mdc-helpers.disable-fallback-declarations {
+ @include mdc-button.static-styles-without-ripple($query: mdc-helpers.$mat-base-styles-query);
+
+ // Keys to exclude from the MDC theme config, allowing us to drop styles we don't need.
+ $override-keys: button-base.mat-private-button-remove-ripple((
+ label-text-font: null,
+ label-text-size: null,
+ label-text-tracking: null,
+ label-text-transform: null,
+ label-text-weight: null,
+ with-icon-icon-size: null,
+ label-text-color: inherit,
+ ));
+
+ // Note that we don't include a feature query, because this mixins declare
+ // all the "slots" for CSS variables that will be defined in the theme.
+ .mat-mdc-button {
+ @include mdc-button-text-theme.theme-styles(
+ map.merge(mdc-button-text-theme.$light-theme, $override-keys));
+ }
+
+ .mat-mdc-unelevated-button {
+ @include mdc-button-filled-theme.theme-styles(
+ map.merge(map.merge(mdc-button-filled-theme.$light-theme, $override-keys), (
+ container-color: transparent,
+ )));
+ }
+
+ .mat-mdc-raised-button {
+ @include mdc-button-protected-theme.theme-styles(
+ map.merge(map.merge(mdc-button-protected-theme.$light-theme, $override-keys), (
+ container-color: transparent,
+ )));
+ }
+
+ .mat-mdc-outlined-button {
+ @include mdc-button-outlined-theme.theme-styles(
+ map.merge(mdc-button-outlined-theme.$light-theme, $override-keys));
+ }
+}
.mat-mdc-button,
.mat-mdc-unelevated-button,
@@ -24,10 +69,10 @@
// mixins will style the icons appropriately.
.mat-mdc-button {
.mat-icon {
- @include mdc-button.icon();
+ @include mdc-button-base.icon();
}
.mdc-button__label + .mat-icon {
- @include mdc-button.icon-trailing();
+ @include mdc-button-base.icon-trailing();
}
}
@@ -36,12 +81,12 @@
.mat-mdc-outlined-button {
// Icons inside contained buttons have different styles due to increased button padding
.mat-icon {
- @include mdc-button.icon();
- @include mdc-button.icon-contained();
+ @include mdc-button-base.icon();
+ @include mdc-button-base.icon-contained();
}
.mdc-button__label + .mat-icon {
- @include mdc-button.icon-contained-trailing();
+ @include mdc-button-base.icon-contained-trailing();
}
}
diff --git a/src/material-experimental/mdc-button/button.spec.ts b/src/material-experimental/mdc-button/button.spec.ts
index 4eccefdff4d6..b19b59c8b9da 100644
--- a/src/material-experimental/mdc-button/button.spec.ts
+++ b/src/material-experimental/mdc-button/button.spec.ts
@@ -1,8 +1,9 @@
import {waitForAsync, ComponentFixture, TestBed} from '@angular/core/testing';
-import {Component, DebugElement} from '@angular/core';
+import {ApplicationRef, Component, DebugElement} from '@angular/core';
import {By} from '@angular/platform-browser';
import {MatButtonModule, MatButton, MatFabDefaultOptions, MAT_FAB_DEFAULT_OPTIONS} from './index';
import {MatRipple, ThemePalette} from '@angular/material-experimental/mdc-core';
+import {createMouseEvent, dispatchEvent} from '@angular/cdk/testing/private';
describe('MDC-based MatButton', () => {
beforeEach(
@@ -229,6 +230,28 @@ describe('MDC-based MatButton', () => {
.withContext('Expected custom tabindex to be overwritten when disabled.')
.toBe('-1');
});
+
+ describe('change detection behavior', () => {
+ it('should not run change detection for disabled anchor but should prevent the default behavior and stop event propagation', () => {
+ const appRef = TestBed.inject(ApplicationRef);
+ const fixture = TestBed.createComponent(TestApp);
+ fixture.componentInstance.isDisabled = true;
+ fixture.detectChanges();
+ const anchorElement = fixture.debugElement.query(By.css('a'))!.nativeElement;
+
+ spyOn(appRef, 'tick');
+
+ const event = createMouseEvent('click');
+ spyOn(event, 'preventDefault').and.callThrough();
+ spyOn(event, 'stopImmediatePropagation').and.callThrough();
+
+ dispatchEvent(anchorElement, event);
+
+ expect(appRef.tick).not.toHaveBeenCalled();
+ expect(event.preventDefault).toHaveBeenCalled();
+ expect(event.stopImmediatePropagation).toHaveBeenCalled();
+ });
+ });
});
// Ripple tests.
diff --git a/src/material-experimental/mdc-button/fab.scss b/src/material-experimental/mdc-button/fab.scss
index b93fe1dbcdf5..cba815029ef9 100644
--- a/src/material-experimental/mdc-button/fab.scss
+++ b/src/material-experimental/mdc-button/fab.scss
@@ -1,16 +1,49 @@
@use '@material/fab' as mdc-fab;
+@use '@material/elevation/elevation-theme' as mdc-elevation-theme;
@use '../../material/core/style/private';
@use '../mdc-helpers/mdc-helpers';
-@use 'button-base';
+@use './button-base';
-@include mdc-fab.without-ripple($query: mdc-helpers.$mat-base-styles-query);
+@include mdc-helpers.disable-fallback-declarations {
+ @include mdc-fab.without-ripple($query: mdc-helpers.$mat-base-styles-query);
+}
.mat-mdc-fab, .mat-mdc-mini-fab {
@include button-base.mat-private-button-interactive();
- @include button-base.mat-private-button-disabled();
@include button-base.mat-private-button-touch-target(true);
@include private.private-animation-noop();
+ @include mdc-helpers.disable-fallback-declarations {
+ // Theme configuration is copied over from MDC, because it isn't exported
+ @include mdc-fab.theme-styles(button-base.mat-private-button-remove-ripple((
+ container-color: transparent,
+ container-shape: mdc-fab.$shape-radius,
+ icon-color: inherit,
+ )));
+
+ // TODO(crisbeto): the elevation can be controlled using `container-elevation` in `theme-styles`
+ // however when it is passed in, MDC throws an error that `container-shadow-color` isn't
+ // passed in. When `container-shadow-color` is passed in, MDC throws another error, because
+ // the produced value isn't valid CSS. Eventually we should switch to it once it's fixed.
+ @include mdc-elevation-theme.elevation(6);
+
+ &:hover, &:focus {
+ @include mdc-elevation-theme.elevation(8);
+ }
+
+ &:active, &:focus:active {
+ @include mdc-elevation-theme.elevation(12);
+ }
+
+ @include button-base.mat-private-button-disabled {
+ @include mdc-elevation-theme.elevation(0);
+ }
+ }
+
+ // TODO(crisbeto): `theme-styles` doesn't allow for the color to be controlled. Define a custom
+ // variable for now so we have something to target.
+ color: var(--mat-mdc-fab-color, inherit);
+
// Prevent the button from shrinking since it's always supposed to be a circle.
flex-shrink: 0;
diff --git a/src/material-experimental/mdc-button/fab.ts b/src/material-experimental/mdc-button/fab.ts
index bfd96b75232c..70a084dfae0f 100644
--- a/src/material-experimental/mdc-button/fab.ts
+++ b/src/material-experimental/mdc-button/fab.ts
@@ -66,20 +66,11 @@ const defaults = MAT_FAB_DEFAULT_OPTIONS_FACTORY();
selector: `button[mat-fab]`,
templateUrl: 'button.html',
styleUrls: ['fab.css'],
- // TODO: change to MAT_BUTTON_INPUTS/MAT_BUTTON_HOST with spread after ViewEngine is deprecated
- inputs: ['disabled', 'disableRipple', 'color', 'extended'],
+ inputs: [...MAT_BUTTON_INPUTS, 'extended'],
host: {
+ ...MAT_BUTTON_HOST,
'[class.mdc-fab--extended]': 'extended',
'[class.mat-mdc-extended-fab]': 'extended',
- '[attr.disabled]': 'disabled || null',
- '[class._mat-animation-noopable]': '_animationMode === "NoopAnimations"',
- // MDC automatically applies the primary theme color to the button, but we want to support
- // an unthemed version. If color is undefined, apply a CSS class that makes it easy to
- // select and style this "theme".
- '[class.mat-unthemed]': '!color',
- // Add a class that applies to all buttons. This makes it easier to target if somebody
- // wants to target all Material buttons.
- '[class.mat-mdc-button-base]': 'true',
},
exportAs: 'matButton',
encapsulation: ViewEncapsulation.None,
@@ -88,13 +79,13 @@ const defaults = MAT_FAB_DEFAULT_OPTIONS_FACTORY();
export class MatFabButton extends MatButtonBase {
override _isFab = true;
- private _extended: boolean;
get extended(): boolean {
return this._extended;
}
- set extended(value: boolean) {
+ set extended(value: BooleanInput) {
this._extended = coerceBooleanProperty(value);
}
+ private _extended: boolean;
constructor(
elementRef: ElementRef,
@@ -107,8 +98,6 @@ export class MatFabButton extends MatButtonBase {
this._options = this._options || defaults;
this.color = this.defaultColor = this._options!.color || defaults.color;
}
-
- static ngAcceptInputType_extended: BooleanInput;
}
/**
@@ -153,26 +142,11 @@ export class MatMiniFabButton extends MatButtonBase {
selector: `a[mat-fab]`,
templateUrl: 'button.html',
styleUrls: ['fab.css'],
- // TODO: change to MAT_ANCHOR_INPUTS/MAT_ANCHOR_HOST with spread after ViewEngine is deprecated
- inputs: ['disabled', 'disableRipple', 'color', 'tabIndex', 'extended'],
+ inputs: [...MAT_ANCHOR_INPUTS, 'extended'],
host: {
+ ...MAT_ANCHOR_HOST,
'[class.mdc-fab--extended]': 'extended',
'[class.mat-mdc-extended-fab]': 'extended',
- '[attr.disabled]': 'disabled || null',
- '[class._mat-animation-noopable]': '_animationMode === "NoopAnimations"',
-
- // Note that we ignore the user-specified tabindex when it's disabled for
- // consistency with the `mat-button` applied on native buttons where even
- // though they have an index, they're not tabbable.
- '[attr.tabindex]': 'disabled ? -1 : (tabIndex || 0)',
- '[attr.aria-disabled]': 'disabled.toString()',
- // MDC automatically applies the primary theme color to the button, but we want to support
- // an unthemed version. If color is undefined, apply a CSS class that makes it easy to
- // select and style this "theme".
- '[class.mat-unthemed]': '!color',
- // Add a class that applies to all buttons. This makes it easier to target if somebody
- // wants to target all Material buttons.
- '[class.mat-mdc-button-base]': 'true',
},
exportAs: 'matButton, matAnchor',
encapsulation: ViewEncapsulation.None,
@@ -181,13 +155,13 @@ export class MatMiniFabButton extends MatButtonBase {
export class MatFabAnchor extends MatAnchor {
override _isFab = true;
- private _extended: boolean;
get extended(): boolean {
return this._extended;
}
- set extended(value: boolean) {
+ set extended(value: BooleanInput) {
this._extended = coerceBooleanProperty(value);
}
+ private _extended: boolean;
constructor(
elementRef: ElementRef,
@@ -200,8 +174,6 @@ export class MatFabAnchor extends MatAnchor {
this._options = this._options || defaults;
this.color = this.defaultColor = this._options!.color || defaults.color;
}
-
- static ngAcceptInputType_extended: BooleanInput;
}
/**
diff --git a/src/material-experimental/mdc-button/icon-button.scss b/src/material-experimental/mdc-button/icon-button.scss
index 379c22de9356..e94bffff67c7 100644
--- a/src/material-experimental/mdc-button/icon-button.scss
+++ b/src/material-experimental/mdc-button/icon-button.scss
@@ -1,15 +1,27 @@
+@use 'sass:map';
@use '@material/icon-button/icon-button' as mdc-icon-button;
+@use '@material/icon-button/icon-button-theme' as mdc-icon-button-theme;
@use '../../material/core/style/private';
@use '../mdc-helpers/mdc-helpers';
@use 'button-base';
-@include mdc-icon-button.without-ripple(
- $query: mdc-helpers.$mat-base-styles-query
-);
+@include mdc-helpers.disable-fallback-declarations {
+ @include mdc-icon-button.without-ripple($query: mdc-helpers.$mat-base-styles-query);
+}
.mat-mdc-icon-button {
- @include button-base.mat-private-button-interactive() {
- border-radius: 50%;
+ @include mdc-helpers.disable-fallback-declarations {
+ $theme-overrides: button-base.mat-private-button-remove-ripple((
+ icon-color: inherit,
+ // We don't change the color on focus/hover so exclude
+ // these styles both to reduce bundle size and specificity.
+ focus-icon-color: null,
+ hover-icon-color: null,
+ pressed-icon-color: null,
+ ));
+
+ @include mdc-icon-button-theme.theme-styles(
+ map.merge(mdc-icon-button-theme.$light-theme, $theme-overrides));
}
// Border radius is inherited by ripple to know its shape. Set to 50% so the ripple is round.
@@ -18,10 +30,20 @@
// Prevent the button from shrinking since it's always supposed to be a circle.
flex-shrink: 0;
- @include button-base.mat-private-button-disabled();
+ @include button-base.mat-private-button-disabled() {
+ // The color is already dimmed when the button is disabled. Restore the opacity both to
+ // help with the color contrast and to align with what we had before switching to the new API.
+ opacity: 1;
+ };
+
+ @include button-base.mat-private-button-interactive();
@include button-base.mat-private-button-touch-target(true);
@include private.private-animation-noop();
+ .mat-mdc-button-persistent-ripple {
+ border-radius: 50%;
+ }
+
// MDC adds some styles to icon buttons that conflict with some of our focus indicator styles
// and don't actually do anything. This undoes those conflicting styles.
&.mat-unthemed,
diff --git a/src/material-experimental/mdc-button/icon-button.ts b/src/material-experimental/mdc-button/icon-button.ts
index d9bf4e8aae6e..548875a196b8 100644
--- a/src/material-experimental/mdc-button/icon-button.ts
+++ b/src/material-experimental/mdc-button/icon-button.ts
@@ -43,8 +43,7 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MatIconButton extends MatButtonBase {
- // Set the ripple to be centered for icon buttons
- override _isRippleCentered = true;
+ override _isIconButton = true;
constructor(
elementRef: ElementRef,
@@ -72,8 +71,7 @@ export class MatIconButton extends MatButtonBase {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MatIconAnchor extends MatAnchorBase {
- // Set the ripple to be centered for icon buttons
- override _isRippleCentered = true;
+ override _isIconButton = true;
constructor(
elementRef: ElementRef,
diff --git a/src/material-experimental/mdc-card/card.scss b/src/material-experimental/mdc-card/card.scss
index d7b438fac737..163ad3fe09e3 100644
--- a/src/material-experimental/mdc-card/card.scss
+++ b/src/material-experimental/mdc-card/card.scss
@@ -10,8 +10,10 @@ $mat-card-header-size: 40px !default;
// Default padding for text content within a card.
$mat-card-default-padding: 16px !default;
-// Include all MDC card styles except for color and typography.
-@include mdc-card.without-ripple($query: mdc-helpers.$mat-base-styles-query);
+@include mdc-helpers.disable-fallback-declarations {
+ // Include all MDC card styles except for color and typography.
+ @include mdc-card.without-ripple($query: mdc-helpers.$mat-base-styles-query);
+}
// Title text and subtitles text within a card. MDC doesn't have pre-made title sections for cards.
// Maintained here for backwards compatibility with the previous generation MatCard.
diff --git a/src/material-experimental/mdc-checkbox/BUILD.bazel b/src/material-experimental/mdc-checkbox/BUILD.bazel
index 2a3a07b54bbd..c5fa71155f34 100644
--- a/src/material-experimental/mdc-checkbox/BUILD.bazel
+++ b/src/material-experimental/mdc-checkbox/BUILD.bazel
@@ -25,7 +25,6 @@ ng_module(
"//src/material-experimental/mdc-core",
"//src/material/checkbox",
"@npm//@angular/animations",
- "@npm//@angular/common",
"@npm//@angular/core",
"@npm//@angular/forms",
"@npm//@material/checkbox",
@@ -48,6 +47,7 @@ sass_binary(
"external/npm/node_modules",
],
deps = [
+ ":mdc_checkbox_scss_lib",
"//src/material-experimental/mdc-helpers:mdc_helpers_scss_lib",
"//src/material-experimental/mdc-helpers:mdc_scss_deps_lib",
"//src/material/core:core_scss_lib",
diff --git a/src/material-experimental/mdc-checkbox/_checkbox-private.scss b/src/material-experimental/mdc-checkbox/_checkbox-private.scss
new file mode 100644
index 000000000000..0a46f6a2a62c
--- /dev/null
+++ b/src/material-experimental/mdc-checkbox/_checkbox-private.scss
@@ -0,0 +1,46 @@
+@use 'sass:map';
+@use 'sass:color';
+@use '@material/checkbox/checkbox-theme' as mdc-checkbox-theme;
+@use '@material/theme/theme-color' as mdc-theme-color;
+
+// Configuration used to define the theme-related CSS variables.
+$private-checkbox-theme-config: map.merge(mdc-checkbox-theme.$light-theme, (
+ // Exclude all of the ripple-related styles.
+ selected-focus-state-layer-color: null,
+ selected-focus-state-layer-opacity: null,
+ selected-hover-state-layer-color: null,
+ selected-hover-state-layer-opacity: null,
+ selected-pressed-state-layer-color: null,
+ selected-pressed-state-layer-opacity: null,
+ unselected-focus-state-layer-color: null,
+ unselected-focus-state-layer-opacity: null,
+ unselected-hover-state-layer-color: null,
+ unselected-hover-state-layer-opacity: null,
+ unselected-pressed-state-layer-color: null,
+ unselected-pressed-state-layer-opacity: null,
+));
+
+// Mixin that includes the checkbox theme styles with a given palette.
+// By default, the MDC checkbox always uses the `secondary` palette.
+@mixin private-checkbox-styles-with-color($color, $mdc-color) {
+ $on-surface: mdc-theme-color.prop-value(on-surface);
+ $border-color: rgba($on-surface, color.opacity(mdc-checkbox-theme.$border-color));
+ $disabled-color: rgba($on-surface, color.opacity(mdc-checkbox-theme.$disabled-color));
+
+ @include mdc-checkbox-theme.theme((
+ selected-checkmark-color: mdc-theme-color.prop-value(on-#{$mdc-color}),
+
+ selected-focus-icon-color: $color,
+ selected-hover-icon-color: $color,
+ selected-icon-color: $color,
+ selected-pressed-icon-color: $color,
+ unselected-focus-icon-color: $color,
+ unselected-hover-icon-color: $color,
+
+ disabled-selected-icon-color: $disabled-color,
+ disabled-unselected-icon-color: $disabled-color,
+
+ unselected-icon-color: $border-color,
+ unselected-pressed-icon-color: $border-color,
+ ));
+}
diff --git a/src/material-experimental/mdc-checkbox/_checkbox-theme.scss b/src/material-experimental/mdc-checkbox/_checkbox-theme.scss
index b07b57cd4eb5..d520043ca765 100644
--- a/src/material-experimental/mdc-checkbox/_checkbox-theme.scss
+++ b/src/material-experimental/mdc-checkbox/_checkbox-theme.scss
@@ -1,44 +1,24 @@
@use '@material/checkbox/checkbox' as mdc-checkbox;
@use '@material/checkbox/checkbox-theme' as mdc-checkbox-theme;
-@use '@material/ripple/ripple-theme' as mdc-ripple-theme;
@use '@material/form-field' as mdc-form-field;
@use '@material/theme/theme-color' as mdc-theme-color;
@use '@material/theme/theme';
@use 'sass:map';
+@use 'sass:color';
@use '../mdc-helpers/mdc-helpers';
@use '../../material/core/typography/typography';
@use '../../material/core/theming/theming';
@use '../../material/core/ripple/ripple-theme';
-
-
-// Mixin that includes the checkbox theme styles with a given palette.
-// By default, the MDC checkbox always uses the `secondary` palette.
-@mixin private-checkbox-styles-with-color($color) {
- @include mdc-checkbox-theme.theme-deprecated(
- (
- density-scale: null,
- checkmark-color: mdc-theme-color.prop-value(on-#{$color}),
- container-checked-color: $color,
- container-checked-hover-color: null,
- container-disabled-color: rgba(mdc-theme-color.prop-value(on-surface), 0.38),
- outline-color: rgba(mdc-theme-color.prop-value(on-surface), 0.54),
- outline-hover-color: null,
- ripple-color: mdc-theme-color.prop-value(on-surface),
- ripple-opacity: mdc-ripple-theme.$dark-ink-opacities,
- ripple-checked-color: $color,
- ripple-checked-opacity: mdc-ripple-theme.$dark-ink-opacities,
- )
- );
-}
+@use './checkbox-private';
// Apply ripple colors to the MatRipple element and the MDC ripple element when the
// checkbox is selected.
-@mixin _selected-ripple-colors($theme, $mdcColor) {
+@mixin _selected-ripple-colors($theme, $mdc-color) {
.mdc-checkbox--selected ~ {
.mat-mdc-checkbox-ripple {
@include ripple-theme.color((
foreground: (
- base: mdc-theme-color.prop-value($mdcColor)
+ base: mdc-theme-color.prop-value($mdc-color)
),
));
}
@@ -55,23 +35,8 @@
$accent: theming.get-color-from-palette(map.get($config, accent));
$warn: theming.get-color-from-palette(map.get($config, warn));
- // Save original values of MDC global variables. We need to save these so we can restore the
- // variables to their original values and prevent unintended side effects from using this mixin.
- $orig-border-color: mdc-checkbox-theme.$border-color;
- $orig-disabled-color: mdc-checkbox-theme.$disabled-color;
-
@include mdc-helpers.mat-using-mdc-theme($config) {
- mdc-checkbox-theme.$border-color: rgba(
- mdc-theme-color.prop-value(on-surface),
- 0.54
- );
- mdc-checkbox-theme.$disabled-color: rgba(
- mdc-theme-color.prop-value(on-surface),
- 0.26
- );
-
.mat-mdc-checkbox {
- @include private-checkbox-styles-with-color(primary);
@include mdc-form-field.core-styles($query: mdc-helpers.$mat-theme-styles-query);
@include ripple-theme.color((
foreground: (
@@ -87,25 +52,21 @@
// class for accent and warn style, and applying the appropriate overrides below. Since we
// don't use MDC's ripple, we also need to set the color for our replacement ripple.
&.mat-primary {
- @include private-checkbox-styles-with-color(primary);
+ @include checkbox-private.private-checkbox-styles-with-color($primary, primary);
@include _selected-ripple-colors($primary, primary);
}
&.mat-accent {
- @include private-checkbox-styles-with-color(secondary);
+ @include checkbox-private.private-checkbox-styles-with-color($accent, secondary);
@include _selected-ripple-colors($accent, secondary);
}
&.mat-warn {
- @include private-checkbox-styles-with-color(error);
+ @include checkbox-private.private-checkbox-styles-with-color($warn, error);
@include _selected-ripple-colors($warn, error);
}
}
}
-
- // Restore original values of MDC global variables.
- mdc-checkbox-theme.$border-color: $orig-border-color;
- mdc-checkbox-theme.$disabled-color: $orig-disabled-color;
}
@mixin typography($config-or-theme) {
@@ -119,16 +80,19 @@
@mixin density($config-or-theme) {
$density-scale: theming.get-density-config($config-or-theme);
- .mat-mdc-checkbox .mdc-checkbox {
- @include mdc-checkbox-theme.density(
- $density-scale,
- $query: mdc-helpers.$mat-base-styles-query
- );
- }
- @if ($density-scale == -2 or $density-scale == 'minimum') {
- .mat-mdc-checkbox-touch-target {
- display: none;
+ @include mdc-helpers.disable-fallback-declarations {
+ .mat-mdc-checkbox .mdc-checkbox {
+ @include mdc-checkbox-theme.density(
+ $density-scale,
+ $query: mdc-helpers.$mat-base-styles-query
+ );
+ }
+
+ @if ($density-scale == -2 or $density-scale == 'minimum') {
+ .mat-mdc-checkbox-touch-target {
+ display: none;
+ }
}
}
}
diff --git a/src/material-experimental/mdc-checkbox/checkbox.html b/src/material-experimental/mdc-checkbox/checkbox.html
index d435598c4b85..7e28c481479f 100644
--- a/src/material-experimental/mdc-checkbox/checkbox.html
+++ b/src/material-experimental/mdc-checkbox/checkbox.html
@@ -5,7 +5,7 @@
{
}));
});
- describe('aria-label', () => {
- let checkboxDebugElement: DebugElement;
- let checkboxNativeElement: HTMLElement;
- let inputElement: HTMLInputElement;
-
+ describe('aria handling', () => {
it('should use the provided aria-label', fakeAsync(() => {
fixture = createComponent(CheckboxWithAriaLabel);
- checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!;
- checkboxNativeElement = checkboxDebugElement.nativeElement;
- inputElement = checkboxNativeElement.querySelector('input');
+ const checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!;
+ const checkboxNativeElement = checkboxDebugElement.nativeElement;
+ const inputElement = checkboxNativeElement.querySelector('input');
fixture.detectChanges();
expect(inputElement.getAttribute('aria-label')).toBe('Super effective');
@@ -662,18 +658,12 @@ describe('MDC-based MatCheckbox', () => {
expect(fixture.nativeElement.querySelector('input').hasAttribute('aria-label')).toBe(false);
}));
- });
-
- describe('with provided aria-labelledby ', () => {
- let checkboxDebugElement: DebugElement;
- let checkboxNativeElement: HTMLElement;
- let inputElement: HTMLInputElement;
it('should use the provided aria-labelledby', fakeAsync(() => {
fixture = createComponent(CheckboxWithAriaLabelledby);
- checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!;
- checkboxNativeElement = checkboxDebugElement.nativeElement;
- inputElement = checkboxNativeElement.querySelector('input');
+ const checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!;
+ const checkboxNativeElement = checkboxDebugElement.nativeElement;
+ const inputElement = checkboxNativeElement.querySelector('input');
fixture.detectChanges();
expect(inputElement.getAttribute('aria-labelledby')).toBe('some-id');
@@ -681,13 +671,22 @@ describe('MDC-based MatCheckbox', () => {
it('should not assign aria-labelledby if none is provided', fakeAsync(() => {
fixture = createComponent(SingleCheckbox);
- checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!;
- checkboxNativeElement = checkboxDebugElement.nativeElement;
- inputElement = checkboxNativeElement.querySelector('input');
+ const checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!;
+ const checkboxNativeElement = checkboxDebugElement.nativeElement;
+ const inputElement = checkboxNativeElement.querySelector('input');
fixture.detectChanges();
expect(inputElement.getAttribute('aria-labelledby')).toBe(null);
}));
+
+ it('should clear the static aria attributes from the host node', () => {
+ fixture = createComponent(CheckboxWithStaticAriaAttributes);
+ const checkbox = fixture.debugElement.query(By.directive(MatCheckbox))!.nativeElement;
+ fixture.detectChanges();
+
+ expect(checkbox.hasAttribute('aria')).toBe(false);
+ expect(checkbox.hasAttribute('aria-labelledby')).toBe(false);
+ });
});
describe('with provided aria-describedby ', () => {
@@ -1147,3 +1146,8 @@ class CheckboxWithoutLabel {
/** Test component with the native tabindex attribute. */
@Component({template: ` `})
class CheckboxWithTabindexAttr {}
+
+@Component({
+ template: ` `,
+})
+class CheckboxWithStaticAriaAttributes {}
diff --git a/src/material-experimental/mdc-checkbox/checkbox.ts b/src/material-experimental/mdc-checkbox/checkbox.ts
index b536779399b7..c7ff04ad77c7 100644
--- a/src/material-experimental/mdc-checkbox/checkbox.ts
+++ b/src/material-experimental/mdc-checkbox/checkbox.ts
@@ -77,9 +77,14 @@ const _MatCheckboxBase = mixinColor(
host: {
'class': 'mat-mdc-checkbox',
'[attr.tabindex]': 'null',
+ '[attr.aria-label]': 'null',
+ '[attr.aria-labelledby]': 'null',
'[class._mat-animation-noopable]': `_animationMode === 'NoopAnimations'`,
'[class.mdc-checkbox--disabled]': 'disabled',
'[id]': 'id',
+ // Add classes that users can use to more easily target disabled or checked checkboxes.
+ '[class.mat-mdc-checkbox-disabled]': 'disabled',
+ '[class.mat-mdc-checkbox-checked]': 'checked',
},
providers: [MAT_CHECKBOX_CONTROL_VALUE_ACCESSOR],
exportAs: 'matCheckbox',
@@ -124,8 +129,9 @@ export class MatCheckbox
get checked(): boolean {
return this._checked;
}
- set checked(checked) {
+ set checked(checked: BooleanInput) {
this._checked = coerceBooleanProperty(checked);
+ this._changeDetectorRef.markForCheck();
}
private _checked = false;
@@ -139,7 +145,7 @@ export class MatCheckbox
get indeterminate(): boolean {
return this._indeterminate;
}
- set indeterminate(indeterminate) {
+ set indeterminate(indeterminate: BooleanInput) {
this._indeterminate = coerceBooleanProperty(indeterminate);
this._syncIndeterminate(this._indeterminate);
}
@@ -150,7 +156,7 @@ export class MatCheckbox
get required(): boolean {
return this._required;
}
- set required(required) {
+ set required(required: BooleanInput) {
this._required = coerceBooleanProperty(required);
}
private _required = false;
@@ -160,7 +166,7 @@ export class MatCheckbox
get disableRipple(): boolean {
return this._disableRipple;
}
- set disableRipple(disableRipple: boolean) {
+ set disableRipple(disableRipple: BooleanInput) {
this._disableRipple = coerceBooleanProperty(disableRipple);
}
private _disableRipple = false;
@@ -192,9 +198,6 @@ export class MatCheckbox
/** The `MDCCheckboxFoundation` instance for this checkbox. */
_checkboxFoundation: MDCCheckboxFoundation;
- /** The set of classes that should be applied to the native input. */
- _classes: {[key: string]: boolean} = {'mdc-checkbox__native-control': true};
-
/** ControlValueAccessor onChange */
private _cvaOnChange = (_: boolean) => {};
@@ -211,8 +214,8 @@ export class MatCheckbox
/** The `MDCCheckboxAdapter` instance for this checkbox. */
private _checkboxAdapter: MDCCheckboxAdapter = {
- addClass: className => this._setClass(className, true),
- removeClass: className => this._setClass(className, false),
+ addClass: className => this._nativeCheckbox.nativeElement.classList.add(className),
+ removeClass: className => this._nativeCheckbox.nativeElement.classList.remove(className),
forceLayout: () => this._checkbox.nativeElement.offsetWidth,
hasNativeControl: () => !!this._nativeCheckbox,
isAttachedToDOM: () => !!this._checkbox.nativeElement.parentNode,
@@ -371,12 +374,6 @@ export class MatCheckbox
return this.indeterminate ? 'mixed' : 'false';
}
- /** Sets whether the given CSS class should be applied to the native input. */
- private _setClass(cssClass: string, active: boolean) {
- this._classes[cssClass] = active;
- this._changeDetectorRef.markForCheck();
- }
-
/**
* Syncs the indeterminate value with the checkbox DOM node.
*
@@ -391,10 +388,4 @@ export class MatCheckbox
nativeCheckbox.nativeElement.indeterminate = value;
}
}
-
- static ngAcceptInputType_checked: BooleanInput;
- static ngAcceptInputType_indeterminate: BooleanInput;
- static ngAcceptInputType_disabled: BooleanInput;
- static ngAcceptInputType_required: BooleanInput;
- static ngAcceptInputType_disableRipple: BooleanInput;
}
diff --git a/src/material-experimental/mdc-checkbox/module.ts b/src/material-experimental/mdc-checkbox/module.ts
index 19685141b76a..8682b40d169c 100644
--- a/src/material-experimental/mdc-checkbox/module.ts
+++ b/src/material-experimental/mdc-checkbox/module.ts
@@ -6,14 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
import {_MatCheckboxRequiredValidatorModule} from '@angular/material/checkbox';
import {MatCommonModule, MatRippleModule} from '@angular/material-experimental/mdc-core';
import {MatCheckbox} from './checkbox';
@NgModule({
- imports: [MatCommonModule, MatRippleModule, CommonModule, _MatCheckboxRequiredValidatorModule],
+ imports: [MatCommonModule, MatRippleModule, _MatCheckboxRequiredValidatorModule],
exports: [MatCheckbox, MatCommonModule, _MatCheckboxRequiredValidatorModule],
declarations: [MatCheckbox],
})
diff --git a/src/material-experimental/mdc-chips/BUILD.bazel b/src/material-experimental/mdc-chips/BUILD.bazel
index 89bf3fd154be..02ae91477dea 100644
--- a/src/material-experimental/mdc-chips/BUILD.bazel
+++ b/src/material-experimental/mdc-chips/BUILD.bazel
@@ -17,7 +17,10 @@ ng_module(
"**/*.spec.ts",
],
),
- assets = [":chips_scss"] + glob(["**/*.html"]),
+ assets = [
+ ":chip_scss",
+ ":chip_set_scss",
+ ] + glob(["**/*.html"]),
deps = [
"//src:dev_mode_types",
"//src/material-experimental/mdc-core",
@@ -40,8 +43,21 @@ sass_library(
)
sass_binary(
- name = "chips_scss",
- src = "chips.scss",
+ name = "chip_scss",
+ src = "chip.scss",
+ include_paths = [
+ "external/npm/node_modules",
+ ],
+ deps = [
+ "//src/material-experimental/mdc-helpers:mdc_helpers_scss_lib",
+ "//src/material-experimental/mdc-helpers:mdc_scss_deps_lib",
+ "//src/material/core:core_scss_lib",
+ ],
+)
+
+sass_binary(
+ name = "chip_set_scss",
+ src = "chip-set.scss",
include_paths = [
"external/npm/node_modules",
],
diff --git a/src/material-experimental/mdc-chips/_chips-theme.scss b/src/material-experimental/mdc-chips/_chips-theme.scss
index 73f54869d0f3..e4b6b9cd73c0 100644
--- a/src/material-experimental/mdc-chips/_chips-theme.scss
+++ b/src/material-experimental/mdc-chips/_chips-theme.scss
@@ -1,93 +1,93 @@
-@use '@material/chips/deprecated' as mdc-chips;
+@use '@material/chips/chip' as mdc-chip;
+@use '@material/chips/chip-theme' as mdc-chip-theme;
+@use '@material/chips/chip-set' as mdc-chip-set;
@use '@material/theme/theme-color' as mdc-theme-color;
+@use '@material/theme/color-palette' as mdc-color-palette;
@use 'sass:color';
@use 'sass:map';
@use '../mdc-helpers/mdc-helpers';
@use '../../material/core/typography/typography';
@use '../../material/core/theming/theming';
-@mixin _selected-color($color) {
- @include mdc-chips.fill-color($color, $query: mdc-helpers.$mat-theme-styles-query);
- @include mdc-chips.ink-color(text-primary-on-dark, $query: mdc-helpers.$mat-theme-styles-query);
- @include mdc-chips.selected-ink-color-without-ripple_(
- text-primary-on-dark,
- $query: mdc-helpers.$mat-theme-styles-query
- );
- @include mdc-chips.leading-icon-color(text-primary-on-dark,
- $query: mdc-helpers.$mat-theme-styles-query);
- @include mdc-chips.trailing-icon-color(text-primary-on-dark,
- $query: mdc-helpers.$mat-theme-styles-query);
+// Customizes the appearance of a chip. Note that ideally we would be doing this using the
+// `theme-styles` mixin, however it has the following problems:
+// 1. Some of MDC's base styles have **very** high specificity. E.g. setting the background of a
+// non-selected, enabled chip uses a selector like `.chip:not(.selected):not(.disabled)` instead of
+// just `.chip`. This specificity increase has a ripple effect over all other components that are
+// built on top of ours, making overrides extremely difficult and brittle.
+// 2. Including the individual mixins allows us to avoid a lot of unnecessary CSS (~35kb in the
+// dev app theme).
+@mixin _chip-variant($background, $foreground) {
+ @include mdc-chip-theme.container-color($background);
+ @include mdc-chip-theme.icon-color($foreground);
+ @include mdc-chip-theme.trailing-action-color($foreground);
+ @include mdc-chip-theme.checkmark-color($foreground);
+ @include mdc-chip-theme.text-label-color($foreground);
+
+ // Technically the avatar is only supposed to have an image, but we also allow for icons.
+ // Set the color so the icons inherit the correct color.
+ .mat-mdc-chip-avatar {
+ color: $foreground;
+ }
+}
+
+@mixin _colored-chip($palette) {
+ $background: theming.get-color-from-palette($palette);
+ $foreground: theming.get-color-from-palette($palette, default-contrast);
+
+ &.mat-mdc-chip-selected,
+ &.mat-mdc-chip-highlighted {
+ @include _chip-variant($background, $foreground);
+ }
}
@mixin color($config-or-theme) {
$config: theming.get-color-config($config-or-theme);
- $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));
- $background: map.get($config, background);
- $unselected-background: theming.get-color-from-palette($background, unselected-chip);
-
- // Save original values of MDC global variables. We need to save these so we can restore the
- // variables to their original values and prevent unintended side effects from using this mixin.
- $orig-mdc-chips-fill-color-default: mdc-chips.$fill-color-default;
- $orig-mdc-chips-ink-color-default: mdc-chips.$ink-color-default;
- $orig-mdc-chips-icon-color: mdc-chips.$icon-color;
+ $primary: map.get($config, primary);
+ $accent: map.get($config, accent);
+ $warn: map.get($config, warn);
+ $foreground: map.get($config, foreground);
+ $is-dark: map.get($config, is-dark);
@include mdc-helpers.mat-using-mdc-theme($config) {
- mdc-chips.$fill-color-default:
- color.mix(mdc-theme-color.prop-value(on-surface), mdc-theme-color.prop-value(surface), 12%);
- mdc-chips.$ink-color-default: rgba(mdc-theme-color.prop-value(on-surface), 0.87);
- mdc-chips.$icon-color: mdc-theme-color.prop-value(on-surface);
-
- @include mdc-chips.set-core-styles($query: mdc-helpers.$mat-theme-styles-query);
- @include mdc-chips.without-ripple($query: mdc-helpers.$mat-theme-styles-query);
+ .mat-mdc-standard-chip {
+ @include _chip-variant(
+ color.mix(mdc-theme-color.prop-value(on-surface), mdc-theme-color.prop-value(surface), 12%),
+ if($is-dark, mdc-color-palette.$grey-50, mdc-color-palette.$grey-900)
+ );
- .mat-mdc-chip {
- @include mdc-chips.fill-color-accessible($unselected-background,
- $query: mdc-helpers.$mat-theme-styles-query);
-
- // mdc-chip-fill-color-accessible includes mdc-chip-selected-ink-color which overrides the
- // opacity so selected chips always show a ripple.
- // Include the same mixins but use mdc-chip-selected-ink-color-without-ripple
&.mat-primary {
- &.mdc-chip--selected, &.mat-mdc-chip-highlighted {
- @include _selected-color($primary);
- }
+ @include _colored-chip($primary);
}
&.mat-accent {
- &.mdc-chip--selected, &.mat-mdc-chip-highlighted {
- @include _selected-color($accent);
- }
+ @include _colored-chip($accent);
}
&.mat-warn {
- &.mdc-chip--selected, &.mat-mdc-chip-highlighted {
- @include _selected-color($warn);
- }
+ @include _colored-chip($warn);
}
}
}
- // Restore original values of MDC global variables.
- mdc-chips.$fill-color-default: $orig-mdc-chips-fill-color-default;
- mdc-chips.$ink-color-default: $orig-mdc-chips-ink-color-default;
- mdc-chips.$icon-color: $orig-mdc-chips-icon-color;
+ .mat-mdc-chip-focus-overlay {
+ background: map.get($foreground, base);
+ }
}
@mixin typography($config-or-theme) {
$config: typography.private-typography-to-2018-config(
theming.get-typography-config($config-or-theme));
- @include mdc-chips.set-core-styles($query: mdc-helpers.$mat-typography-styles-query);
+ @include mdc-chip-set.core-styles($query: mdc-helpers.$mat-typography-styles-query);
@include mdc-helpers.mat-using-mdc-typography($config) {
- @include mdc-chips.without-ripple($query: mdc-helpers.$mat-typography-styles-query);
+ @include mdc-chip.without-ripple-styles($query: mdc-helpers.$mat-typography-styles-query);
}
}
@mixin density($config-or-theme) {
$density-scale: theming.get-density-config($config-or-theme);
.mat-mdc-chip {
- @include mdc-chips.density($density-scale, $query: mdc-helpers.$mat-base-styles-query);
+ @include mdc-chip-theme.density($density-scale, $query: mdc-helpers.$mat-base-styles-query);
}
}
diff --git a/src/material-experimental/mdc-chips/chip-action.ts b/src/material-experimental/mdc-chips/chip-action.ts
new file mode 100644
index 000000000000..d19bb24409b5
--- /dev/null
+++ b/src/material-experimental/mdc-chips/chip-action.ts
@@ -0,0 +1,151 @@
+/**
+ * @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 {
+ AfterViewInit,
+ ChangeDetectorRef,
+ Directive,
+ ElementRef,
+ Inject,
+ Input,
+ OnChanges,
+ OnDestroy,
+ SimpleChanges,
+} from '@angular/core';
+import {DOCUMENT} from '@angular/common';
+import {
+ MDCChipActionAdapter,
+ MDCChipActionFoundation,
+ MDCChipActionType,
+ MDCChipPrimaryActionFoundation,
+} from '@material/chips';
+import {emitCustomEvent} from './emit-event';
+import {
+ CanDisable,
+ HasTabIndex,
+ mixinDisabled,
+ mixinTabIndex,
+} from '@angular/material-experimental/mdc-core';
+
+const _MatChipActionMixinBase = mixinTabIndex(mixinDisabled(class {}), -1);
+
+/**
+ * Interactive element within a chip.
+ * @docs-private
+ */
+@Directive({
+ selector: '[matChipAction]',
+ inputs: ['disabled', 'tabIndex'],
+ host: {
+ 'class': 'mdc-evolution-chip__action mat-mdc-chip-action',
+ '[class.mdc-evolution-chip__action--primary]': `_getFoundation().actionType() === ${MDCChipActionType.PRIMARY}`,
+ // Note that while our actions are interactive, we have to add the `--presentational` class,
+ // in order to avoid some super-specific `:hover` styles from MDC.
+ '[class.mdc-evolution-chip__action--presentational]': `_getFoundation().actionType() === ${MDCChipActionType.PRIMARY}`,
+ '[class.mdc-evolution-chip__action--trailing]': `_getFoundation().actionType() === ${MDCChipActionType.TRAILING}`,
+ '[attr.tabindex]': '(disabled || !isInteractive) ? null : tabIndex',
+ '[attr.disabled]': "disabled ? '' : null",
+ '[attr.aria-disabled]': 'disabled',
+ '(click)': '_handleClick($event)',
+ '(keydown)': '_handleKeydown($event)',
+ },
+})
+export class MatChipAction
+ extends _MatChipActionMixinBase
+ implements AfterViewInit, OnDestroy, CanDisable, HasTabIndex, OnChanges
+{
+ private _document: Document;
+ private _foundation: MDCChipActionFoundation;
+ private _adapter: MDCChipActionAdapter = {
+ focus: () => this.focus(),
+ getAttribute: (name: string) => this._elementRef.nativeElement.getAttribute(name),
+ setAttribute: (name: string, value: string) => {
+ // MDC tries to update the tabindex directly in the DOM when navigating using the keyboard
+ // which overrides our own handling. If we detect such a case, assign it to the same property
+ // as the Angular binding in order to maintain consistency.
+ if (name === 'tabindex') {
+ this._updateTabindex(parseInt(value));
+ } else {
+ this._elementRef.nativeElement.setAttribute(name, value);
+ }
+ },
+ removeAttribute: (name: string) => {
+ if (name !== 'tabindex') {
+ this._elementRef.nativeElement.removeAttribute(name);
+ }
+ },
+ getElementID: () => this._elementRef.nativeElement.id,
+ emitEvent: (eventName: string, data: T) => {
+ emitCustomEvent(this._elementRef.nativeElement, this._document, eventName, data, true);
+ },
+ };
+
+ /** Whether the action is interactive. */
+ @Input() isInteractive = true;
+
+ _handleClick(_event: MouseEvent) {
+ // Usually these events can't happen while the chip is disabled since the browser won't
+ // allow them which is what MDC seems to rely on, however the event can be faked in tests.
+ if (!this.disabled && this.isInteractive) {
+ this._foundation.handleClick();
+ }
+ }
+
+ _handleKeydown(event: KeyboardEvent) {
+ // Usually these events can't happen while the chip is disabled since the browser won't
+ // allow them which is what MDC seems to rely on, however the event can be faked in tests.
+ if (!this.disabled && this.isInteractive) {
+ this._foundation.handleKeydown(event);
+ }
+ }
+
+ protected _createFoundation(adapter: MDCChipActionAdapter): MDCChipActionFoundation {
+ return new MDCChipPrimaryActionFoundation(adapter);
+ }
+
+ constructor(
+ public _elementRef: ElementRef,
+ @Inject(DOCUMENT) _document: any,
+ private _changeDetectorRef: ChangeDetectorRef,
+ ) {
+ super();
+ this._foundation = this._createFoundation(this._adapter);
+
+ if (_elementRef.nativeElement.nodeName === 'BUTTON') {
+ _elementRef.nativeElement.setAttribute('type', 'button');
+ }
+ }
+
+ ngAfterViewInit() {
+ this._foundation.init();
+ this._foundation.setDisabled(this.disabled);
+ }
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes['disabled']) {
+ this._foundation.setDisabled(this.disabled);
+ }
+ }
+
+ ngOnDestroy() {
+ this._foundation.destroy();
+ }
+
+ focus() {
+ this._elementRef.nativeElement.focus();
+ }
+
+ _getFoundation() {
+ return this._foundation;
+ }
+
+ _updateTabindex(value: number) {
+ this.tabIndex = value;
+ this._changeDetectorRef.markForCheck();
+ }
+}
diff --git a/src/material-experimental/mdc-chips/chip-edit-input.spec.ts b/src/material-experimental/mdc-chips/chip-edit-input.spec.ts
index 6670c129de76..4d46370e978c 100644
--- a/src/material-experimental/mdc-chips/chip-edit-input.spec.ts
+++ b/src/material-experimental/mdc-chips/chip-edit-input.spec.ts
@@ -46,6 +46,6 @@ describe('MDC-based MatChipEditInput', () => {
});
@Component({
- template: ` `,
+ template: ` `,
})
class ChipEditInputContainer {}
diff --git a/src/material-experimental/mdc-chips/chip-edit-input.ts b/src/material-experimental/mdc-chips/chip-edit-input.ts
index 32410b20f948..9f6ffc6e8437 100644
--- a/src/material-experimental/mdc-chips/chip-edit-input.ts
+++ b/src/material-experimental/mdc-chips/chip-edit-input.ts
@@ -16,7 +16,7 @@ import {DOCUMENT} from '@angular/common';
@Directive({
selector: 'span[matChipEditInput]',
host: {
- 'class': 'mdc-chip__primary-action mat-chip-edit-input',
+ 'class': 'mat-chip-edit-input',
'role': 'textbox',
'tabindex': '-1',
'contenteditable': 'true',
diff --git a/src/material-experimental/mdc-chips/chip-grid.spec.ts b/src/material-experimental/mdc-chips/chip-grid.spec.ts
index 8e4874891730..96e44bcb7d25 100644
--- a/src/material-experimental/mdc-chips/chip-grid.spec.ts
+++ b/src/material-experimental/mdc-chips/chip-grid.spec.ts
@@ -13,59 +13,46 @@ import {
TAB,
} from '@angular/cdk/keycodes';
import {
- createKeyboardEvent,
- dispatchEvent,
dispatchFakeEvent,
dispatchKeyboardEvent,
- dispatchMouseEvent,
MockNgZone,
typeInElement,
-} from '../../cdk/testing/private';
+} from '@angular/cdk/testing/private';
import {
Component,
DebugElement,
NgZone,
- Provider,
QueryList,
Type,
ViewChild,
ViewChildren,
+ EventEmitter,
} from '@angular/core';
-import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
+import {ComponentFixture, fakeAsync, flush, TestBed, tick} from '@angular/core/testing';
import {FormControl, FormsModule, NgForm, ReactiveFormsModule, Validators} from '@angular/forms';
import {MatFormFieldModule} from '@angular/material-experimental/mdc-form-field';
import {MatInputModule} from '@angular/material-experimental/mdc-input';
import {By} from '@angular/platform-browser';
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
-import {Subject} from 'rxjs';
-import {GridFocusKeyManager} from './grid-focus-key-manager';
-import {
- MatChipEvent,
- MatChipGrid,
- MatChipInputEvent,
- MatChipRemove,
- MatChipRow,
- MatChipsModule,
-} from './index';
+import {MDCChipAnimation} from '@material/chips';
+import {MatChipEvent, MatChipGrid, MatChipInputEvent, MatChipRow, MatChipsModule} from './index';
describe('MDC-based MatChipGrid', () => {
let chipGridDebugElement: DebugElement;
let chipGridNativeElement: HTMLElement;
let chipGridInstance: MatChipGrid;
let chips: QueryList;
- let manager: GridFocusKeyManager;
let zone: MockNgZone;
let testComponent: StandardChipGrid;
- let dirChange: Subject;
+ let directionality: {value: Direction; change: EventEmitter};
+ let primaryActions: NodeListOf;
const expectNoCellFocused = () => {
- expect(manager.activeRowIndex).toBe(-1);
- expect(manager.activeColumnIndex).toBe(-1);
+ expect(Array.from(primaryActions)).not.toContain(document.activeElement as HTMLElement);
};
const expectLastCellFocused = () => {
- expect(manager.activeRowIndex).toBe(chips.length - 1);
- expect(manager.activeColumnIndex).toBe(0);
+ expect(document.activeElement).toBe(primaryActions[primaryActions.length - 1]);
};
describe('StandardChipGrid', () => {
@@ -73,7 +60,7 @@ describe('MDC-based MatChipGrid', () => {
let fixture: ComponentFixture;
beforeEach(() => {
- fixture = setupStandardGrid();
+ fixture = createComponent(StandardChipGrid);
});
it('should add the `mat-mdc-chip-set` class', () => {
@@ -124,24 +111,21 @@ describe('MDC-based MatChipGrid', () => {
| ComponentFixture;
beforeEach(() => {
- fixture = setupStandardGrid();
+ fixture = createComponent(StandardChipGrid);
});
it('should focus the first chip on focus', () => {
chipGridInstance.focus();
fixture.detectChanges();
- expect(manager.activeRowIndex).toBe(0);
- expect(manager.activeColumnIndex).toBe(0);
+ expect(document.activeElement).toBe(primaryActions[0]);
});
- it('should watch for chip focus', () => {
- const lastIndex = chips.length - 1;
-
+ it('should focus the primary action when calling the `focus` method', () => {
chips.last.focus();
fixture.detectChanges();
- expect(manager.activeRowIndex).toBe(lastIndex);
+ expect(document.activeElement).toBe(primaryActions[primaryActions.length - 1]);
});
it('should not be able to become focused when disabled', () => {
@@ -180,13 +164,11 @@ describe('MDC-based MatChipGrid', () => {
testComponent.chips.splice(2, 1);
fixture.detectChanges();
- // It focuses the 4th item (now at index 2)
- expect(manager.activeRowIndex).toEqual(2);
+ // It focuses the 4th item
+ expect(document.activeElement).toBe(primaryActions[3]);
});
it('should focus the previous item', () => {
- const lastIndex = chips.length - 1;
-
// Focus the last item
chips.last.focus();
@@ -195,7 +177,7 @@ describe('MDC-based MatChipGrid', () => {
fixture.detectChanges();
// It focuses the next-to-last item
- expect(manager.activeRowIndex).toEqual(lastIndex - 1);
+ expect(document.activeElement).toBe(primaryActions[primaryActions.length - 2]);
});
it('should not focus if chip grid is not focused', fakeAsync(() => {
@@ -212,7 +194,7 @@ describe('MDC-based MatChipGrid', () => {
fixture.detectChanges();
// Should not have focus
- expect(chipGridInstance._keyManager.activeRowIndex).toEqual(-1);
+ expect(chipGridNativeElement.contains(document.activeElement)).toBe(false);
}));
it('should focus the grid if the last focused item is removed', () => {
@@ -234,93 +216,81 @@ describe('MDC-based MatChipGrid', () => {
fixture.destroy();
TestBed.resetTestingModule();
- fixture = createComponent(StandardChipGridWithAnimations, [], BrowserAnimationsModule);
+ fixture = createComponent(StandardChipGridWithAnimations, BrowserAnimationsModule);
chips.last.focus();
fixture.detectChanges();
- expect(chipGridInstance._keyManager.activeRowIndex).toBe(chips.length - 1);
+ expect(document.activeElement).toBe(primaryActions[primaryActions.length - 1]);
dispatchKeyboardEvent(chips.last._elementRef.nativeElement, 'keydown', BACKSPACE);
fixture.detectChanges();
tick(500);
- expect(chipGridInstance._keyManager.activeRowIndex).toBe(chips.length - 1);
- expect(chipGridInstance._keyManager.activeColumnIndex).toBe(0);
+ expect(document.activeElement).toBe(primaryActions[primaryActions.length - 2]);
}),
);
});
it('should have a focus indicator', () => {
- const focusableTextNativeElements = Array.from(
- chipGridNativeElement.querySelectorAll('.mat-mdc-chip-row-focusable-text-content'),
+ const focusIndicators = chipGridNativeElement.querySelectorAll(
+ '.mat-mdc-chip-primary-focus-indicator',
);
-
- expect(
- focusableTextNativeElements.every(element =>
- element.classList.contains('mat-mdc-focus-indicator'),
- ),
- ).toBe(true);
+ expect(focusIndicators.length).toBeGreaterThan(0);
+ expect(focusIndicators.length).toBe(chips.length);
});
});
describe('keyboard behavior', () => {
describe('LTR (default)', () => {
let fixture: ComponentFixture;
+ let trailingActions: NodeListOf;
- beforeEach(() => {
+ beforeEach(fakeAsync(() => {
fixture = createComponent(ChipGridWithRemove);
- });
+ flush();
+ trailingActions = chipGridNativeElement.querySelectorAll(
+ '.mdc-evolution-chip__action--trailing',
+ );
+ }));
it('should focus previous column when press LEFT ARROW', () => {
- let nativeChips = chipGridNativeElement.querySelectorAll('mat-chip-row');
- let lastNativeChip = nativeChips[nativeChips.length - 1] as HTMLElement;
-
- const lastRowIndex = chips.length - 1;
+ const lastIndex = primaryActions.length - 1;
// Focus the first column of the last chip in the array
chips.last.focus();
- expectLastCellFocused();
+ expect(document.activeElement).toBe(primaryActions[lastIndex]);
// Press the LEFT arrow
- dispatchKeyboardEvent(lastNativeChip, 'keydown', LEFT_ARROW);
-
- chipGridInstance._blur(); // Simulate focus leaving the list and going to the chip.
+ dispatchKeyboardEvent(primaryActions[lastIndex], 'keydown', LEFT_ARROW);
fixture.detectChanges();
// It focuses the last column of the previous chip
- expect(manager.activeRowIndex).toEqual(lastRowIndex - 1);
- expect(manager.activeColumnIndex).toEqual(1);
+ expect(document.activeElement).toBe(trailingActions[lastIndex - 1]);
});
it('should focus next column when press RIGHT ARROW', () => {
- let nativeChips = chipGridNativeElement.querySelectorAll('mat-chip-row');
- let firstNativeChip = nativeChips[0] as HTMLElement;
-
// Focus the first column of the first chip in the array
chips.first.focus();
- expect(manager.activeRowIndex).toEqual(0);
- expect(manager.activeColumnIndex).toEqual(0);
+ expect(document.activeElement).toBe(primaryActions[0]);
// Press the RIGHT arrow
- dispatchKeyboardEvent(firstNativeChip, 'keydown', RIGHT_ARROW);
- chipGridInstance._blur(); // Simulate focus leaving the list and going to the chip.
+ dispatchKeyboardEvent(primaryActions[0], 'keydown', RIGHT_ARROW);
fixture.detectChanges();
// It focuses the next column of the chip
- expect(manager.activeRowIndex).toEqual(0);
- expect(manager.activeColumnIndex).toEqual(1);
+ expect(document.activeElement).toBe(trailingActions[0]);
});
it('should not handle arrow key events from non-chip elements', () => {
- const initialActiveIndex = manager.activeRowIndex;
+ const previousActiveElement = document.activeElement;
dispatchKeyboardEvent(chipGridNativeElement, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
- expect(manager.activeRowIndex)
+ expect(document.activeElement)
.withContext('Expected focused item not to have changed.')
- .toBe(initialActiveIndex);
+ .toBe(previousActiveElement);
});
});
@@ -328,46 +298,35 @@ describe('MDC-based MatChipGrid', () => {
let fixture: ComponentFixture;
beforeEach(() => {
- fixture = setupStandardGrid('rtl');
+ fixture = createComponent(StandardChipGrid, undefined, 'rtl');
});
it('should focus previous column when press RIGHT ARROW', () => {
- let nativeChips = chipGridNativeElement.querySelectorAll('mat-chip-row');
- let lastNativeChip = nativeChips[nativeChips.length - 1] as HTMLElement;
-
- const lastRowIndex = chips.length - 1;
+ const lastIndex = primaryActions.length - 1;
// Focus the first column of the last chip in the array
chips.last.focus();
- expectLastCellFocused();
+ expect(document.activeElement).toBe(primaryActions[lastIndex]);
// Press the RIGHT arrow
- dispatchKeyboardEvent(lastNativeChip, 'keydown', RIGHT_ARROW);
- chipGridInstance._blur(); // Simulate focus leaving the list and going to the chip.
+ dispatchKeyboardEvent(primaryActions[lastIndex], 'keydown', RIGHT_ARROW);
fixture.detectChanges();
// It focuses the last column of the previous chip
- expect(manager.activeRowIndex).toEqual(lastRowIndex - 1);
- expect(manager.activeColumnIndex).toEqual(0);
+ expect(document.activeElement).toBe(primaryActions[lastIndex - 1]);
});
it('should focus next column when press LEFT ARROW', () => {
- let nativeChips = chipGridNativeElement.querySelectorAll('mat-chip-row');
- let firstNativeChip = nativeChips[0] as HTMLElement;
-
// Focus the first column of the first chip in the array
chips.first.focus();
- expect(manager.activeRowIndex).toEqual(0);
- expect(manager.activeColumnIndex).toEqual(0);
+ expect(document.activeElement).toBe(primaryActions[0]);
// Press the LEFT arrow
- dispatchKeyboardEvent(firstNativeChip, 'keydown', LEFT_ARROW);
- chipGridInstance._blur(); // Simulate focus leaving the list and going to the chip.
+ dispatchKeyboardEvent(primaryActions[0], 'keydown', LEFT_ARROW);
fixture.detectChanges();
// It focuses the next column of the chip
- expect(manager.activeRowIndex).toEqual(1);
- expect(manager.activeColumnIndex).toEqual(0);
+ expect(document.activeElement).toBe(primaryActions[1]);
});
it('should allow focus to escape when tabbing away', fakeAsync(() => {
@@ -380,7 +339,7 @@ describe('MDC-based MatChipGrid', () => {
.withContext('Expected tabIndex to be set to -1 temporarily.')
.toBe(-1);
- tick();
+ flush();
expect(chipGridInstance.tabIndex)
.withContext('Expected tabIndex to be reset back to 0')
@@ -389,7 +348,6 @@ describe('MDC-based MatChipGrid', () => {
it(`should use user defined tabIndex`, fakeAsync(() => {
chipGridInstance.tabIndex = 4;
-
fixture.detectChanges();
expect(chipGridInstance.tabIndex)
@@ -404,7 +362,7 @@ describe('MDC-based MatChipGrid', () => {
.withContext('Expected tabIndex to be set to -1 temporarily.')
.toBe(-1);
- tick();
+ flush();
expect(chipGridInstance.tabIndex)
.withContext('Expected tabIndex to be reset back to 4')
@@ -416,95 +374,71 @@ describe('MDC-based MatChipGrid', () => {
let fixture: ComponentFixture;
beforeEach(() => {
- fixture = setupStandardGrid();
+ fixture = createComponent(StandardChipGrid);
});
it('should account for the direction changing', () => {
- const firstNativeChip = chipGridNativeElement.querySelectorAll(
- 'mat-chip-row',
- )[0] as HTMLElement;
-
- const RIGHT_EVENT = createKeyboardEvent('keydown', RIGHT_ARROW);
-
chips.first.focus();
- expect(manager.activeRowIndex).toBe(0);
- expect(manager.activeColumnIndex).toBe(0);
+ expect(document.activeElement).toBe(primaryActions[0]);
- dispatchEvent(firstNativeChip, RIGHT_EVENT);
- chipGridInstance._blur();
+ dispatchKeyboardEvent(primaryActions[0], 'keydown', RIGHT_ARROW);
fixture.detectChanges();
- expect(manager.activeRowIndex).toBe(1);
- expect(manager.activeColumnIndex).toBe(0);
+ expect(document.activeElement).toBe(primaryActions[1]);
- dirChange.next('rtl');
+ directionality.value = 'rtl';
fixture.detectChanges();
- chipGridInstance._keydown(RIGHT_EVENT);
- chipGridInstance._blur();
+ dispatchKeyboardEvent(primaryActions[1], 'keydown', RIGHT_ARROW);
fixture.detectChanges();
- expect(manager.activeRowIndex).toBe(0);
- expect(manager.activeColumnIndex).toBe(0);
+ expect(document.activeElement).toBe(primaryActions[0]);
});
it('should move focus to the first chip when pressing HOME', () => {
- const nativeChips = chipGridNativeElement.querySelectorAll('mat-chip-row');
- const lastNativeChip = nativeChips[nativeChips.length - 1] as HTMLElement;
-
- const HOME_EVENT = createKeyboardEvent('keydown', HOME);
chips.last.focus();
+ expect(document.activeElement).toBe(primaryActions[4]);
- expect(manager.activeRowIndex).toBe(4);
- expect(manager.activeColumnIndex).toBe(0);
-
- dispatchEvent(lastNativeChip, HOME_EVENT);
+ const event = dispatchKeyboardEvent(primaryActions[4], 'keydown', HOME);
fixture.detectChanges();
- expect(HOME_EVENT.defaultPrevented).toBe(true);
- expect(manager.activeRowIndex).toBe(0);
- expect(manager.activeColumnIndex).toBe(0);
+ expect(event.defaultPrevented).toBe(true);
+ expect(document.activeElement).toBe(primaryActions[0]);
});
it('should move focus to the last chip when pressing END', () => {
- const nativeChips = chipGridNativeElement.querySelectorAll('mat-chip-row');
- const firstNativeChip = nativeChips[0] as HTMLElement;
-
- const END_EVENT = createKeyboardEvent('keydown', END);
chips.first.focus();
+ expect(document.activeElement).toBe(primaryActions[0]);
- expect(manager.activeRowIndex).toBe(0);
- expect(manager.activeColumnIndex).toBe(0);
-
- dispatchEvent(firstNativeChip, END_EVENT);
+ const event = dispatchKeyboardEvent(primaryActions[0], 'keydown', END);
fixture.detectChanges();
- expect(END_EVENT.defaultPrevented).toBe(true);
- expect(manager.activeRowIndex).toBe(4);
- expect(manager.activeColumnIndex).toBe(0);
+ expect(event.defaultPrevented).toBe(true);
+ expect(document.activeElement).toBe(primaryActions[4]);
});
- it('should ignore all non-tab navigation keyboard events from an editing chip', () => {
+ it('should ignore all non-tab navigation keyboard events from an editing chip', fakeAsync(() => {
testComponent.editable = true;
fixture.detectChanges();
chips.first.focus();
+ expect(document.activeElement).toBe(primaryActions[0]);
- dispatchKeyboardEvent(document.activeElement!, 'keydown', ENTER, 'Enter');
+ dispatchKeyboardEvent(document.activeElement!, 'keydown', ENTER);
fixture.detectChanges();
+ flush();
- const activeRowIndex = manager.activeRowIndex;
- const activeColumnIndex = manager.activeColumnIndex;
+ const previousActiveElement = document.activeElement;
+ const keysToIgnore = [HOME, END, LEFT_ARROW, RIGHT_ARROW];
- const KEYS_TO_IGNORE = [HOME, END, LEFT_ARROW, RIGHT_ARROW];
- for (const key of KEYS_TO_IGNORE) {
+ for (const key of keysToIgnore) {
dispatchKeyboardEvent(document.activeElement!, 'keydown', key);
fixture.detectChanges();
+ flush();
- expect(manager.activeRowIndex).toBe(activeRowIndex);
- expect(manager.activeColumnIndex).toBe(activeColumnIndex);
+ expect(document.activeElement).toBe(previousActiveElement);
}
- });
+ }));
});
});
});
@@ -513,14 +447,15 @@ describe('MDC-based MatChipGrid', () => {
let fixture: ComponentFixture;
beforeEach(() => {
- fixture = setupInputGrid();
+ fixture = createComponent(FormFieldChipGrid);
});
describe('keyboard behavior', () => {
it('should maintain focus if the active chip is deleted', () => {
const secondChip = fixture.nativeElement.querySelectorAll('.mat-mdc-chip')[1];
+ const secondChipAction = secondChip.querySelector('.mdc-evolution-chip__action--primary');
- secondChip.focus();
+ secondChipAction.focus();
fixture.detectChanges();
expect(chipGridInstance._chips.toArray().findIndex(chip => chip._hasFocus())).toBe(1);
@@ -601,27 +536,36 @@ describe('MDC-based MatChipGrid', () => {
describe('with chip remove', () => {
let fixture: ComponentFixture;
- let chipGrid: MatChipGrid;
- let chipRemoveDebugElements: DebugElement[];
+ let trailingActions: NodeListOf;
- beforeEach(() => {
+ beforeEach(fakeAsync(() => {
fixture = createComponent(ChipGridWithRemove);
+ flush();
+ trailingActions = chipGridNativeElement.querySelectorAll(
+ '.mdc-evolution-chip__action--trailing',
+ );
+ }));
- chipGrid = fixture.debugElement.query(By.directive(MatChipGrid))!.componentInstance;
- chipRemoveDebugElements = fixture.debugElement.queryAll(By.directive(MatChipRemove));
- });
-
- it('should properly focus next item if chip is removed through click', () => {
- chips.get(2)!.focus();
+ it('should properly focus next item if chip is removed through click', fakeAsync(() => {
+ const chip = chips.get(2)!;
+ chip.focus();
+ fixture.detectChanges();
// Destroy the third focused chip by dispatching a bubbling click event on the
// associated chip remove element.
- dispatchMouseEvent(chipRemoveDebugElements[2].nativeElement, 'click');
+ trailingActions[2].click();
+ fixture.detectChanges();
+ (chip as any)._handleAnimationend({
+ animationName: MDCChipAnimation.EXIT,
+ target: chip._elementRef.nativeElement,
+ });
+ flush();
+ (chip as any)._handleTransitionend({target: chip._elementRef.nativeElement});
+ flush();
fixture.detectChanges();
- expect(chips.get(2)!.value).not.toBe(2, 'Expected the third chip to be removed.');
- expect(chipGrid._keyManager.activeRowIndex).toBe(2);
- });
+ expect(document.activeElement).toBe(primaryActions[3]);
+ }));
});
describe('chip grid with chip input', () => {
@@ -671,10 +615,10 @@ describe('MDC-based MatChipGrid', () => {
fixture.detectChanges();
dispatchKeyboardEvent(nativeInput, 'keydown', ENTER);
fixture.detectChanges();
- tick();
+ flush();
dispatchFakeEvent(nativeInput, 'blur');
- tick();
+ flush();
expect(fixture.componentInstance.control.value).toContain('123-8');
}));
@@ -727,10 +671,10 @@ describe('MDC-based MatChipGrid', () => {
fixture.detectChanges();
dispatchKeyboardEvent(nativeInput, 'keydown', ENTER);
fixture.detectChanges();
- tick();
+ flush();
dispatchFakeEvent(nativeInput, 'blur');
- tick();
+ flush();
expect(fixture.componentInstance.control.dirty)
.withContext(`Expected control to be dirty after value was changed by user.`)
@@ -750,7 +694,9 @@ describe('MDC-based MatChipGrid', () => {
});
it('should set an asterisk after the placeholder if the control is required', () => {
- let requiredMarker = fixture.debugElement.query(By.css('.mdc-floating-label--required'))!;
+ let requiredMarker = fixture.debugElement.query(
+ By.css('.mat-mdc-form-field-required-marker'),
+ )!;
expect(requiredMarker)
.withContext(`Expected placeholder not to have an asterisk, as control was not required.`)
.toBeNull();
@@ -758,7 +704,7 @@ describe('MDC-based MatChipGrid', () => {
fixture.componentInstance.chipGrid.required = true;
fixture.detectChanges();
- requiredMarker = fixture.debugElement.query(By.css('.mdc-floating-label--required'))!;
+ requiredMarker = fixture.debugElement.query(By.css('.mat-mdc-form-field-required-marker'))!;
expect(requiredMarker)
.not.withContext(`Expected placeholder to have an asterisk, as control was required.`)
.toBeNull();
@@ -770,7 +716,9 @@ describe('MDC-based MatChipGrid', () => {
fixture.componentInstance.control = new FormControl(undefined, [Validators.required]);
fixture.detectChanges();
- expect(fixture.nativeElement.querySelector('.mdc-floating-label--required')).toBeTruthy();
+ expect(
+ fixture.nativeElement.querySelector('.mat-mdc-form-field-required-marker'),
+ ).toBeTruthy();
});
it('should blur the form field when the active chip is blurred', fakeAsync(() => {
@@ -832,10 +780,10 @@ describe('MDC-based MatChipGrid', () => {
fixture.detectChanges();
dispatchKeyboardEvent(input, 'keydown', ENTER);
fixture.detectChanges();
- tick();
+ flush();
dispatchFakeEvent(input, 'blur');
- tick();
+ flush();
fixture.detectChanges();
expect(input.getAttribute('aria-invalid')).toBe('false');
@@ -1036,11 +984,16 @@ describe('MDC-based MatChipGrid', () => {
function createComponent(
component: Type,
- providers: Provider[] = [],
animationsModule:
| Type
| Type = NoopAnimationsModule,
+ direction: Direction = 'ltr',
): ComponentFixture {
+ directionality = {
+ value: direction,
+ change: new EventEmitter(),
+ } as Directionality;
+
TestBed.configureTestingModule({
imports: [
FormsModule,
@@ -1052,11 +1005,8 @@ describe('MDC-based MatChipGrid', () => {
],
declarations: [component],
providers: [
- {
- provide: NgZone,
- useFactory: () => (zone = new MockNgZone()),
- },
- ...providers,
+ {provide: NgZone, useFactory: () => (zone = new MockNgZone())},
+ {provide: Directionality, useValue: directionality},
],
}).compileComponents();
@@ -1068,28 +1018,12 @@ describe('MDC-based MatChipGrid', () => {
chipGridInstance = chipGridDebugElement.componentInstance;
testComponent = fixture.debugElement.componentInstance;
chips = chipGridInstance._chips;
- manager = chipGridInstance._keyManager;
+ primaryActions = chipGridNativeElement.querySelectorAll(
+ '.mdc-evolution-chip__action--primary',
+ );
return fixture;
}
-
- function setupStandardGrid(direction: Direction = 'ltr') {
- dirChange = new Subject();
-
- return createComponent(StandardChipGrid, [
- {
- provide: Directionality,
- useFactory: () => ({
- value: direction.toLowerCase(),
- change: dirChange,
- }),
- },
- ]);
- }
-
- function setupInputGrid() {
- return createComponent(FormFieldChipGrid);
- }
});
@Component({
diff --git a/src/material-experimental/mdc-chips/chip-grid.ts b/src/material-experimental/mdc-chips/chip-grid.ts
index b641f87b8a46..119d184d49b5 100644
--- a/src/material-experimental/mdc-chips/chip-grid.ts
+++ b/src/material-experimental/mdc-chips/chip-grid.ts
@@ -6,7 +6,6 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {Directionality} from '@angular/cdk/bidi';
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
import {TAB} from '@angular/cdk/keycodes';
import {
@@ -19,6 +18,7 @@ import {
DoCheck,
ElementRef,
EventEmitter,
+ Inject,
Input,
OnDestroy,
Optional,
@@ -34,19 +34,20 @@ import {
NgForm,
Validators,
} from '@angular/forms';
+import {DOCUMENT} from '@angular/common';
import {
CanUpdateErrorState,
ErrorStateMatcher,
mixinErrorState,
} from '@angular/material-experimental/mdc-core';
import {MatFormFieldControl} from '@angular/material-experimental/mdc-form-field';
+import {LiveAnnouncer} from '@angular/cdk/a11y';
import {MatChipTextControl} from './chip-text-control';
-import {merge, Observable, Subscription} from 'rxjs';
+import {Observable, Subject} from 'rxjs';
import {startWith, takeUntil} from 'rxjs/operators';
import {MatChipEvent} from './chip';
import {MatChipRow} from './chip-row';
import {MatChipSet} from './chip-set';
-import {GridFocusKeyManager} from './grid-focus-key-manager';
/** Change event object that is emitted when the chip grid value has changed. */
export class MatChipGridChange {
@@ -63,17 +64,29 @@ export class MatChipGridChange {
* @docs-private
*/
class MatChipGridBase extends MatChipSet {
+ /**
+ * Emits whenever the component state changes and should cause the parent
+ * form-field to update. Implemented as part of `MatFormFieldControl`.
+ * @docs-private
+ */
+ readonly stateChanges = new Subject();
+
constructor(
- _elementRef: ElementRef,
- _changeDetectorRef: ChangeDetectorRef,
- _dir: Directionality,
+ liveAnnouncer: LiveAnnouncer,
+ document: any,
+ elementRef: ElementRef,
+ changeDetectorRef: ChangeDetectorRef,
public _defaultErrorStateMatcher: ErrorStateMatcher,
public _parentForm: NgForm,
public _parentFormGroup: FormGroupDirective,
- /** @docs-private */
+ /**
+ * Form control bound to the component.
+ * Implemented as part of `MatFormFieldControl`.
+ * @docs-private
+ */
public ngControl: NgControl,
) {
- super(_elementRef, _changeDetectorRef, _dir);
+ super(liveAnnouncer, document, elementRef, changeDetectorRef);
}
}
const _MatChipGridMixinBase = mixinErrorState(MatChipGridBase);
@@ -84,11 +97,15 @@ const _MatChipGridMixinBase = mixinErrorState(MatChipGridBase);
*/
@Component({
selector: 'mat-chip-grid',
- template: ' ',
- styleUrls: ['chips.css'],
+ template: `
+
+