Skip to content

Commit

Permalink
fix(autocomplete): placeholder not animating on focus (#3992)
Browse files Browse the repository at this point in the history
Fixes #5755.
  • Loading branch information
crisbeto authored and kara committed Aug 23, 2017
1 parent e41d0f3 commit ff54969
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 29 deletions.
63 changes: 37 additions & 26 deletions src/lib/autocomplete/autocomplete-trigger.ts
Expand Up @@ -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]
Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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 {
Expand Down
10 changes: 10 additions & 0 deletions src/lib/autocomplete/autocomplete.spec.ts
Expand Up @@ -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', () => {
Expand Down
1 change: 1 addition & 0 deletions src/lib/form-field/form-field.html
Expand Up @@ -16,6 +16,7 @@
[class.mat-form-field-float]="_canPlaceholderFloat"
[class.mat-accent]="color == 'accent'"
[class.mat-warn]="color == 'warn'"
#placeholder
*ngIf="_hasPlaceholder()">
<ng-content select="md-placeholder, mat-placeholder"></ng-content>
{{_control.placeholder}}
Expand Down
25 changes: 23 additions & 2 deletions src/lib/form-field/form-field.ts
Expand Up @@ -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;
Expand Down Expand Up @@ -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'; }
Expand Down Expand Up @@ -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<any>;
@ContentChild(MdPlaceholder) _placeholderChild: MdPlaceholder;
@ContentChildren(MdError) _errorChildren: QueryList<MdError>;
Expand Down Expand Up @@ -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).
Expand Down
29 changes: 28 additions & 1 deletion src/lib/input/input.spec.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -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', () => {
Expand Down

0 comments on commit ff54969

Please sign in to comment.