mirror of
https://github.com/penpot/penpot.git
synced 2026-03-28 14:20:27 +01:00
♻️ Extract use-portal-container hook to reduce duplication (#8798)
The dedicated-container portal pattern was repeated across 6 components. Extract it into a reusable use-portal-container hook under app.main.ui.hooks.
This commit is contained in:
@@ -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)))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -283,4 +286,4 @@
|
||||
[:div {:class (stl/css :tooltip-content)} content]
|
||||
[:div {:class (stl/css :tooltip-arrow)
|
||||
:id "tooltip-arrow"}]]])
|
||||
(.-body js/document)))]))
|
||||
container))]))
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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))))
|
||||
|
||||
@@ -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))))
|
||||
|
||||
@@ -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))))
|
||||
|
||||
@@ -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))]))
|
||||
|
||||
Reference in New Issue
Block a user