From 0914f29fc3a630d5b43cd6466fa4170a12655e6a Mon Sep 17 00:00:00 2001 From: Kara Erickson Date: Tue, 31 Jan 2017 10:04:15 -0800 Subject: [PATCH] fix(autocomplete): close panel when options list is empty --- src/lib/autocomplete/autocomplete-trigger.ts | 41 +- src/lib/autocomplete/autocomplete.spec.ts | 519 ++++++++++--------- 2 files changed, 294 insertions(+), 266 deletions(-) diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts index bcf3384e1210..78d5dd9d26f4 100644 --- a/src/lib/autocomplete/autocomplete-trigger.ts +++ b/src/lib/autocomplete/autocomplete-trigger.ts @@ -4,8 +4,10 @@ import { ElementRef, forwardRef, Input, + NgZone, Optional, OnDestroy, + QueryList, ViewContainerRef, } from '@angular/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; @@ -18,6 +20,7 @@ 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 {Subscription} from 'rxjs/Subscription'; +import 'rxjs/add/observable/of'; import 'rxjs/add/observable/merge'; import {Dir} from '../core/rtl/dir'; import 'rxjs/add/operator/startWith'; @@ -57,7 +60,7 @@ export const MD_AUTOCOMPLETE_VALUE_ACCESSOR: any = { '[attr.aria-owns]': 'autocomplete?.id', '(focus)': 'openPanel()', '(blur)': '_onTouched()', - '(input)': '_onChange($event.target.value)', + '(input)': '_handleInput($event.target.value)', '(keydown)': '_handleKeydown($event)', }, providers: [MD_AUTOCOMPLETE_VALUE_ACCESSOR] @@ -85,7 +88,7 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce constructor(private _element: ElementRef, private _overlay: Overlay, private _viewContainerRef: ViewContainerRef, - @Optional() private _dir: Dir) {} + @Optional() private _dir: Dir, private _zone: NgZone) {} ngAfterContentInit() { this._keyManager = new ActiveDescendantKeyManager(this.autocomplete.options).withWrap(); @@ -131,7 +134,7 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce * A stream of actions that should close the autocomplete panel, including * when an option is selected and when the backdrop is clicked. */ - get panelClosingActions(): Observable { + get panelClosingActions(): Observable { return Observable.merge( ...this.optionSelections, this._overlayRef.backdropClick(), @@ -140,7 +143,7 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce } /** Stream of autocomplete option selections. */ - get optionSelections(): Observable[] { + get optionSelections(): Observable[] { return this.autocomplete.options.map(option => option.onSelect); } @@ -185,14 +188,19 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce if (this.activeOption && event.keyCode === ENTER) { this.activeOption._selectViaInteraction(); } else { - this.openPanel(); this._keyManager.onKeydown(event); if (event.keyCode === UP_ARROW || event.keyCode === DOWN_ARROW) { + this.openPanel(); this._scrollToOption(); } } } + _handleInput(value: string): void { + this._onChange(value); + this.openPanel(); + } + /** * Given that we are not actually focusing active options, we must manually adjust scroll * to reveal options below the fold. First, we find the offset of the option from the top @@ -211,15 +219,17 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce * stream every time the option list changes. */ private _subscribeToClosingActions(): void { - // Every time the option list changes... - this.autocomplete.options.changes - // and also at initialization, before there are any option changes... - .startWith(null) + const initialOptions = this._getStableOptions(); + + // When the zone is stable initially, and when the option list changes... + Observable.merge(initialOptions, 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(() => { + .switchMap(options => { this._resetPanel(); - return this.panelClosingActions; + // If the options list is empty, emit close event immediately. + // Otherwise, listen for panel closing actions... + return options.length ? this.panelClosingActions : Observable.of(null); }) // when the first closing event occurs... .first() @@ -227,6 +237,15 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce .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> { + return this._zone.onStable.first().map(() => this.autocomplete.options); + } + /** Destroys the autocomplete suggestion panel. */ private _destroyPanel(): void { if (this._overlayRef) { diff --git a/src/lib/autocomplete/autocomplete.spec.ts b/src/lib/autocomplete/autocomplete.spec.ts index 5a5671c5a70e..0c0ddd5e2d86 100644 --- a/src/lib/autocomplete/autocomplete.spec.ts +++ b/src/lib/autocomplete/autocomplete.spec.ts @@ -58,7 +58,8 @@ describe('MdAutocomplete', () => { }); it('should open the panel when the input is focused', () => { - expect(fixture.componentInstance.trigger.panelOpen).toBe(false); + expect(fixture.componentInstance.trigger.panelOpen) + .toBe(false, `Expected panel state to start out closed.`); dispatchEvent('focus', input); fixture.detectChanges(); @@ -71,7 +72,8 @@ describe('MdAutocomplete', () => { }); it('should open the panel programmatically', () => { - expect(fixture.componentInstance.trigger.panelOpen).toBe(false); + expect(fixture.componentInstance.trigger.panelOpen) + .toBe(false, `Expected panel state to start out closed.`); fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); @@ -87,16 +89,18 @@ describe('MdAutocomplete', () => { dispatchEvent('focus', input); fixture.detectChanges(); - const backdrop = - overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement; - backdrop.click(); - fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.componentInstance.trigger.panelOpen) - .toBe(false, `Expected clicking outside the panel to set its state to closed.`); - expect(overlayContainerElement.textContent) - .toEqual('', `Expected clicking outside the panel to close the panel.`); + const backdrop = + overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement; + backdrop.click(); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(fixture.componentInstance.trigger.panelOpen) + .toBe(false, `Expected clicking outside the panel to set its state to closed.`); + expect(overlayContainerElement.textContent) + .toEqual('', `Expected clicking outside the panel to close the panel.`); + }); }); })); @@ -104,59 +108,53 @@ describe('MdAutocomplete', () => { dispatchEvent('focus', input); fixture.detectChanges(); - const option = overlayContainerElement.querySelector('md-option') as HTMLElement; - option.click(); - fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.componentInstance.trigger.panelOpen) - .toBe(false, `Expected clicking an option to set the panel state to closed.`); - expect(overlayContainerElement.textContent) - .toEqual('', `Expected clicking an option to close the panel.`); + const option = overlayContainerElement.querySelector('md-option') as HTMLElement; + option.click(); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(fixture.componentInstance.trigger.panelOpen) + .toBe(false, `Expected clicking an option to set the panel state to closed.`); + expect(overlayContainerElement.textContent) + .toEqual('', `Expected clicking an option to close the panel.`); + }); }); })); - it('should close the panel when a newly filtered option is clicked', async(() => { + it('should close the panel when a newly created option is clicked', async(() => { dispatchEvent('focus', input); fixture.detectChanges(); - // Filter down the option list to a subset of original options ('Alabama', 'California') - input.value = 'al'; - dispatchEvent('input', input); - fixture.detectChanges(); - - let options = - overlayContainerElement.querySelectorAll('md-option') as NodeListOf; - options[0].click(); - fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.componentInstance.trigger.panelOpen) - .toBe(false, `Expected clicking a filtered option to set the panel state to closed.`); - expect(overlayContainerElement.textContent) - .toEqual('', `Expected clicking a filtered option to close the panel.`); - - dispatchEvent('focus', input); + // Filter down the option list to a subset of original options ('Alabama', 'California') + input.value = 'al'; + dispatchEvent('input', input); fixture.detectChanges(); + let options = + overlayContainerElement.querySelectorAll('md-option') as NodeListOf; + options[0].click(); + // Changing value from 'Alabama' to 'al' to re-populate the option list, // ensuring that 'California' is created new. input.value = 'al'; dispatchEvent('input', input); fixture.detectChanges(); - options = - overlayContainerElement.querySelectorAll('md-option') as NodeListOf; - options[1].click(); - fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.componentInstance.trigger.panelOpen) - .toBe(false, `Expected clicking a new option to set the panel state to closed.`); - expect(overlayContainerElement.textContent) - .toEqual('', `Expected clicking a new option to close the panel.`); - }); + options = + overlayContainerElement.querySelectorAll('md-option') as NodeListOf; + options[1].click(); + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(fixture.componentInstance.trigger.panelOpen) + .toBe(false, `Expected clicking a new option to set the panel state to closed.`); + expect(overlayContainerElement.textContent) + .toEqual('', `Expected clicking a new option to close the panel.`); + }); + }); }); })); @@ -173,6 +171,25 @@ describe('MdAutocomplete', () => { expect(overlayContainerElement.textContent) .toEqual('', `Expected closing programmatically to close the panel.`); }); + + })); + + it('should close the panel when the options list is empty', async(() => { + dispatchEvent('focus', input); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + // Filter down the option list such that no options match the value + input.value = 'af'; + dispatchEvent('input', input); + fixture.detectChanges(); + fixture.whenStable().then(() => { + 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.`); + }); + }); })); }); @@ -221,59 +238,61 @@ describe('MdAutocomplete', () => { .toEqual('al', 'Expected control value to be updated as user types.'); }); - it('should update control value when option is selected with option value', () => { + it('should update control value when option is selected with option value', async(() => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); - const options = - overlayContainerElement.querySelectorAll('md-option') as NodeListOf; - options[1].click(); - fixture.detectChanges(); + fixture.whenStable().then(() => { + const options = + overlayContainerElement.querySelectorAll('md-option') as NodeListOf; + options[1].click(); + fixture.detectChanges(); - expect(fixture.componentInstance.stateCtrl.value) - .toEqual({code: 'CA', name: 'California'}, 'Expected control value to be option value.'); - }); + expect(fixture.componentInstance.stateCtrl.value) + .toEqual({code: 'CA', name: 'California'}, + 'Expected control value to equal the selected option value.'); + }); + })); - it('should update control back to string if user types after option is selected', () => { + it('should update control back to string if user types after option is selected', async(() => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); - const options = - overlayContainerElement.querySelectorAll('md-option') as NodeListOf; - options[1].click(); - fixture.detectChanges(); + fixture.whenStable().then(() => { + const options = + overlayContainerElement.querySelectorAll('md-option') as NodeListOf; + options[1].click(); + fixture.detectChanges(); - input.value = 'Californi'; - dispatchEvent('input', input); - fixture.detectChanges(); + input.value = 'Californi'; + dispatchEvent('input', input); + fixture.detectChanges(); - expect(fixture.componentInstance.stateCtrl.value) - .toEqual('Californi', 'Expected control value to revert back to string.'); - }); + expect(fixture.componentInstance.stateCtrl.value) + .toEqual('Californi', 'Expected control value to revert back to string.'); + }); + })); it('should fill the text field with display value when an option is selected', async(() => { - fixture.whenStable().then(() => { - fixture.componentInstance.trigger.openPanel(); - fixture.detectChanges(); + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + fixture.whenStable().then(() => { const options = overlayContainerElement.querySelectorAll('md-option') as NodeListOf; options[1].click(); fixture.detectChanges(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(input.value) - .toContain('California', `Expected text field to fill with selected value.`); - }); + expect(input.value) + .toContain('California', `Expected text field to fill with selected value.`); }); })); it('should fill the text field with value if displayWith is not set', async(() => { - fixture.whenStable().then(() => { - fixture.componentInstance.trigger.openPanel(); - fixture.detectChanges(); + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + fixture.whenStable().then(() => { fixture.componentInstance.panel.displayWith = null; fixture.componentInstance.options.toArray()[1].value = 'test value'; fixture.detectChanges(); @@ -282,18 +301,16 @@ describe('MdAutocomplete', () => { overlayContainerElement.querySelectorAll('md-option') as NodeListOf; options[1].click(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(input.value) - .toContain('test value', `Expected input to fall back to selected option's value.`); + fixture.detectChanges(); + expect(input.value) + .toContain('test value', `Expected input to fall back to selected option's value.`); }); - }); })); it('should fill the text field correctly if value is set to obj programmatically', async(() => { fixture.whenStable().then(() => { - fixture.detectChanges(); fixture.componentInstance.stateCtrl.setValue({code: 'AL', name: 'Alabama'}); + fixture.detectChanges(); fixture.whenStable().then(() => { fixture.detectChanges(); @@ -304,12 +321,11 @@ describe('MdAutocomplete', () => { })); it('should clear the text field if value is reset programmatically', async(() => { - fixture.whenStable().then(() => { - input.value = 'Alabama'; - dispatchEvent('input', input); - fixture.detectChanges(); + input.value = 'Alabama'; + dispatchEvent('input', input); + fixture.detectChanges(); - expect(input.value).toEqual('Alabama', `Expected input to start out with a value.`); + fixture.whenStable().then(() => { fixture.componentInstance.stateCtrl.reset(); fixture.whenStable().then(() => { @@ -339,8 +355,6 @@ describe('MdAutocomplete', () => { it('should mark the autocomplete control as dirty as user types', () => { - fixture.componentInstance.trigger.openPanel(); - fixture.detectChanges(); expect(fixture.componentInstance.stateCtrl.dirty) .toBe(false, `Expected control to start out pristine.`); @@ -352,20 +366,23 @@ describe('MdAutocomplete', () => { .toBe(true, `Expected control to become dirty when the user types into the input.`); }); - it('should mark the autocomplete control as dirty when an option is selected', () => { - fixture.componentInstance.trigger.openPanel(); - fixture.detectChanges(); + it('should mark the autocomplete control as dirty when an option is selected', async(() => { expect(fixture.componentInstance.stateCtrl.dirty) .toBe(false, `Expected control to start out pristine.`); - const options = - overlayContainerElement.querySelectorAll('md-option') as NodeListOf; - options[1].click(); + fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); - expect(fixture.componentInstance.stateCtrl.dirty) - .toBe(true, `Expected control to become dirty when an option was selected.`); - }); + fixture.whenStable().then(() => { + const options = + overlayContainerElement.querySelectorAll('md-option') as NodeListOf; + options[1].click(); + fixture.detectChanges(); + + expect(fixture.componentInstance.stateCtrl.dirty) + .toBe(true, `Expected control to become dirty when an option was selected.`); + }); + })); it('should not mark the control dirty when the value is set programmatically', () => { expect(fixture.componentInstance.stateCtrl.dirty) @@ -406,197 +423,186 @@ describe('MdAutocomplete', () => { input = fixture.debugElement.query(By.css('input')).nativeElement; DOWN_ARROW_EVENT = new FakeKeyboardEvent(DOWN_ARROW) as KeyboardEvent; ENTER_EVENT = new FakeKeyboardEvent(ENTER) as KeyboardEvent; - }); - it('should should not focus the option when DOWN key is pressed', () => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); - - spyOn(fixture.componentInstance.options.first, 'focus'); - - fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); - expect(fixture.componentInstance.options.first.focus).not.toHaveBeenCalled(); }); - it('should set the active item to the first option when DOWN key is pressed', async(() => { - fixture.componentInstance.trigger.openPanel(); - fixture.detectChanges(); - - const optionEls = - overlayContainerElement.querySelectorAll('md-option') as NodeListOf; + it('should should not focus the option when DOWN key is pressed', async(() => { + fixture.whenStable().then(() => { + spyOn(fixture.componentInstance.options.first, 'focus'); - fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); + fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); + expect(fixture.componentInstance.options.first.focus).not.toHaveBeenCalled(); + }); + })); + it('should set the active item to the first option when DOWN key is pressed', async(() => { fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(fixture.componentInstance.trigger.activeOption) - .toBe(fixture.componentInstance.options.first, 'Expected first option to be active.'); - expect(optionEls[0].classList).toContain('md-active'); - expect(optionEls[1].classList).not.toContain('md-active'); + const optionEls = + overlayContainerElement.querySelectorAll('md-option') as NodeListOf; fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); - fixture.whenStable().then(() => { fixture.detectChanges(); expect(fixture.componentInstance.trigger.activeOption) - .toBe(fixture.componentInstance.options.toArray()[1], - 'Expected second option to be active.'); - expect(optionEls[0].classList).not.toContain('md-active'); - expect(optionEls[1].classList).toContain('md-active'); + .toBe(fixture.componentInstance.options.first, 'Expected first option to be active.'); + expect(optionEls[0].classList).toContain('md-active'); + expect(optionEls[1].classList).not.toContain('md-active'); + + fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); + + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(fixture.componentInstance.trigger.activeOption) + .toBe(fixture.componentInstance.options.toArray()[1], + 'Expected second option to be active.'); + expect(optionEls[0].classList).not.toContain('md-active'); + expect(optionEls[1].classList).toContain('md-active'); + }); }); }); })); it('should set the active item to the last option when UP key is pressed', async(() => { - fixture.componentInstance.trigger.openPanel(); - fixture.detectChanges(); - - const optionEls = - overlayContainerElement.querySelectorAll('md-option') as NodeListOf; - - const UP_ARROW_EVENT = new FakeKeyboardEvent(UP_ARROW) as KeyboardEvent; - fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT); - fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(fixture.componentInstance.trigger.activeOption) - .toBe(fixture.componentInstance.options.last, 'Expected last option to be active.'); - expect(optionEls[10].classList).toContain('md-active'); - expect(optionEls[0].classList).not.toContain('md-active'); + const optionEls = + overlayContainerElement.querySelectorAll('md-option') as NodeListOf; - fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); + const UP_ARROW_EVENT = new FakeKeyboardEvent(UP_ARROW) as KeyboardEvent; + fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT); fixture.whenStable().then(() => { fixture.detectChanges(); expect(fixture.componentInstance.trigger.activeOption) - .toBe(fixture.componentInstance.options.first, - 'Expected first option to be active.'); - expect(optionEls[0].classList).toContain('md-active'); - expect(optionEls[10].classList).not.toContain('md-active'); + .toBe(fixture.componentInstance.options.last, 'Expected last option to be active.'); + expect(optionEls[10].classList).toContain('md-active'); + expect(optionEls[0].classList).not.toContain('md-active'); + + fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); + + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(fixture.componentInstance.trigger.activeOption) + .toBe(fixture.componentInstance.options.first, + 'Expected first option to be active.'); + expect(optionEls[0].classList).toContain('md-active'); + expect(optionEls[10].classList).not.toContain('md-active'); + }); }); }); })); it('should set the active item properly after filtering', async(() => { - fixture.componentInstance.trigger.openPanel(); - fixture.detectChanges(); - - fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); - fixture.whenStable().then(() => { - fixture.detectChanges(); - input.value = 'o'; - dispatchEvent('input', input); - fixture.detectChanges(); - - const optionEls = - overlayContainerElement.querySelectorAll('md-option') as NodeListOf; - fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); + fixture.detectChanges(); fixture.whenStable().then(() => { + input.value = 'o'; + dispatchEvent('input', input); fixture.detectChanges(); - expect(fixture.componentInstance.trigger.activeOption) - .toBe(fixture.componentInstance.options.first, 'Expected first option to be active.'); - expect(optionEls[0].classList).toContain('md-active'); - expect(optionEls[1].classList).not.toContain('md-active'); + + fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); + + fixture.whenStable().then(() => { + fixture.detectChanges(); + const optionEls = + overlayContainerElement.querySelectorAll('md-option') as NodeListOf; + + expect(fixture.componentInstance.trigger.activeOption) + .toBe(fixture.componentInstance.options.first, + 'Expected first option to be active.'); + expect(optionEls[0].classList).toContain('md-active'); + expect(optionEls[1].classList).not.toContain('md-active'); + }); }); }); })); it('should fill the text field when an option is selected with ENTER', async(() => { fixture.whenStable().then(() => { - fixture.componentInstance.trigger.openPanel(); - fixture.detectChanges(); - fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT); - fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(input.value) - .toContain('Alabama', `Expected text field to fill with selected value on ENTER.`); - }); + fixture.detectChanges(); + expect(input.value) + .toContain('Alabama', `Expected text field to fill with selected value on ENTER.`); }); })); - it('should fill the text field, not select an option, when SPACE is entered', () => { - fixture.componentInstance.trigger.openPanel(); - fixture.detectChanges(); - - input.value = 'New'; - dispatchEvent('input', input); - fixture.detectChanges(); - - const SPACE_EVENT = new FakeKeyboardEvent(SPACE) as KeyboardEvent; - fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); - fixture.componentInstance.trigger._handleKeydown(SPACE_EVENT); - fixture.detectChanges(); + it('should fill the text field, not select an option, when SPACE is entered', async(() => { + fixture.whenStable().then(() => { + input.value = 'New'; + dispatchEvent('input', input); + fixture.detectChanges(); - expect(input.value) - .not.toContain('New York', `Expected option not to be selected on SPACE.`); - }); + const SPACE_EVENT = new FakeKeyboardEvent(SPACE) as KeyboardEvent; + fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); + fixture.componentInstance.trigger._handleKeydown(SPACE_EVENT); + fixture.detectChanges(); - it('should mark the control as dirty when an option is selected from the keyboard', () => { - fixture.componentInstance.trigger.openPanel(); - fixture.detectChanges(); + expect(input.value) + .not.toContain('New York', `Expected option not to be selected on SPACE.`); + }); + })); - expect(fixture.componentInstance.stateCtrl.dirty) - .toBe(false, `Expected control to start out pristine.`); + it('should mark the control dirty when selecting an option from the keyboard', async(() => { + fixture.whenStable().then(() => { + expect(fixture.componentInstance.stateCtrl.dirty) + .toBe(false, `Expected control to start out pristine.`); - fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); - fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT); - fixture.detectChanges(); + fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); + fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT); + fixture.detectChanges(); - expect(fixture.componentInstance.stateCtrl.dirty) - .toBe(true, `Expected control to become dirty when option was selected by ENTER.`); - }); + expect(fixture.componentInstance.stateCtrl.dirty) + .toBe(true, `Expected control to become dirty when option was selected by ENTER.`); + }); + })); it('should open the panel again when typing after making a selection', async(() => { - fixture.componentInstance.trigger.openPanel(); - fixture.detectChanges(); - - fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); - fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT); - fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.componentInstance.trigger.panelOpen) - .toBe(false, `Expected panel state to read closed after ENTER key.`); - expect(overlayContainerElement.textContent) - .toEqual('', `Expected panel to close after ENTER key.`); - - // 65 is the keycode for "a" - const A_KEY = new FakeKeyboardEvent(65) as KeyboardEvent; - fixture.componentInstance.trigger._handleKeydown(A_KEY); + fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); + fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT); fixture.detectChanges(); - expect(fixture.componentInstance.trigger.panelOpen) - .toBe(true, `Expected panel state to read open when typing in input.`); - expect(overlayContainerElement.textContent) - .toContain('Alabama', `Expected panel to display when typing in input.`); + fixture.whenStable().then(() => { + expect(fixture.componentInstance.trigger.panelOpen) + .toBe(false, `Expected panel state to read closed after ENTER key.`); + expect(overlayContainerElement.textContent) + .toEqual('', `Expected panel to close after ENTER key.`); + + input.value = 'Alabam'; + dispatchEvent('input', input); + fixture.detectChanges(); + + expect(fixture.componentInstance.trigger.panelOpen) + .toBe(true, `Expected panel state to read open when typing in input.`); + expect(overlayContainerElement.textContent) + .toContain('Alabama', `Expected panel to display when typing in input.`); + }); }); })); - it('should scroll to active options below the fold', () => { - fixture.componentInstance.trigger.openPanel(); - fixture.detectChanges(); + it('should scroll to active options below the fold', async(() => { + fixture.whenStable().then(() => { + const scrollContainer = document.querySelector('.cdk-overlay-pane .md-autocomplete-panel'); - const scrollContainer = document.querySelector('.cdk-overlay-pane .md-autocomplete-panel'); + fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); + fixture.detectChanges(); + expect(scrollContainer.scrollTop).toEqual(0, `Expected panel not to scroll.`); - fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); - fixture.detectChanges(); - expect(scrollContainer.scrollTop).toEqual(0, `Expected panel not to scroll.`); + // These down arrows will set the 6th option active, below the fold. + [1, 2, 3, 4, 5].forEach(() => { + fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); + }); + fixture.detectChanges(); - // These down arrows will set the 6th option active, below the fold. - [1, 2, 3, 4, 5].forEach(() => { - fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); + // Expect option bottom minus the panel height (288 - 256 = 32) + expect(scrollContainer.scrollTop).toEqual(32, `Expected panel to reveal the sixth option.`); }); - fixture.detectChanges(); - - // Expect option bottom minus the panel height (288 - 256 = 32) - expect(scrollContainer.scrollTop).toEqual(32, `Expected panel to reveal the sixth option.`); - }); + })); }); @@ -636,28 +642,29 @@ describe('MdAutocomplete', () => { .toEqual('false', 'Expected aria-multiline attribute to equal false.'); }); - it('should set aria-activedescendant based on the active option', () => { + it('should set aria-activedescendant based on the active option', async(() => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(input.hasAttribute('aria-activedescendant')) + .toBe(false, 'Expected aria-activedescendant to be absent if no active item.'); - expect(input.hasAttribute('aria-activedescendant')) - .toBe(false, 'Expected aria-activedescendant to be absent if no active item.'); - - const DOWN_ARROW_EVENT = new FakeKeyboardEvent(DOWN_ARROW) as KeyboardEvent; - fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); - fixture.detectChanges(); + const DOWN_ARROW_EVENT = new FakeKeyboardEvent(DOWN_ARROW) as KeyboardEvent; + fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); + fixture.detectChanges(); - expect(input.getAttribute('aria-activedescendant')) - .toEqual(fixture.componentInstance.options.first.id, - 'Expected aria-activedescendant to match the active item after 1 down arrow.'); + expect(input.getAttribute('aria-activedescendant')) + .toEqual(fixture.componentInstance.options.first.id, + 'Expected aria-activedescendant to match the active item after 1 down arrow.'); - fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); - fixture.detectChanges(); + fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT); + fixture.detectChanges(); - expect(input.getAttribute('aria-activedescendant')) - .toEqual(fixture.componentInstance.options.toArray()[1].id, - 'Expected aria-activedescendant to match the active item after 2 down arrows.'); - }); + expect(input.getAttribute('aria-activedescendant')) + .toEqual(fixture.componentInstance.options.toArray()[1].id, + 'Expected aria-activedescendant to match the active item after 2 down arrows.'); + }); + })); it('should set aria-expanded based on whether the panel is open', async(() => { expect(input.getAttribute('aria-expanded')) @@ -736,7 +743,7 @@ describe('MdAutocomplete', () => { .toEqual('above', `Expected autocomplete positionY to be "above" if panel won't fit.`); }); - it('should align panel properly when filtering in "above" position', () => { + it('should align panel properly when filtering in "above" position', async(() => { // Push the autocomplete trigger down so it won't have room to open "below" input.style.top = '600px'; input.style.position = 'relative'; @@ -744,20 +751,22 @@ describe('MdAutocomplete', () => { fixture.componentInstance.trigger.openPanel(); fixture.detectChanges(); - input.value = 'f'; - dispatchEvent('input', input); - fixture.detectChanges(); + fixture.whenStable().then(() => { + input.value = 'f'; + dispatchEvent('input', input); + fixture.detectChanges(); - const inputTop = input.getBoundingClientRect().top; - const panel = overlayContainerElement.querySelector('.md-autocomplete-panel'); - const panelBottom = panel.getBoundingClientRect().bottom; + const inputTop = input.getBoundingClientRect().top; + const panel = overlayContainerElement.querySelector('.md-autocomplete-panel'); + const panelBottom = panel.getBoundingClientRect().bottom; - // Panel is offset by 24px in styles so that the label has room to display. - expect((inputTop - 24).toFixed(2)) - .toEqual(panelBottom.toFixed(2), `Expected panel to stay aligned after filtering.`); - expect(fixture.componentInstance.trigger.autocomplete.positionY) - .toEqual('above', `Expected autocomplete positionY to be "above" if panel won't fit.`); - }); + // Panel is offset by 24px in styles so that the label has room to display. + expect((inputTop - 24).toFixed(2)) + .toEqual(panelBottom.toFixed(2), `Expected panel to stay aligned after filtering.`); + expect(fixture.componentInstance.trigger.autocomplete.positionY) + .toEqual('above', `Expected autocomplete positionY to be "above" if panel won't fit.`); + }); + })); });