From f5db4be4471712e768459b138c1b4563f32cbf51 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 30 Jan 2018 17:19:08 -0800 Subject: [PATCH 1/6] add utility for monitoring input autofill --- .github/CODEOWNERS | 1 + src/cdk/input/BUILD.bazel | 10 +++ src/cdk/input/_autofill.scss | 13 ++++ src/cdk/input/autofill-prebuilt.scss | 3 + src/cdk/input/autofill.ts | 100 +++++++++++++++++++++++++++ src/cdk/input/index.ts | 9 +++ src/cdk/input/input-module.ts | 17 +++++ src/cdk/input/public-api.ts | 10 +++ src/cdk/input/tsconfig-build.json | 14 ++++ src/demo-app/demo-material-module.ts | 23 +++--- src/demo-app/input/input-demo.html | 14 ++++ src/demo-app/input/input-demo.ts | 1 + src/demo-app/system-config.ts | 1 + src/e2e-app/system-config.ts | 1 + src/lib/core/_core.scss | 2 + test/karma-test-shim.js | 1 + 16 files changed, 210 insertions(+), 10 deletions(-) create mode 100644 src/cdk/input/BUILD.bazel create mode 100644 src/cdk/input/_autofill.scss create mode 100644 src/cdk/input/autofill-prebuilt.scss create mode 100644 src/cdk/input/autofill.ts create mode 100644 src/cdk/input/index.ts create mode 100644 src/cdk/input/input-module.ts create mode 100644 src/cdk/input/public-api.ts create mode 100644 src/cdk/input/tsconfig-build.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3e027a0978d6..3c21bfc05c8d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -59,6 +59,7 @@ /src/cdk/bidi/** @jelbourn /src/cdk/coercion/** @jelbourn /src/cdk/collections/** @jelbourn @crisbeto @andrewseguin +/src/cdl/input/** @mmalerba /src/cdk/keycodes/** @jelbourn /src/cdk/layout/** @josephperrott /src/cdk/observers/** @jelbourn @crisbeto diff --git a/src/cdk/input/BUILD.bazel b/src/cdk/input/BUILD.bazel new file mode 100644 index 000000000000..d80b8de8e459 --- /dev/null +++ b/src/cdk/input/BUILD.bazel @@ -0,0 +1,10 @@ +package(default_visibility=["//visibility:public"]) +load("@angular//:index.bzl", "ng_module") + +ng_module( + name = "input", + srcs = glob(["**/*.ts"], exclude=["**/*.spec.ts"]), + module_name = "@angular/cdk/input", + deps = ["@rxjs"], + tsconfig = ":tsconfig-build.json", +) diff --git a/src/cdk/input/_autofill.scss b/src/cdk/input/_autofill.scss new file mode 100644 index 000000000000..2d2672392d1f --- /dev/null +++ b/src/cdk/input/_autofill.scss @@ -0,0 +1,13 @@ +@mixin cdk-input-autofill { + // Keyframes that do nothing, but allow us to monitor when an input becomes autofilled. + @keyframes cdk-input-autofill-start {from {} to {}} + @keyframes cdk-input-autofill-end {from {} to {}} + + .cdk-input-autofill-monitored:-webkit-autofill { + animation-name: cdk-input-autofill-start; + } + + .cdk-input-autofill-monitored:not(:-webkit-autofill) { + animation-name: cdk-input-autofill-end; + } +} diff --git a/src/cdk/input/autofill-prebuilt.scss b/src/cdk/input/autofill-prebuilt.scss new file mode 100644 index 000000000000..3c17ee1cc484 --- /dev/null +++ b/src/cdk/input/autofill-prebuilt.scss @@ -0,0 +1,3 @@ +@import './autofill'; + +@include cdk-input-autofill(); diff --git a/src/cdk/input/autofill.ts b/src/cdk/input/autofill.ts new file mode 100644 index 000000000000..3c1859da257e --- /dev/null +++ b/src/cdk/input/autofill.ts @@ -0,0 +1,100 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Directive, ElementRef, EventEmitter, Injectable, OnDestroy, Output} from '@angular/core'; +import {Observable} from 'rxjs/Observable'; +import {Subject} from 'rxjs/Subject'; + + +/** An event that is emitted when the autofill state of an input changes. */ +export type AutofillEvent = { + target: Element; + isAutofilled: boolean; +}; + + +/** Used to track info about currently monitored elements. */ +type MonitoredElementInfo = { + subject: Subject; + unlisten: () => void; +}; + + +/** + * 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(); + + monitor(element: Element): Observable { + const info = this._monitoredElements.get(element); + if (info) { + return info.subject.asObservable(); + } + + const result = new Subject(); + const listener = (event: AnimationEvent) => { + if (event.animationName === 'cdk-input-autofill-start') { + element.classList.add('cdk-input-autofilled'); + result.next({target: event.target as Element, isAutofilled: true}); + } else if (event.animationName === 'cdk-input-autofill-end') { + element.classList.remove('cdk-input-autofilled'); + result.next({target: event.target as Element, isAutofilled: false}); + } + }; + + element.addEventListener('animationstart', listener); + element.classList.add('cdk-input-autofill-monitored'); + + this._monitoredElements.set(element, { + subject: result, + unlisten: () => { + element.removeEventListener('animationstart', listener); + } + }); + + return result.asObservable(); + } + + stopMonitoring(element: Element) { + const info = this._monitoredElements.get(element); + if (info) { + info.unlisten(); + element.classList.remove('cdk-input-autofill-monitored'); + 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: '[cdkAutofill]', +}) +export class CdkAutofill implements OnDestroy { + @Output() cdkAutofill = new EventEmitter(); + + constructor(private _elementRef: ElementRef, private _autofillMonitor: AutofillMonitor) { + this._autofillMonitor.monitor(this._elementRef.nativeElement) + .subscribe(event => this.cdkAutofill.emit(event)); + } + + ngOnDestroy() { + this._autofillMonitor.stopMonitoring(this._elementRef.nativeElement); + } +} diff --git a/src/cdk/input/index.ts b/src/cdk/input/index.ts new file mode 100644 index 000000000000..676ca90f1ffa --- /dev/null +++ b/src/cdk/input/index.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export * from './public-api'; diff --git a/src/cdk/input/input-module.ts b/src/cdk/input/input-module.ts new file mode 100644 index 000000000000..10fe20ec539a --- /dev/null +++ b/src/cdk/input/input-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 {AutofillMonitor, CdkAutofill} from './autofill'; + +@NgModule({ + declarations: [CdkAutofill], + exports: [CdkAutofill], + providers: [AutofillMonitor], +}) +export class InputModule {} diff --git a/src/cdk/input/public-api.ts b/src/cdk/input/public-api.ts new file mode 100644 index 000000000000..c0ef6c277424 --- /dev/null +++ b/src/cdk/input/public-api.ts @@ -0,0 +1,10 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export * from './autofill'; +export * from './input-module'; diff --git a/src/cdk/input/tsconfig-build.json b/src/cdk/input/tsconfig-build.json new file mode 100644 index 000000000000..a42aef6087e4 --- /dev/null +++ b/src/cdk/input/tsconfig-build.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig-build", + "files": [ + "public-api.ts" + ], + "angularCompilerOptions": { + "annotateForClosureCompiler": true, + "strictMetadataEmit": true, + "flatModuleOutFile": "index.js", + "flatModuleId": "@angular/cdk/input", + "skipTemplateCodegen": true, + "fullTemplateTypeCheck": true + } +} diff --git a/src/demo-app/demo-material-module.ts b/src/demo-app/demo-material-module.ts index 33ff49f3c389..de4fd0175748 100644 --- a/src/demo-app/demo-material-module.ts +++ b/src/demo-app/demo-material-module.ts @@ -6,6 +6,15 @@ * 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 {InputModule} from '@angular/cdk/input'; +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 +33,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. @@ -92,6 +94,7 @@ import {PortalModule} from '@angular/cdk/portal'; A11yModule, BidiModule, CdkAccordionModule, + InputModule, ObserversModule, OverlayModule, PlatformModule, diff --git a/src/demo-app/input/input-demo.html b/src/demo-app/input/input-demo.html index 2703447a2a52..bcd102981582 100644 --- a/src/demo-app/input/input-demo.html +++ b/src/demo-app/input/input-demo.html @@ -631,3 +631,17 @@

<textarea> with ngModel

+ + + Autofill Monitor + +
+ + Autofill monitor + + + + is autofilled? {{isAutofilled ? 'yes' : 'no'}} +
+
+
diff --git a/src/demo-app/input/input-demo.ts b/src/demo-app/input/input-demo.ts index 3e6839b15e5b..613926b88958 100644 --- a/src/demo-app/input/input-demo.ts +++ b/src/demo-app/input/input-demo.ts @@ -51,6 +51,7 @@ export class InputDemo { emailFormControl = new FormControl('', [Validators.required, Validators.pattern(EMAIL_REGEX)]); delayedFormControl = new FormControl(''); model = 'hello'; + isAutofilled = false; legacyAppearance: string; standardAppearance: string; diff --git a/src/demo-app/system-config.ts b/src/demo-app/system-config.ts index fa0a07207038..2b68e37eb91f 100644 --- a/src/demo-app/system-config.ts +++ b/src/demo-app/system-config.ts @@ -48,6 +48,7 @@ System.config({ '@angular/cdk/bidi': 'dist/packages/cdk/bidi/index.js', '@angular/cdk/coercion': 'dist/packages/cdk/coercion/index.js', '@angular/cdk/collections': 'dist/packages/cdk/collections/index.js', + '@angular/cdk/input': 'dist/packages/cdk/input/index.js', '@angular/cdk/keycodes': 'dist/packages/cdk/keycodes/index.js', '@angular/cdk/layout': 'dist/packages/cdk/layout/index.js', '@angular/cdk/observers': 'dist/packages/cdk/observers/index.js', diff --git a/src/e2e-app/system-config.ts b/src/e2e-app/system-config.ts index 20ddfba9d6ec..343ce5b4bd32 100644 --- a/src/e2e-app/system-config.ts +++ b/src/e2e-app/system-config.ts @@ -38,6 +38,7 @@ System.config({ '@angular/cdk/bidi': 'dist/bundles/cdk-bidi.umd.js', '@angular/cdk/coercion': 'dist/bundles/cdk-coercion.umd.js', '@angular/cdk/collections': 'dist/bundles/cdk-collections.umd.js', + '@angular/cdk/input': 'dist/bundles/cdk-input.umd.js', '@angular/cdk/keycodes': 'dist/bundles/cdk-keycodes.umd.js', '@angular/cdk/layout': 'dist/bundles/cdk-layout.umd.js', '@angular/cdk/observers': 'dist/bundles/cdk-observers.umd.js', diff --git a/src/lib/core/_core.scss b/src/lib/core/_core.scss index ae78d5e77828..c84f5ff5580c 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 '../../cdk/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 cdk-input-autofill(); } // Mixin that renders all of the core styles that depend on the theme. diff --git a/test/karma-test-shim.js b/test/karma-test-shim.js index 0bb2ecd4aa3f..e64f46399a1b 100644 --- a/test/karma-test-shim.js +++ b/test/karma-test-shim.js @@ -59,6 +59,7 @@ System.config({ '@angular/cdk/bidi': 'dist/packages/cdk/bidi/index.js', '@angular/cdk/coercion': 'dist/packages/cdk/coercion/index.js', '@angular/cdk/collections': 'dist/packages/cdk/collections/index.js', + '@angular/cdk/input': 'dist/packages/cdk/input/index.js', '@angular/cdk/keycodes': 'dist/packages/cdk/keycodes/index.js', '@angular/cdk/layout': 'dist/packages/cdk/layout/index.js', '@angular/cdk/observers': 'dist/packages/cdk/observers/index.js', From 04db236e1c8fc48b3013458fc6c9cd244edbe5e3 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Wed, 31 Jan 2018 08:47:02 -0800 Subject: [PATCH 2/6] add scss mixin for styling input autofill colors --- src/cdk/input/_autofill.scss | 29 +++++++++++++++++++++++++++-- src/demo-app/input/input-demo.html | 10 +++++++--- src/demo-app/input/input-demo.scss | 6 ++++++ src/demo-app/input/input-demo.ts | 1 + 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/cdk/input/_autofill.scss b/src/cdk/input/_autofill.scss index 2d2672392d1f..23b0c8cbcdc4 100644 --- a/src/cdk/input/_autofill.scss +++ b/src/cdk/input/_autofill.scss @@ -1,7 +1,8 @@ +// Core styles that enable monitoring autofill state of inputs. @mixin cdk-input-autofill { // Keyframes that do nothing, but allow us to monitor when an input becomes autofilled. - @keyframes cdk-input-autofill-start {from {} to {}} - @keyframes cdk-input-autofill-end {from {} to {}} + @keyframes cdk-input-autofill-start {} + @keyframes cdk-input-autofill-end {} .cdk-input-autofill-monitored:-webkit-autofill { animation-name: cdk-input-autofill-start; @@ -11,3 +12,27 @@ animation-name: cdk-input-autofill-end; } } + +// Used to generate UIDs for keyframes used to change the input autofill styles. +$cdk-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 cdk-input-autofill-color($background, $foreground:'') { + @keyframes cdk-input-autofill-color-#{$cdk-input-autofill-color-frame-count} { + to { + background: $background; + @if $foreground != '' { color: $foreground; } + } + } + + &:-webkit-autofill, + &.cdk-input-autofill-monitored:-webkit-autofill { + animation-name: cdk-input-autofill-start, + cdk-input-autofill-color-#{$cdk-input-autofill-color-frame-count}; + animation-fill-mode: both; + } + + $cdk-input-autofill-color-frame-count: $cdk-input-autofill-color-frame-count + 1 !global; +} diff --git a/src/demo-app/input/input-demo.html b/src/demo-app/input/input-demo.html index bcd102981582..2ad41c4c841b 100644 --- a/src/demo-app/input/input-demo.html +++ b/src/demo-app/input/input-demo.html @@ -633,12 +633,16 @@

<textarea> with ngModel

- Autofill Monitor + Autofill
+ + Use custom autofill style + - Autofill monitor - + Autofill monitored + is autofilled? {{isAutofilled ? 'yes' : 'no'}} diff --git a/src/demo-app/input/input-demo.scss b/src/demo-app/input/input-demo.scss index d7761343e44b..c54b8215bb73 100644 --- a/src/demo-app/input/input-demo.scss +++ b/src/demo-app/input/input-demo.scss @@ -1,3 +1,5 @@ +@import '../../../dist/packages/cdk/input/autofill'; + .demo-basic { padding: 0; } @@ -27,3 +29,7 @@ padding: 0; background: lightblue; } + +.demo-custom-autofill-style { + @include cdk-input-autofill-color(transparent, red); +} diff --git a/src/demo-app/input/input-demo.ts b/src/demo-app/input/input-demo.ts index 613926b88958..e8989603029b 100644 --- a/src/demo-app/input/input-demo.ts +++ b/src/demo-app/input/input-demo.ts @@ -52,6 +52,7 @@ export class InputDemo { delayedFormControl = new FormControl(''); model = 'hello'; isAutofilled = false; + customAutofillStyle = true; legacyAppearance: string; standardAppearance: string; From dfbbd0fb77026dd7132e82f07e3b56cdd3bfd225 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Wed, 31 Jan 2018 10:50:42 -0800 Subject: [PATCH 3/6] tests --- .github/CODEOWNERS | 2 +- src/cdk/input/_autofill.scss | 13 ++- src/cdk/input/autofill.spec.ts | 186 +++++++++++++++++++++++++++++++++ src/cdk/input/autofill.ts | 1 + 4 files changed, 196 insertions(+), 6 deletions(-) create mode 100644 src/cdk/input/autofill.spec.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3c21bfc05c8d..2e193a2f70bd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -59,7 +59,7 @@ /src/cdk/bidi/** @jelbourn /src/cdk/coercion/** @jelbourn /src/cdk/collections/** @jelbourn @crisbeto @andrewseguin -/src/cdl/input/** @mmalerba +/src/cdk/input/** @mmalerba /src/cdk/keycodes/** @jelbourn /src/cdk/layout/** @josephperrott /src/cdk/observers/** @jelbourn @crisbeto diff --git a/src/cdk/input/_autofill.scss b/src/cdk/input/_autofill.scss index 23b0c8cbcdc4..b5205c8239ef 100644 --- a/src/cdk/input/_autofill.scss +++ b/src/cdk/input/_autofill.scss @@ -16,9 +16,9 @@ // Used to generate UIDs for keyframes used to change the input autofill styles. $cdk-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 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 cdk-input-autofill-color($background, $foreground:'') { @keyframes cdk-input-autofill-color-#{$cdk-input-autofill-color-frame-count} { to { @@ -27,11 +27,14 @@ $cdk-input-autofill-color-frame-count: 0; } } - &:-webkit-autofill, + &:-webkit-autofill { + animation-name: cdk-input-autofill-color-#{$cdk-input-autofill-color-frame-count}; + animation-fill-mode: both; + } + &.cdk-input-autofill-monitored:-webkit-autofill { animation-name: cdk-input-autofill-start, cdk-input-autofill-color-#{$cdk-input-autofill-color-frame-count}; - animation-fill-mode: both; } $cdk-input-autofill-color-frame-count: $cdk-input-autofill-color-frame-count + 1 !global; diff --git a/src/cdk/input/autofill.spec.ts b/src/cdk/input/autofill.spec.ts new file mode 100644 index 000000000000..e4b09b795b5a --- /dev/null +++ b/src/cdk/input/autofill.spec.ts @@ -0,0 +1,186 @@ +/** + * @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 {Component, ElementRef, ViewChild} from '@angular/core'; +import {ComponentFixture, inject, TestBed} from '@angular/core/testing'; +import {AutofillEvent, AutofillMonitor} from './autofill'; +import {InputModule} from './input-module'; +import {empty as observableEmpty} from 'rxjs/observable/empty'; + +describe('AutofillMonitor', () => { + let autofillMonitor: AutofillMonitor; + let fixture: ComponentFixture; + let testComponent: Inputs; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [InputModule], + 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('cdk-input-autofill-monitored'); + expect(inputEl.addEventListener).toHaveBeenCalledWith('animationstart', jasmine.any(Function)); + }); + + 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('cdk-input-autofill-monitored'); + expect(inputEl.removeEventListener).not.toHaveBeenCalled(); + + autofillMonitor.stopMonitoring(inputEl); + expect(inputEl.classList).not.toContain('cdk-input-autofill-monitored'); + expect(inputEl.removeEventListener) + .toHaveBeenCalledWith('animationstart', jasmine.any(Function)); + }); + + 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('cdk-input-autofilled'); + + animationStartCallback({animationName: 'cdk-input-autofill-start', target: inputEl}); + expect(inputEl.classList).toContain('cdk-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: 'cdk-input-autofill-start', target: inputEl}); + expect(inputEl.classList).toContain('cdk-input-autofilled'); + expect(autofillStreamEvent).toEqual({target: inputEl, isAutofilled: true} as any); + + animationStartCallback({animationName: 'cdk-input-autofill-end', target: inputEl}); + expect(inputEl.classList).not.toContain('cdk-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: 'cdk-input-autofill-start', target: inputEl}); + expect(inputEl.classList).toContain('cdk-input-autofilled'); + + autofillMonitor.stopMonitoring(inputEl); + expect(inputEl.classlist).not.toContain('cdk-input-autofilled'); + }); +}); + +describe('cdkAutofill', () => { + let autofillMonitor: AutofillMonitor; + let fixture: ComponentFixture; + let testComponent: InputWithCdkAutofilled; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [InputModule], + declarations: [InputWithCdkAutofilled], + }).compileComponents(); + }); + + beforeEach(inject([AutofillMonitor], (afm: AutofillMonitor) => { + autofillMonitor = afm; + spyOn(autofillMonitor, 'monitor').and.returnValue(observableEmpty()); + spyOn(autofillMonitor, 'stopMonitoring'); + fixture = TestBed.createComponent(InputWithCdkAutofilled); + 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 InputWithCdkAutofilled { + @ViewChild('input') input: ElementRef; +} diff --git a/src/cdk/input/autofill.ts b/src/cdk/input/autofill.ts index 3c1859da257e..21e5d19d0c90 100644 --- a/src/cdk/input/autofill.ts +++ b/src/cdk/input/autofill.ts @@ -69,6 +69,7 @@ export class AutofillMonitor implements OnDestroy { if (info) { info.unlisten(); element.classList.remove('cdk-input-autofill-monitored'); + element.classList.remove('cdk-input-autofilled'); this._monitoredElements.delete(element); } } From a06340164678694768b74950ab16cc60fd7a687a Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Thu, 1 Feb 2018 14:40:30 -0800 Subject: [PATCH 4/6] move everything from cdk to MatInputModule --- .github/CODEOWNERS | 1 - src/cdk/input/BUILD.bazel | 10 ------ src/cdk/input/_autofill.scss | 41 ---------------------- src/cdk/input/autofill-prebuilt.scss | 3 -- src/cdk/input/index.ts | 9 ----- src/cdk/input/input-module.ts | 17 --------- src/cdk/input/public-api.ts | 10 ------ src/cdk/input/tsconfig-build.json | 14 -------- src/demo-app/demo-material-module.ts | 2 -- src/demo-app/input/input-demo.html | 2 +- src/demo-app/input/input-demo.scss | 4 +-- src/demo-app/system-config.ts | 1 - src/e2e-app/system-config.ts | 1 - src/lib/core/_core.scss | 4 +-- src/lib/input/BUILD.bazel | 1 + src/lib/input/_autofill.scss | 41 ++++++++++++++++++++++ src/lib/input/autofill-prebuilt.scss | 3 ++ src/{cdk => lib}/input/autofill.spec.ts | 46 ++++++++++++------------- src/{cdk => lib}/input/autofill.ts | 22 ++++++------ src/lib/input/input-module.ts | 7 ++-- src/lib/input/public-api.ts | 3 +- test/karma-test-shim.js | 1 - 22 files changed, 91 insertions(+), 152 deletions(-) delete mode 100644 src/cdk/input/BUILD.bazel delete mode 100644 src/cdk/input/_autofill.scss delete mode 100644 src/cdk/input/autofill-prebuilt.scss delete mode 100644 src/cdk/input/index.ts delete mode 100644 src/cdk/input/input-module.ts delete mode 100644 src/cdk/input/public-api.ts delete mode 100644 src/cdk/input/tsconfig-build.json create mode 100644 src/lib/input/_autofill.scss create mode 100644 src/lib/input/autofill-prebuilt.scss rename src/{cdk => lib}/input/autofill.spec.ts (82%) rename src/{cdk => lib}/input/autofill.ts (79%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2e193a2f70bd..3e027a0978d6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -59,7 +59,6 @@ /src/cdk/bidi/** @jelbourn /src/cdk/coercion/** @jelbourn /src/cdk/collections/** @jelbourn @crisbeto @andrewseguin -/src/cdk/input/** @mmalerba /src/cdk/keycodes/** @jelbourn /src/cdk/layout/** @josephperrott /src/cdk/observers/** @jelbourn @crisbeto diff --git a/src/cdk/input/BUILD.bazel b/src/cdk/input/BUILD.bazel deleted file mode 100644 index d80b8de8e459..000000000000 --- a/src/cdk/input/BUILD.bazel +++ /dev/null @@ -1,10 +0,0 @@ -package(default_visibility=["//visibility:public"]) -load("@angular//:index.bzl", "ng_module") - -ng_module( - name = "input", - srcs = glob(["**/*.ts"], exclude=["**/*.spec.ts"]), - module_name = "@angular/cdk/input", - deps = ["@rxjs"], - tsconfig = ":tsconfig-build.json", -) diff --git a/src/cdk/input/_autofill.scss b/src/cdk/input/_autofill.scss deleted file mode 100644 index b5205c8239ef..000000000000 --- a/src/cdk/input/_autofill.scss +++ /dev/null @@ -1,41 +0,0 @@ -// Core styles that enable monitoring autofill state of inputs. -@mixin cdk-input-autofill { - // Keyframes that do nothing, but allow us to monitor when an input becomes autofilled. - @keyframes cdk-input-autofill-start {} - @keyframes cdk-input-autofill-end {} - - .cdk-input-autofill-monitored:-webkit-autofill { - animation-name: cdk-input-autofill-start; - } - - .cdk-input-autofill-monitored:not(:-webkit-autofill) { - animation-name: cdk-input-autofill-end; - } -} - -// Used to generate UIDs for keyframes used to change the input autofill styles. -$cdk-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 cdk-input-autofill-color($background, $foreground:'') { - @keyframes cdk-input-autofill-color-#{$cdk-input-autofill-color-frame-count} { - to { - background: $background; - @if $foreground != '' { color: $foreground; } - } - } - - &:-webkit-autofill { - animation-name: cdk-input-autofill-color-#{$cdk-input-autofill-color-frame-count}; - animation-fill-mode: both; - } - - &.cdk-input-autofill-monitored:-webkit-autofill { - animation-name: cdk-input-autofill-start, - cdk-input-autofill-color-#{$cdk-input-autofill-color-frame-count}; - } - - $cdk-input-autofill-color-frame-count: $cdk-input-autofill-color-frame-count + 1 !global; -} diff --git a/src/cdk/input/autofill-prebuilt.scss b/src/cdk/input/autofill-prebuilt.scss deleted file mode 100644 index 3c17ee1cc484..000000000000 --- a/src/cdk/input/autofill-prebuilt.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import './autofill'; - -@include cdk-input-autofill(); diff --git a/src/cdk/input/index.ts b/src/cdk/input/index.ts deleted file mode 100644 index 676ca90f1ffa..000000000000 --- a/src/cdk/input/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -export * from './public-api'; diff --git a/src/cdk/input/input-module.ts b/src/cdk/input/input-module.ts deleted file mode 100644 index 10fe20ec539a..000000000000 --- a/src/cdk/input/input-module.ts +++ /dev/null @@ -1,17 +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 {AutofillMonitor, CdkAutofill} from './autofill'; - -@NgModule({ - declarations: [CdkAutofill], - exports: [CdkAutofill], - providers: [AutofillMonitor], -}) -export class InputModule {} diff --git a/src/cdk/input/public-api.ts b/src/cdk/input/public-api.ts deleted file mode 100644 index c0ef6c277424..000000000000 --- a/src/cdk/input/public-api.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -export * from './autofill'; -export * from './input-module'; diff --git a/src/cdk/input/tsconfig-build.json b/src/cdk/input/tsconfig-build.json deleted file mode 100644 index a42aef6087e4..000000000000 --- a/src/cdk/input/tsconfig-build.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "../tsconfig-build", - "files": [ - "public-api.ts" - ], - "angularCompilerOptions": { - "annotateForClosureCompiler": true, - "strictMetadataEmit": true, - "flatModuleOutFile": "index.js", - "flatModuleId": "@angular/cdk/input", - "skipTemplateCodegen": true, - "fullTemplateTypeCheck": true - } -} diff --git a/src/demo-app/demo-material-module.ts b/src/demo-app/demo-material-module.ts index de4fd0175748..7e4ffac77e73 100644 --- a/src/demo-app/demo-material-module.ts +++ b/src/demo-app/demo-material-module.ts @@ -9,7 +9,6 @@ import {A11yModule} from '@angular/cdk/a11y'; import {CdkAccordionModule} from '@angular/cdk/accordion'; import {BidiModule} from '@angular/cdk/bidi'; -import {InputModule} from '@angular/cdk/input'; import {ObserversModule} from '@angular/cdk/observers'; import {OverlayModule} from '@angular/cdk/overlay'; import {PlatformModule} from '@angular/cdk/platform'; @@ -94,7 +93,6 @@ import { A11yModule, BidiModule, CdkAccordionModule, - InputModule, ObserversModule, OverlayModule, PlatformModule, diff --git a/src/demo-app/input/input-demo.html b/src/demo-app/input/input-demo.html index 2ad41c4c841b..d538ad3c9194 100644 --- a/src/demo-app/input/input-demo.html +++ b/src/demo-app/input/input-demo.html @@ -641,7 +641,7 @@

<textarea> with ngModel

Autofill monitored - diff --git a/src/demo-app/input/input-demo.scss b/src/demo-app/input/input-demo.scss index c54b8215bb73..a15c23b80af5 100644 --- a/src/demo-app/input/input-demo.scss +++ b/src/demo-app/input/input-demo.scss @@ -1,4 +1,4 @@ -@import '../../../dist/packages/cdk/input/autofill'; +@import '../../../dist/packages/material/input/autofill'; .demo-basic { padding: 0; @@ -31,5 +31,5 @@ } .demo-custom-autofill-style { - @include cdk-input-autofill-color(transparent, red); + @include mat-input-autofill-color(transparent, red); } diff --git a/src/demo-app/system-config.ts b/src/demo-app/system-config.ts index 2b68e37eb91f..fa0a07207038 100644 --- a/src/demo-app/system-config.ts +++ b/src/demo-app/system-config.ts @@ -48,7 +48,6 @@ System.config({ '@angular/cdk/bidi': 'dist/packages/cdk/bidi/index.js', '@angular/cdk/coercion': 'dist/packages/cdk/coercion/index.js', '@angular/cdk/collections': 'dist/packages/cdk/collections/index.js', - '@angular/cdk/input': 'dist/packages/cdk/input/index.js', '@angular/cdk/keycodes': 'dist/packages/cdk/keycodes/index.js', '@angular/cdk/layout': 'dist/packages/cdk/layout/index.js', '@angular/cdk/observers': 'dist/packages/cdk/observers/index.js', diff --git a/src/e2e-app/system-config.ts b/src/e2e-app/system-config.ts index 343ce5b4bd32..20ddfba9d6ec 100644 --- a/src/e2e-app/system-config.ts +++ b/src/e2e-app/system-config.ts @@ -38,7 +38,6 @@ System.config({ '@angular/cdk/bidi': 'dist/bundles/cdk-bidi.umd.js', '@angular/cdk/coercion': 'dist/bundles/cdk-coercion.umd.js', '@angular/cdk/collections': 'dist/bundles/cdk-collections.umd.js', - '@angular/cdk/input': 'dist/bundles/cdk-input.umd.js', '@angular/cdk/keycodes': 'dist/bundles/cdk-keycodes.umd.js', '@angular/cdk/layout': 'dist/bundles/cdk-layout.umd.js', '@angular/cdk/observers': 'dist/bundles/cdk-observers.umd.js', diff --git a/src/lib/core/_core.scss b/src/lib/core/_core.scss index c84f5ff5580c..748e3a5735b7 100644 --- a/src/lib/core/_core.scss +++ b/src/lib/core/_core.scss @@ -2,7 +2,7 @@ // up into a single flat scss file for material. @import '../../cdk/overlay/overlay'; @import '../../cdk/a11y/a11y'; -@import '../../cdk/input/autofill'; +@import '../input/autofill'; // Core styles that can be used to apply material design treatments to any element. @import 'style/elevation'; @@ -27,7 +27,7 @@ @include mat-ripple(); @include cdk-a11y(); @include cdk-overlay(); - @include cdk-input-autofill(); + @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..5bd30ab5d79f --- /dev/null +++ b/src/lib/input/_autofill.scss @@ -0,0 +1,41 @@ +// Core styles that enable monitoring autofill state of inputs. +@mixin mat-input-autofill { + // Keyframes that do nothing, but allow us to monitor when an input becomes autofilled. + @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/cdk/input/autofill.spec.ts b/src/lib/input/autofill.spec.ts similarity index 82% rename from src/cdk/input/autofill.spec.ts rename to src/lib/input/autofill.spec.ts index e4b09b795b5a..554fc4dbc4af 100644 --- a/src/cdk/input/autofill.spec.ts +++ b/src/lib/input/autofill.spec.ts @@ -9,7 +9,7 @@ import {Component, ElementRef, ViewChild} from '@angular/core'; import {ComponentFixture, inject, TestBed} from '@angular/core/testing'; import {AutofillEvent, AutofillMonitor} from './autofill'; -import {InputModule} from './input-module'; +import {MatInputModule} from './input-module'; import {empty as observableEmpty} from 'rxjs/observable/empty'; describe('AutofillMonitor', () => { @@ -19,7 +19,7 @@ describe('AutofillMonitor', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [InputModule], + imports: [MatInputModule], declarations: [Inputs], }).compileComponents(); }); @@ -47,7 +47,7 @@ describe('AutofillMonitor', () => { expect(inputEl.addEventListener).not.toHaveBeenCalled(); autofillMonitor.monitor(inputEl); - expect(inputEl.classList).toContain('cdk-input-autofill-monitored'); + expect(inputEl.classList).toContain('mat-input-autofill-monitored'); expect(inputEl.addEventListener).toHaveBeenCalledWith('animationstart', jasmine.any(Function)); }); @@ -63,11 +63,11 @@ describe('AutofillMonitor', () => { it('should remove monitored class and listener upon stop monitoring', () => { const inputEl = testComponent.input1.nativeElement; autofillMonitor.monitor(inputEl); - expect(inputEl.classList).toContain('cdk-input-autofill-monitored'); + expect(inputEl.classList).toContain('mat-input-autofill-monitored'); expect(inputEl.removeEventListener).not.toHaveBeenCalled(); autofillMonitor.stopMonitoring(inputEl); - expect(inputEl.classList).not.toContain('cdk-input-autofill-monitored'); + expect(inputEl.classList).not.toContain('mat-input-autofill-monitored'); expect(inputEl.removeEventListener) .toHaveBeenCalledWith('animationstart', jasmine.any(Function)); }); @@ -97,10 +97,10 @@ describe('AutofillMonitor', () => { const autofillStream = autofillMonitor.monitor(inputEl); autofillStream.subscribe(event => autofillStreamEvent = event); expect(autofillStreamEvent).toBeNull(); - expect(inputEl.classList).not.toContain('cdk-input-autofilled'); + expect(inputEl.classList).not.toContain('mat-input-autofilled'); - animationStartCallback({animationName: 'cdk-input-autofill-start', target: inputEl}); - expect(inputEl.classList).toContain('cdk-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); }); @@ -111,12 +111,12 @@ describe('AutofillMonitor', () => { inputEl.addEventListener.and.callFake((_, cb) => animationStartCallback = cb); const autofillStream = autofillMonitor.monitor(inputEl); autofillStream.subscribe(event => autofillStreamEvent = event); - animationStartCallback({animationName: 'cdk-input-autofill-start', target: inputEl}); - expect(inputEl.classList).toContain('cdk-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); - animationStartCallback({animationName: 'cdk-input-autofill-end', target: inputEl}); - expect(inputEl.classList).not.toContain('cdk-input-autofilled'); + 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); }); @@ -125,23 +125,23 @@ describe('AutofillMonitor', () => { let animationStartCallback: Function = () => {}; inputEl.addEventListener.and.callFake((_, cb) => animationStartCallback = cb); autofillMonitor.monitor(inputEl); - animationStartCallback({animationName: 'cdk-input-autofill-start', target: inputEl}); - expect(inputEl.classList).toContain('cdk-input-autofilled'); + animationStartCallback({animationName: 'mat-input-autofill-start', target: inputEl}); + expect(inputEl.classList).toContain('mat-input-autofilled'); autofillMonitor.stopMonitoring(inputEl); - expect(inputEl.classlist).not.toContain('cdk-input-autofilled'); + expect(inputEl.classlist).not.toContain('mat-input-autofilled'); }); }); -describe('cdkAutofill', () => { +describe('matAutofill', () => { let autofillMonitor: AutofillMonitor; - let fixture: ComponentFixture; - let testComponent: InputWithCdkAutofilled; + let fixture: ComponentFixture; + let testComponent: InputWithMatAutofilled; beforeEach(() => { TestBed.configureTestingModule({ - imports: [InputModule], - declarations: [InputWithCdkAutofilled], + imports: [MatInputModule], + declarations: [InputWithMatAutofilled], }).compileComponents(); }); @@ -149,7 +149,7 @@ describe('cdkAutofill', () => { autofillMonitor = afm; spyOn(autofillMonitor, 'monitor').and.returnValue(observableEmpty()); spyOn(autofillMonitor, 'stopMonitoring'); - fixture = TestBed.createComponent(InputWithCdkAutofilled); + fixture = TestBed.createComponent(InputWithMatAutofilled); testComponent = fixture.componentInstance; fixture.detectChanges(); })); @@ -179,8 +179,8 @@ class Inputs { } @Component({ - template: `` + template: `` }) -class InputWithCdkAutofilled { +class InputWithMatAutofilled { @ViewChild('input') input: ElementRef; } diff --git a/src/cdk/input/autofill.ts b/src/lib/input/autofill.ts similarity index 79% rename from src/cdk/input/autofill.ts rename to src/lib/input/autofill.ts index 21e5d19d0c90..43c532cd921e 100644 --- a/src/cdk/input/autofill.ts +++ b/src/lib/input/autofill.ts @@ -42,17 +42,17 @@ export class AutofillMonitor implements OnDestroy { const result = new Subject(); const listener = (event: AnimationEvent) => { - if (event.animationName === 'cdk-input-autofill-start') { - element.classList.add('cdk-input-autofilled'); + 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 === 'cdk-input-autofill-end') { - element.classList.remove('cdk-input-autofilled'); + } else if (event.animationName === 'mat-input-autofill-end') { + element.classList.remove('mat-input-autofilled'); result.next({target: event.target as Element, isAutofilled: false}); } }; element.addEventListener('animationstart', listener); - element.classList.add('cdk-input-autofill-monitored'); + element.classList.add('mat-input-autofill-monitored'); this._monitoredElements.set(element, { subject: result, @@ -68,8 +68,8 @@ export class AutofillMonitor implements OnDestroy { const info = this._monitoredElements.get(element); if (info) { info.unlisten(); - element.classList.remove('cdk-input-autofill-monitored'); - element.classList.remove('cdk-input-autofilled'); + element.classList.remove('mat-input-autofill-monitored'); + element.classList.remove('mat-input-autofilled'); this._monitoredElements.delete(element); } } @@ -85,14 +85,14 @@ export class AutofillMonitor implements OnDestroy { /** A directive that can be used to monitor the autofill state of an input. */ @Directive({ - selector: '[cdkAutofill]', + selector: '[matAutofill]', }) -export class CdkAutofill implements OnDestroy { - @Output() cdkAutofill = new EventEmitter(); +export class MatAutofill implements OnDestroy { + @Output() matAutofill = new EventEmitter(); constructor(private _elementRef: ElementRef, private _autofillMonitor: AutofillMonitor) { this._autofillMonitor.monitor(this._elementRef.nativeElement) - .subscribe(event => this.cdkAutofill.emit(event)); + .subscribe(event => this.matAutofill.emit(event)); } ngOnDestroy() { 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'; diff --git a/test/karma-test-shim.js b/test/karma-test-shim.js index e64f46399a1b..0bb2ecd4aa3f 100644 --- a/test/karma-test-shim.js +++ b/test/karma-test-shim.js @@ -59,7 +59,6 @@ System.config({ '@angular/cdk/bidi': 'dist/packages/cdk/bidi/index.js', '@angular/cdk/coercion': 'dist/packages/cdk/coercion/index.js', '@angular/cdk/collections': 'dist/packages/cdk/collections/index.js', - '@angular/cdk/input': 'dist/packages/cdk/input/index.js', '@angular/cdk/keycodes': 'dist/packages/cdk/keycodes/index.js', '@angular/cdk/layout': 'dist/packages/cdk/layout/index.js', '@angular/cdk/observers': 'dist/packages/cdk/observers/index.js', From b247539a78bd956d37c2d66d66de341968db9113 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Thu, 1 Feb 2018 16:04:21 -0800 Subject: [PATCH 5/6] address comments --- src/lib/input/_autofill.scss | 4 +++- src/lib/input/autofill.spec.ts | 12 +++++++++--- src/lib/input/autofill.ts | 33 ++++++++++++++++++++++++++++----- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/lib/input/_autofill.scss b/src/lib/input/_autofill.scss index 5bd30ab5d79f..fc778c84a9eb 100644 --- a/src/lib/input/_autofill.scss +++ b/src/lib/input/_autofill.scss @@ -1,6 +1,8 @@ // Core styles that enable monitoring autofill state of inputs. @mixin mat-input-autofill { - // Keyframes that do nothing, but allow us to monitor when an input becomes autofilled. + // 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 {} diff --git a/src/lib/input/autofill.spec.ts b/src/lib/input/autofill.spec.ts index 554fc4dbc4af..f175ff4efa70 100644 --- a/src/lib/input/autofill.spec.ts +++ b/src/lib/input/autofill.spec.ts @@ -6,11 +6,16 @@ * 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'; -import {empty as observableEmpty} from 'rxjs/observable/empty'; + + +const listenerOptions: any = supportsPassiveEventListeners() ? {passive: true} : false; + describe('AutofillMonitor', () => { let autofillMonitor: AutofillMonitor; @@ -48,7 +53,8 @@ describe('AutofillMonitor', () => { autofillMonitor.monitor(inputEl); expect(inputEl.classList).toContain('mat-input-autofill-monitored'); - expect(inputEl.addEventListener).toHaveBeenCalledWith('animationstart', jasmine.any(Function)); + expect(inputEl.addEventListener) + .toHaveBeenCalledWith('animationstart', jasmine.any(Function), listenerOptions); }); it('should not add multiple listeners to the same element', () => { @@ -69,7 +75,7 @@ describe('AutofillMonitor', () => { autofillMonitor.stopMonitoring(inputEl); expect(inputEl.classList).not.toContain('mat-input-autofill-monitored'); expect(inputEl.removeEventListener) - .toHaveBeenCalledWith('animationstart', jasmine.any(Function)); + .toHaveBeenCalledWith('animationstart', jasmine.any(Function), listenerOptions); }); it('should stop monitoring all monitored elements upon destroy', () => { diff --git a/src/lib/input/autofill.ts b/src/lib/input/autofill.ts index 43c532cd921e..9832ec53d106 100644 --- a/src/lib/input/autofill.ts +++ b/src/lib/input/autofill.ts @@ -6,8 +6,18 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, ElementRef, EventEmitter, Injectable, OnDestroy, Output} from '@angular/core'; +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'; @@ -25,6 +35,10 @@ type MonitoredElementInfo = { }; +/** 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: @@ -34,7 +48,13 @@ type MonitoredElementInfo = { export class AutofillMonitor implements OnDestroy { private _monitoredElements = new Map(); + constructor(private _platform: Platform, private _ngZone: NgZone) {} + monitor(element: Element): Observable { + if (!this._platform.isBrowser) { + return observableEmpty(); + } + const info = this._monitoredElements.get(element); if (info) { return info.subject.asObservable(); @@ -51,13 +71,15 @@ export class AutofillMonitor implements OnDestroy { } }; - element.addEventListener('animationstart', listener); + 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); + element.removeEventListener('animationstart', listener, listenerOptions); } }); @@ -90,9 +112,10 @@ export class AutofillMonitor implements OnDestroy { export class MatAutofill implements OnDestroy { @Output() matAutofill = new EventEmitter(); - constructor(private _elementRef: ElementRef, private _autofillMonitor: AutofillMonitor) { + constructor(private _elementRef: ElementRef, private _autofillMonitor: AutofillMonitor, + ngZone: NgZone) { this._autofillMonitor.monitor(this._elementRef.nativeElement) - .subscribe(event => this.matAutofill.emit(event)); + .subscribe(event => ngZone.run(() => this.matAutofill.emit(event))); } ngOnDestroy() { From c038a7861f3cec7f3829efff57496a3bcaba5676 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Thu, 1 Feb 2018 16:08:16 -0800 Subject: [PATCH 6/6] add doc comments --- src/lib/input/autofill.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/lib/input/autofill.ts b/src/lib/input/autofill.ts index 9832ec53d106..84dd4cc5f098 100644 --- a/src/lib/input/autofill.ts +++ b/src/lib/input/autofill.ts @@ -23,7 +23,9 @@ 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; }; @@ -50,6 +52,11 @@ export class AutofillMonitor implements OnDestroy { 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(); @@ -86,6 +93,10 @@ export class AutofillMonitor implements OnDestroy { 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) {