Skip to content

Commit 5da9ee7

Browse files
committed
refactor(autocomplete): make keyboard and visibility calls synchronous
Reworks a few methods, that were changing the view in a `Promise.resolve`, to be synchronous. The initial idea behind deferring those calls was to avoid "changed after checked" errors, however they also make things harder to test. It seems like the root of the issue for all of them was that we were trying to modify view properties after the `options.changed` callback had fired, which comes after change detection. These changes have the advantage of making things a lot easier and cleaner to test, as well as making the API easier to use.
1 parent b5fc68b commit 5da9ee7

File tree

6 files changed

+219
-330
lines changed

6 files changed

+219
-330
lines changed

src/cdk/a11y/activedescendant-key-manager.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,13 @@ export class ActiveDescendantKeyManager<T> extends ListKeyManager<Highlightable
2626
* styles from the previously active item.
2727
*/
2828
setActiveItem(index: number): void {
29-
Promise.resolve().then(() => {
30-
if (this.activeItem) {
31-
this.activeItem.setInactiveStyles();
32-
}
33-
super.setActiveItem(index);
34-
if (this.activeItem) {
35-
this.activeItem.setActiveStyles();
36-
}
37-
});
29+
if (this.activeItem) {
30+
this.activeItem.setInactiveStyles();
31+
}
32+
super.setActiveItem(index);
33+
if (this.activeItem) {
34+
this.activeItem.setActiveStyles();
35+
}
3836
}
3937

4038
}

src/cdk/a11y/list-key-manager.spec.ts

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -505,13 +505,12 @@ describe('Key managers', () => {
505505
describe('ActiveDescendantKeyManager', () => {
506506
let keyManager: ActiveDescendantKeyManager<FakeHighlightable>;
507507

508-
beforeEach(fakeAsync(() => {
508+
beforeEach(() => {
509509
itemList.items = [new FakeHighlightable(), new FakeHighlightable(), new FakeHighlightable()];
510510
keyManager = new ActiveDescendantKeyManager<FakeHighlightable>(itemList);
511511

512512
// first item is already focused
513513
keyManager.setFirstItemActive();
514-
tick();
515514

516515
spyOn(itemList.items[0], 'setActiveStyles');
517516
spyOn(itemList.items[1], 'setActiveStyles');
@@ -520,44 +519,38 @@ describe('Key managers', () => {
520519
spyOn(itemList.items[0], 'setInactiveStyles');
521520
spyOn(itemList.items[1], 'setInactiveStyles');
522521
spyOn(itemList.items[2], 'setInactiveStyles');
523-
}));
522+
});
524523

525-
it('should set subsequent items as active with the DOWN arrow', fakeAsync(() => {
524+
it('should set subsequent items as active with the DOWN arrow', () => {
526525
keyManager.onKeydown(fakeKeyEvents.downArrow);
527-
tick();
528526

529527
expect(itemList.items[1].setActiveStyles).toHaveBeenCalled();
530528
expect(itemList.items[2].setActiveStyles).not.toHaveBeenCalled();
531529

532530
keyManager.onKeydown(fakeKeyEvents.downArrow);
533-
tick();
534531

535532
expect(itemList.items[2].setActiveStyles).toHaveBeenCalled();
536-
}));
533+
});
537534

538-
it('should set previous items as active with the UP arrow', fakeAsync(() => {
535+
it('should set previous items as active with the UP arrow', () => {
539536
keyManager.setLastItemActive();
540-
tick();
541-
542537
keyManager.onKeydown(fakeKeyEvents.upArrow);
543-
tick();
538+
544539
expect(itemList.items[1].setActiveStyles).toHaveBeenCalled();
545540
expect(itemList.items[0].setActiveStyles).not.toHaveBeenCalled();
546541

547542
keyManager.onKeydown(fakeKeyEvents.upArrow);
548-
tick();
543+
549544
expect(itemList.items[0].setActiveStyles).toHaveBeenCalled();
550-
}));
545+
});
551546

552-
it('should set inactive styles on previously active items', fakeAsync(() => {
547+
it('should set inactive styles on previously active items', () => {
553548
keyManager.onKeydown(fakeKeyEvents.downArrow);
554-
tick();
555549
expect(itemList.items[0].setInactiveStyles).toHaveBeenCalled();
556550

557551
keyManager.onKeydown(fakeKeyEvents.upArrow);
558-
tick();
559552
expect(itemList.items[1].setInactiveStyles).toHaveBeenCalled();
560-
}));
553+
});
561554

562555
});
563556

src/lib/autocomplete/autocomplete-trigger.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
2424
import {DOCUMENT} from '@angular/platform-browser';
2525
import {Directionality} from '@angular/cdk/bidi';
26-
import {filter, first, map, RxChain, switchMap} from '@angular/cdk/rxjs';
26+
import {filter, first, RxChain, switchMap} from '@angular/cdk/rxjs';
2727
import {
2828
ConnectedPositionStrategy,
2929
Overlay,
@@ -309,11 +309,9 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
309309
this.openPanel();
310310
}
311311

312-
Promise.resolve().then(() => {
313-
if (isArrowKey || this.autocomplete._keyManager.activeItem !== prevActiveItem) {
314-
this._scrollToOption();
315-
}
316-
});
312+
if (isArrowKey || this.autocomplete._keyManager.activeItem !== prevActiveItem) {
313+
this._scrollToOption();
314+
}
317315
}
318316
}
319317

@@ -378,8 +376,13 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
378376
*/
379377
private _subscribeToClosingActions(): Subscription {
380378
const firstStable = first.call(this._zone.onStable);
381-
const optionChanges = map.call(this.autocomplete.options.changes, () =>
382-
this._positionStrategy.recalculateLastPosition());
379+
const optionChanges = switchMap.call(this.autocomplete.options.changes, () => {
380+
this._positionStrategy.recalculateLastPosition();
381+
382+
// Defer emitting to the stream until the next tick, because changing
383+
// bindings in here will cause "changed after checked" errors.
384+
return Promise.resolve();
385+
});
383386

384387
// When the zone is stable initially, and when the option list changes...
385388
return RxChain.from(merge(firstStable, optionChanges))

0 commit comments

Comments
 (0)