Skip to content

Commit c1caf0e

Browse files
committed
fix(autocomplete): fix down arrow use with ngIf
1 parent f27617a commit c1caf0e

File tree

5 files changed

+186
-132
lines changed

5 files changed

+186
-132
lines changed

src/lib/autocomplete/autocomplete-trigger.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
205205
this.autocomplete._keyManager.onKeydown(event);
206206
if (event.keyCode === UP_ARROW || event.keyCode === DOWN_ARROW) {
207207
this.openPanel();
208-
this._scrollToOption();
208+
Promise.resolve().then(() => this._scrollToOption());
209209
}
210210
}
211211
}

src/lib/autocomplete/autocomplete.spec.ts

Lines changed: 156 additions & 114 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,54 +629,74 @@ 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

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

670+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
671+
tick();
672+
fixture.detectChanges();
673+
expect(scrollContainer.scrollTop).toEqual(0, `Expected panel not to scroll.`);
674+
675+
// These down arrows will set the 6th option active, below the fold.
676+
[1, 2, 3, 4, 5].forEach(() => {
669677
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
670-
fixture.detectChanges();
671-
expect(scrollContainer.scrollTop).toEqual(0, `Expected panel not to scroll.`);
678+
tick();
679+
});
672680

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();
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+
}));
678685

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-
});
686+
it('should scroll to active options on UP arrow', fakeAsync(() => {
687+
tick();
688+
const scrollContainer =
689+
document.querySelector('.cdk-overlay-pane .mat-autocomplete-panel');
690+
691+
const UP_ARROW_EVENT = new MockKeyboardEvent(UP_ARROW) as KeyboardEvent;
692+
fixture.componentInstance.trigger._handleKeydown(UP_ARROW_EVENT);
693+
tick();
694+
fixture.detectChanges();
682695

696+
// Expect option bottom minus the panel height (528 - 256 = 272)
697+
expect(scrollContainer.scrollTop).toEqual(272, `Expected panel to reveal last option.`);
683698
}));
699+
684700
});
685701

686702
describe('aria', () => {
@@ -728,18 +744,23 @@ describe('MdAutocomplete', () => {
728744

729745
const DOWN_ARROW_EVENT = new MockKeyboardEvent(DOWN_ARROW) as KeyboardEvent;
730746
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
731-
fixture.detectChanges();
732747

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.');
748+
fixture.whenStable().then(() => {
749+
fixture.detectChanges();
750+
expect(input.getAttribute('aria-activedescendant'))
751+
.toEqual(fixture.componentInstance.options.first.id,
752+
'Expected aria-activedescendant to match the active item after 1 down arrow.');
736753

737-
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
738-
fixture.detectChanges();
754+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
755+
fixture.whenStable().then(() => {
756+
fixture.detectChanges();
757+
758+
expect(input.getAttribute('aria-activedescendant'))
759+
.toEqual(fixture.componentInstance.options.toArray()[1].id,
760+
'Expected aria-activedescendant to match the active item after 2 down arrows.');
761+
});
762+
});
739763

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.');
743764
});
744765
}));
745766

@@ -879,6 +900,26 @@ describe('MdAutocomplete', () => {
879900
.toContain('Two', `Expected panel to display when input is focused.`);
880901
});
881902

903+
it('should filter properly with ngIf after setting the active item', fakeAsync(() => {
904+
const fixture = TestBed.createComponent(NgIfAutocomplete);
905+
fixture.detectChanges();
906+
907+
fixture.componentInstance.trigger.openPanel();
908+
tick();
909+
fixture.detectChanges();
910+
911+
const DOWN_ARROW_EVENT = new MockKeyboardEvent(DOWN_ARROW) as KeyboardEvent;
912+
fixture.componentInstance.trigger._handleKeydown(DOWN_ARROW_EVENT);
913+
tick();
914+
fixture.detectChanges();
915+
916+
const input = fixture.debugElement.query(By.css('input')).nativeElement;
917+
typeInElement('o', input);
918+
fixture.detectChanges();
919+
920+
expect(fixture.componentInstance.mdOptions.length).toBe(2);
921+
}));
922+
882923
});
883924
});
884925

@@ -956,9 +997,10 @@ class NgIfAutocomplete {
956997
optionCtrl = new FormControl();
957998
filteredOptions: Observable<any>;
958999
isVisible = true;
1000+
options = ['One', 'Two', 'Three'];
9591001

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

9631005
constructor() {
9641006
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)