Skip to content

Commit

Permalink
fix(autocomplete): hide instead of close when options empty (#2997)
Browse files Browse the repository at this point in the history
  • Loading branch information
kara authored and tinayuangao committed Feb 9, 2017
1 parent be19b2d commit a022035
Show file tree
Hide file tree
Showing 5 changed files with 42 additions and 27 deletions.
22 changes: 4 additions & 18 deletions src/lib/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
NgZone,
Optional,
OnDestroy,
QueryList,
ViewContainerRef,
} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
Expand All @@ -23,7 +22,6 @@ import {ENTER, UP_ARROW, DOWN_ARROW} from '../core/keyboard/keycodes';
import {Dir} from '../core/rtl/dir';
import {Subscription} from 'rxjs/Subscription';
import {Subject} from 'rxjs/Subject';
import 'rxjs/add/observable/of';
import 'rxjs/add/observable/merge';
import 'rxjs/add/operator/startWith';
import 'rxjs/add/operator/switchMap';
Expand Down Expand Up @@ -258,33 +256,20 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce
* stream every time the option list changes.
*/
private _subscribeToClosingActions(): void {
const initialOptions = this._getStableOptions();

// When the zone is stable initially, and when the option list changes...
Observable.merge(initialOptions, this.autocomplete.options.changes)
Observable.merge(this._zone.onStable.first(), this.autocomplete.options.changes)
// create a new stream of panelClosingActions, replacing any previous streams
// that were created, and flatten it so our stream only emits closing events...
.switchMap(options => {
.switchMap(() => {
this._resetPanel();
// If the options list is empty, emit close event immediately.
// Otherwise, listen for panel closing actions...
return options.length ? this.panelClosingActions : Observable.of(null);
return this.panelClosingActions;
})
// when the first closing event occurs...
.first()
// set the value, close the panel, and complete.
.subscribe(event => this._setValueAndClose(event));
}

/**
* Retrieves the option list once the zone stabilizes. It's important to wait until
* stable so that change detection can run first and update the query list
* with the options available under the current filter.
*/
private _getStableOptions(): Observable<QueryList<MdOption>> {
return this._zone.onStable.first().map(() => this.autocomplete.options);
}

/** Destroys the autocomplete suggestion panel. */
private _destroyPanel(): void {
if (this._overlayRef) {
Expand Down Expand Up @@ -364,6 +349,7 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce
private _resetPanel() {
this._resetActiveItem();
this._positionStrategy.recalculateLastPosition();
this.autocomplete._setVisibility();
}

}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/autocomplete/autocomplete.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div class="mat-autocomplete-panel" role="listbox" [id]="id" [ngClass]="_getPositionClass()" #panel>
<div class="mat-autocomplete-panel" role="listbox" [id]="id" [ngClass]="_getClassList()" #panel>
<ng-content></ng-content>
</div>
</template>
9 changes: 9 additions & 0 deletions src/lib/autocomplete/autocomplete.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ $mat-autocomplete-panel-above-offset: -24px !default;

.mat-autocomplete-panel {
@include mat-menu-base();
visibility: hidden;

max-height: $mat-autocomplete-panel-max-height;
position: relative;
Expand All @@ -25,4 +26,12 @@ $mat-autocomplete-panel-above-offset: -24px !default;
&.mat-autocomplete-panel-above {
top: $mat-autocomplete-panel-above-offset;
}

&.mat-autocomplete-visible {
visibility: visible;
}

&.mat-autocomplete-hidden {
visibility: hidden;
}
}
22 changes: 16 additions & 6 deletions src/lib/autocomplete/autocomplete.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,20 +165,30 @@ describe('MdAutocomplete', () => {
.toEqual('', `Expected closing programmatically to close the panel.`);
});

it('should close the panel when the options list is empty', async(() => {
it('should hide the panel when the options list is empty', async(() => {
dispatchEvent('focus', input);
fixture.detectChanges();

fixture.whenStable().then(() => {
fixture.detectChanges();

const panel =
overlayContainerElement.querySelector('.mat-autocomplete-panel') as HTMLElement;
expect(panel.classList)
.toContain('mat-autocomplete-visible', `Expected panel to start out visible.`);

// Filter down the option list such that no options match the value
input.value = 'af';
dispatchEvent('input', input);
fixture.detectChanges();

expect(fixture.componentInstance.trigger.panelOpen)
.toBe(false, `Expected panel to close when options list is empty.`);
expect(overlayContainerElement.textContent)
.toEqual('', `Expected panel to close when options list is empty.`);
fixture.whenStable().then(() => {
fixture.detectChanges();

expect(fixture.componentInstance.trigger.panelOpen)
.toBe(true, `Expected panel to stay open when options list is empty.`);
expect(panel.classList)
.toContain('mat-autocomplete-hidden', `Expected panel to hide itself when empty.`);
});
});
}));

Expand Down
14 changes: 12 additions & 2 deletions src/lib/autocomplete/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export class MdAutocomplete {
/** Whether the autocomplete panel displays above or below its trigger. */
positionY: AutocompletePositionY = 'below';

/** Whether the autocomplete panel should be visible, depending on option length. */
showPanel = false;

@ViewChild(TemplateRef) template: TemplateRef<any>;
@ViewChild('panel') panel: ElementRef;
@ContentChildren(MdOption) options: QueryList<MdOption>;
Expand All @@ -54,11 +57,18 @@ export class MdAutocomplete {
}
}

/** Panel should hide itself when the option list is empty. */
_setVisibility() {
Promise.resolve().then(() => this.showPanel = !!this.options.length);
}

/** Sets a class on the panel based on its position (used to set y-offset). */
_getPositionClass() {
_getClassList() {
return {
'mat-autocomplete-panel-below': this.positionY === 'below',
'mat-autocomplete-panel-above': this.positionY === 'above'
'mat-autocomplete-panel-above': this.positionY === 'above',
'mat-autocomplete-visible': this.showPanel,
'mat-autocomplete-hidden': !this.showPanel
};
}

Expand Down

0 comments on commit a022035

Please sign in to comment.