From b372e892827f272acf307950d9bdec47cc4aa66d Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 10 Jul 2026 07:22:11 +0000 Subject: [PATCH] :bug: Fix leaked deferred DOM ops on dashboard navigation and template clone The React reconciliation "removeChild" error surfaced during rapid dashboard navigation because several effects scheduled deferred DOM operations (focus, CSS positioning) without returning a cleanup that cancelled them. When the component unmounted before the callback fired, it ran against stale DOM and desynchronized React fiber tree from the actual DOM. - context_menu_a11y.cljs: replace tm/schedule-on-idle (30s idle window) with tm/schedule (setTimeout 0) and return a rx/dispose! cleanup. - dropdown.cljs: capture the tm/schedule handle and dispose it in the effect cleanup. - tooltip.cljs: capture the ts/raf handle and cancel it on cleanup. AI-assisted-by: opencode-go/mimo-v2.5-pro --- .../main/ui/components/context_menu_a11y.cljs | 8 +++- .../src/app/main/ui/components/dropdown.cljs | 8 ++-- .../src/app/main/ui/ds/tooltip/tooltip.cljs | 37 ++++++++++--------- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/frontend/src/app/main/ui/components/context_menu_a11y.cljs b/frontend/src/app/main/ui/components/context_menu_a11y.cljs index 75c97554ce..e2e824c19e 100644 --- a/frontend/src/app/main/ui/components/context_menu_a11y.cljs +++ b/frontend/src/app/main/ui/components/context_menu_a11y.cljs @@ -18,6 +18,7 @@ [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] [app.util.timers :as tm] + [beicon.v2.core :as rx] [rumext.v2 :as mf])) (def ^:private xf:options @@ -229,8 +230,11 @@ (partial ug/unlisten "penpot:context-menu:open" on-event))) (mf/with-effect [ids] - (tm/schedule-on-idle - #(dom/focus! (dom/get-element (first ids))))) + (let [handle (tm/schedule + (fn [] + (some-> (dom/get-element (first ids)) + (dom/focus!))))] + #(rx/dispose! handle))) (when (some? levels) [:> dropdown-content* props diff --git a/frontend/src/app/main/ui/components/dropdown.cljs b/frontend/src/app/main/ui/components/dropdown.cljs index 894d9ef204..4a0a1b0590 100644 --- a/frontend/src/app/main/ui/components/dropdown.cljs +++ b/frontend/src/app/main/ui/components/dropdown.cljs @@ -11,6 +11,7 @@ [app.util.globals :as globals] [app.util.keyboard :as kbd] [app.util.timers :as tm] + [beicon.v2.core :as rx] [goog.events :as events] [rumext.v2 :as mf]) (:import goog.events.EventType)) @@ -45,9 +46,10 @@ (fn [] (let [keys [(events/listen globals/document EventType.CLICK on-click) (events/listen globals/document EventType.CONTEXTMENU on-click) - (events/listen globals/document EventType.KEYUP on-keyup)]] - (tm/schedule #(mf/set-ref-val! listening-ref true)) - #(run! events/unlistenByKey keys)))] + (events/listen globals/document EventType.KEYUP on-keyup)] + timer (tm/schedule #(mf/set-ref-val! listening-ref true))] + #(do (rx/dispose! timer) + (run! events/unlistenByKey keys))))] (mf/use-effect on-mount) children)) diff --git a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs index bc20c517c9..e3d6b153fb 100644 --- a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs +++ b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs @@ -322,25 +322,26 @@ (let [trigger-el (mf/ref-val trigger-ref) tooltip-el (mf/ref-val tooltip-ref)] (when (and trigger-el tooltip-el) - (ts/raf - (fn [] - (let [origin-brect (dom/get-bounding-rect trigger-el) - tooltip-brect (dom/get-bounding-rect tooltip-el) - window-size (dom/get-window-size)] - (when-let [[new-placement placement-rect] - (find-matching-placement - placement - tooltip-brect - origin-brect - window-size - offset)] - (dom/set-css-property! tooltip-el "inset-block-start" - (str (:top placement-rect) "px")) - (dom/set-css-property! tooltip-el "inset-inline-start" - (str (:left placement-rect) "px")) + (let [raf-id (ts/raf + (fn [] + (let [origin-brect (dom/get-bounding-rect trigger-el) + tooltip-brect (dom/get-bounding-rect tooltip-el) + window-size (dom/get-window-size)] + (when-let [[new-placement placement-rect] + (find-matching-placement + placement + tooltip-brect + origin-brect + window-size + offset)] + (dom/set-css-property! tooltip-el "inset-block-start" + (str (:top placement-rect) "px")) + (dom/set-css-property! tooltip-el "inset-inline-start" + (str (:left placement-rect) "px")) - (when (not= new-placement placement) - (reset! placement* new-placement))))))))))) + (when (not= new-placement placement) + (reset! placement* new-placement))))))] + #(ts/cancel-af! raf-id))))))) [:> :div props children