diff --git a/frontend/src/app/main/ui/components/portal.cljs b/frontend/src/app/main/ui/components/portal.cljs index ff9f3558d4..381db4b66c 100644 --- a/frontend/src/app/main/ui/components/portal.cljs +++ b/frontend/src/app/main/ui/components/portal.cljs @@ -6,16 +6,12 @@ (ns app.main.ui.components.portal (:require - [app.util.dom :as dom] + [app.main.ui.hooks :as hooks] [rumext.v2 :as mf])) (mf/defc portal-on-document* [{:keys [children]}] - (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))) + (let [container (hooks/use-portal-container)] (mf/portal (mf/html [:* children]) container))) diff --git a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs index 4c35d37303..f2375f2aa8 100644 --- a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs +++ b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs @@ -9,6 +9,7 @@ [app.main.style :as stl]) (:require [app.common.data :as d] + [app.main.ui.hooks :as hooks] [app.util.dom :as dom] [app.util.keyboard :as kbd] [app.util.timers :as ts] @@ -159,6 +160,8 @@ tooltip-ref (mf/use-ref nil) + container (hooks/use-portal-container) + id (d/nilv id internal-id) @@ -292,4 +295,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/hooks.cljs b/frontend/src/app/main/ui/hooks.cljs index b4ad8fe616..42560cd8fe 100644 --- a/frontend/src/app/main/ui/hooks.cljs +++ b/frontend/src/app/main/ui/hooks.cljs @@ -380,6 +380,18 @@ state)) +(defn use-portal-container + "Creates a dedicated div container for React portals. The container + is appended to document.body on mount and removed on cleanup, preventing + removeChild race conditions when multiple portals target the same 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))) + container)) + (defn use-dynamic-grid-item-width ([] (use-dynamic-grid-item-width nil)) ([itemsize] diff --git a/frontend/src/app/main/ui/modal.cljs b/frontend/src/app/main/ui/modal.cljs index 9d260de69e..5df1cc3daa 100644 --- a/frontend/src/app/main/ui/modal.cljs +++ b/frontend/src/app/main/ui/modal.cljs @@ -10,6 +10,7 @@ [app.common.data.macros :as dm] [app.main.data.modal :as modal] [app.main.store :as st] + [app.main.ui.hooks :as hooks] [app.util.dom :as dom] [app.util.keyboard :as k] [goog.events :as events] @@ -83,7 +84,8 @@ (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 (hooks/use-portal-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..0dd980f73f 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 @@ -20,6 +20,7 @@ [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] + [app.main.ui.hooks :as hooks] [app.util.dom :as dom] [app.util.i18n :refer [tr]] [app.util.timers :as timers] @@ -515,7 +516,8 @@ 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 (hooks/use-portal-container)] (mf/use-effect (mf/deps is-open?) @@ -554,4 +556,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..3e6dec43b1 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 @@ -19,6 +19,7 @@ [app.main.ui.ds.controls.shared.options-dropdown :refer [options-dropdown*]] [app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.forms :as fc] + [app.main.ui.hooks :as hooks] [app.main.ui.workspace.tokens.management.forms.controls.combobox-navigation :refer [use-navigation]] [app.main.ui.workspace.tokens.management.forms.controls.floating-dropdown :refer [use-floating-dropdown]] [app.main.ui.workspace.tokens.management.forms.controls.token-parsing :as tp] @@ -92,6 +93,8 @@ icon-button-ref (mf/use-ref nil) ref (or ref internal-ref) + container (hooks/use-portal-container) + raw-tokens-by-type (mf/use-ctx muc/active-tokens-by-type) filtered-tokens-by-type @@ -305,4 +308,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..d37e628d02 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 @@ -6,6 +6,7 @@ [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] + [app.main.ui.hooks :as hooks] [app.util.dom :as dom] [app.util.i18n :refer [tr]] [okulary.core :as l] @@ -35,6 +36,7 @@ dropdown-direction-change* (mf/use-ref 0) top (+ (get-in mdata [:position :y]) 5) left (+ (get-in mdata [:position :x]) 5) + container (hooks/use-portal-container) delete-node (mf/use-fn (mf/deps mdata) @@ -80,4 +82,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..a8687c9719 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 @@ -17,6 +17,7 @@ [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.ds.foundations.typography.text :refer [text*]] + [app.main.ui.hooks :as hooks] [app.util.dom :as dom] [app.util.i18n :refer [tr]] [cuerdas.core :as str] @@ -111,7 +112,9 @@ (let [rect (dom/get-bounding-rect node)] (swap! state* assoc :is-open? true - :rect rect))))))] + :rect rect)))))) + + container (hooks/use-portal-container)] [:div {:on-click on-open-dropdown :disabled (not can-edit?) @@ -140,4 +143,4 @@ [:& theme-options {:active-theme-paths active-theme-paths :themes themes :on-close on-close-dropdown}]]]) - (dom/get-body)))])) + container))]))