Skip to content

Commit ff54969

Browse files
crisbetokara
authored andcommitted
fix(autocomplete): placeholder not animating on focus (#3992)
Fixes #5755.
1 parent e41d0f3 commit ff54969

File tree

5 files changed

+99
-29
lines changed

5 files changed

+99
-29
lines changed

src/lib/autocomplete/autocomplete-trigger.ts

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,9 @@ export function getMdAutocompleteMissingPanelError(): Error {
109109
'[attr.aria-owns]': 'autocomplete?.id',
110110
// Note: we use `focusin`, as opposed to `focus`, in order to open the panel
111111
// a little earlier. This avoids issues where IE delays the focusing of the input.
112-
'(focusin)': 'openPanel()',
113-
'(input)': '_handleInput($event)',
112+
'(focusin)': '_handleFocus()',
114113
'(blur)': '_onTouched()',
114+
'(input)': '_handleInput($event)',
115115
'(keydown)': '_handleKeydown($event)',
116116
},
117117
providers: [MD_AUTOCOMPLETE_VALUE_ACCESSOR]
@@ -169,26 +169,8 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
169169

170170
/** Opens the autocomplete suggestion panel. */
171171
openPanel(): void {
172-
if (!this.autocomplete) {
173-
throw getMdAutocompleteMissingPanelError();
174-
}
175-
176-
if (!this._overlayRef) {
177-
this._createOverlay();
178-
} else {
179-
/** Update the panel width, in case the host width has changed */
180-
this._overlayRef.getState().width = this._getHostWidth();
181-
this._overlayRef.updateSize();
182-
}
183-
184-
if (this._overlayRef && !this._overlayRef.hasAttached()) {
185-
this._overlayRef.attach(this._portal);
186-
this._closingActionsSubscription = this._subscribeToClosingActions();
187-
}
188-
189-
this.autocomplete._setVisibility();
172+
this._attachOverlay();
190173
this._floatPlaceholder();
191-
this._panelOpen = true;
192174
}
193175

194176
/** Closes the autocomplete suggestion panel. */
@@ -327,14 +309,25 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
327309
}
328310
}
329311

312+
_handleFocus(): void {
313+
this._attachOverlay();
314+
this._floatPlaceholder(true);
315+
}
316+
330317
/**
331318
* In "auto" mode, the placeholder will animate down as soon as focus is lost.
332319
* This causes the value to jump when selecting an option with the mouse.
333320
* This method manually floats the placeholder until the panel can be closed.
321+
* @param shouldAnimate Whether the placeholder should be animated when it is floated.
334322
*/
335-
private _floatPlaceholder(): void {
323+
private _floatPlaceholder(shouldAnimate = false): void {
336324
if (this._formField && this._formField.floatPlaceholder === 'auto') {
337-
this._formField.floatPlaceholder = 'always';
325+
if (shouldAnimate) {
326+
this._formField._animateAndLockPlaceholder();
327+
} else {
328+
this._formField.floatPlaceholder = 'always';
329+
}
330+
338331
this._manuallyFloatingPlaceholder = true;
339332
}
340333
}
@@ -450,9 +443,27 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
450443
});
451444
}
452445

453-
private _createOverlay(): void {
454-
this._portal = new TemplatePortal(this.autocomplete.template, this._viewContainerRef);
455-
this._overlayRef = this._overlay.create(this._getOverlayConfig());
446+
private _attachOverlay(): void {
447+
if (!this.autocomplete) {
448+
throw getMdAutocompleteMissingPanelError();
449+
}
450+
451+
if (!this._overlayRef) {
452+
this._portal = new TemplatePortal(this.autocomplete.template, this._viewContainerRef);
453+
this._overlayRef = this._overlay.create(this._getOverlayConfig());
454+
} else {
455+
/** Update the panel width, in case the host width has changed */
456+
this._overlayRef.getState().width = this._getHostWidth();
457+
this._overlayRef.updateSize();
458+
}
459+
460+
if (this._overlayRef && !this._overlayRef.hasAttached()) {
461+
this._overlayRef.attach(this._portal);
462+
this._closingActionsSubscription = this._subscribeToClosingActions();
463+
}
464+
465+
this.autocomplete._setVisibility();
466+
this._panelOpen = true;
456467
}
457468

458469
private _getOverlayConfig(): OverlayState {

src/lib/autocomplete/autocomplete.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,16 @@ describe('MdAutocomplete', () => {
377377
.toContain('mat-autocomplete-visible', 'Expected panel to be visible.');
378378
}));
379379

380+
it('should animate the placeholder when the input is focused', () => {
381+
const inputContainer = fixture.componentInstance.formField;
382+
383+
spyOn(inputContainer, '_animateAndLockPlaceholder');
384+
expect(inputContainer._animateAndLockPlaceholder).not.toHaveBeenCalled();
385+
386+
dispatchFakeEvent(fixture.debugElement.query(By.css('input')).nativeElement, 'focusin');
387+
expect(inputContainer._animateAndLockPlaceholder).toHaveBeenCalled();
388+
});
389+
380390
});
381391

382392
it('should have the correct text direction in RTL', () => {

src/lib/form-field/form-field.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
[class.mat-form-field-float]="_canPlaceholderFloat"
1717
[class.mat-accent]="color == 'accent'"
1818
[class.mat-warn]="color == 'warn'"
19+
#placeholder
1920
*ngIf="_hasPlaceholder()">
2021
<ng-content select="md-placeholder, mat-placeholder"></ng-content>
2122
{{_control.placeholder}}

src/lib/form-field/form-field.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,14 @@ import {
3535
MD_PLACEHOLDER_GLOBAL_OPTIONS,
3636
PlaceholderOptions
3737
} from '../core/placeholder/placeholder-options';
38-
import {startWith} from '@angular/cdk/rxjs';
38+
import {startWith, first} from '@angular/cdk/rxjs';
3939
import {MdError} from './error';
4040
import {MdFormFieldControl} from './form-field-control';
4141
import {MdHint} from './hint';
4242
import {MdPlaceholder} from './placeholder';
4343
import {MdPrefix} from './prefix';
4444
import {MdSuffix} from './suffix';
45+
import {fromEvent} from 'rxjs/observable/fromEvent';
4546

4647

4748
let nextUniqueId = 0;
@@ -104,8 +105,13 @@ export class MdFormField implements AfterViewInit, AfterContentInit, AfterConten
104105
}
105106
private _hideRequiredMarker: boolean;
106107

108+
/** Override for the logic that disables the placeholder animation in certain cases. */
109+
private _showAlwaysAnimate = false;
110+
107111
/** Whether the floating label should always float or not. */
108-
get _shouldAlwaysFloat() { return this._floatPlaceholder === 'always'; }
112+
get _shouldAlwaysFloat() {
113+
return this._floatPlaceholder === 'always' && !this._showAlwaysAnimate;
114+
}
109115

110116
/** Whether the placeholder can float or not. */
111117
get _canPlaceholderFloat() { return this._floatPlaceholder !== 'never'; }
@@ -139,6 +145,7 @@ export class MdFormField implements AfterViewInit, AfterContentInit, AfterConten
139145
/** Reference to the form field's underline element. */
140146
@ViewChild('underline') underlineRef: ElementRef;
141147
@ViewChild('connectionContainer') _connectionContainerRef: ElementRef;
148+
@ViewChild('placeholder') private _placeholder: ElementRef;
142149
@ContentChild(MdFormFieldControl) _control: MdFormFieldControl<any>;
143150
@ContentChild(MdPlaceholder) _placeholderChild: MdPlaceholder;
144151
@ContentChildren(MdError) _errorChildren: QueryList<MdError>;
@@ -210,6 +217,20 @@ export class MdFormField implements AfterViewInit, AfterContentInit, AfterConten
210217
this._control.errorState) ? 'error' : 'hint';
211218
}
212219

220+
/** Animates the placeholder up and locks it in position. */
221+
_animateAndLockPlaceholder(): void {
222+
if (this._placeholder && this._canPlaceholderFloat) {
223+
this._showAlwaysAnimate = true;
224+
this._floatPlaceholder = 'always';
225+
226+
first.call(fromEvent(this._placeholder.nativeElement, 'transitionend')).subscribe(() => {
227+
this._showAlwaysAnimate = false;
228+
});
229+
230+
this._changeDetectorRef.markForCheck();
231+
}
232+
}
233+
213234
/**
214235
* Ensure that there is only one placeholder (either `placeholder` attribute on the child control
215236
* or child element with the `md-placeholder` directive).

src/lib/input/input.spec.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {MdInputModule} from './index';
1515
import {MdInput} from './input';
1616
import {Platform} from '../core/platform/platform';
1717
import {PlatformModule} from '../core/platform/index';
18-
import {wrappedErrorMessage, dispatchFakeEvent} from '@angular/cdk/testing';
18+
import {wrappedErrorMessage, dispatchFakeEvent, createFakeEvent} from '@angular/cdk/testing';
1919
import {
2020
MdFormField,
2121
MdFormFieldModule,
@@ -656,6 +656,33 @@ describe('MdInput without forms', function () {
656656

657657
expect(container.classList).toContain('mat-focused');
658658
});
659+
660+
it('should be able to animate the placeholder up and lock it in position', () => {
661+
let fixture = TestBed.createComponent(MdInputTextTestController);
662+
fixture.detectChanges();
663+
664+
let inputContainer = fixture.debugElement.query(By.directive(MdFormField))
665+
.componentInstance as MdFormField;
666+
let placeholder = fixture.debugElement.query(By.css('.mat-input-placeholder')).nativeElement;
667+
668+
expect(inputContainer.floatPlaceholder).toBe('auto');
669+
670+
inputContainer._animateAndLockPlaceholder();
671+
fixture.detectChanges();
672+
673+
expect(inputContainer._shouldAlwaysFloat).toBe(false);
674+
expect(inputContainer.floatPlaceholder).toBe('always');
675+
676+
const fakeEvent = Object.assign(createFakeEvent('transitionend'), {
677+
propertyName: 'transform'
678+
});
679+
680+
placeholder.dispatchEvent(fakeEvent);
681+
fixture.detectChanges();
682+
683+
expect(inputContainer._shouldAlwaysFloat).toBe(true);
684+
expect(inputContainer.floatPlaceholder).toBe('always');
685+
});
659686
});
660687

661688
describe('MdInput with forms', () => {

0 commit comments

Comments
 (0)