diff --git a/src/demo-app/demo-material-module.ts b/src/demo-app/demo-material-module.ts
index 33ff49f3c389..7e4ffac77e73 100644
--- a/src/demo-app/demo-material-module.ts
+++ b/src/demo-app/demo-material-module.ts
@@ -6,6 +6,14 @@
* found in the LICENSE file at https://angular.io/license
*/
+import {A11yModule} from '@angular/cdk/a11y';
+import {CdkAccordionModule} from '@angular/cdk/accordion';
+import {BidiModule} from '@angular/cdk/bidi';
+import {ObserversModule} from '@angular/cdk/observers';
+import {OverlayModule} from '@angular/cdk/overlay';
+import {PlatformModule} from '@angular/cdk/platform';
+import {PortalModule} from '@angular/cdk/portal';
+import {CdkTableModule} from '@angular/cdk/table';
import {NgModule} from '@angular/core';
import {
MatAutocompleteModule,
@@ -24,31 +32,24 @@ import {
MatInputModule,
MatListModule,
MatMenuModule,
+ MatNativeDateModule,
MatPaginatorModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatRadioModule,
+ MatRippleModule,
MatSelectModule,
MatSidenavModule,
MatSliderModule,
MatSlideToggleModule,
MatSnackBarModule,
MatSortModule,
+ MatStepperModule,
MatTableModule,
MatTabsModule,
MatToolbarModule,
MatTooltipModule,
- MatStepperModule,
} from '@angular/material';
-import {MatNativeDateModule, MatRippleModule} from '@angular/material';
-import {CdkTableModule} from '@angular/cdk/table';
-import {CdkAccordionModule} from '@angular/cdk/accordion';
-import {A11yModule} from '@angular/cdk/a11y';
-import {BidiModule} from '@angular/cdk/bidi';
-import {OverlayModule} from '@angular/cdk/overlay';
-import {PlatformModule} from '@angular/cdk/platform';
-import {ObserversModule} from '@angular/cdk/observers';
-import {PortalModule} from '@angular/cdk/portal';
/**
* NgModule that includes all Material modules that are required to serve the demo-app.
diff --git a/src/demo-app/input/input-demo.html b/src/demo-app/input/input-demo.html
index 2703447a2a52..d538ad3c9194 100644
--- a/src/demo-app/input/input-demo.html
+++ b/src/demo-app/input/input-demo.html
@@ -631,3 +631,21 @@
<textarea> with ngModel
+
+
+ Autofill
+
+
+
+
diff --git a/src/demo-app/input/input-demo.scss b/src/demo-app/input/input-demo.scss
index d7761343e44b..a15c23b80af5 100644
--- a/src/demo-app/input/input-demo.scss
+++ b/src/demo-app/input/input-demo.scss
@@ -1,3 +1,5 @@
+@import '../../../dist/packages/material/input/autofill';
+
.demo-basic {
padding: 0;
}
@@ -27,3 +29,7 @@
padding: 0;
background: lightblue;
}
+
+.demo-custom-autofill-style {
+ @include mat-input-autofill-color(transparent, red);
+}
diff --git a/src/demo-app/input/input-demo.ts b/src/demo-app/input/input-demo.ts
index 3e6839b15e5b..e8989603029b 100644
--- a/src/demo-app/input/input-demo.ts
+++ b/src/demo-app/input/input-demo.ts
@@ -51,6 +51,8 @@ export class InputDemo {
emailFormControl = new FormControl('', [Validators.required, Validators.pattern(EMAIL_REGEX)]);
delayedFormControl = new FormControl('');
model = 'hello';
+ isAutofilled = false;
+ customAutofillStyle = true;
legacyAppearance: string;
standardAppearance: string;
diff --git a/src/lib/core/_core.scss b/src/lib/core/_core.scss
index ae78d5e77828..748e3a5735b7 100644
--- a/src/lib/core/_core.scss
+++ b/src/lib/core/_core.scss
@@ -2,6 +2,7 @@
// up into a single flat scss file for material.
@import '../../cdk/overlay/overlay';
@import '../../cdk/a11y/a11y';
+@import '../input/autofill';
// Core styles that can be used to apply material design treatments to any element.
@import 'style/elevation';
@@ -26,6 +27,7 @@
@include mat-ripple();
@include cdk-a11y();
@include cdk-overlay();
+ @include mat-input-autofill();
}
// Mixin that renders all of the core styles that depend on the theme.
diff --git a/src/lib/input/BUILD.bazel b/src/lib/input/BUILD.bazel
index 6bd7b7bbe66f..413c7d8606f7 100644
--- a/src/lib/input/BUILD.bazel
+++ b/src/lib/input/BUILD.bazel
@@ -8,6 +8,7 @@ ng_module(
srcs = glob(["**/*.ts"], exclude=["**/*.spec.ts"]),
module_name = "@angular/material/input",
deps = [
+ "@rxjs",
"//src/lib/core",
"//src/lib/form-field",
"//src/cdk/coercion",
diff --git a/src/lib/input/_autofill.scss b/src/lib/input/_autofill.scss
new file mode 100644
index 000000000000..fc778c84a9eb
--- /dev/null
+++ b/src/lib/input/_autofill.scss
@@ -0,0 +1,43 @@
+// Core styles that enable monitoring autofill state of inputs.
+@mixin mat-input-autofill {
+ // Keyframes that apply no styles, but allow us to monitor when an input becomes autofilled
+ // by watching for the animation events that are fired when they start.
+ // Based on: https://medium.com/@brunn/detecting-autofilled-fields-in-javascript-aed598d25da7
+ @keyframes mat-input-autofill-start {}
+ @keyframes mat-input-autofill-end {}
+
+ .mat-input-autofill-monitored:-webkit-autofill {
+ animation-name: mat-input-autofill-start;
+ }
+
+ .mat-input-autofill-monitored:not(:-webkit-autofill) {
+ animation-name: mat-input-autofill-end;
+ }
+}
+
+// Used to generate UIDs for keyframes used to change the input autofill styles.
+$mat-input-autofill-color-frame-count: 0;
+
+// Mixin used to apply custom background and foreground colors to an autofilled input. Based on:
+// https://stackoverflow.com/questions/2781549/
+// removing-input-background-colour-for-chrome-autocomplete#answer-37432260
+@mixin mat-input-autofill-color($background, $foreground:'') {
+ @keyframes mat-input-autofill-color-#{$mat-input-autofill-color-frame-count} {
+ to {
+ background: $background;
+ @if $foreground != '' { color: $foreground; }
+ }
+ }
+
+ &:-webkit-autofill {
+ animation-name: mat-input-autofill-color-#{$mat-input-autofill-color-frame-count};
+ animation-fill-mode: both;
+ }
+
+ &.mat-input-autofill-monitored:-webkit-autofill {
+ animation-name: mat-input-autofill-start,
+ mat-input-autofill-color-#{$mat-input-autofill-color-frame-count};
+ }
+
+ $mat-input-autofill-color-frame-count: $mat-input-autofill-color-frame-count + 1 !global;
+}
diff --git a/src/lib/input/autofill-prebuilt.scss b/src/lib/input/autofill-prebuilt.scss
new file mode 100644
index 000000000000..39ae312c13f3
--- /dev/null
+++ b/src/lib/input/autofill-prebuilt.scss
@@ -0,0 +1,3 @@
+@import 'autofill';
+
+@include mat-input-autofill();
diff --git a/src/lib/input/autofill.spec.ts b/src/lib/input/autofill.spec.ts
new file mode 100644
index 000000000000..f175ff4efa70
--- /dev/null
+++ b/src/lib/input/autofill.spec.ts
@@ -0,0 +1,192 @@
+/**
+ * @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 {supportsPassiveEventListeners} from '@angular/cdk/platform';
+import {Component, ElementRef, ViewChild} from '@angular/core';
+import {ComponentFixture, inject, TestBed} from '@angular/core/testing';
+import {empty as observableEmpty} from 'rxjs/observable/empty';
+import {AutofillEvent, AutofillMonitor} from './autofill';
+import {MatInputModule} from './input-module';
+
+
+const listenerOptions: any = supportsPassiveEventListeners() ? {passive: true} : false;
+
+
+describe('AutofillMonitor', () => {
+ let autofillMonitor: AutofillMonitor;
+ let fixture: ComponentFixture;
+ let testComponent: Inputs;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [MatInputModule],
+ declarations: [Inputs],
+ }).compileComponents();
+ });
+
+ beforeEach(inject([AutofillMonitor], (afm: AutofillMonitor) => {
+ autofillMonitor = afm;
+ fixture = TestBed.createComponent(Inputs);
+ testComponent = fixture.componentInstance;
+
+ for (const input of [testComponent.input1, testComponent.input2, testComponent.input3]) {
+ spyOn(input.nativeElement, 'addEventListener');
+ spyOn(input.nativeElement, 'removeEventListener');
+ }
+
+ fixture.detectChanges();
+ }));
+
+ afterEach(() => {
+ // Call destroy to make sure we clean up all listeners.
+ autofillMonitor.ngOnDestroy();
+ });
+
+ it('should add monitored class and listener upon monitoring', () => {
+ const inputEl = testComponent.input1.nativeElement;
+ expect(inputEl.addEventListener).not.toHaveBeenCalled();
+
+ autofillMonitor.monitor(inputEl);
+ expect(inputEl.classList).toContain('mat-input-autofill-monitored');
+ expect(inputEl.addEventListener)
+ .toHaveBeenCalledWith('animationstart', jasmine.any(Function), listenerOptions);
+ });
+
+ it('should not add multiple listeners to the same element', () => {
+ const inputEl = testComponent.input1.nativeElement;
+ expect(inputEl.addEventListener).not.toHaveBeenCalled();
+
+ autofillMonitor.monitor(inputEl);
+ autofillMonitor.monitor(inputEl);
+ expect(inputEl.addEventListener).toHaveBeenCalledTimes(1);
+ });
+
+ it('should remove monitored class and listener upon stop monitoring', () => {
+ const inputEl = testComponent.input1.nativeElement;
+ autofillMonitor.monitor(inputEl);
+ expect(inputEl.classList).toContain('mat-input-autofill-monitored');
+ expect(inputEl.removeEventListener).not.toHaveBeenCalled();
+
+ autofillMonitor.stopMonitoring(inputEl);
+ expect(inputEl.classList).not.toContain('mat-input-autofill-monitored');
+ expect(inputEl.removeEventListener)
+ .toHaveBeenCalledWith('animationstart', jasmine.any(Function), listenerOptions);
+ });
+
+ it('should stop monitoring all monitored elements upon destroy', () => {
+ const inputEl1 = testComponent.input1.nativeElement;
+ const inputEl2 = testComponent.input2.nativeElement;
+ const inputEl3 = testComponent.input3.nativeElement;
+ autofillMonitor.monitor(inputEl1);
+ autofillMonitor.monitor(inputEl2);
+ autofillMonitor.monitor(inputEl3);
+ expect(inputEl1.removeEventListener).not.toHaveBeenCalled();
+ expect(inputEl2.removeEventListener).not.toHaveBeenCalled();
+ expect(inputEl3.removeEventListener).not.toHaveBeenCalled();
+
+ autofillMonitor.ngOnDestroy();
+ expect(inputEl1.removeEventListener).toHaveBeenCalled();
+ expect(inputEl2.removeEventListener).toHaveBeenCalled();
+ expect(inputEl3.removeEventListener).toHaveBeenCalled();
+ });
+
+ it('should emit and add filled class upon start animation', () => {
+ const inputEl = testComponent.input1.nativeElement;
+ let animationStartCallback: Function = () => {};
+ let autofillStreamEvent: AutofillEvent | null = null;
+ inputEl.addEventListener.and.callFake((_, cb) => animationStartCallback = cb);
+ const autofillStream = autofillMonitor.monitor(inputEl);
+ autofillStream.subscribe(event => autofillStreamEvent = event);
+ expect(autofillStreamEvent).toBeNull();
+ expect(inputEl.classList).not.toContain('mat-input-autofilled');
+
+ animationStartCallback({animationName: 'mat-input-autofill-start', target: inputEl});
+ expect(inputEl.classList).toContain('mat-input-autofilled');
+ expect(autofillStreamEvent).toEqual({target: inputEl, isAutofilled: true} as any);
+ });
+
+ it('should emit and remove filled class upon end animation', () => {
+ const inputEl = testComponent.input1.nativeElement;
+ let animationStartCallback: Function = () => {};
+ let autofillStreamEvent: AutofillEvent | null = null;
+ inputEl.addEventListener.and.callFake((_, cb) => animationStartCallback = cb);
+ const autofillStream = autofillMonitor.monitor(inputEl);
+ autofillStream.subscribe(event => autofillStreamEvent = event);
+ animationStartCallback({animationName: 'mat-input-autofill-start', target: inputEl});
+ expect(inputEl.classList).toContain('mat-input-autofilled');
+ expect(autofillStreamEvent).toEqual({target: inputEl, isAutofilled: true} as any);
+
+ animationStartCallback({animationName: 'mat-input-autofill-end', target: inputEl});
+ expect(inputEl.classList).not.toContain('mat-input-autofilled');
+ expect(autofillStreamEvent).toEqual({target: inputEl, isAutofilled: false} as any);
+ });
+
+ it('should cleanup filled class if monitoring stopped in autofilled state', () => {
+ const inputEl = testComponent.input1.nativeElement;
+ let animationStartCallback: Function = () => {};
+ inputEl.addEventListener.and.callFake((_, cb) => animationStartCallback = cb);
+ autofillMonitor.monitor(inputEl);
+ animationStartCallback({animationName: 'mat-input-autofill-start', target: inputEl});
+ expect(inputEl.classList).toContain('mat-input-autofilled');
+
+ autofillMonitor.stopMonitoring(inputEl);
+ expect(inputEl.classlist).not.toContain('mat-input-autofilled');
+ });
+});
+
+describe('matAutofill', () => {
+ let autofillMonitor: AutofillMonitor;
+ let fixture: ComponentFixture;
+ let testComponent: InputWithMatAutofilled;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [MatInputModule],
+ declarations: [InputWithMatAutofilled],
+ }).compileComponents();
+ });
+
+ beforeEach(inject([AutofillMonitor], (afm: AutofillMonitor) => {
+ autofillMonitor = afm;
+ spyOn(autofillMonitor, 'monitor').and.returnValue(observableEmpty());
+ spyOn(autofillMonitor, 'stopMonitoring');
+ fixture = TestBed.createComponent(InputWithMatAutofilled);
+ testComponent = fixture.componentInstance;
+ fixture.detectChanges();
+ }));
+
+ it('should monitor host element on init', () => {
+ expect(autofillMonitor.monitor).toHaveBeenCalledWith(testComponent.input.nativeElement);
+ });
+
+ it('should stop monitoring host element on destroy', () => {
+ expect(autofillMonitor.stopMonitoring).not.toHaveBeenCalled();
+ fixture.destroy();
+ expect(autofillMonitor.stopMonitoring).toHaveBeenCalledWith(testComponent.input.nativeElement);
+ });
+});
+
+@Component({
+ template: `
+
+
+
+ `
+})
+class Inputs {
+ @ViewChild('input1') input1: ElementRef;
+ @ViewChild('input2') input2: ElementRef;
+ @ViewChild('input3') input3: ElementRef;
+}
+
+@Component({
+ template: ``
+})
+class InputWithMatAutofilled {
+ @ViewChild('input') input: ElementRef;
+}
diff --git a/src/lib/input/autofill.ts b/src/lib/input/autofill.ts
new file mode 100644
index 000000000000..84dd4cc5f098
--- /dev/null
+++ b/src/lib/input/autofill.ts
@@ -0,0 +1,135 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {Platform, supportsPassiveEventListeners} from '@angular/cdk/platform';
+import {
+ Directive,
+ ElementRef,
+ EventEmitter,
+ Injectable,
+ NgZone,
+ OnDestroy,
+ Output
+} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {empty as observableEmpty} from 'rxjs/observable/empty';
+import {Subject} from 'rxjs/Subject';
+
+
+/** An event that is emitted when the autofill state of an input changes. */
+export type AutofillEvent = {
+ /** The element whose autofill state changes. */
+ target: Element;
+ /** Whether the element is currently autofilled. */
+ isAutofilled: boolean;
+};
+
+
+/** Used to track info about currently monitored elements. */
+type MonitoredElementInfo = {
+ subject: Subject;
+ unlisten: () => void;
+};
+
+
+/** Options to pass to the animationstart listener. */
+const listenerOptions: any = supportsPassiveEventListeners() ? {passive: true} : false;
+
+
+/**
+ * An injectable service that can be used to monitor the autofill state of an input.
+ * Based on the following blog post:
+ * https://medium.com/@brunn/detecting-autofilled-fields-in-javascript-aed598d25da7
+ */
+@Injectable()
+export class AutofillMonitor implements OnDestroy {
+ private _monitoredElements = new Map();
+
+ constructor(private _platform: Platform, private _ngZone: NgZone) {}
+
+ /**
+ * Monitor for changes in the autofill state of the given input element.
+ * @param element The element to monitor.
+ * @return A stream of autofill state changes.
+ */
+ monitor(element: Element): Observable {
+ if (!this._platform.isBrowser) {
+ return observableEmpty();
+ }
+
+ const info = this._monitoredElements.get(element);
+ if (info) {
+ return info.subject.asObservable();
+ }
+
+ const result = new Subject();
+ const listener = (event: AnimationEvent) => {
+ if (event.animationName === 'mat-input-autofill-start') {
+ element.classList.add('mat-input-autofilled');
+ result.next({target: event.target as Element, isAutofilled: true});
+ } else if (event.animationName === 'mat-input-autofill-end') {
+ element.classList.remove('mat-input-autofilled');
+ result.next({target: event.target as Element, isAutofilled: false});
+ }
+ };
+
+ this._ngZone.runOutsideAngular(() => {
+ element.addEventListener('animationstart', listener, listenerOptions);
+ });
+ element.classList.add('mat-input-autofill-monitored');
+
+ this._monitoredElements.set(element, {
+ subject: result,
+ unlisten: () => {
+ element.removeEventListener('animationstart', listener, listenerOptions);
+ }
+ });
+
+ return result.asObservable();
+ }
+
+ /**
+ * Stop monitoring the autofill state of the given input element.
+ * @param element The element to stop monitoring.
+ */
+ stopMonitoring(element: Element) {
+ const info = this._monitoredElements.get(element);
+ if (info) {
+ info.unlisten();
+ element.classList.remove('mat-input-autofill-monitored');
+ element.classList.remove('mat-input-autofilled');
+ this._monitoredElements.delete(element);
+ }
+ }
+
+ ngOnDestroy() {
+ this._monitoredElements.forEach(info => {
+ info.unlisten();
+ info.subject.complete();
+ });
+ }
+}
+
+
+/** A directive that can be used to monitor the autofill state of an input. */
+@Directive({
+ selector: '[matAutofill]',
+})
+export class MatAutofill implements OnDestroy {
+ @Output() matAutofill = new EventEmitter();
+
+ constructor(private _elementRef: ElementRef, private _autofillMonitor: AutofillMonitor,
+ ngZone: NgZone) {
+ this._autofillMonitor.monitor(this._elementRef.nativeElement)
+ .subscribe(event => ngZone.run(() => this.matAutofill.emit(event)));
+ }
+
+ ngOnDestroy() {
+ this._autofillMonitor.stopMonitoring(this._elementRef.nativeElement);
+ }
+}
diff --git a/src/lib/input/input-module.ts b/src/lib/input/input-module.ts
index d89a13c3db5b..00f9a838637d 100644
--- a/src/lib/input/input-module.ts
+++ b/src/lib/input/input-module.ts
@@ -9,14 +9,16 @@
import {PlatformModule} from '@angular/cdk/platform';
import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
+import {ErrorStateMatcher} from '@angular/material/core';
import {MatFormFieldModule} from '@angular/material/form-field';
+import {AutofillMonitor, MatAutofill} from './autofill';
import {MatTextareaAutosize} from './autosize';
import {MatInput} from './input';
-import {ErrorStateMatcher} from '@angular/material/core';
@NgModule({
declarations: [
+ MatAutofill,
MatInput,
MatTextareaAutosize,
],
@@ -26,12 +28,13 @@ import {ErrorStateMatcher} from '@angular/material/core';
PlatformModule,
],
exports: [
+ MatAutofill,
// We re-export the `MatFormFieldModule` since `MatInput` will almost always
// be used together with `MatFormField`.
MatFormFieldModule,
MatInput,
MatTextareaAutosize,
],
- providers: [ErrorStateMatcher],
+ providers: [ErrorStateMatcher, AutofillMonitor],
})
export class MatInputModule {}
diff --git a/src/lib/input/public-api.ts b/src/lib/input/public-api.ts
index 5eaaf7d50575..f2f80acf31e8 100644
--- a/src/lib/input/public-api.ts
+++ b/src/lib/input/public-api.ts
@@ -7,9 +7,10 @@
*/
-export * from './input-module';
+export * from './autofill';
export * from './autosize';
export * from './input';
export * from './input-errors';
+export * from './input-module';
export * from './input-value-accessor';