Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(autocomplete): fix key manager instantiation #3274

Merged
merged 1 commit into from
Feb 27, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you use a named version instead? this seems fragile

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Talked in person about AOT


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