From ff60503ce6230aef34d10daf6ecebbf9f371f11a Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 23 Mar 2026 12:41:47 +0000 Subject: [PATCH] :bug: Fix removeChild crash on all portal components The previous fix (80b64c440c) only addressed portal-on-document* but there were 6 additional components that portaled directly to document.body, causing the same race condition when React attempted to remove a node that had already been detached during concurrent state updates (e.g. navigating away while a context menu is open). Apply the dedicated-container pattern consistently to all portal sites: modal, context menus, combobox dropdown, theme selector, and tooltip. Each component now creates a dedicated
container appended to body on mount and removed on cleanup, giving React an exclusive containerInfo for each portal instance. Signed-off-by: Andrey Antukh --- frontend/src/app/main/ui/ds/tooltip/tooltip.cljs | 9 ++++++++- frontend/src/app/main/ui/modal.cljs | 13 +++++++++---- .../workspace/tokens/management/context_menu.cljs | 10 ++++++++-- .../tokens/management/forms/controls/combobox.cljs | 9 ++++++++- .../tokens/management/node_context_menu.cljs | 8 +++++++- .../ui/workspace/tokens/themes/theme_selector.cljs | 11 +++++++++-- 6 files changed, 49 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs index 968361f865..f1598364d4 100644 --- a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs +++ b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs @@ -159,6 +159,8 @@ tooltip-ref (mf/use-ref nil) + container (mf/use-memo #(dom/create-element "div")) + id (d/nilv id internal-id) @@ -244,6 +246,11 @@ content aria-label)})] + (mf/with-effect [] + (let [body (dom/get-body)] + (dom/append-child! body container) + #(dom/remove-child! body container))) + (mf/use-effect (mf/deps tooltip-id) (fn [] @@ -295,4 +302,4 @@ [:div {:class (stl/css :tooltip-content)} content] [:div {:class (stl/css :tooltip-arrow) :id "tooltip-arrow"}]]]) - (.-body js/document)))])) + container))])) diff --git a/frontend/src/app/main/ui/modal.cljs b/frontend/src/app/main/ui/modal.cljs index 9d260de69e..ce09cc71da 100644 --- a/frontend/src/app/main/ui/modal.cljs +++ b/frontend/src/app/main/ui/modal.cljs @@ -83,7 +83,12 @@ (mf/defc modal-container* {::mf/props :obj} [] - (when-let [modal (mf/deref ref:modal)] - (mf/portal - (mf/html [:> modal-wrapper* {:data modal :key (dm/str (:id modal))}]) - (dom/get-body)))) + (let [container (mf/use-memo #(dom/create-element "div"))] + (mf/with-effect [] + (let [body (dom/get-body)] + (dom/append-child! body container) + #(dom/remove-child! body container))) + (when-let [modal (mf/deref ref:modal)] + (mf/portal + (mf/html [:> modal-wrapper* {:data modal :key (dm/str (:id modal))}]) + container)))) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs index eb43f4a23b..22e1915e64 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs @@ -515,7 +515,13 @@ dropdown-direction (deref dropdown-direction*) dropdown-direction-change* (mf/use-ref 0) top (+ (get-in mdata [:position :y]) 5) - left (+ (get-in mdata [:position :x]) 5)] + left (+ (get-in mdata [:position :x]) 5) + container (mf/use-memo #(dom/create-element "div"))] + + (mf/with-effect [] + (let [body (dom/get-body)] + (dom/append-child! body container) + #(dom/remove-child! body container))) (mf/use-effect (mf/deps is-open?) @@ -554,4 +560,4 @@ :on-context-menu prevent-default} (when mdata [:& token-context-menu-tree (assoc mdata :width @width :on-delete-token on-delete-token)])]]) - (dom/get-body))))) + container)))) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs index d53b8d0d60..2c5b4e25cd 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs @@ -92,6 +92,8 @@ icon-button-ref (mf/use-ref nil) ref (or ref internal-ref) + container (mf/use-memo #(dom/create-element "div")) + raw-tokens-by-type (mf/use-ctx muc/active-tokens-by-type) filtered-tokens-by-type @@ -267,6 +269,11 @@ (mf/with-effect [dropdown-options] (mf/set-ref-val! options-ref dropdown-options)) + (mf/with-effect [] + (let [body (dom/get-body)] + (dom/append-child! body container) + #(dom/remove-child! body container))) + (mf/with-effect [is-open* ref wrapper-ref] (when is-open (let [handler (fn [event] @@ -305,4 +312,4 @@ :empty-to-end empty-to-end :wrapper-ref dropdown-ref :ref set-option-ref}]) - (dom/get-body))))])) \ No newline at end of file + container)))])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.cljs index 4e272f7bdd..c9b6947316 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.cljs @@ -35,6 +35,7 @@ dropdown-direction-change* (mf/use-ref 0) top (+ (get-in mdata [:position :y]) 5) left (+ (get-in mdata [:position :x]) 5) + container (mf/use-memo #(dom/create-element "div")) delete-node (mf/use-fn (mf/deps mdata) @@ -44,6 +45,11 @@ (when node (on-delete-node node type)))))] + (mf/with-effect [] + (let [body (dom/get-body)] + (dom/append-child! body container) + #(dom/remove-child! body container))) + (mf/with-effect [is-open?] (when (and (not= 0 (mf/ref-val dropdown-direction-change*)) (= false is-open?)) (reset! dropdown-direction* "down") @@ -80,4 +86,4 @@ :type "button" :on-click delete-node} (tr "labels.delete")]]])]]) - (dom/get-body))))) + container)))) diff --git a/frontend/src/app/main/ui/workspace/tokens/themes/theme_selector.cljs b/frontend/src/app/main/ui/workspace/tokens/themes/theme_selector.cljs index 3d799e0b59..35b775c9c6 100644 --- a/frontend/src/app/main/ui/workspace/tokens/themes/theme_selector.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/themes/theme_selector.cljs @@ -111,7 +111,14 @@ (let [rect (dom/get-bounding-rect node)] (swap! state* assoc :is-open? true - :rect rect))))))] + :rect rect)))))) + + container (mf/use-memo #(dom/create-element "div"))] + + (mf/with-effect [] + (let [body (dom/get-body)] + (dom/append-child! body container) + #(dom/remove-child! body container))) [:div {:on-click on-open-dropdown :disabled (not can-edit?) @@ -140,4 +147,4 @@ [:& theme-options {:active-theme-paths active-theme-paths :themes themes :on-close on-close-dropdown}]]]) - (dom/get-body)))])) + container))]))