fix(autocomplete): close panel when options list is empty (#2834)

This commit is contained in:
Kara
2017-01-31 10:07:08 -08:00
parent a3a2ec45bc
commit 8a3b6fd157
2 changed files with 328 additions and 300 deletions
+30 -11
View File
@@ -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<any> {
get panelClosingActions(): Observable<MdOptionSelectEvent> {
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<any>[] {
get optionSelections(): Observable<MdOptionSelectEvent>[] {
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<QueryList<MdOption>> {
return this._zone.onStable.first().map(() => this.autocomplete.options);
}
/** Destroys the autocomplete suggestion panel. */
private _destroyPanel(): void {
if (this._overlayRef) {
+298 -289
View File
@@ -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<HTMLElement>;
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<HTMLElement>;
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<HTMLElement>;
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<HTMLElement>;
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<HTMLElement>;
options[1].click();
fixture.detectChanges();
expect(fixture.componentInstance.stateCtrl.value)
.toEqual({code: 'CA', name: 'California'}, 'Expected control value to be option value.');
});
it('should update control back to string if user types after option is selected', () => {
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
const options =
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
options[1].click();
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.');
});
it('should fill the text field with display value when an option is selected', async(() => {
fixture.whenStable().then(() => {
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
const options =
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
options[1].click();
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(input.value)
.toContain('California', `Expected text field to fill with selected 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', async(() => {
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
fixture.whenStable().then(() => {
const options =
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
options[1].click();
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.');
});
}));
it('should fill the text field with display value when an option is selected', async(() => {
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
fixture.whenStable().then(() => {
const options =
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
options[1].click();
fixture.detectChanges();
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<HTMLElement>;
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<HTMLElement>;
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<HTMLElement>;
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,198 +423,187 @@ 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 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);
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<HTMLElement>;
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[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<HTMLElement>;
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');
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<HTMLElement>;
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[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.whenStable().then(() => {
const optionEls =
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
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');
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.whenStable().then(() => {
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
fixture.detectChanges();
fixture.whenStable().then(() => {
input.value = 'o';
dispatchEvent('input', input);
fixture.detectChanges();
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
fixture.whenStable().then(() => {
fixture.detectChanges();
const optionEls =
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
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.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', async(() => {
fixture.whenStable().then(() => {
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();
expect(input.value)
.not.toContain('New York', `Expected option not to be selected on SPACE.`);
});
}));
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();
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.whenStable().then(() => {
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.`);
input.value = 'Alabam';
dispatchEvent('input', input);
fixture.detectChanges();
expect(input.value)
.toContain('Alabama', `Expected text field to fill with selected value on ENTER.`);
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 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();
expect(input.value)
.not.toContain('New York', `Expected option not to be selected on SPACE.`);
});
it('should mark the control as dirty when an option is selected from the keyboard', () => {
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
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();
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();
it('should scroll to active options below the fold', async(() => {
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.`);
const scrollContainer = document.querySelector('.cdk-overlay-pane .md-autocomplete-panel');
// 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.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();
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.`);
// Expect option bottom minus the panel height (288 - 256 = 32)
expect(scrollContainer.scrollTop).toEqual(32, `Expected panel to reveal the sixth option.`);
});
}));
it('should scroll to active options below the fold', () => {
fixture.componentInstance.trigger.openPanel();
fixture.detectChanges();
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.`);
// 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();
// Expect option bottom minus the panel height (288 - 256 = 32)
expect(scrollContainer.scrollTop).toEqual(32, `Expected panel to reveal the sixth option.`);
});
});
describe('aria', () => {
@@ -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.`);
});
}));
});