Skip to content

Commit 44c9e0b

Browse files
crisbetoandrewseguin
authored andcommitted
fix(overlay): overlay directives not emitting when detached externally (#7950)
Currently the `ConnectedOverlayDirective` only emits the `detach` event when it _thinks_ that the overlay is detached (escape press, backdrop click etc.), but this won't necessarily be correct (e.g. when it was closed by a scroll strategy). These changes refactor the outputs to always be one-to-one with the `OverlayRef` detachments.
1 parent ba5c2fe commit 44c9e0b

File tree

2 files changed

+72
-16
lines changed

2 files changed

+72
-16
lines changed

src/cdk/overlay/overlay-directives.spec.ts

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,21 @@ import {
88
dispatchEvent,
99
} from '@angular/cdk/testing/private';
1010
import {ESCAPE, A} from '@angular/cdk/keycodes';
11-
import {Overlay, CdkConnectedOverlay, OverlayModule, CdkOverlayOrigin} from './index';
11+
import {
12+
Overlay,
13+
CdkConnectedOverlay,
14+
OverlayModule,
15+
CdkOverlayOrigin,
16+
ScrollDispatcher,
17+
ScrollStrategy,
18+
} from './index';
1219
import {OverlayContainer} from './overlay-container';
1320
import {
1421
ConnectedOverlayPositionChange,
1522
ConnectionPositionPair,
1623
} from './position/connected-position';
1724
import {FlexibleConnectedPositionStrategy} from './position/flexible-connected-position-strategy';
25+
import {Subject} from 'rxjs';
1826

1927

2028
describe('Overlay directives', () => {
@@ -23,12 +31,17 @@ describe('Overlay directives', () => {
2331
let overlayContainerElement: HTMLElement;
2432
let fixture: ComponentFixture<ConnectedOverlayDirectiveTest>;
2533
let dir: {value: string};
34+
let scrolledSubject = new Subject();
2635

2736
beforeEach(() => {
2837
TestBed.configureTestingModule({
2938
imports: [OverlayModule],
3039
declarations: [ConnectedOverlayDirectiveTest, ConnectedOverlayPropertyInitOrder],
31-
providers: [{provide: Directionality, useFactory: () => dir = {value: 'ltr'}}],
40+
providers: [{provide: Directionality, useFactory: () => dir = {value: 'ltr'}},
41+
{provide: ScrollDispatcher, useFactory: () => ({
42+
scrolled: () => scrolledSubject.asObservable()
43+
})}
44+
],
3245
});
3346
});
3447

@@ -529,7 +542,7 @@ describe('Overlay directives', () => {
529542
});
530543

531544
describe('outputs', () => {
532-
it('should emit backdropClick appropriately', () => {
545+
it('should emit when the backdrop was clicked', () => {
533546
fixture.componentInstance.hasBackdrop = true;
534547
fixture.componentInstance.isOpen = true;
535548
fixture.detectChanges();
@@ -543,7 +556,7 @@ describe('Overlay directives', () => {
543556
.toHaveBeenCalledWith(jasmine.any(MouseEvent));
544557
});
545558

546-
it('should emit positionChange appropriately', () => {
559+
it('should emit when the position has changed', () => {
547560
expect(fixture.componentInstance.positionChangeHandler).not.toHaveBeenCalled();
548561
fixture.componentInstance.isOpen = true;
549562
fixture.detectChanges();
@@ -556,15 +569,24 @@ describe('Overlay directives', () => {
556569
.toBe(true, `Expected directive to emit an instance of ConnectedOverlayPositionChange.`);
557570
});
558571

559-
it('should emit attach and detach appropriately', () => {
572+
it('should emit when attached', () => {
560573
expect(fixture.componentInstance.attachHandler).not.toHaveBeenCalled();
561-
expect(fixture.componentInstance.detachHandler).not.toHaveBeenCalled();
562574
fixture.componentInstance.isOpen = true;
563575
fixture.detectChanges();
564576

565577
expect(fixture.componentInstance.attachHandler).toHaveBeenCalled();
566578
expect(fixture.componentInstance.attachResult instanceof HTMLElement)
567579
.toBe(true, `Expected pane to be populated with HTML elements when attach was called.`);
580+
581+
fixture.componentInstance.isOpen = false;
582+
fixture.detectChanges();
583+
});
584+
585+
it('should emit when detached', () => {
586+
expect(fixture.componentInstance.detachHandler).not.toHaveBeenCalled();
587+
fixture.componentInstance.isOpen = true;
588+
fixture.detectChanges();
589+
568590
expect(fixture.componentInstance.detachHandler).not.toHaveBeenCalled();
569591

570592
fixture.componentInstance.isOpen = false;
@@ -584,11 +606,40 @@ describe('Overlay directives', () => {
584606
expect(fixture.componentInstance.keydownHandler).toHaveBeenCalledWith(event);
585607
});
586608

609+
it('should emit when detached externally', () => {
610+
expect(fixture.componentInstance.detachHandler).not.toHaveBeenCalled();
611+
fixture.componentInstance.scrollStrategy = overlay.scrollStrategies.close();
612+
fixture.componentInstance.isOpen = true;
613+
fixture.detectChanges();
614+
615+
expect(fixture.componentInstance.detachHandler).not.toHaveBeenCalled();
616+
617+
scrolledSubject.next();
618+
fixture.detectChanges();
619+
620+
expect(fixture.componentInstance.detachHandler).toHaveBeenCalled();
621+
});
622+
623+
// This is intended as a simplified example of a more complicated bug in g3. Technically
624+
// these events shouldn't invoke their listeners after destruction anyway, but in some
625+
// tests it can happen. For more context: https://github.com/crisbeto/material2/pull/10
626+
it('should not emit after the directive is destroyed', () => {
627+
const spy = jasmine.createSpy('detach spy');
628+
const subscription =
629+
fixture.componentInstance.connectedOverlayDirective.detach.subscribe(spy);
630+
631+
fixture.componentInstance.isOpen = true;
632+
fixture.detectChanges();
633+
fixture.destroy();
634+
635+
expect(spy).not.toHaveBeenCalled();
636+
subscription.unsubscribe();
637+
});
638+
587639
});
588640

589641
});
590642

591-
592643
@Component({
593644
template: `
594645
<button cdk-overlay-origin id="trigger" #trigger="cdkOverlayOrigin">Toggle menu</button>
@@ -605,6 +656,7 @@ describe('Overlay directives', () => {
605656
[cdkConnectedOverlayFlexibleDimensions]="flexibleDimensions"
606657
[cdkConnectedOverlayGrowAfterOpen]="growAfterOpen"
607658
[cdkConnectedOverlayPush]="push"
659+
[cdkConnectedOverlayScrollStrategy]="scrollStrategy"
608660
cdkConnectedOverlayBackdropClass="mat-test-class"
609661
cdkConnectedOverlayPanelClass="cdk-test-panel-class"
610662
(backdropClick)="backdropClickHandler($event)"
@@ -640,13 +692,14 @@ class ConnectedOverlayDirectiveTest {
640692
flexibleDimensions: boolean;
641693
growAfterOpen: boolean;
642694
push: boolean;
695+
scrollStrategy: ScrollStrategy;
643696
backdropClickHandler = jasmine.createSpy('backdropClick handler');
644697
positionChangeHandler = jasmine.createSpy('positionChange handler');
645698
keydownHandler = jasmine.createSpy('keydown handler');
646699
positionOverrides: ConnectionPositionPair[];
647700
attachHandler = jasmine.createSpy('attachHandler').and.callFake(() => {
648-
this.attachResult =
649-
this.connectedOverlayDirective.overlayRef.overlayElement.querySelector('p') as HTMLElement;
701+
const overlayElement = this.connectedOverlayDirective.overlayRef.overlayElement;
702+
this.attachResult = overlayElement.querySelector('p') as HTMLElement;
650703
});
651704
detachHandler = jasmine.createSpy('detachHandler');
652705
attachResult: HTMLElement;

src/cdk/overlay/overlay-directives.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
111111
private _flexibleDimensions = false;
112112
private _push = false;
113113
private _backdropSubscription = Subscription.EMPTY;
114+
private _attachSubscription = Subscription.EMPTY;
115+
private _detachSubscription = Subscription.EMPTY;
114116
private _offsetX: number;
115117
private _offsetY: number;
116118
private _position: FlexibleConnectedPositionStrategy;
@@ -246,11 +248,13 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
246248
}
247249

248250
ngOnDestroy() {
251+
this._attachSubscription.unsubscribe();
252+
this._detachSubscription.unsubscribe();
253+
this._backdropSubscription.unsubscribe();
254+
249255
if (this._overlayRef) {
250256
this._overlayRef.dispose();
251257
}
252-
253-
this._backdropSubscription.unsubscribe();
254258
}
255259

256260
ngOnChanges(changes: SimpleChanges) {
@@ -279,9 +283,10 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
279283
this.positions = defaultPositionList;
280284
}
281285

282-
this._overlayRef = this._overlay.create(this._buildConfig());
283-
284-
this._overlayRef.keydownEvents().subscribe((event: KeyboardEvent) => {
286+
const overlayRef = this._overlayRef = this._overlay.create(this._buildConfig());
287+
this._attachSubscription = overlayRef.attachments().subscribe(() => this.attach.emit());
288+
this._detachSubscription = overlayRef.detachments().subscribe(() => this.detach.emit());
289+
overlayRef.keydownEvents().subscribe((event: KeyboardEvent) => {
285290
this.overlayKeydown.next(event);
286291

287292
if (event.keyCode === ESCAPE && !hasModifierKey(event)) {
@@ -373,7 +378,6 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
373378

374379
if (!this._overlayRef.hasAttached()) {
375380
this._overlayRef.attach(this._templatePortal);
376-
this.attach.emit();
377381
}
378382

379383
if (this.hasBackdrop) {
@@ -389,7 +393,6 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
389393
private _detachOverlay() {
390394
if (this._overlayRef) {
391395
this._overlayRef.detach();
392-
this.detach.emit();
393396
}
394397

395398
this._backdropSubscription.unsubscribe();

0 commit comments

Comments
 (0)