fix(autocomplete): fix down arrow use with ngIf (#3493)

This commit is contained in:
Kara
2017-03-10 11:19:37 -08:00
committed by tinayuangao
parent 4d4a63e641
commit a4e2de710f
5 changed files with 188 additions and 134 deletions
+1 -1
View File
@@ -205,7 +205,7 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
this.autocomplete._keyManager.onKeydown(event);
if (event.keyCode === UP_ARROW || event.keyCode === DOWN_ARROW) {
this.openPanel();
this._scrollToOption();
Promise.resolve().then(() => this._scrollToOption());
}
}
}
+157 -115
View File
@@ -1,4 +1,4 @@
import {TestBed, async, ComponentFixture} from '@angular/core/testing';
import {TestBed, async, fakeAsync, tick, ComponentFixture} from '@angular/core/testing';
import {Component, OnDestroy, QueryList, ViewChild, ViewChildren} from '@angular/core';
import {By} from '@angular/platform-browser';
import {MdAutocompleteModule, MdAutocompleteTrigger} from './index';
@@ -523,97 +523,88 @@ describe('MdAutocomplete', () => {
});
}));
it('should set the active item to the first option when DOWN key is pressed', async(() => {
fixture.whenStable().then(() => {
const optionEls =
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
it('should set the active item to the first option when DOWN key is pressed', fakeAsync(() => {
tick();
const optionEls =
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
tick();
fixture.detectChanges();
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('mat-active');
expect(optionEls[1].classList).not.toContain('mat-active');
expect(fixture.componentInstance.trigger.activeOption)
.toBe(fixture.componentInstance.options.first, 'Expected first option to be active.');
expect(optionEls[0].classList).toContain('mat-active');
expect(optionEls[1].classList).not.toContain('mat-active');
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
tick();
fixture.detectChanges();
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('mat-active');
expect(optionEls[1].classList).toContain('mat-active');
});
});
});
expect(fixture.componentInstance.trigger.activeOption)
.toBe(fixture.componentInstance.options.toArray()[1],
'Expected second option to be active.');
expect(optionEls[0].classList).not.toContain('mat-active');
expect(optionEls[1].classList).toContain('mat-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>;
it('should set the active item to the last option when UP key is pressed', fakeAsync(() => {
tick();
const optionEls =
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
const UP_ARROW_EVENT = new MockKeyboardEvent(UP_ARROW) as KeyboardEvent;
fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
const UP_ARROW_EVENT = new MockKeyboardEvent(UP_ARROW) as KeyboardEvent;
fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
tick();
fixture.detectChanges();
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('mat-active');
expect(optionEls[0].classList).not.toContain('mat-active');
expect(fixture.componentInstance.trigger.activeOption)
.toBe(fixture.componentInstance.options.last, 'Expected last option to be active.');
expect(optionEls[10].classList).toContain('mat-active');
expect(optionEls[0].classList).not.toContain('mat-active');
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
tick();
fixture.detectChanges();
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('mat-active');
expect(optionEls[10].classList).not.toContain('mat-active');
});
});
});
expect(fixture.componentInstance.trigger.activeOption)
.toBe(fixture.componentInstance.options.first,
'Expected first option to be active.');
expect(optionEls[0].classList).toContain('mat-active');
}));
it('should set the active item properly after filtering', async(() => {
fixture.whenStable().then(() => {
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
fixture.detectChanges();
it('should set the active item properly after filtering', fakeAsync(() => {
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
tick();
fixture.detectChanges();
fixture.whenStable().then(() => {
typeInElement('o', input);
fixture.detectChanges();
typeInElement('o', input);
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
tick();
fixture.detectChanges();
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
const optionEls =
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
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('mat-active');
expect(optionEls[1].classList).not.toContain('mat-active');
});
});
});
expect(fixture.componentInstance.trigger.activeOption)
.toBe(fixture.componentInstance.options.first,
'Expected first option to be active.');
expect(optionEls[0].classList).toContain('mat-active');
expect(optionEls[1].classList).not.toContain('mat-active');
}));
it('should fill the text field when an option is selected with ENTER', async(() => {
fixture.whenStable().then(() => {
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.`);
fixture.whenStable().then(() => {
fixture.detectChanges();
fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT);
fixture.detectChanges();
expect(input.value)
.toContain('Alabama', `Expected text field to fill with selected value on ENTER.`);
});
});
}));
@@ -624,11 +615,16 @@ describe('MdAutocomplete', () => {
const SPACE_EVENT = new MockKeyboardEvent(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.`);
fixture.whenStable().then(() => {
fixture.detectChanges();
fixture.componentInstance.trigger._handleKeydown(SPACE_EVENT);
fixture.detectChanges();
expect(input.value)
.not.toContain('New York', `Expected option not to be selected on SPACE.`);
});
});
}));
@@ -638,54 +634,74 @@ describe('MdAutocomplete', () => {
.toBe(false, `Expected control to start out pristine.`);
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT);
fixture.detectChanges();
fixture.whenStable().then(() => {
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.whenStable().then(() => {
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT);
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT);
fixture.detectChanges();
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.`);
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.`);
typeInElement('Alabama', input);
fixture.detectChanges();
typeInElement('Alabama', 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.`);
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', async(() => {
fixture.whenStable().then(() => {
const scrollContainer = document.querySelector('.cdk-overlay-pane .mat-autocomplete-panel');
it('should scroll to active options below the fold', fakeAsync(() => {
tick();
const scrollContainer =
document.querySelector('.cdk-overlay-pane .mat-autocomplete-panel');
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
tick();
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(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.`);
tick();
});
// 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 on UP arrow', fakeAsync(() => {
tick();
const scrollContainer =
document.querySelector('.cdk-overlay-pane .mat-autocomplete-panel');
const UP_ARROW_EVENT = new MockKeyboardEvent(UP_ARROW) as KeyboardEvent;
fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
tick();
fixture.detectChanges();
// Expect option bottom minus the panel height (528 - 256 = 272)
expect(scrollContainer.scrollTop).toEqual(272, `Expected panel to reveal last option.`);
}));
});
describe('aria', () => {
@@ -733,18 +749,23 @@ describe('MdAutocomplete', () => {
const DOWN_ARROW_EVENT = new MockKeyboardEvent(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.');
fixture.whenStable().then(() => {
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.');
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
fixture.detectChanges();
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
fixture.whenStable().then(() => {
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.');
});
}));
@@ -896,6 +917,26 @@ describe('MdAutocomplete', () => {
.toContain('Two', `Expected panel to display when input is focused.`);
});
it('should filter properly with ngIf after setting the active item', fakeAsync(() => {
const fixture = TestBed.createComponent(NgIfAutocomplete);
fixture.detectChanges();
fixture.componentInstance.trigger.openPanel();
tick();
fixture.detectChanges();
const DOWN_ARROW_EVENT = new MockKeyboardEvent(DOWN_ARROW) as KeyboardEvent;
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
tick();
fixture.detectChanges();
const input = fixture.debugElement.query(By.css('input')).nativeElement;
typeInElement('o', input);
fixture.detectChanges();
expect(fixture.componentInstance.mdOptions.length).toBe(2);
}));
});
});
@@ -973,9 +1014,10 @@ class NgIfAutocomplete {
optionCtrl = new FormControl();
filteredOptions: Observable<any>;
isVisible = true;
options = ['One', 'Two', 'Three'];
@ViewChild(MdAutocompleteTrigger) trigger: MdAutocompleteTrigger;
options = ['One', 'Two', 'Three'];
@ViewChildren(MdOption) mdOptions: QueryList<MdOption>;
constructor() {
this.filteredOptions = this.optionCtrl.valueChanges.startWith(null).map((val) => {
@@ -23,13 +23,15 @@ export class ActiveDescendantKeyManager extends ListKeyManager<Highlightable> {
* styles from the previously active item.
*/
setActiveItem(index: number): void {
if (this.activeItem) {
this.activeItem.setInactiveStyles();
}
super.setActiveItem(index);
if (this.activeItem) {
this.activeItem.setActiveStyles();
}
Promise.resolve().then(() => {
if (this.activeItem) {
this.activeItem.setInactiveStyles();
}
super.setActiveItem(index);
if (this.activeItem) {
this.activeItem.setActiveStyles();
}
});
}
}
+19 -9
View File
@@ -1,4 +1,5 @@
import {QueryList} from '@angular/core';
import {fakeAsync, tick} from '@angular/core/testing';
import {FocusKeyManager} from './focus-key-manager';
import {DOWN_ARROW, UP_ARROW, TAB, HOME, END} from '../keyboard/keycodes';
import {ListKeyManager} from './list-key-manager';
@@ -459,7 +460,7 @@ describe('Key managers', () => {
describe('ActiveDescendantKeyManager', () => {
let keyManager: ActiveDescendantKeyManager;
beforeEach(() => {
beforeEach(fakeAsync(() => {
itemList.items = [
new FakeHighlightable(),
new FakeHighlightable(),
@@ -470,6 +471,7 @@ describe('Key managers', () => {
// first item is already focused
keyManager.setFirstItemActive();
tick();
spyOn(itemList.items[0], 'setActiveStyles');
spyOn(itemList.items[1], 'setActiveStyles');
@@ -478,36 +480,44 @@ describe('Key managers', () => {
spyOn(itemList.items[0], 'setInactiveStyles');
spyOn(itemList.items[1], 'setInactiveStyles');
spyOn(itemList.items[2], 'setInactiveStyles');
});
}));
it('should set subsequent items as active with the DOWN arrow', () => {
it('should set subsequent items as active with the DOWN arrow', fakeAsync(() => {
keyManager.onKeydown(DOWN_ARROW_EVENT);
tick();
expect(itemList.items[1].setActiveStyles).toHaveBeenCalled();
expect(itemList.items[2].setActiveStyles).not.toHaveBeenCalled();
keyManager.onKeydown(DOWN_ARROW_EVENT);
expect(itemList.items[2].setActiveStyles).toHaveBeenCalled();
});
tick();
it('should set previous items as active with the UP arrow', () => {
expect(itemList.items[2].setActiveStyles).toHaveBeenCalled();
}));
it('should set previous items as active with the UP arrow', fakeAsync(() => {
keyManager.setLastItemActive();
tick();
keyManager.onKeydown(UP_ARROW_EVENT);
tick();
expect(itemList.items[1].setActiveStyles).toHaveBeenCalled();
expect(itemList.items[0].setActiveStyles).not.toHaveBeenCalled();
keyManager.onKeydown(UP_ARROW_EVENT);
tick();
expect(itemList.items[0].setActiveStyles).toHaveBeenCalled();
});
}));
it('should set inactive styles on previously active items', () => {
it('should set inactive styles on previously active items', fakeAsync(() => {
keyManager.onKeydown(DOWN_ARROW_EVENT);
tick();
expect(itemList.items[0].setInactiveStyles).toHaveBeenCalled();
keyManager.onKeydown(UP_ARROW_EVENT);
tick();
expect(itemList.items[1].setInactiveStyles).toHaveBeenCalled();
});
}));
});
+2 -2
View File
@@ -129,7 +129,7 @@ export class MdOption {
* events will display the proper options as active on arrow key events.
*/
setActiveStyles(): void {
Promise.resolve(null).then(() => this._active = true);
this._active = true;
}
/**
@@ -138,7 +138,7 @@ export class MdOption {
* events will display the proper options as active on arrow key events.
*/
setInactiveStyles(): void {
Promise.resolve(null).then(() => this._active = false);
this._active = false;
}
/** Ensures the option is selected when activated from the keyboard. */