From 28250c8b6d9759de5be660721a89577b22138a37 Mon Sep 17 00:00:00 2001
From: "Ahmad K. Bawaneh" <ahmad.bawaneh@dominokit.com>
Date: Thu, 25 Jul 2024 20:44:19 +0300
Subject: [PATCH] fix #948 Memory leak in menu and Popover.

---
 .../org/dominokit/domino/ui/menu/Menu.java    | 49 +++++++++++++------
 .../dominokit/domino/ui/popover/Popover.java  | 17 +++----
 .../domino/ui/utils/BaseDominoElement.java    | 35 +++++++++++++
 3 files changed, 76 insertions(+), 25 deletions(-)

diff --git a/domino-ui/src/main/java/org/dominokit/domino/ui/menu/Menu.java b/domino-ui/src/main/java/org/dominokit/domino/ui/menu/Menu.java
index 1bb8ed0c7..c4ec45df1 100644
--- a/domino-ui/src/main/java/org/dominokit/domino/ui/menu/Menu.java
+++ b/domino-ui/src/main/java/org/dominokit/domino/ui/menu/Menu.java
@@ -156,6 +156,7 @@ public class Menu<V> extends BaseDominoElement<HTMLDivElement, Menu<V>>
   private EventListener lostFocusListener;
   private boolean closeOnBlur = DominoUIConfig.CONFIG.isClosePopupOnBlur();
   private OpenMenuCondition<V> openMenuCondition = (menu) -> true;
+  private List<MediaQuery.MediaQueryListenerRecord> mediaQueryRecords = new ArrayList<>();
 
   /**
    * Factory method to create a new Menu instance.
@@ -355,19 +356,6 @@ public Menu() {
 
     element.addEventListener("keydown", keyboardNavigation);
 
-    MediaQuery.addOnSmallAndDownListener(
-        () -> {
-          if (centerOnSmallScreens) {
-            this.smallScreen = true;
-          }
-        });
-    MediaQuery.addOnMediumAndUpListener(
-        () -> {
-          if (centerOnSmallScreens) {
-            this.smallScreen = false;
-            backArrowContainer.remove();
-          }
-        });
     backIcon = LazyChild.of(Icons.keyboard_backspace().addCss(dui_menu_back_icon), menuHeader);
     backIcon.whenInitialized(
         () -> {
@@ -403,6 +391,38 @@ public Menu() {
           }
         };
 
+    nowAndWhenAttached(
+        () -> {
+          mediaQueryRecords.add(
+              MediaQuery.addOnSmallAndDownListener(
+                  () -> {
+                    if (centerOnSmallScreens) {
+                      this.smallScreen = true;
+                    }
+                  }));
+
+          mediaQueryRecords.add(
+              MediaQuery.addOnMediumAndUpListener(
+                  () -> {
+                    if (centerOnSmallScreens) {
+                      this.smallScreen = false;
+                      backArrowContainer.remove();
+                    }
+                  }));
+
+          DomGlobal.document.body.addEventListener("blur", lostFocusListener, true);
+          if (this.dropDown) {
+            document.addEventListener("scroll", repositionListener, true);
+          }
+        });
+
+    nowAndWhenDetached(
+        () -> {
+          DomGlobal.document.body.removeEventListener("blur", lostFocusListener, true);
+          document.removeEventListener("scroll", repositionListener, true);
+          mediaQueryRecords.forEach(MediaQuery.MediaQueryListenerRecord::remove);
+        });
+
     this.addEventListener(EventType.touchstart.getName(), Event::stopPropagation);
     this.addEventListener(EventType.touchend.getName(), Event::stopPropagation);
   }
@@ -1378,7 +1398,6 @@ private void doOpen(boolean focus) {
         menuHeader.get().insertFirst(backArrowContainer);
       }
       show();
-      DomGlobal.document.body.addEventListener("blur", lostFocusListener, true);
     }
   }
 
@@ -1570,7 +1589,6 @@ public Menu<V> close() {
                 menuTarget -> {
                   menuTarget.getTargetElement().element().focus();
                 });
-        DomGlobal.document.body.removeEventListener("blur", lostFocusListener, true);
         if (isSearchable()) {
           searchBox.get().clearSearch();
         }
@@ -1785,7 +1803,6 @@ private void setDropDown(boolean dropdown) {
     if (dropdown) {
       this.setAttribute("domino-ui-root-menu", true).setAttribute(DOMINO_UI_AUTO_CLOSABLE, true);
       menuElement.elevate(Elevation.LEVEL_1);
-      document.addEventListener("scroll", repositionListener, true);
     } else {
       this.removeAttribute("domino-ui-root-menu").removeAttribute(DOMINO_UI_AUTO_CLOSABLE);
       menuElement.elevate(Elevation.NONE);
diff --git a/domino-ui/src/main/java/org/dominokit/domino/ui/popover/Popover.java b/domino-ui/src/main/java/org/dominokit/domino/ui/popover/Popover.java
index d4caa916e..b664fd086 100644
--- a/domino-ui/src/main/java/org/dominokit/domino/ui/popover/Popover.java
+++ b/domino-ui/src/main/java/org/dominokit/domino/ui/popover/Popover.java
@@ -21,6 +21,8 @@
 
 import elemental2.dom.EventListener;
 import elemental2.dom.HTMLElement;
+import java.util.ArrayList;
+import java.util.List;
 import org.dominokit.domino.ui.IsElement;
 import org.dominokit.domino.ui.animations.Transition;
 import org.dominokit.domino.ui.collapsible.AnimationCollapseStrategy;
@@ -47,6 +49,7 @@
  */
 public class Popover extends BasePopover<Popover> {
 
+  private static boolean asDialog = false;
   /** Static initialization block to add a global click event listener for closing popovers. */
   static {
     document.body.addEventListener(
@@ -54,14 +57,18 @@ public class Popover extends BasePopover<Popover> {
         element -> {
           ModalBackDrop.INSTANCE.closePopovers("");
         });
+
+    MediaQuery.addOnSmallAndDownListener(() -> asDialog = true);
+
+    MediaQuery.addOnMediumAndUpListener(() -> asDialog = false);
   }
 
   private final EventListener showListener;
   private boolean openOnClick = true;
   private boolean closeOnEscape = true;
-  private boolean asDialog = false;
   private final DropDirection dialog = DropDirection.MIDDLE_SCREEN;
   private boolean modal = false;
+  private final List<MediaQuery.MediaQueryListenerRecord> mediaListenersRecords = new ArrayList<>();
 
   /**
    * Creates a new `Popover` instance for the specified HTML element target.
@@ -102,14 +109,6 @@ public Popover(HTMLElement target) {
         new AnimationCollapseStrategy(
             Transition.FADE_IN, Transition.FADE_OUT, CollapsibleDuration._300ms));
 
-    MediaQuery.addOnSmallAndDownListener(
-        () -> {
-          this.asDialog = true;
-        });
-    MediaQuery.addOnMediumAndUpListener(
-        () -> {
-          this.asDialog = false;
-        });
     addCollapseListener(() -> removeEventListener(DUI_REMOVE_POPOVERS, closeAllListener));
   }
 
diff --git a/domino-ui/src/main/java/org/dominokit/domino/ui/utils/BaseDominoElement.java b/domino-ui/src/main/java/org/dominokit/domino/ui/utils/BaseDominoElement.java
index d3ead7fe6..fa22d7d7b 100644
--- a/domino-ui/src/main/java/org/dominokit/domino/ui/utils/BaseDominoElement.java
+++ b/domino-ui/src/main/java/org/dominokit/domino/ui/utils/BaseDominoElement.java
@@ -694,6 +694,24 @@ public T nowOrWhenAttached(Runnable handler) {
     return (T) this;
   }
 
+  /**
+   * Executes a given handler either immediately if the element is already detached from the DOM or
+   * when it gets detached.
+   *
+   * @param handler The handler to execute.
+   * @return The modified DOM element.
+   */
+  @Editor.Ignore
+  public T nowOrWhenDetached(Runnable handler) {
+    if (isAttached()) {
+      onDetached(mutationRecord -> handler.run());
+    } else {
+      handler.run();
+    }
+    dominoUuidInitializer.apply();
+    return (T) this;
+  }
+
   /**
    * Executes a given handler when the element is attached to the DOM. If the element is already
    * attached, the handler is executed immediately.
@@ -711,6 +729,23 @@ public T nowAndWhenAttached(Runnable handler) {
     return (T) this;
   }
 
+  /**
+   * Executes a given handler when the element is detached from the DOM. If the element is already
+   * detached, the handler is executed immediately.
+   *
+   * @param handler The handler to execute.
+   * @return The modified DOM element.
+   */
+  @Editor.Ignore
+  public T nowAndWhenDetached(Runnable handler) {
+    if (!isAttached()) {
+      handler.run();
+    }
+    onDetached(mutationRecord -> handler.run());
+    dominoUuidInitializer.apply();
+    return (T) this;
+  }
+
   /**
    * Registers a resize handler to be notified when the size of this element changes.
    *