From 89998aa1d9c163a37dae665a04519905552b1eaa Mon Sep 17 00:00:00 2001 From: Yossi Synett Date: Mon, 19 May 2025 14:27:43 +0300 Subject: [PATCH 1/2] Fix memory leak due to focus listeners not being deleted --- src/Virtual.ts | 34 +++++++++------------------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/src/Virtual.ts b/src/Virtual.ts index 564ff2f..388c9ff 100644 --- a/src/Virtual.ts +++ b/src/Virtual.ts @@ -257,8 +257,6 @@ export class Virtual { this.#treeCache = this.#container && tree ? flattenTree(this.#container, tree, null) : []; - - this.#attachFocusListeners(); } return this.#treeCache; @@ -290,28 +288,9 @@ export class Virtual { } #invalidateTreeCache() { - this.#detachFocusListeners(); this.#treeCache = null; } - #attachFocusListeners() { - this.#getAccessibilityTree().forEach((treeNode) => { - treeNode.node.addEventListener( - "focus", - this.#handleFocusChange.bind(this) - ); - }); - } - - #detachFocusListeners() { - this.#getAccessibilityTree().forEach((treeNode) => { - treeNode.node.removeEventListener( - "focus", - this.#handleFocusChange.bind(this) - ); - }); - } - async #handleFocusChange({ target }: Event) { await tick(); @@ -322,10 +301,13 @@ export class Virtual { return; } - // We've covered the tree having no length so there must be at least one - // index or we default back to the beginning of the tree. - const newActiveNode = - tree.find(({ node }) => node === target) ?? tree.at(0)!; + // We've covered the tree having no length so there should be at least one + // matching node, but if not we will not update the state + const newActiveNode = tree.find(({ node }) => node === target); + + if (!newActiveNode) { + return; + } this.#updateState(newActiveNode, true); } @@ -619,6 +601,8 @@ export class Virtual { return; } + this.#container.addEventListener("focusin", this.#handleFocusChange.bind(this)); + this.#updateState(tree[0]); return; From 46e431183fccf71e2aba2734626424edb29b0631 Mon Sep 17 00:00:00 2001 From: Yossi Synett Date: Mon, 19 May 2025 18:51:15 +0300 Subject: [PATCH 2/2] Remove event listener on container on stop --- src/Virtual.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Virtual.ts b/src/Virtual.ts index 388c9ff..e5b13a4 100644 --- a/src/Virtual.ts +++ b/src/Virtual.ts @@ -205,6 +205,7 @@ export class Virtual { #spokenPhraseLog: string[] = []; #treeCache: AccessibilityNode[] | null = null; #disconnectDOMObserver: (() => void) | null = null; + #boundHandleFocusChange: ((event: Event) => Promise) | null = null; #checkContainer() { if (!this.#container) { @@ -601,7 +602,9 @@ export class Virtual { return; } - this.#container.addEventListener("focusin", this.#handleFocusChange.bind(this)); + this.#boundHandleFocusChange = this.#handleFocusChange.bind(this); + + this.#container.addEventListener("focusin", this.#boundHandleFocusChange); this.#updateState(tree[0]); @@ -631,6 +634,7 @@ export class Virtual { */ async stop() { this.#disconnectDOMObserver?.(); + this.#container?.removeEventListener("focusin", this.#boundHandleFocusChange); this.#invalidateTreeCache(); if (this.#cursor) { @@ -642,7 +646,7 @@ export class Virtual { this.#container = null; this.#itemTextLog = []; this.#spokenPhraseLog = []; - + this.#boundHandleFocusChange = null; return; }