Skip to content

Commit e5d961c

Browse files
authored
Merge pull request #3038 from Akshat55/overflow-mem-leak
fix: mark event service as deprecated and unsubscribe from fromEvent observables
2 parents 627c272 + edb8ab2 commit e5d961c

File tree

3 files changed

+62
-41
lines changed

3 files changed

+62
-41
lines changed

src/dialog/dialog.directive.ts

Lines changed: 52 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { DialogService } from "./dialog.service";
1717
import { CloseMeta, CloseReasons, DialogConfig } from "./dialog-config.interface";
1818
import { EventService } from "carbon-components-angular/utils";
1919
import { Dialog } from "./dialog.component";
20+
import { fromEvent, Subscription } from "rxjs";
2021

2122
/**
2223
* A generic directive that can be inherited from to create dialogs (for example, a tooltip or popover)
@@ -125,6 +126,8 @@ export class DialogDirective implements OnInit, OnDestroy, OnChanges {
125126
*/
126127
protected dialogRef: ComponentRef<Dialog>;
127128

129+
private subscriptions: Subscription[] = [];
130+
128131
/**
129132
* Creates an instance of DialogDirective.
130133
* @param elementRef
@@ -136,6 +139,9 @@ export class DialogDirective implements OnInit, OnDestroy, OnChanges {
136139
protected elementRef: ElementRef,
137140
protected viewContainerRef: ViewContainerRef,
138141
protected dialogService: DialogService,
142+
/**
143+
* Deprecated as of v5
144+
*/
139145
protected eventService: EventService
140146
) {}
141147

@@ -180,49 +186,55 @@ export class DialogDirective implements OnInit, OnDestroy, OnChanges {
180186
// fix for safari hijacking clicks
181187
this.dialogService.singletonClickListen();
182188

183-
const element = this.elementRef.nativeElement;
189+
const element: HTMLElement = this.elementRef.nativeElement;
184190

185-
this.eventService.on(element, "keydown", (event: KeyboardEvent) => {
186-
if (event.target === this.dialogConfig.parentRef.nativeElement &&
187-
(event.key === "Tab" || event.key === "Tab" && event.shiftKey) ||
188-
event.key === "Escape") {
189-
this.close({
190-
reason: CloseReasons.interaction,
191-
target: event.target
192-
});
193-
}
194-
});
191+
this.subscriptions.push(
192+
fromEvent(element, "keydown").subscribe((event: KeyboardEvent) => {
193+
if (event.target === this.dialogConfig.parentRef.nativeElement &&
194+
(event.key === "Tab" || event.key === "Tab" && event.shiftKey) ||
195+
event.key === "Escape") {
196+
this.close({
197+
reason: CloseReasons.interaction,
198+
target: event.target
199+
});
200+
}
201+
})
202+
);
195203

196204
// bind events for hovering or clicking the host
197205
if (this.trigger === "hover" || this.trigger === "mouseenter") {
198-
this.eventService.on(element, "mouseenter", this.open.bind(this));
199-
this.eventService.on(element, this.closeTrigger, (event) => {
200-
this.close({
201-
reason: CloseReasons.interaction,
202-
target: event.target
203-
});
204-
});
205-
this.eventService.on(element, "focus", this.open.bind(this));
206-
this.eventService.on(element, "blur", (event) => {
207-
this.close({
208-
reason: CloseReasons.interaction,
209-
target: event.target
210-
});
211-
});
206+
this.subscriptions.push(
207+
fromEvent(element, "mouseenter").subscribe(() => this.open()),
208+
fromEvent(element, this.closeTrigger).subscribe((event) => {
209+
this.close({
210+
reason: CloseReasons.interaction,
211+
target: event.target
212+
});
213+
}),
214+
fromEvent(element, "focus").subscribe(() => this.open()),
215+
fromEvent(element, "blur").subscribe((event) => {
216+
this.close({
217+
reason: CloseReasons.interaction,
218+
target: event.target
219+
});
220+
})
221+
);
212222
} else {
213-
this.eventService.on(element, "click", (event) => {
214-
this.toggle({
215-
reason: CloseReasons.interaction,
216-
target: event.target
217-
});
218-
});
219-
this.eventService.on(element, "keydown", (event: KeyboardEvent) => {
220-
if (event.key === "Enter" || event.key === " ") {
221-
setTimeout(() => {
222-
this.open();
223+
this.subscriptions.push(
224+
fromEvent(element, "click").subscribe((event) => {
225+
this.toggle({
226+
reason: CloseReasons.interaction,
227+
target: event.target
223228
});
224-
}
225-
});
229+
}),
230+
fromEvent(element, "keydown").subscribe((event: KeyboardEvent) => {
231+
if (event.key === "Enter" || event.key === " ") {
232+
setTimeout(() => {
233+
this.open();
234+
});
235+
}
236+
})
237+
);
226238
}
227239

228240
DialogDirective.dialogCounter++;
@@ -241,6 +253,7 @@ export class DialogDirective implements OnInit, OnDestroy, OnChanges {
241253
this.close({
242254
reason: CloseReasons.destroyed
243255
});
256+
this.subscriptions.forEach((subscription) => subscription.unsubscribe());
244257
}
245258

246259
/**
@@ -259,7 +272,7 @@ export class DialogDirective implements OnInit, OnDestroy, OnChanges {
259272

260273
// Handles emitting all the close events to clean everything up
261274
// Also enforce accessibility on close by updating an aria attr on the nativeElement.
262-
this.dialogRef.instance.close.subscribe((meta: CloseMeta) => {
275+
const subscription = this.dialogRef.instance.close.subscribe((meta: CloseMeta) => {
263276
if (!this.dialogRef) { return; }
264277
if (this.dialogConfig.shouldClose && this.dialogConfig.shouldClose(meta)) {
265278
// close the dialog, emit events, and clear out the open states
@@ -268,6 +281,7 @@ export class DialogDirective implements OnInit, OnDestroy, OnChanges {
268281
this.isOpen = false;
269282
this.onClose.emit();
270283
this.isOpenChange.emit(false);
284+
subscription.unsubscribe();
271285
}
272286
});
273287

src/dialog/overflow-menu/overflow-menu.stories.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const Template = (args) => ({
5454
});
5555
export const Basic = Template.bind({});
5656
Basic.args = {
57+
show: true,
5758
open: false,
5859
flip: false,
5960
offset: {

src/toggletip/toggletip.component.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import {
99
HostListener,
1010
Input,
1111
NgZone,
12+
OnDestroy,
1213
Renderer2
1314
} from "@angular/core";
14-
import { fromEvent } from "rxjs";
15+
import { fromEvent, Subscription } from "rxjs";
1516
import { PopoverContainer } from "carbon-components-angular/popover";
1617
import { ToggletipButton } from "./toggletip-button.directive";
1718

@@ -34,7 +35,7 @@ import { ToggletipButton } from "./toggletip-button.directive";
3435
</cds-popover-content>
3536
`
3637
})
37-
export class Toggletip extends PopoverContainer implements AfterViewInit {
38+
export class Toggletip extends PopoverContainer implements AfterViewInit, OnDestroy {
3839
static toggletipCounter = 0;
3940

4041
@Input() id = `tooltip-${Toggletip.toggletipCounter++}`;
@@ -45,6 +46,7 @@ export class Toggletip extends PopoverContainer implements AfterViewInit {
4546
@ContentChild(ToggletipButton, { read: ElementRef }) btn!: ElementRef;
4647

4748
documentClick = this.handleFocusOut.bind(this);
49+
private subscription: Subscription;
4850

4951
constructor(
5052
protected hostElement: ElementRef,
@@ -61,7 +63,7 @@ export class Toggletip extends PopoverContainer implements AfterViewInit {
6163
this.initializeReferences();
6264

6365
// Listen for click events on trigger
64-
fromEvent(this.btn.nativeElement, "click")
66+
this.subscription = fromEvent(this.btn.nativeElement, "click")
6567
.subscribe((event: Event) => {
6668
// Add/Remove event listener based on isOpen to improve performance when there
6769
// are a lot of toggletips
@@ -98,6 +100,10 @@ export class Toggletip extends PopoverContainer implements AfterViewInit {
98100
}
99101
}
100102

103+
ngOnDestroy(): void {
104+
this.subscription.unsubscribe();
105+
}
106+
101107
private handleExpansion(state = false, event: Event) {
102108
this.handleChange(state, event);
103109
if (this.btn) {

0 commit comments

Comments
 (0)