Skip to content

Commit 7ee7131

Browse files
committed
fix(autocomplete): fix down arrow use with ngIf
1 parent d78a370 commit 7ee7131

File tree

4 files changed

+170
-128
lines changed

4 files changed

+170
-128
lines changed

src/lib/autocomplete/autocomplete.spec.ts

Lines changed: 141 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {TestBed, async, ComponentFixture} from '@angular/core/testing';
1+
import {TestBed, async, fakeAsync, tick, ComponentFixture} from '@angular/core/testing';
22
import {Component, OnDestroy, QueryList, ViewChild, ViewChildren} from '@angular/core';
33
import {By} from '@angular/platform-browser';
44
import {MdAutocompleteModule, MdAutocompleteTrigger} from './index';
@@ -518,97 +518,88 @@ describe('MdAutocomplete', () => {
518518
});
519519
}));
520520

521-
it('should set the active item to the first option when DOWN key is pressed', async(() => {
522-
fixture.whenStable().then(() => {
523-
const optionEls =
524-
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
521+
it('should set the active item to the first option when DOWN key is pressed', fakeAsync(() => {
522+
tick();
523+
const optionEls =
524+
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
525525

526-
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
526+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
527+
tick();
528+
fixture.detectChanges();
527529

528-
fixture.whenStable().then(() => {
529-
fixture.detectChanges();
530-
expect(fixture.componentInstance.trigger.activeOption)
531-
.toBe(fixture.componentInstance.options.first, 'Expected first option to be active.');
532-
expect(optionEls[0].classList).toContain('mat-active');
533-
expect(optionEls[1].classList).not.toContain('mat-active');
530+
expect(fixture.componentInstance.trigger.activeOption)
531+
.toBe(fixture.componentInstance.options.first, 'Expected first option to be active.');
532+
expect(optionEls[0].classList).toContain('mat-active');
533+
expect(optionEls[1].classList).not.toContain('mat-active');
534534

535-
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
535+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
536+
tick();
537+
fixture.detectChanges();
536538

537-
fixture.whenStable().then(() => {
538-
fixture.detectChanges();
539-
expect(fixture.componentInstance.trigger.activeOption)
540-
.toBe(fixture.componentInstance.options.toArray()[1],
541-
'Expected second option to be active.');
542-
expect(optionEls[0].classList).not.toContain('mat-active');
543-
expect(optionEls[1].classList).toContain('mat-active');
544-
});
545-
});
546-
});
539+
expect(fixture.componentInstance.trigger.activeOption)
540+
.toBe(fixture.componentInstance.options.toArray()[1],
541+
'Expected second option to be active.');
542+
expect(optionEls[0].classList).not.toContain('mat-active');
543+
expect(optionEls[1].classList).toContain('mat-active');
547544
}));
548545

549-
it('should set the active item to the last option when UP key is pressed', async(() => {
550-
fixture.whenStable().then(() => {
551-
const optionEls =
552-
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
546+
it('should set the active item to the last option when UP key is pressed', fakeAsync(() => {
547+
tick();
548+
const optionEls =
549+
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
553550

554-
const UP_ARROW_EVENT = new MockKeyboardEvent(UP_ARROW) as KeyboardEvent;
555-
fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
551+
const UP_ARROW_EVENT = new MockKeyboardEvent(UP_ARROW) as KeyboardEvent;
552+
fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
553+
tick();
554+
fixture.detectChanges();
556555

557-
fixture.whenStable().then(() => {
558-
fixture.detectChanges();
559-
expect(fixture.componentInstance.trigger.activeOption)
560-
.toBe(fixture.componentInstance.options.last, 'Expected last option to be active.');
561-
expect(optionEls[10].classList).toContain('mat-active');
562-
expect(optionEls[0].classList).not.toContain('mat-active');
556+
expect(fixture.componentInstance.trigger.activeOption)
557+
.toBe(fixture.componentInstance.options.last, 'Expected last option to be active.');
558+
expect(optionEls[10].classList).toContain('mat-active');
559+
expect(optionEls[0].classList).not.toContain('mat-active');
563560

564-
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
561+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
562+
tick();
563+
fixture.detectChanges();
565564

566-
fixture.whenStable().then(() => {
567-
fixture.detectChanges();
568-
expect(fixture.componentInstance.trigger.activeOption)
569-
.toBe(fixture.componentInstance.options.first,
570-
'Expected first option to be active.');
571-
expect(optionEls[0].classList).toContain('mat-active');
572-
expect(optionEls[10].classList).not.toContain('mat-active');
573-
});
574-
});
575-
});
565+
expect(fixture.componentInstance.trigger.activeOption)
566+
.toBe(fixture.componentInstance.options.first,
567+
'Expected first option to be active.');
568+
expect(optionEls[0].classList).toContain('mat-active');
576569
}));
577570

578-
it('should set the active item properly after filtering', async(() => {
579-
fixture.whenStable().then(() => {
580-
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
581-
fixture.detectChanges();
571+
it('should set the active item properly after filtering', fakeAsync(() => {
572+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
573+
tick();
574+
fixture.detectChanges();
582575

583-
fixture.whenStable().then(() => {
584-
typeInElement('o', input);
585-
fixture.detectChanges();
576+
typeInElement('o', input);
577+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
578+
tick();
579+
fixture.detectChanges();
586580

587-
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
581+
const optionEls =
582+
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
588583

589-
fixture.whenStable().then(() => {
590-
fixture.detectChanges();
591-
const optionEls =
592-
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
593-
594-
expect(fixture.componentInstance.trigger.activeOption)
595-
.toBe(fixture.componentInstance.options.first,
596-
'Expected first option to be active.');
597-
expect(optionEls[0].classList).toContain('mat-active');
598-
expect(optionEls[1].classList).not.toContain('mat-active');
599-
});
600-
});
601-
});
584+
expect(fixture.componentInstance.trigger.activeOption)
585+
.toBe(fixture.componentInstance.options.first,
586+
'Expected first option to be active.');
587+
expect(optionEls[0].classList).toContain('mat-active');
588+
expect(optionEls[1].classList).not.toContain('mat-active');
602589
}));
603590

604591
it('should fill the text field when an option is selected with ENTER', async(() => {
605592
fixture.whenStable().then(() => {
606593
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
607-
fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT);
608594

609-
fixture.detectChanges();
610-
expect(input.value)
611-
.toContain('Alabama', `Expected text field to fill with selected value on ENTER.`);
595+
fixture.whenStable().then(() => {
596+
fixture.detectChanges();
597+
598+
fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT);
599+
fixture.detectChanges();
600+
expect(input.value)
601+
.toContain('Alabama', `Expected text field to fill with selected value on ENTER.`);
602+
});
612603
});
613604
}));
614605

@@ -619,11 +610,16 @@ describe('MdAutocomplete', () => {
619610

620611
const SPACE_EVENT = new MockKeyboardEvent(SPACE) as KeyboardEvent;
621612
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
622-
fixture.componentInstance.trigger._handleKeydown(SPACE_EVENT);
623-
fixture.detectChanges();
624613

625-
expect(input.value)
626-
.not.toContain('New York', `Expected option not to be selected on SPACE.`);
614+
fixture.whenStable().then(() => {
615+
fixture.detectChanges();
616+
617+
fixture.componentInstance.trigger._handleKeydown(SPACE_EVENT);
618+
fixture.detectChanges();
619+
620+
expect(input.value)
621+
.not.toContain('New York', `Expected option not to be selected on SPACE.`);
622+
});
627623
});
628624
}));
629625

@@ -633,51 +629,59 @@ describe('MdAutocomplete', () => {
633629
.toBe(false, `Expected control to start out pristine.`);
634630

635631
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
636-
fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT);
637-
fixture.detectChanges();
632+
fixture.whenStable().then(() => {
633+
fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT);
634+
fixture.detectChanges();
638635

639-
expect(fixture.componentInstance.stateCtrl.dirty)
640-
.toBe(true, `Expected control to become dirty when option was selected by ENTER.`);
636+
expect(fixture.componentInstance.stateCtrl.dirty)
637+
.toBe(true, `Expected control to become dirty when option was selected by ENTER.`);
638+
});
641639
});
642640
}));
643641

644642
it('should open the panel again when typing after making a selection', async(() => {
645643
fixture.whenStable().then(() => {
646644
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
647-
fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT);
648-
fixture.detectChanges();
645+
fixture.whenStable().then(() => {
646+
fixture.componentInstance.trigger._handleKeydown(ENTER_EVENT);
647+
fixture.detectChanges();
649648

650-
expect(fixture.componentInstance.trigger.panelOpen)
651-
.toBe(false, `Expected panel state to read closed after ENTER key.`);
652-
expect(overlayContainerElement.textContent)
653-
.toEqual('', `Expected panel to close after ENTER key.`);
649+
expect(fixture.componentInstance.trigger.panelOpen)
650+
.toBe(false, `Expected panel state to read closed after ENTER key.`);
651+
expect(overlayContainerElement.textContent)
652+
.toEqual('', `Expected panel to close after ENTER key.`);
654653

655-
typeInElement('Alabama', input);
656-
fixture.detectChanges();
654+
typeInElement('Alabama', input);
655+
fixture.detectChanges();
657656

658-
expect(fixture.componentInstance.trigger.panelOpen)
659-
.toBe(true, `Expected panel state to read open when typing in input.`);
660-
expect(overlayContainerElement.textContent)
661-
.toContain('Alabama', `Expected panel to display when typing in input.`);
657+
expect(fixture.componentInstance.trigger.panelOpen)
658+
.toBe(true, `Expected panel state to read open when typing in input.`);
659+
expect(overlayContainerElement.textContent)
660+
.toContain('Alabama', `Expected panel to display when typing in input.`);
661+
});
662662
});
663663
}));
664664

665665
it('should scroll to active options below the fold', async(() => {
666666
fixture.whenStable().then(() => {
667-
const scrollContainer = document.querySelector('.cdk-overlay-pane .mat-autocomplete-panel');
667+
const scrollContainer =
668+
document.querySelector('.cdk-overlay-pane .mat-autocomplete-panel');
668669

669670
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
670-
fixture.detectChanges();
671-
expect(scrollContainer.scrollTop).toEqual(0, `Expected panel not to scroll.`);
671+
fixture.whenStable().then(() => {
672+
fixture.detectChanges();
673+
expect(scrollContainer.scrollTop).toEqual(0, `Expected panel not to scroll.`);
672674

673-
// These down arrows will set the 6th option active, below the fold.
674-
[1, 2, 3, 4, 5].forEach(() => {
675-
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
676-
});
677-
fixture.detectChanges();
675+
// These down arrows will set the 6th option active, below the fold.
676+
[1, 2, 3, 4, 5].forEach(() => {
677+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
678+
});
679+
fixture.detectChanges();
678680

679-
// Expect option bottom minus the panel height (288 - 256 = 32)
680-
expect(scrollContainer.scrollTop).toEqual(32, `Expected panel to reveal the sixth option.`);
681+
// Expect option bottom minus the panel height (288 - 256 = 32)
682+
expect(scrollContainer.scrollTop)
683+
.toEqual(32, `Expected panel to reveal the sixth option.`);
684+
});
681685
});
682686

683687
}));
@@ -728,18 +732,23 @@ describe('MdAutocomplete', () => {
728732

729733
const DOWN_ARROW_EVENT = new MockKeyboardEvent(DOWN_ARROW) as KeyboardEvent;
730734
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
731-
fixture.detectChanges();
732735

733-
expect(input.getAttribute('aria-activedescendant'))
734-
.toEqual(fixture.componentInstance.options.first.id,
735-
'Expected aria-activedescendant to match the active item after 1 down arrow.');
736+
fixture.whenStable().then(() => {
737+
fixture.detectChanges();
738+
expect(input.getAttribute('aria-activedescendant'))
739+
.toEqual(fixture.componentInstance.options.first.id,
740+
'Expected aria-activedescendant to match the active item after 1 down arrow.');
736741

737-
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
738-
fixture.detectChanges();
742+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
743+
fixture.whenStable().then(() => {
744+
fixture.detectChanges();
745+
746+
expect(input.getAttribute('aria-activedescendant'))
747+
.toEqual(fixture.componentInstance.options.toArray()[1].id,
748+
'Expected aria-activedescendant to match the active item after 2 down arrows.');
749+
});
750+
});
739751

740-
expect(input.getAttribute('aria-activedescendant'))
741-
.toEqual(fixture.componentInstance.options.toArray()[1].id,
742-
'Expected aria-activedescendant to match the active item after 2 down arrows.');
743752
});
744753
}));
745754

@@ -879,6 +888,26 @@ describe('MdAutocomplete', () => {
879888
.toContain('Two', `Expected panel to display when input is focused.`);
880889
});
881890

891+
it('should filter properly with ngIf after setting the active item', fakeAsync(() => {
892+
const fixture = TestBed.createComponent(NgIfAutocomplete);
893+
fixture.detectChanges();
894+
895+
fixture.componentInstance.trigger.openPanel();
896+
tick();
897+
fixture.detectChanges();
898+
899+
const DOWN_ARROW_EVENT = new MockKeyboardEvent(DOWN_ARROW) as KeyboardEvent;
900+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
901+
tick();
902+
fixture.detectChanges();
903+
904+
const input = fixture.debugElement.query(By.css('input')).nativeElement;
905+
typeInElement('o', input);
906+
fixture.detectChanges();
907+
908+
expect(fixture.componentInstance.mdOptions.length).toBe(2);
909+
}));
910+
882911
});
883912
});
884913

@@ -956,9 +985,10 @@ class NgIfAutocomplete {
956985
optionCtrl = new FormControl();
957986
filteredOptions: Observable<any>;
958987
isVisible = true;
988+
options = ['One', 'Two', 'Three'];
959989

960990
@ViewChild(MdAutocompleteTrigger) trigger: MdAutocompleteTrigger;
961-
options = ['One', 'Two', 'Three'];
991+
@ViewChildren(MdOption) mdOptions: QueryList<MdOption>;
962992

963993
constructor() {
964994
this.filteredOptions = this.optionCtrl.valueChanges.startWith(null).map((val) => {

src/lib/core/a11y/activedescendant-key-manager.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,15 @@ export class ActiveDescendantKeyManager extends ListKeyManager<Highlightable> {
2323
* styles from the previously active item.
2424
*/
2525
setActiveItem(index: number): void {
26-
if (this.activeItem) {
27-
this.activeItem.setInactiveStyles();
28-
}
29-
super.setActiveItem(index);
30-
if (this.activeItem) {
31-
this.activeItem.setActiveStyles();
32-
}
26+
Promise.resolve().then(() => {
27+
if (this.activeItem) {
28+
this.activeItem.setInactiveStyles();
29+
}
30+
super.setActiveItem(index);
31+
if (this.activeItem) {
32+
this.activeItem.setActiveStyles();
33+
}
34+
});
3335
}
3436

3537
}

0 commit comments

Comments
 (0)