Skip to content

Commit 1939a7e

Browse files
committed
fix(autocomplete): placeholder should float while panel is open
1 parent 1357209 commit 1939a7e

File tree

2 files changed

+90
-15
lines changed

2 files changed

+90
-15
lines changed

src/lib/autocomplete/autocomplete-trigger.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {
2-
AfterContentInit, Directive, ElementRef, Input, ViewContainerRef, Optional, OnDestroy
2+
AfterContentInit, Directive, ElementRef, Host, Input, ViewContainerRef, Optional, OnDestroy
33
} from '@angular/core';
44
import {NgControl} from '@angular/forms';
55
import {Overlay, OverlayRef, OverlayState, TemplatePortal} from '../core';
@@ -16,6 +16,7 @@ import {Dir} from '../core/rtl/dir';
1616
import 'rxjs/add/operator/startWith';
1717
import 'rxjs/add/operator/switchMap';
1818
import {keyAffectsActiveItem} from '../core/a11y/list-key-manager';
19+
import {MdInputContainer, FloatPlaceholderType} from '../input/input-container';
1920

2021
/**
2122
* The following style constants are necessary to save here in order
@@ -59,7 +60,8 @@ export class MdAutocompleteTrigger implements AfterContentInit, OnDestroy {
5960

6061
constructor(private _element: ElementRef, private _overlay: Overlay,
6162
private _viewContainerRef: ViewContainerRef,
62-
@Optional() private _controlDir: NgControl, @Optional() private _dir: Dir) {}
63+
@Optional() private _controlDir: NgControl, @Optional() private _dir: Dir,
64+
@Optional() @Host() private _inputContainer: MdInputContainer) {}
6365

6466
ngAfterContentInit() {
6567
this._keyManager = new ActiveDescendantKeyManager(this.autocomplete.options);
@@ -90,6 +92,7 @@ export class MdAutocompleteTrigger implements AfterContentInit, OnDestroy {
9092
}
9193

9294
this._panelOpen = true;
95+
this._floatPlaceholder('always');
9396
}
9497

9598
/** Closes the autocomplete suggestion panel. */
@@ -99,6 +102,7 @@ export class MdAutocompleteTrigger implements AfterContentInit, OnDestroy {
99102
}
100103

101104
this._panelOpen = false;
105+
this._floatPlaceholder('auto');
102106
}
103107

104108
/**
@@ -135,6 +139,17 @@ export class MdAutocompleteTrigger implements AfterContentInit, OnDestroy {
135139
}
136140
}
137141

142+
/**
143+
* In "auto" mode, the placeholder will animate down as soon as focus is lost.
144+
* This causes the value to jump when selecting an option with the mouse.
145+
* This method manually floats the placeholder until the panel can be closed.
146+
*/
147+
private _floatPlaceholder(state: FloatPlaceholderType): void {
148+
if (this._inputContainer) {
149+
this._inputContainer.floatPlaceholder = state;
150+
}
151+
}
152+
138153
/**
139154
* Given that we are not actually focusing active options, we must manually adjust scroll
140155
* to reveal options below the fold. First, we find the offset of the option from the top

src/lib/autocomplete/autocomplete.spec.ts

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,11 @@ import {Subscription} from 'rxjs/Subscription';
1010
import {ENTER, DOWN_ARROW, SPACE} from '../core/keyboard/keycodes';
1111
import {MdOption} from '../core/option/option';
1212
import {ViewportRuler} from '../core/overlay/position/viewport-ruler';
13+
import {MdInputContainer} from '../input/input-container';
1314

1415
describe('MdAutocomplete', () => {
1516
let overlayContainerElement: HTMLElement;
1617
let dir: LayoutDirection;
17-
let fixture: ComponentFixture<SimpleAutocomplete>;
18-
let input: HTMLInputElement;
1918

2019
beforeEach(async(() => {
2120
dir = 'ltr';
@@ -45,14 +44,17 @@ describe('MdAutocomplete', () => {
4544
TestBed.compileComponents();
4645
}));
4746

48-
beforeEach(() => {
49-
fixture = TestBed.createComponent(SimpleAutocomplete);
50-
fixture.detectChanges();
47+
describe('panel toggling', () => {
48+
let fixture: ComponentFixture<SimpleAutocomplete>;
49+
let input: HTMLInputElement;
5150

52-
input = fixture.debugElement.query(By.css('input')).nativeElement;
53-
});
51+
beforeEach(() => {
52+
fixture = TestBed.createComponent(SimpleAutocomplete);
53+
fixture.detectChanges();
54+
55+
input = fixture.debugElement.query(By.css('input')).nativeElement;
56+
});
5457

55-
describe('panel toggling', () => {
5658
it('should open the panel when the input is focused', () => {
5759
expect(fixture.componentInstance.trigger.panelOpen).toBe(false);
5860
dispatchEvent('focus', input);
@@ -171,23 +173,52 @@ describe('MdAutocomplete', () => {
171173
});
172174
}));
173175

176+
it('should keep the label floating until the panel closes', () => {
177+
fixture.componentInstance.trigger.openPanel();
178+
fixture.detectChanges();
179+
180+
dispatchEvent('blur', input);
181+
fixture.detectChanges();
182+
183+
expect(fixture.componentInstance.inputContainer.floatPlaceholder)
184+
.toEqual('always', 'Expected placeholder to keep floating on blur.');
185+
186+
const backdrop =
187+
overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
188+
backdrop.click();
189+
fixture.detectChanges();
190+
191+
expect(fixture.componentInstance.inputContainer.floatPlaceholder)
192+
.toEqual('auto', 'Expected placeholder to return to auto state after panel closes.');
193+
});
194+
174195
});
175196

176197
it('should have the correct text direction in RTL', () => {
177198
dir = 'rtl';
178199

179-
const fixture = TestBed.createComponent(SimpleAutocomplete);
180-
fixture.detectChanges();
200+
const rtlFixture = TestBed.createComponent(SimpleAutocomplete);
201+
rtlFixture.detectChanges();
181202

182-
fixture.componentInstance.trigger.openPanel();
183-
fixture.detectChanges();
203+
rtlFixture.componentInstance.trigger.openPanel();
204+
rtlFixture.detectChanges();
184205

185206
const overlayPane = overlayContainerElement.querySelector('.cdk-overlay-pane');
186207
expect(overlayPane.getAttribute('dir')).toEqual('rtl');
187208

188209
});
189210

190211
describe('forms integration', () => {
212+
let fixture: ComponentFixture<SimpleAutocomplete>;
213+
let input: HTMLInputElement;
214+
215+
216+
beforeEach(() => {
217+
fixture = TestBed.createComponent(SimpleAutocomplete);
218+
fixture.detectChanges();
219+
220+
input = fixture.debugElement.query(By.css('input')).nativeElement;
221+
});
191222

192223
it('should fill the text field when an option is selected', () => {
193224
fixture.componentInstance.trigger.openPanel();
@@ -228,7 +259,19 @@ describe('MdAutocomplete', () => {
228259
.toBe(false, `Expected control to stay pristine if value is set programmatically.`);
229260
});
230261

262+
});
263+
231264
describe('aria', () => {
265+
let fixture: ComponentFixture<SimpleAutocomplete>;
266+
let input: HTMLInputElement;
267+
268+
269+
beforeEach(() => {
270+
fixture = TestBed.createComponent(SimpleAutocomplete);
271+
fixture.detectChanges();
272+
273+
input = fixture.debugElement.query(By.css('input')).nativeElement;
274+
});
232275

233276
it('should set role of input to combobox', () => {
234277
expect(input.getAttribute('role'))
@@ -307,10 +350,16 @@ describe('MdAutocomplete', () => {
307350
});
308351

309352
describe('keyboard events', () => {
353+
let fixture: ComponentFixture<SimpleAutocomplete>;
354+
let input: HTMLInputElement;
310355
let DOWN_ARROW_EVENT: KeyboardEvent;
311356
let ENTER_EVENT: KeyboardEvent;
312357

313358
beforeEach(() => {
359+
fixture = TestBed.createComponent(SimpleAutocomplete);
360+
fixture.detectChanges();
361+
362+
input = fixture.debugElement.query(By.css('input')).nativeElement;
314363
DOWN_ARROW_EVENT = new FakeKeyboardEvent(DOWN_ARROW) as KeyboardEvent;
315364
ENTER_EVENT = new FakeKeyboardEvent(ENTER) as KeyboardEvent;
316365
});
@@ -462,9 +511,10 @@ describe('MdAutocomplete', () => {
462511
expect(scrollContainer.scrollTop).toEqual(0, `Expected panel not to scroll.`);
463512

464513
// These down arrows will set the 6th option active, below the fold.
465-
[1,2,3,4, 5].forEach(() => {
514+
[1, 2, 3, 4, 5].forEach(() => {
466515
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
467516
});
517+
468518
fixture.detectChanges();
469519

470520
// Expect option bottom minus the panel height (288 - 256 = 32)
@@ -474,6 +524,15 @@ describe('MdAutocomplete', () => {
474524
});
475525

476526
describe('Fallback positions', () => {
527+
let fixture: ComponentFixture<SimpleAutocomplete>;
528+
let input: HTMLInputElement;
529+
530+
beforeEach(() => {
531+
fixture = TestBed.createComponent(SimpleAutocomplete);
532+
fixture.detectChanges();
533+
534+
input = fixture.debugElement.query(By.css('input')).nativeElement;
535+
});
477536

478537
it('should use below positioning by default', () => {
479538
fixture.componentInstance.trigger.openPanel();
@@ -532,6 +591,7 @@ class SimpleAutocomplete implements OnDestroy {
532591
valueSub: Subscription;
533592

534593
@ViewChild(MdAutocompleteTrigger) trigger: MdAutocompleteTrigger;
594+
@ViewChild(MdInputContainer) inputContainer: MdInputContainer;
535595
@ViewChildren(MdOption) options: QueryList<MdOption>;
536596

537597
states = [

0 commit comments

Comments
 (0)