Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(autocomplete): placeholder not animating on focus #3992

Merged
merged 1 commit into from
Aug 23, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
63 changes: 37 additions & 26 deletions src/lib/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
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 @@ -448,9 +441,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
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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