Skip to content

Commit

Permalink
fix(autocomplete): fix key manager instantiation (#3274)
Browse files Browse the repository at this point in the history
  • Loading branch information
kara authored Feb 27, 2017
1 parent 5ef3084 commit c21ff40
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 21 deletions.
8 changes: 5 additions & 3 deletions src/demo-app/autocomplete/autocomplete-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@
</md-card>

<md-card>

<div>Template-driven value (currentState): {{ currentState }}</div>
<div>Template-driven dirty: {{ modelDir.dirty }}</div>
<div>Template-driven dirty: {{ modelDir?.dirty }}</div>

<md-input-container>
<input mdInput placeholder="State" [mdAutocomplete]="tdAuto" [(ngModel)]="currentState" #modelDir="ngModel"
<!-- Added an ngIf below to test that autocomplete works with ngIf -->
<md-input-container *ngIf="true">
<input mdInput placeholder="State" [mdAutocomplete]="tdAuto" [(ngModel)]="currentState"
(ngModelChange)="tdStates = filterStates(currentState)" [disabled]="tdDisabled">
</md-input-container>

Expand Down
6 changes: 4 additions & 2 deletions src/demo-app/autocomplete/autocomplete-demo.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Component, ViewEncapsulation} from '@angular/core';
import {FormControl} from '@angular/forms';
import {Component, ViewChild, ViewEncapsulation} from '@angular/core';
import {FormControl, NgModel} from '@angular/forms';
import 'rxjs/add/operator/startWith';

@Component({
Expand All @@ -19,6 +19,8 @@ export class AutocompleteDemo {

tdDisabled = false;

@ViewChild(NgModel) modelDir: NgModel;

states = [
{code: 'AL', name: 'Alabama'},
{code: 'AK', name: 'Alaska'},
Expand Down
23 changes: 9 additions & 14 deletions src/lib/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
AfterContentInit,
Directive,
ElementRef,
forwardRef,
Expand All @@ -17,7 +16,6 @@ import {PositionStrategy} from '../core/overlay/position/position-strategy';
import {ConnectedPositionStrategy} from '../core/overlay/position/connected-position-strategy';
import {Observable} from 'rxjs/Observable';
import {MdOptionSelectEvent, MdOption} from '../core/option/option';
import {ActiveDescendantKeyManager} from '../core/a11y/activedescendant-key-manager';
import {ENTER, UP_ARROW, DOWN_ARROW} from '../core/keyboard/keycodes';
import {Dir} from '../core/rtl/dir';
import {Subscription} from 'rxjs/Subscription';
Expand Down Expand Up @@ -66,16 +64,14 @@ export const MD_AUTOCOMPLETE_VALUE_ACCESSOR: any = {
},
providers: [MD_AUTOCOMPLETE_VALUE_ACCESSOR]
})
export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAccessor, OnDestroy {
export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
private _overlayRef: OverlayRef;
private _portal: TemplatePortal;
private _panelOpen: boolean = false;

/** The subscription to positioning changes in the autocomplete panel. */
private _panelPositionSubscription: Subscription;

/** Manages active item in option list based on key events. */
private _keyManager: ActiveDescendantKeyManager;
private _positionStrategy: ConnectedPositionStrategy;

/** Stream of blur events that should close the panel. */
Expand Down Expand Up @@ -108,10 +104,6 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce
@Optional() private _dir: Dir, private _zone: NgZone,
@Optional() @Host() private _inputContainer: MdInputContainer) {}

ngAfterContentInit() {
this._keyManager = new ActiveDescendantKeyManager(this.autocomplete.options).withWrap();
}

ngOnDestroy() {
if (this._panelPositionSubscription) {
this._panelPositionSubscription.unsubscribe();
Expand Down Expand Up @@ -158,7 +150,7 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce
return Observable.merge(
this.optionSelections,
this._blurStream.asObservable(),
this._keyManager.tabOut
this.autocomplete._keyManager.tabOut
);
}

Expand All @@ -169,7 +161,9 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce

/** The currently active option, coerced to MdOption type. */
get activeOption(): MdOption {
return this._keyManager.activeItem as MdOption;
if (this.autocomplete._keyManager) {
return this.autocomplete._keyManager.activeItem as MdOption;
}
}

/**
Expand Down Expand Up @@ -208,7 +202,7 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce
if (this.activeOption && event.keyCode === ENTER) {
this.activeOption._selectViaInteraction();
} else {
this._keyManager.onKeydown(event);
this.autocomplete._keyManager.onKeydown(event);
if (event.keyCode === UP_ARROW || event.keyCode === DOWN_ARROW) {
this.openPanel();
this._scrollToOption();
Expand Down Expand Up @@ -262,7 +256,8 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce
* height, so the active option will be just visible at the bottom of the panel.
*/
private _scrollToOption(): void {
const optionOffset = this._keyManager.activeItemIndex * AUTOCOMPLETE_OPTION_HEIGHT;
const optionOffset =
this.autocomplete._keyManager.activeItemIndex * AUTOCOMPLETE_OPTION_HEIGHT;
const newScrollTop =
Math.max(0, optionOffset - AUTOCOMPLETE_PANEL_HEIGHT + AUTOCOMPLETE_OPTION_HEIGHT);
this.autocomplete._setScrollTop(newScrollTop);
Expand Down Expand Up @@ -356,7 +351,7 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce

/** Reset active item to null so arrow events will activate the correct options.*/
private _resetActiveItem(): void {
this._keyManager.setActiveItem(null);
this.autocomplete._keyManager.setActiveItem(null);
}

/**
Expand Down
49 changes: 48 additions & 1 deletion src/lib/autocomplete/autocomplete.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {ViewportRuler} from '../core/overlay/position/viewport-ruler';
import {FakeViewportRuler} from '../core/overlay/position/fake-viewport-ruler';
import {MdAutocomplete} from './autocomplete';
import {MdInputContainer} from '../input/input-container';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/map';

describe('MdAutocomplete', () => {
let overlayContainerElement: HTMLElement;
Expand All @@ -24,7 +26,7 @@ describe('MdAutocomplete', () => {
imports: [
MdAutocompleteModule.forRoot(), MdInputModule.forRoot(), ReactiveFormsModule
],
declarations: [SimpleAutocomplete, AutocompleteWithoutForms],
declarations: [SimpleAutocomplete, AutocompleteWithoutForms, NgIfAutocomplete],
providers: [
{provide: OverlayContainer, useFactory: () => {
overlayContainerElement = document.createElement('div');
Expand Down Expand Up @@ -859,6 +861,22 @@ describe('MdAutocomplete', () => {
}).not.toThrowError();
});

it('should work when input is wrapped in ngIf', () => {
const fixture = TestBed.createComponent(NgIfAutocomplete);
fixture.detectChanges();

const input = fixture.debugElement.query(By.css('input')).nativeElement;
dispatchEvent('focus', input);
fixture.detectChanges();

expect(fixture.componentInstance.trigger.panelOpen)
.toBe(true, `Expected panel state to read open when input is focused.`);
expect(overlayContainerElement.textContent)
.toContain('One', `Expected panel to display when input is focused.`);
expect(overlayContainerElement.textContent)
.toContain('Two', `Expected panel to display when input is focused.`);
});

});
});

Expand Down Expand Up @@ -919,6 +937,35 @@ class SimpleAutocomplete implements OnDestroy {

}

@Component({
template: `
<md-input-container *ngIf="isVisible">
<input mdInput placeholder="Choose" [mdAutocomplete]="auto" [formControl]="optionCtrl">
</md-input-container>
<md-autocomplete #auto="mdAutocomplete">
<md-option *ngFor="let option of filteredOptions | async" [value]="option">
{{option}}
</md-option>
</md-autocomplete>
`
})
class NgIfAutocomplete {
optionCtrl = new FormControl();
filteredOptions: Observable<any>;
isVisible = true;

@ViewChild(MdAutocompleteTrigger) trigger: MdAutocompleteTrigger;
options = ['One', 'Two', 'Three'];

constructor() {
this.filteredOptions = this.optionCtrl.valueChanges.startWith(null).map((val) => {
return val ? this.options.filter(option => new RegExp(val, 'gi').test(option))
: this.options.slice();
});
}
}


@Component({
template: `
Expand Down
11 changes: 10 additions & 1 deletion src/lib/autocomplete/autocomplete.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
AfterContentInit,
Component,
ContentChildren,
ElementRef,
Expand All @@ -9,6 +10,7 @@ import {
ViewEncapsulation
} from '@angular/core';
import {MdOption} from '../core';
import {ActiveDescendantKeyManager} from '../core/a11y/activedescendant-key-manager';

/**
* Autocomplete IDs need to be unique across components, so this counter exists outside of
Expand All @@ -29,7 +31,10 @@ export type AutocompletePositionY = 'above' | 'below';
'[class.mat-autocomplete]': 'true'
}
})
export class MdAutocomplete {
export class MdAutocomplete implements AfterContentInit {

/** Manages active item in option list based on key events. */
_keyManager: ActiveDescendantKeyManager;

/** Whether the autocomplete panel displays above or below its trigger. */
positionY: AutocompletePositionY = 'below';
Expand All @@ -47,6 +52,10 @@ export class MdAutocomplete {
/** Unique ID to be used by autocomplete trigger's "aria-owns" property. */
id: string = `md-autocomplete-${_uniqueAutocompleteIdCounter++}`;

ngAfterContentInit() {
this._keyManager = new ActiveDescendantKeyManager(this.options).withWrap();
}

/**
* Sets the panel scrollTop. This allows us to manually scroll to display
* options below the fold, as they are not actually being focused when active.
Expand Down

0 comments on commit c21ff40

Please sign in to comment.