From ff54969d17ca1baa9777f88330ab16af986ab689 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 23 Aug 2017 02:41:27 +0200 Subject: [PATCH] fix(autocomplete): placeholder not animating on focus (#3992) Fixes #5755. --- src/lib/autocomplete/autocomplete-trigger.ts | 63 ++++++++++++-------- src/lib/autocomplete/autocomplete.spec.ts | 10 ++++ src/lib/form-field/form-field.html | 1 + src/lib/form-field/form-field.ts | 25 +++++++- src/lib/input/input.spec.ts | 29 ++++++++- 5 files changed, 99 insertions(+), 29 deletions(-) diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts index 207701b32912..c0d0281f1a48 100644 --- a/src/lib/autocomplete/autocomplete-trigger.ts +++ b/src/lib/autocomplete/autocomplete-trigger.ts @@ -109,9 +109,9 @@ export function getMdAutocompleteMissingPanelError(): Error { '[attr.aria-owns]': 'autocomplete?.id', // 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)': 'openPanel()', - '(input)': '_handleInput($event)', + '(focusin)': '_handleFocus()', '(blur)': '_onTouched()', + '(input)': '_handleInput($event)', '(keydown)': '_handleKeydown($event)', }, providers: [MD_AUTOCOMPLETE_VALUE_ACCESSOR] @@ -169,26 +169,8 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy { /** Opens the autocomplete suggestion panel. */ openPanel(): void { - if (!this.autocomplete) { - throw getMdAutocompleteMissingPanelError(); - } - - if (!this._overlayRef) { - this._createOverlay(); - } else { - /** Update the panel width, in case the host width has changed */ - this._overlayRef.getState().width = this._getHostWidth(); - this._overlayRef.updateSize(); - } - - if (this._overlayRef && !this._overlayRef.hasAttached()) { - this._overlayRef.attach(this._portal); - this._closingActionsSubscription = this._subscribeToClosingActions(); - } - - this.autocomplete._setVisibility(); + this._attachOverlay(); this._floatPlaceholder(); - this._panelOpen = true; } /** Closes the autocomplete suggestion panel. */ @@ -327,14 +309,25 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy { } } + _handleFocus(): void { + this._attachOverlay(); + this._floatPlaceholder(true); + } + /** * In "auto" mode, the placeholder will animate down as soon as focus is lost. * This causes the value to jump when selecting an option with the mouse. * This method manually floats the placeholder until the panel can be closed. + * @param shouldAnimate Whether the placeholder should be animated when it is floated. */ - private _floatPlaceholder(): void { + private _floatPlaceholder(shouldAnimate = false): void { if (this._formField && this._formField.floatPlaceholder === 'auto') { - this._formField.floatPlaceholder = 'always'; + if (shouldAnimate) { + this._formField._animateAndLockPlaceholder(); + } else { + this._formField.floatPlaceholder = 'always'; + } + this._manuallyFloatingPlaceholder = true; } } @@ -450,9 +443,27 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy { }); } - private _createOverlay(): void { - this._portal = new TemplatePortal(this.autocomplete.template, this._viewContainerRef); - this._overlayRef = this._overlay.create(this._getOverlayConfig()); + private _attachOverlay(): void { + if (!this.autocomplete) { + throw getMdAutocompleteMissingPanelError(); + } + + if (!this._overlayRef) { + this._portal = new TemplatePortal(this.autocomplete.template, this._viewContainerRef); + this._overlayRef = this._overlay.create(this._getOverlayConfig()); + } else { + /** Update the panel width, in case the host width has changed */ + this._overlayRef.getState().width = this._getHostWidth(); + this._overlayRef.updateSize(); + } + + if (this._overlayRef && !this._overlayRef.hasAttached()) { + this._overlayRef.attach(this._portal); + this._closingActionsSubscription = this._subscribeToClosingActions(); + } + + this.autocomplete._setVisibility(); + this._panelOpen = true; } private _getOverlayConfig(): OverlayState { diff --git a/src/lib/autocomplete/autocomplete.spec.ts b/src/lib/autocomplete/autocomplete.spec.ts index f9bf7961fb22..91f7f6a5e0fc 100644 --- a/src/lib/autocomplete/autocomplete.spec.ts +++ b/src/lib/autocomplete/autocomplete.spec.ts @@ -377,6 +377,16 @@ describe('MdAutocomplete', () => { .toContain('mat-autocomplete-visible', 'Expected panel to be visible.'); })); + it('should animate the placeholder when the input is focused', () => { + const inputContainer = fixture.componentInstance.formField; + + spyOn(inputContainer, '_animateAndLockPlaceholder'); + expect(inputContainer._animateAndLockPlaceholder).not.toHaveBeenCalled(); + + dispatchFakeEvent(fixture.debugElement.query(By.css('input')).nativeElement, 'focusin'); + expect(inputContainer._animateAndLockPlaceholder).toHaveBeenCalled(); + }); + }); it('should have the correct text direction in RTL', () => { diff --git a/src/lib/form-field/form-field.html b/src/lib/form-field/form-field.html index cfb7bfedab83..d34e8b7c40f9 100644 --- a/src/lib/form-field/form-field.html +++ b/src/lib/form-field/form-field.html @@ -16,6 +16,7 @@ [class.mat-form-field-float]="_canPlaceholderFloat" [class.mat-accent]="color == 'accent'" [class.mat-warn]="color == 'warn'" + #placeholder *ngIf="_hasPlaceholder()"> {{_control.placeholder}} diff --git a/src/lib/form-field/form-field.ts b/src/lib/form-field/form-field.ts index eba04bde9791..bf545b58453d 100644 --- a/src/lib/form-field/form-field.ts +++ b/src/lib/form-field/form-field.ts @@ -35,13 +35,14 @@ import { MD_PLACEHOLDER_GLOBAL_OPTIONS, PlaceholderOptions } from '../core/placeholder/placeholder-options'; -import {startWith} from '@angular/cdk/rxjs'; +import {startWith, first} from '@angular/cdk/rxjs'; import {MdError} from './error'; import {MdFormFieldControl} from './form-field-control'; import {MdHint} from './hint'; import {MdPlaceholder} from './placeholder'; import {MdPrefix} from './prefix'; import {MdSuffix} from './suffix'; +import {fromEvent} from 'rxjs/observable/fromEvent'; let nextUniqueId = 0; @@ -104,8 +105,13 @@ export class MdFormField implements AfterViewInit, AfterContentInit, AfterConten } private _hideRequiredMarker: boolean; + /** Override for the logic that disables the placeholder animation in certain cases. */ + private _showAlwaysAnimate = false; + /** Whether the floating label should always float or not. */ - get _shouldAlwaysFloat() { return this._floatPlaceholder === 'always'; } + get _shouldAlwaysFloat() { + return this._floatPlaceholder === 'always' && !this._showAlwaysAnimate; + } /** Whether the placeholder can float or not. */ get _canPlaceholderFloat() { return this._floatPlaceholder !== 'never'; } @@ -139,6 +145,7 @@ export class MdFormField implements AfterViewInit, AfterContentInit, AfterConten /** Reference to the form field's underline element. */ @ViewChild('underline') underlineRef: ElementRef; @ViewChild('connectionContainer') _connectionContainerRef: ElementRef; + @ViewChild('placeholder') private _placeholder: ElementRef; @ContentChild(MdFormFieldControl) _control: MdFormFieldControl; @ContentChild(MdPlaceholder) _placeholderChild: MdPlaceholder; @ContentChildren(MdError) _errorChildren: QueryList; @@ -210,6 +217,20 @@ export class MdFormField implements AfterViewInit, AfterContentInit, AfterConten this._control.errorState) ? 'error' : 'hint'; } + /** Animates the placeholder up and locks it in position. */ + _animateAndLockPlaceholder(): void { + if (this._placeholder && this._canPlaceholderFloat) { + this._showAlwaysAnimate = true; + this._floatPlaceholder = 'always'; + + first.call(fromEvent(this._placeholder.nativeElement, 'transitionend')).subscribe(() => { + this._showAlwaysAnimate = false; + }); + + this._changeDetectorRef.markForCheck(); + } + } + /** * Ensure that there is only one placeholder (either `placeholder` attribute on the child control * or child element with the `md-placeholder` directive). diff --git a/src/lib/input/input.spec.ts b/src/lib/input/input.spec.ts index 3c8b344ca4d4..ade499ec53d5 100644 --- a/src/lib/input/input.spec.ts +++ b/src/lib/input/input.spec.ts @@ -15,7 +15,7 @@ import {MdInputModule} from './index'; import {MdInput} from './input'; import {Platform} from '../core/platform/platform'; import {PlatformModule} from '../core/platform/index'; -import {wrappedErrorMessage, dispatchFakeEvent} from '@angular/cdk/testing'; +import {wrappedErrorMessage, dispatchFakeEvent, createFakeEvent} from '@angular/cdk/testing'; import { MdFormField, MdFormFieldModule, @@ -656,6 +656,33 @@ describe('MdInput without forms', function () { expect(container.classList).toContain('mat-focused'); }); + + it('should be able to animate the placeholder up and lock it in position', () => { + let fixture = TestBed.createComponent(MdInputTextTestController); + fixture.detectChanges(); + + let inputContainer = fixture.debugElement.query(By.directive(MdFormField)) + .componentInstance as MdFormField; + let placeholder = fixture.debugElement.query(By.css('.mat-input-placeholder')).nativeElement; + + expect(inputContainer.floatPlaceholder).toBe('auto'); + + inputContainer._animateAndLockPlaceholder(); + fixture.detectChanges(); + + expect(inputContainer._shouldAlwaysFloat).toBe(false); + expect(inputContainer.floatPlaceholder).toBe('always'); + + const fakeEvent = Object.assign(createFakeEvent('transitionend'), { + propertyName: 'transform' + }); + + placeholder.dispatchEvent(fakeEvent); + fixture.detectChanges(); + + expect(inputContainer._shouldAlwaysFloat).toBe(true); + expect(inputContainer.floatPlaceholder).toBe('always'); + }); }); describe('MdInput with forms', () => {