Skip to content

Commit 43e163b

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 ec4ea06 commit 43e163b

File tree

5 files changed

+213
-322
lines changed

5 files changed

+213
-322
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
@@ -540,13 +540,12 @@ describe('Key managers', () => {
540540
describe('ActiveDescendantKeyManager', () => {
541541
let keyManager: ActiveDescendantKeyManager<FakeHighlightable>;
542542

543-
beforeEach(fakeAsync(() => {
543+
beforeEach(() => {
544544
itemList.items = [new FakeHighlightable(), new FakeHighlightable(), new FakeHighlightable()];
545545
keyManager = new ActiveDescendantKeyManager<FakeHighlightable>(itemList);
546546

547547
// first item is already focused
548548
keyManager.setFirstItemActive();
549-
tick();
550549

551550
spyOn(itemList.items[0], 'setActiveStyles');
552551
spyOn(itemList.items[1], 'setActiveStyles');
@@ -555,44 +554,38 @@ describe('Key managers', () => {
555554
spyOn(itemList.items[0], 'setInactiveStyles');
556555
spyOn(itemList.items[1], 'setInactiveStyles');
557556
spyOn(itemList.items[2], 'setInactiveStyles');
558-
}));
557+
});
559558

560-
it('should set subsequent items as active with the DOWN arrow', fakeAsync(() => {
559+
it('should set subsequent items as active with the DOWN arrow', () => {
561560
keyManager.onKeydown(fakeKeyEvents.downArrow);
562-
tick();
563561

564562
expect(itemList.items[1].setActiveStyles).toHaveBeenCalled();
565563
expect(itemList.items[2].setActiveStyles).not.toHaveBeenCalled();
566564

567565
keyManager.onKeydown(fakeKeyEvents.downArrow);
568-
tick();
569566

570567
expect(itemList.items[2].setActiveStyles).toHaveBeenCalled();
571-
}));
568+
});
572569

573-
it('should set previous items as active with the UP arrow', fakeAsync(() => {
570+
it('should set previous items as active with the UP arrow', () => {
574571
keyManager.setLastItemActive();
575-
tick();
576-
577572
keyManager.onKeydown(fakeKeyEvents.upArrow);
578-
tick();
573+
579574
expect(itemList.items[1].setActiveStyles).toHaveBeenCalled();
580575
expect(itemList.items[0].setActiveStyles).not.toHaveBeenCalled();
581576

582577
keyManager.onKeydown(fakeKeyEvents.upArrow);
583-
tick();
578+
584579
expect(itemList.items[0].setActiveStyles).toHaveBeenCalled();
585-
}));
580+
});
586581

587-
it('should set inactive styles on previously active items', fakeAsync(() => {
582+
it('should set inactive styles on previously active items', () => {
588583
keyManager.onKeydown(fakeKeyEvents.downArrow);
589-
tick();
590584
expect(itemList.items[0].setInactiveStyles).toHaveBeenCalled();
591585

592586
keyManager.onKeydown(fakeKeyEvents.upArrow);
593-
tick();
594587
expect(itemList.items[1].setInactiveStyles).toHaveBeenCalled();
595-
}));
588+
});
596589

597590
});
598591

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,
@@ -291,11 +291,9 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
291291
this.openPanel();
292292
}
293293

294-
Promise.resolve().then(() => {
295-
if (isArrowKey || this.autocomplete._keyManager.activeItem !== prevActiveItem) {
296-
this._scrollToOption();
297-
}
298-
});
294+
if (isArrowKey || this.autocomplete._keyManager.activeItem !== prevActiveItem) {
295+
this._scrollToOption();
296+
}
299297
}
300298
}
301299

@@ -373,8 +371,13 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
373371
*/
374372
private _subscribeToClosingActions(): Subscription {
375373
const firstStable = first.call(this._zone.onStable);
376-
const optionChanges = map.call(this.autocomplete.options.changes, () =>
377-
this._positionStrategy.recalculateLastPosition());
374+
const optionChanges = switchMap.call(this.autocomplete.options.changes, () => {
375+
this._positionStrategy.recalculateLastPosition();
376+
377+
// Defer emitting to the stream until the next tick, because changing
378+
// bindings in here will cause "changed after checked" errors.
379+
return Promise.resolve();
380+
});
378381

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

0 commit comments

Comments
 (0)