Skip to content

Commit

Permalink
feat(cdk-input): move input autofill and autosize utils into cdk (#9831)
Browse files Browse the repository at this point in the history
* move autofill to cdk

* move textarea autosize into cdk

* fix bazel build

* rename input to text-field

* set resize back to none for autosize in input css

* fix tsconfig
  • Loading branch information
mmalerba committed Mar 13, 2018
1 parent 54252d5 commit ced9c90
Show file tree
Hide file tree
Showing 37 changed files with 563 additions and 403 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -72,6 +72,7 @@
/src/cdk/stepper/** @mmalerba
/src/cdk/table/** @andrewseguin
/src/cdk/testing/** @devversion
/src/cdk/text-field/** @mmalerba
/src/cdk/tree/** @tinayuangao

# Moment adapter package
Expand Down
34 changes: 34 additions & 0 deletions src/cdk/text-field/BUILD.bazel
@@ -0,0 +1,34 @@
package(default_visibility=["//visibility:public"])
load("@angular//:index.bzl", "ng_module")
load("@io_bazel_rules_sass//sass:sass.bzl", "sass_library", "sass_binary")

ng_module(
name = "text-field",
srcs = glob(["**/*.ts"], exclude=["**/*.spec.ts"]),
module_name = "@angular/cdk/text-field",
deps = [
"@rxjs",
"//src/cdk/platform",
],
tsconfig = ":tsconfig-build.json",
)

sass_library(
name = "text_field_scss_lib",
srcs = glob(["**/_*.scss"]),
)

sass_binary(
name = "text_field_prebuilt_scss",
src = "text-field-prebuilt.scss",
deps = [":text_field_scss_lib"],
)

# TODO(jelbourn): remove this when sass_binary supports specifying an output filename and dir.
# Copy the output of the sass_binary such that the filename and path match what we expect.
genrule(
name = "text_field_prebuilt_css",
srcs = [":text_field_prebuilt_scss"],
outs = ["text-field-prebuilt.css"],
cmd = "cat $(locations :text_field_prebuilt_scss) > $@",
)
50 changes: 50 additions & 0 deletions src/cdk/text-field/_text-field.scss
@@ -0,0 +1,50 @@
// Core styles that enable monitoring autofill state of text fields.
@mixin cdk-text-field {
// Keyframes that apply no styles, but allow us to monitor when an text field 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 cdk-text-field-autofill-start {}
@keyframes cdk-text-field-autofill-end {}

.cdk-text-field-autofill-monitored:-webkit-autofill {
animation-name: cdk-text-field-autofill-start;
}

.cdk-text-field-autofill-monitored:not(:-webkit-autofill) {
animation-name: cdk-text-field-autofill-end;
}

// Remove the resize handle on autosizing textareas, because whatever height
// the user resized to will be overwritten once they start typing again.
textarea.cdk-textarea-autosize {
resize: none;
}
}

// Used to generate UIDs for keyframes used to change the text field autofill styles.
$cdk-text-field-autofill-color-frame-count: 0;

// Mixin used to apply custom background and foreground colors to an autofilled text field.
// Based on: https://stackoverflow.com/questions/2781549/
// removing-input-background-colour-for-chrome-autocomplete#answer-37432260
@mixin cdk-text-field-autofill-color($background, $foreground:'') {
@keyframes cdk-text-field-autofill-color-#{$cdk-text-field-autofill-color-frame-count} {
to {
background: $background;
@if $foreground != '' { color: $foreground; }
}
}

&:-webkit-autofill {
animation-name: cdk-text-field-autofill-color-#{$cdk-text-field-autofill-color-frame-count};
animation-fill-mode: both;
}

&.cdk-text-field-autofill-monitored:-webkit-autofill {
animation-name: cdk-text-field-autofill-start,
cdk-text-field-autofill-color-#{$cdk-text-field-autofill-color-frame-count};
}

$cdk-text-field-autofill-color-frame-count:
$cdk-text-field-autofill-color-frame-count + 1 !global;
}
Expand Up @@ -11,7 +11,7 @@ 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 {TextFieldModule} from './text-field-module';


const listenerOptions: any = supportsPassiveEventListeners() ? {passive: true} : false;
Expand All @@ -24,7 +24,7 @@ describe('AutofillMonitor', () => {

beforeEach(() => {
TestBed.configureTestingModule({
imports: [MatInputModule],
imports: [TextFieldModule],
declarations: [Inputs],
}).compileComponents();
});
Expand Down Expand Up @@ -52,7 +52,7 @@ describe('AutofillMonitor', () => {
expect(inputEl.addEventListener).not.toHaveBeenCalled();

autofillMonitor.monitor(inputEl);
expect(inputEl.classList).toContain('mat-input-autofill-monitored');
expect(inputEl.classList).toContain('cdk-text-field-autofill-monitored');
expect(inputEl.addEventListener)
.toHaveBeenCalledWith('animationstart', jasmine.any(Function), listenerOptions);
});
Expand All @@ -69,11 +69,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('mat-input-autofill-monitored');
expect(inputEl.classList).toContain('cdk-text-field-autofill-monitored');
expect(inputEl.removeEventListener).not.toHaveBeenCalled();

autofillMonitor.stopMonitoring(inputEl);
expect(inputEl.classList).not.toContain('mat-input-autofill-monitored');
expect(inputEl.classList).not.toContain('cdk-text-field-autofill-monitored');
expect(inputEl.removeEventListener)
.toHaveBeenCalledWith('animationstart', jasmine.any(Function), listenerOptions);
});
Expand Down Expand Up @@ -103,10 +103,10 @@ describe('AutofillMonitor', () => {
const autofillStream = autofillMonitor.monitor(inputEl);
autofillStream.subscribe(event => autofillStreamEvent = event);
expect(autofillStreamEvent).toBeNull();
expect(inputEl.classList).not.toContain('mat-input-autofilled');
expect(inputEl.classList).not.toContain('cdk-text-field-autofilled');

animationStartCallback({animationName: 'mat-input-autofill-start', target: inputEl});
expect(inputEl.classList).toContain('mat-input-autofilled');
animationStartCallback({animationName: 'cdk-text-field-autofill-start', target: inputEl});
expect(inputEl.classList).toContain('cdk-text-field-autofilled');
expect(autofillStreamEvent).toEqual({target: inputEl, isAutofilled: true} as any);
});

Expand All @@ -117,12 +117,12 @@ describe('AutofillMonitor', () => {
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');
animationStartCallback({animationName: 'cdk-text-field-autofill-start', target: inputEl});
expect(inputEl.classList).toContain('cdk-text-field-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');
animationStartCallback({animationName: 'cdk-text-field-autofill-end', target: inputEl});
expect(inputEl.classList).not.toContain('cdk-text-field-autofilled');
expect(autofillStreamEvent).toEqual({target: inputEl, isAutofilled: false} as any);
});

Expand All @@ -131,11 +131,11 @@ describe('AutofillMonitor', () => {
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');
animationStartCallback({animationName: 'cdk-text-field-autofill-start', target: inputEl});
expect(inputEl.classList).toContain('cdk-text-field-autofilled');

autofillMonitor.stopMonitoring(inputEl);
expect(inputEl.classlist).not.toContain('mat-input-autofilled');
expect(inputEl.classlist).not.toContain('cdk-text-field-autofilled');
});

it('should complete the stream when monitoring is stopped', () => {
Expand All @@ -152,23 +152,23 @@ describe('AutofillMonitor', () => {

});

describe('matAutofill', () => {
describe('cdkAutofill', () => {
let autofillMonitor: AutofillMonitor;
let fixture: ComponentFixture<InputWithMatAutofilled>;
let testComponent: InputWithMatAutofilled;
let fixture: ComponentFixture<InputWithCdkAutofilled>;
let testComponent: InputWithCdkAutofilled;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [MatInputModule],
declarations: [InputWithMatAutofilled],
imports: [TextFieldModule],
declarations: [InputWithCdkAutofilled],
}).compileComponents();
});

beforeEach(inject([AutofillMonitor], (afm: AutofillMonitor) => {
autofillMonitor = afm;
spyOn(autofillMonitor, 'monitor').and.returnValue(observableEmpty());
spyOn(autofillMonitor, 'stopMonitoring');
fixture = TestBed.createComponent(InputWithMatAutofilled);
fixture = TestBed.createComponent(InputWithCdkAutofilled);
testComponent = fixture.componentInstance;
fixture.detectChanges();
}));
Expand Down Expand Up @@ -198,8 +198,8 @@ class Inputs {
}

@Component({
template: `<input #input matAutofill>`
template: `<input #input cdkAutofill>`
})
class InputWithMatAutofilled {
class InputWithCdkAutofilled {
@ViewChild('input') input: ElementRef;
}
22 changes: 11 additions & 11 deletions src/lib/input/autofill.ts → src/cdk/text-field/autofill.ts
Expand Up @@ -69,17 +69,17 @@ export class AutofillMonitor implements OnDestroy {

const result = new Subject<AutofillEvent>();
const listener = (event: AnimationEvent) => {
if (event.animationName === 'mat-input-autofill-start') {
element.classList.add('mat-input-autofilled');
if (event.animationName === 'cdk-text-field-autofill-start') {
element.classList.add('cdk-text-field-autofilled');
result.next({target: event.target as Element, isAutofilled: true});
} else if (event.animationName === 'mat-input-autofill-end') {
element.classList.remove('mat-input-autofilled');
} else if (event.animationName === 'cdk-text-field-autofill-end') {
element.classList.remove('cdk-text-field-autofilled');
result.next({target: event.target as Element, isAutofilled: false});
}
};

element.addEventListener('animationstart', listener, listenerOptions);
element.classList.add('mat-input-autofill-monitored');
element.classList.add('cdk-text-field-autofill-monitored');

this._monitoredElements.set(element, {
subject: result,
Expand All @@ -101,8 +101,8 @@ export class AutofillMonitor implements OnDestroy {
if (info) {
info.unlisten();
info.subject.complete();
element.classList.remove('mat-input-autofill-monitored');
element.classList.remove('mat-input-autofilled');
element.classList.remove('cdk-text-field-autofill-monitored');
element.classList.remove('cdk-text-field-autofilled');
this._monitoredElements.delete(element);
}
}
Expand All @@ -115,17 +115,17 @@ export class AutofillMonitor implements OnDestroy {

/** A directive that can be used to monitor the autofill state of an input. */
@Directive({
selector: '[matAutofill]',
selector: '[cdkAutofill]',
})
export class MatAutofill implements OnDestroy, OnInit {
@Output() matAutofill: EventEmitter<AutofillEvent> = new EventEmitter<AutofillEvent>();
export class CdkAutofill implements OnDestroy, OnInit {
@Output() cdkAutofill: EventEmitter<AutofillEvent> = new EventEmitter<AutofillEvent>();

constructor(private _elementRef: ElementRef, private _autofillMonitor: AutofillMonitor) {}

ngOnInit() {
this._autofillMonitor
.monitor(this._elementRef.nativeElement)
.subscribe(event => this.matAutofill.emit(event));
.subscribe(event => this.cdkAutofill.emit(event));
}

ngOnDestroy() {
Expand Down

0 comments on commit ced9c90

Please sign in to comment.