0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-21 06:02:32 -05:00

Make modal work through react portal mechanism

The rationale behind this change is to allow use of already
declared react context on modals; because with portals, react
propagates top context to the children, independently if they
are direct descendant on dom or not.
This commit is contained in:
Andrey Antukh 2024-12-10 16:01:50 +01:00
parent 7758d5f747
commit d8a3c10191
7 changed files with 60 additions and 58 deletions

View file

@ -20,8 +20,8 @@
:git/url "https://github.com/funcool/beicon.git"}
funcool/rumext
{:git/tag "v2.14"
:git/sha "0016623"
{:git/tag "v2.15"
:git/sha "28783a7"
:git/url "https://github.com/funcool/rumext.git"}
instaparse/instaparse {:mvn/version "1.5.0"}

View file

@ -23,7 +23,6 @@
[app.main.ui.confirm]
[app.main.ui.css-cursors :as cur]
[app.main.ui.delete-shared]
[app.main.ui.modal :refer [modal]]
[app.main.ui.routes :as rt]
[app.main.worker :as worker]
[app.plugins :as plugins]
@ -52,14 +51,9 @@
(let [el (dom/get-element "app")]
(mf/create-root el)))
(defonce modal-root
(let [el (dom/get-element "modal")]
(mf/create-root el)))
(defn init-ui
[]
(mf/render! app-root (mf/element ui/app))
(mf/render! modal-root (mf/element modal)))
(mf/render! app-root (mf/element ui/app)))
(defn- initialize-profile
"Event used mainly on application bootstrap; it fetches the profile
@ -132,9 +126,7 @@
;; The hard flag will force to unmount the whole UI and will redraw every component
(when hard?
(mf/unmount! app-root)
(mf/unmount! modal-root)
(set! app-root (mf/create-root (dom/get-element "app")))
(set! modal-root (mf/create-root (dom/get-element "modal"))))
(set! app-root (mf/create-root (dom/get-element "app"))))
(st/emit! (ev/initialize))
(init-ui)))

View file

@ -29,6 +29,7 @@
[app.main.ui.dashboard.team :refer [team-settings-page* team-members-page* team-invitations-page* webhooks-page*]]
[app.main.ui.dashboard.templates :refer [templates-section*]]
[app.main.ui.hooks :as hooks]
[app.main.ui.modal :refer [modal-container*]]
[app.main.ui.workspace.plugins]
[app.plugins.register :as preg]
[app.util.dom :as dom]
@ -240,6 +241,7 @@
(use-plugin-register plugin-url team-id (:id default-project))
[:& (mf/provider ctx/current-project-id) {:value project-id}
[:> modal-container*]
;; NOTE: dashboard events and other related functions assumes
;; that the team is a implicit context variable that is
;; available using react context or accessing

View file

@ -7,7 +7,8 @@
(ns app.main.ui.modal
(:require-macros [app.main.style :as stl])
(:require
[app.main.data.modal :as dm]
[app.common.data.macros :as dm]
[app.main.data.modal :as modal]
[app.main.store :as st]
[app.util.dom :as dom]
[app.util.keyboard :as k]
@ -20,13 +21,13 @@
[event allow-click-outside]
(when (and (k/esc? event) (not allow-click-outside))
(dom/stop-propagation event)
(st/emit! (dm/hide))))
(st/emit! (modal/hide))))
(defn- on-pop-state
[event]
(dom/prevent-default event)
(dom/stop-propagation event)
(st/emit! (dm/hide))
(st/emit! (modal/hide))
(.forward js/history))
(defn- on-click-outside
@ -41,15 +42,14 @@
(= (.-button event) 0))
(dom/stop-propagation event)
(dom/prevent-default event)
(st/emit! (dm/hide)))))
(st/emit! (modal/hide)))))
(mf/defc modal-wrapper
{::mf/wrap-props false
(mf/defc modal-wrapper*
{::mf/props :obj
::mf/wrap [mf/memo]}
[props]
(let [data (unchecked-get props "data")
wrapper-ref (mf/use-ref nil)
components (mf/deref dm/components)
[{:keys [data]}]
(let [wrapper-ref (mf/use-ref nil)
components (mf/deref modal/components)
allow-click-outside (:allow-click-outside data)
@ -61,31 +61,29 @@
(fn [event]
(on-esc-clicked event allow-click-outside))]
(mf/use-layout-effect
(mf/deps allow-click-outside)
(fn []
(let [keys [(events/listen js/window EventType.POPSTATE on-pop-state)
(events/listen js/document EventType.KEYDOWN handle-keydown)
(mf/with-effect [allow-click-outside]
(let [keys [(events/listen js/window EventType.POPSTATE on-pop-state)
(events/listen js/document EventType.KEYDOWN handle-keydown)
;; Changing to js/document breaks the color picker
(events/listen (dom/get-root) EventType.POINTERDOWN handle-click-outside)
;; Changing to js/document breaks the color picker
(events/listen (dom/get-root) EventType.POINTERDOWN handle-click-outside)
(events/listen js/document EventType.CONTEXTMENU handle-click-outside)]]
#(doseq [key keys]
(events/unlistenByKey key)))))
(events/listen js/document EventType.CONTEXTMENU handle-click-outside)]]
(fn []
(run! events/unlistenByKey keys))))
(when-let [component (get components (:type data))]
[:div {:ref wrapper-ref
:class (stl/css :modal-wrapper)}
(mf/element component (:props data))])))
(def modal-ref
(l/derived ::dm/modal st/state))
(def ^:private ref:modal
(l/derived ::modal/modal st/state))
(mf/defc modal
{::mf/wrap-props false}
(mf/defc modal-container*
{::mf/props :obj}
[]
(let [modal (mf/deref modal-ref)]
(when modal
[:& modal-wrapper {:data modal
:key (:id modal)}])))
(when-let [modal (mf/deref ref:modal)]
(mf/portal
(mf/html [:> modal-wrapper* {:data modal :key (dm/str (:id modal))}])
(.-body js/document))))

View file

@ -12,6 +12,7 @@
[app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.hooks :as hooks]
[app.main.ui.modal :refer [modal-container*]]
[app.main.ui.settings.access-tokens :refer [access-tokens-page]]
[app.main.ui.settings.change-email]
[app.main.ui.settings.delete-account]
@ -41,25 +42,29 @@
(when (nil? profile)
(st/emit! (rt/nav :auth-login))))
[:section {:class (stl/css :dashboard-layout-refactor :dashboard)}
[:& sidebar {:profile profile
:section section}]
[:*
[:> modal-container*]
[:section {:class (stl/css :dashboard-layout-refactor :dashboard)}
[:div {:class (stl/css :dashboard-content)}
[:& header]
[:section {:class (stl/css :dashboard-container)}
(case section
:settings-profile
[:& profile-page]
:settings-feedback
[:& feedback-page]
[:& sidebar {:profile profile
:section section}]
:settings-password
[:& password-page]
[:div {:class (stl/css :dashboard-content)}
[:& header]
[:section {:class (stl/css :dashboard-container)}
(case section
:settings-profile
[:& profile-page]
:settings-options
[:& options-page]
:settings-feedback
[:& feedback-page]
:settings-access-tokens
[:& access-tokens-page])]]]))
:settings-password
[:& password-page]
:settings-options
[:& options-page]
:settings-access-tokens
[:& access-tokens-page])]]]]))

View file

@ -25,6 +25,7 @@
[app.main.ui.ds.product.loader :refer [loader*]]
[app.main.ui.hooks :as hooks]
[app.main.ui.icons :as i]
[app.main.ui.modal :refer [modal-container*]]
[app.main.ui.viewer.comments :refer [comments-layer comments-sidebar*]]
[app.main.ui.viewer.header :as header]
[app.main.ui.viewer.inspect :as inspect]
@ -633,7 +634,9 @@
(if-let [data (mf/deref refs/viewer-data)]
(let [props (obj/merge props #js {:data data :key (dm/str file-id)})]
[:> viewer-content* props])
[:*
[:> modal-container*]
[:> viewer-content* props]])
[:> loader* {:title (tr "labels.loading")
:overlay true}]))

View file

@ -19,6 +19,7 @@
[app.main.ui.ds.product.loader :refer [loader*]]
[app.main.ui.hooks :as hooks]
[app.main.ui.hooks.resize :refer [use-resize-observer]]
[app.main.ui.modal :refer [modal-container*]]
[app.main.ui.workspace.colorpicker]
[app.main.ui.workspace.context-menu :refer [context-menu]]
[app.main.ui.workspace.coordinates :as coordinates]
@ -211,6 +212,7 @@
[:& (mf/provider ctx/components-v2) {:value components-v2?}
[:& (mf/provider ctx/design-tokens) {:value design-tokens?}
[:& (mf/provider ctx/workspace-read-only?) {:value read-only?}
[:> modal-container*]
[:section {:class (stl/css :workspace)
:style {:background-color background-color
:touch-action "none"}}