Skip to content

Commit

Permalink
fix(autocomplete): fix down arrow use with ngIf (#3493)
Browse files Browse the repository at this point in the history
  • Loading branch information
kara authored and tinayuangao committed Mar 10, 2017
1 parent 4d4a63e commit a4e2de7
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 132 deletions.
2 changes: 1 addition & 1 deletion src/lib/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
}
Expand Down
270 changes: 156 additions & 114 deletions src/lib/autocomplete/autocomplete.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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.`);
});
});
}));

Expand All @@ -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.`);
});
});
}));

Expand All @@ -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.`);
tick();
});

// 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.`);
}));

// 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', () => {
Expand Down Expand Up @@ -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.');
});
}));

Expand Down Expand Up @@ -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);
}));

});
});

Expand Down Expand Up @@ -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) => {
Expand Down
16 changes: 9 additions & 7 deletions src/lib/core/a11y/activedescendant-key-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
});
}

}
Loading

0 comments on commit a4e2de7

Please sign in to comment.