mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(autocomplete): close panel when options list is empty (#2834)
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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.`);
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user