Skip to content

Commit

Permalink
fix(autocomplete): placeholder not animating on focus
Browse files Browse the repository at this point in the history
Fixes the placeholder not being animated on focus.

**Note:** The `_handleFocus` and `openPanel` methods do pretty much the same (aside from the extra boolean), but I wanted to avoid having to pass the `$event` to `openPanel` since the event isn't really relevant to the API for opening the autocomplete programmatically.
  • Loading branch information
crisbeto committed Apr 18, 2017
1 parent 11b97aa commit 140f403
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 22 deletions.
51 changes: 31 additions & 20 deletions src/lib/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const MD_AUTOCOMPLETE_VALUE_ACCESSOR: any = {
'[attr.aria-activedescendant]': 'activeOption?.id',
'[attr.aria-expanded]': 'panelOpen.toString()',
'[attr.aria-owns]': 'autocomplete?.id',
'(focus)': 'openPanel()',
'(focus)': '_handleFocus()',
'(blur)': '_handleBlur($event.relatedTarget?.tagName)',
'(input)': '_handleInput($event)',
'(keydown)': '_handleKeydown($event)',
Expand Down Expand Up @@ -119,21 +119,8 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {

/** Opens the autocomplete suggestion panel. */
openPanel(): void {
if (!this._overlayRef) {
this._createOverlay();
} else {
/** Update the panel width, in case the host width has changed */
this._overlayRef.getState().width = this._getHostWidth();
}

if (!this._overlayRef.hasAttached()) {
this._overlayRef.attach(this._portal);
this._subscribeToClosingActions();
}

this.autocomplete._setVisibility();
this._attachOverlay();
this._floatPlaceholder();
this._panelOpen = true;
}

/** Closes the autocomplete suggestion panel. */
Expand Down Expand Up @@ -234,14 +221,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._inputContainer && this._inputContainer.floatPlaceholder === 'auto') {
this._inputContainer.floatPlaceholder = 'always';
if (shouldAnimate) {
this._inputContainer._animateAndLockPlaceholder();
} else {
this._inputContainer.floatPlaceholder = 'always';
}

this._manuallyFloatingPlaceholder = true;
}
}
Expand Down Expand Up @@ -327,9 +325,22 @@ 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._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();
}

if (!this._overlayRef.hasAttached()) {
this._overlayRef.attach(this._portal);
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 @@ -293,6 +293,16 @@ describe('MdAutocomplete', () => {
});
}));

it('should animate the placeholder when the input is focused', () => {
const inputContainer = fixture.componentInstance.inputContainer;

spyOn(inputContainer, '_animateAndLockPlaceholder');
expect(inputContainer._animateAndLockPlaceholder).not.toHaveBeenCalled();

dispatchFakeEvent(fixture.debugElement.query(By.css('input')).nativeElement, 'focus');
expect(inputContainer._animateAndLockPlaceholder).toHaveBeenCalled();
});

});

it('should have the correct text direction in RTL', () => {
Expand Down
1 change: 1 addition & 0 deletions src/lib/input/input-container.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
[class.mat-float]="_canPlaceholderFloat"
[class.mat-accent]="color == 'accent'"
[class.mat-warn]="color == 'warn'"
#placeholder
*ngIf="_hasPlaceholder()">
<ng-content select="md-placeholder, mat-placeholder"></ng-content>
{{_mdInputChild.placeholder}}
Expand Down
28 changes: 28 additions & 0 deletions src/lib/input/input-container.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {Platform} from '../core/platform/platform';
import {PlatformModule} from '../core/platform/index';
import {wrappedErrorMessage} from '../core/testing/wrapped-error-message';
import {dispatchFakeEvent} from '../core/testing/dispatch-events';
import {createFakeEvent} from '../core/testing/event-objects';
import {
MdInputContainerDuplicatedHintError,
MdInputContainerMissingMdInputError,
Expand Down Expand Up @@ -565,6 +566,33 @@ describe('MdInputContainer', function () {
expect(labelEl.classList).not.toContain('mat-float');
});

it('should be able to animate the placeholder up and lock it in position', () => {
let fixture = TestBed.createComponent(MdInputContainerTextTestController);
fixture.detectChanges();

let inputContainer = fixture.debugElement.query(By.directive(MdInputContainer))
.componentInstance as MdInputContainer;
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('error messages', () => {
let fixture: ComponentFixture<MdInputContainerWithFormErrorMessages>;
let testComponent: MdInputContainerWithFormErrorMessages;
Expand Down
30 changes: 28 additions & 2 deletions src/lib/input/input-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
QueryList,
Renderer,
Self,
ViewEncapsulation
ViewEncapsulation,
ViewChild,
} from '@angular/core';
import {animate, state, style, transition, trigger} from '@angular/animations';
import {coerceBooleanProperty} from '../core';
Expand All @@ -26,6 +27,9 @@ import {
MdInputContainerPlaceholderConflictError,
MdInputContainerUnsupportedTypeError
} from './input-container-errors';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/add/operator/first';


// Invalid input type. Using one of these will throw an MdInputContainerUnsupportedTypeError.
Expand Down Expand Up @@ -297,8 +301,13 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit {
get dividerColor() { return this.color; }
set dividerColor(value) { this.color = value; }

/** 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 @@ -338,6 +347,8 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit {

@ContentChildren(MdSuffix) _suffixChildren: QueryList<MdSuffix>;

@ViewChild('placeholder') private _placeholder: ElementRef;

constructor(
private _changeDetectorRef: ChangeDetectorRef,
@Optional() private _parentForm: NgForm,
Expand Down Expand Up @@ -390,6 +401,21 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit {
return (this._errorChildren.length > 0 && this._isErrorState()) ? 'error' : 'hint';
}

/** Animates the placeholder up and locks it in position. */
_animateAndLockPlaceholder(): void {
if (this._placeholder && !this._mdInputChild.focused && this._canPlaceholderFloat) {
this._showAlwaysAnimate = true;
this._floatPlaceholder = 'always';

Observable
.fromEvent(this._placeholder.nativeElement, 'transitionend')
.first((event: TransitionEvent) => event.propertyName === 'transform')
.subscribe(() => this._showAlwaysAnimate = false);

this._changeDetectorRef.markForCheck();
}
}

/**
* Ensure that there is only one placeholder (either `input` attribute or child element with the
* `md-placeholder` attribute.
Expand Down

0 comments on commit 140f403

Please sign in to comment.