♻️ Refactor dropdown-menu and make dropdown visibility exclusive (#6956)

* 🐛 Fix having multiple dropdown menus opened on dashboard page

* ♻️ Refactor dropdown-menu

Make it follow new standards and make it external api more usable,
not depending on manually provided list of ids.

This also implements the autoclosing of "other" active/open
dropdown-menu (or other similar components).

* 📎 Add PR feedback changes

* 🐛 Fix incorrect event handling on project-menu

* 🐛 Fix unexpected exception

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
Elena Torró 2025-08-01 16:14:15 +02:00 committed by GitHub
parent 07b15819d4
commit c8f5ec4698
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 332 additions and 479 deletions

View File

@ -20,6 +20,7 @@
- Fix missing font when copy&paste a chunk of text [Taiga #11522](https://tree.taiga.io/project/penpot/issue/11522) - Fix missing font when copy&paste a chunk of text [Taiga #11522](https://tree.taiga.io/project/penpot/issue/11522)
- Fix bad swap slot after two swaps [Taiga #11659](https://tree.taiga.io/project/penpot/issue/11659) - Fix bad swap slot after two swaps [Taiga #11659](https://tree.taiga.io/project/penpot/issue/11659)
- Fix missing package for the penport_exporter Docker image [GitHub #7205](https://github.com/penpot/penpot/issues/7025) - Fix missing package for the penport_exporter Docker image [GitHub #7205](https://github.com/penpot/penpot/issues/7025)
- Fix issue where multiple dropdown menus could be opened simultaneously on the dashboard page [Taiga #11500](https://tree.taiga.io/project/penpot/issue/11500)
## 2.9.0 (Unreleased) ## 2.9.0 (Unreleased)

View File

@ -8,45 +8,44 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.config :as cfg] [app.config :as cfg]
[app.main.store :as st]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.globals :as globals] [app.util.globals :as globals]
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
[app.util.object :as obj] [beicon.v2.core :as rx]
[goog.events :as events] [goog.events :as events]
[goog.object :as gobj] [potok.v2.core :as ptk]
[rumext.v2 :as mf]) [rumext.v2 :as mf])
(:import goog.events.EventType)) (:import goog.events.EventType))
(mf/defc dropdown-menu-item* (mf/defc dropdown-menu-item*
{::mf/wrap-props false} [{:keys [can-focus] :rest props}]
[props] (let [can-focus (d/nilv can-focus true)
(let [props (-> (obj/clone props) tab-index (if can-focus "0" "-1")
(obj/set! "role" "menuitem"))] props (mf/spread-props props {:role "menuitem" :tab-index tab-index})]
[:> :li props])) [:> :li props]))
(mf/defc dropdown-menu' (mf/defc internal-dropdown-menu*
{::mf/wrap-props false} {::mf/private true}
[props] [{:keys [on-close children class id]}]
(let [children (gobj/get props "children")
on-close (gobj/get props "on-close")
ref (gobj/get props "container")
ids (gobj/get props "ids")
list-class (gobj/get props "list-class")
ids (filter some? ids)
on-click
(fn [event]
(let [target (dom/get-target event)
;; MacOS ctrl+click sends two events: context-menu and click. (assert (fn? on-close) "missing `on-close` prop")
;; In order to not have two handlings we ignore ctrl+click for this platform
mac-ctrl-click? (and (cfg/check-platform? :macos) (kbd/ctrl? event))] (let [on-click
(when (and (not mac-ctrl-click?) (mf/use-fn
(not (.-data-no-close ^js target))) (mf/deps on-close)
(if ref (fn [event]
(let [parent (mf/ref-val ref)] (let [target (dom/get-target event)
(when-not (or (not parent) (.contains parent target)) ;; MacOS ctrl+click sends two events: context-menu and click.
(on-close))) ;; In order to not have two handlings we ignore ctrl+click for this platform
(on-close))))) mac-ctrl-click? (and (cfg/check-platform? :macos) (kbd/ctrl? event))]
(when (and (not mac-ctrl-click?)
(not (.-data-no-close ^js target))
(fn? on-close))
(on-close)))))
container
(mf/use-ref)
on-keyup on-keyup
(fn [event] (fn [event]
@ -55,35 +54,49 @@
on-key-down on-key-down
(fn [event] (fn [event]
(let [first-id (dom/get-element (first ids)) (when-let [container (mf/ref-val container)]
first-element (dom/get-element first-id) (let [entries (vec (dom/query-all container "[role=menuitem]"))]
len (count ids)]
(when (kbd/home? event) (cond
(when first-element (kbd/up-arrow? event)
(dom/focus! first-element))) (let [selected (dom/get-active)
index (d/index-of-pred entries #(identical? % selected))
target (if (nil? index)
(peek entries)
(or (get entries (dec index)) (peek entries)))]
(when (kbd/up-arrow? event) (dom/focus! target))
(let [actual-selected (dom/get-active)
actual-id (dom/get-attribute actual-selected "id")
actual-index (d/index-of ids actual-id)
previous-id (if (= 0 actual-index)
(last ids)
(get ids (- actual-index 1) (last ids)))]
(dom/focus! (dom/get-element previous-id))))
(when (kbd/down-arrow? event) (kbd/down-arrow? event)
(let [actual-selected (dom/get-active) (let [selected (dom/get-active)
actual-id (dom/get-attribute actual-selected "id") index (d/index-of-pred entries #(identical? % selected))
actual-index (d/index-of ids actual-id) target (if (nil? index)
next-id (if (= (- len 1) actual-index) (first entries)
(first ids) (or (get entries (inc index)) (first entries)))]
(get ids (+ 1 actual-index) (first ids))) (dom/focus! target))
node-item (dom/get-element next-id)]
(dom/focus! node-item)))
(when (kbd/tab? event) (kbd/enter? event)
(on-close))))] (let [selected (dom/get-active)]
(dom/prevent-default event)
(dom/click! selected))
(kbd/tab? event)
(on-close)))))]
(mf/with-effect [id]
(when id
(st/emit! (ptk/data-event :dropdown/open {:id id}))))
(mf/with-effect [on-close id]
(when id
(let [stream (->> st/stream
(rx/filter (ptk/type? :dropdown/open))
(rx/map deref)
(rx/filter #(not= id (:id %)))
(rx/take 1))
subs (rx/subs! nil nil on-close stream)]
(fn []
(rx/dispose! subs)))))
(mf/with-effect [] (mf/with-effect []
(let [keys [(events/listen globals/document EventType.CLICK on-click) (let [keys [(events/listen globals/document EventType.CLICK on-click)
@ -93,21 +106,10 @@
#(doseq [key keys] #(doseq [key keys]
(events/unlistenByKey key)))) (events/unlistenByKey key))))
[:ul {:class list-class :role "menu"} children])) [:ul {:class class :role "menu" :ref container} children]))
(mf/defc dropdown-menu (mf/defc dropdown-menu*
{::mf/props :obj} [{:keys [show] :as props}]
[props] (when show
(assert (fn? (gobj/get props "on-close")) "missing `on-close` prop") [:> internal-dropdown-menu* props]))
(assert (boolean? (gobj/get props "show")) "missing `show` prop")
(let [ids (obj/get props "ids")
ids (or ids
(->> (obj/get props "children")
(keep (fn [o]
(let [props (obj/get o "props")]
(obj/get props "id"))))))]
(when (gobj/get props "show")
(mf/element
dropdown-menu'
(mf/spread-props props {:ids ids})))))

View File

@ -84,7 +84,7 @@
::ev/origin "dashboard"}))))) ::ev/origin "dashboard"})))))
[:div {:class (stl/css :dashboard-comments-section)} [:div {:class (stl/css :dashboard-comments-section)}
[:& dropdown {:show show? :on-close on-hide-comments} [:& dropdown {:show show? :on-close on-hide-comments :dropdown-id "dashboard-comments"}
[:div {:class (stl/css :dropdown :comments-section :comment-threads-section)} [:div {:class (stl/css :dropdown :comments-section :comment-threads-section)}
[:div {:class (stl/css :header)} [:div {:class (stl/css :header)}
[:h3 {:class (stl/css :header-title)} (tr "dashboard.notifications")] [:h3 {:class (stl/css :header-title)} (tr "dashboard.notifications")]

View File

@ -21,6 +21,7 @@
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[potok.v2.core :as ptk]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(defn- get-project-name (defn- get-project-name
@ -55,11 +56,11 @@
projects)) projects))
(mf/defc file-menu* (mf/defc file-menu*
[{:keys [files on-edit on-menu-close top left navigate origin parent-id can-edit]}] [{:keys [files on-edit on-close top left navigate origin parent-id can-edit]}]
(assert (seq files) "missing `files` prop") (assert (seq files) "missing `files` prop")
(assert (fn? on-edit) "missing `on-edit` prop") (assert (fn? on-edit) "missing `on-edit` prop")
(assert (fn? on-menu-close) "missing `on-menu-close` prop") (assert (fn? on-close) "missing `on-close` prop")
(assert (boolean? navigate) "missing `navigate` prop") (assert (boolean? navigate) "missing `navigate` prop")
(let [is-lib-page? (= :libraries origin) (let [is-lib-page? (= :libraries origin)
@ -161,13 +162,12 @@
(on-move-accept params team-id project-id)))))) (on-move-accept params team-id project-id))))))
add-shared add-shared
#(st/emit! (dd/set-file-shared (assoc file :is-shared true))) (fn []
(st/emit! (dd/set-file-shared (assoc file :is-shared true))))
del-shared del-shared
(mf/use-fn (fn [_]
(mf/deps files) (run! #(st/emit! (dd/set-file-shared (assoc % :is-shared false))) files))
(fn [_]
(run! #(st/emit! (dd/set-file-shared (assoc % :is-shared false))) files)))
on-add-shared on-add-shared
(fn [event] (fn [event]
@ -186,32 +186,35 @@
:count-libraries file-count}))) :count-libraries file-count})))
on-export-files on-export-files
(mf/use-fn (fn [format]
(mf/deps files) (st/emit! (with-meta (fexp/export-files files format)
(fn [format] {::ev/origin "dashboard"})))
(st/emit! (with-meta (fexp/export-files files format)
{::ev/origin "dashboard"}))))
on-export-binary-files on-export-binary-files
(mf/use-fn (partial on-export-files :binfile-v1)
(mf/deps on-export-files)
(partial on-export-files :binfile-v1))
on-export-binary-files-v3 on-export-binary-files-v3
(mf/use-fn (partial on-export-files :binfile-v3)
(mf/deps on-export-files)
(partial on-export-files :binfile-v3))
on-export-standard-files on-export-standard-files
(mf/use-fn (partial on-export-files :legacy-zip)]
(mf/deps on-export-files)
(partial on-export-files :legacy-zip))]
(mf/with-effect [] (mf/with-effect []
(->> (rp/cmd! :get-all-projects) (->> (rp/cmd! :get-all-projects)
(rx/map group-by-team) (rx/map group-by-team)
(rx/subs! #(reset! teams* %)))) (rx/subs! #(reset! teams* %))))
(mf/with-effect [on-close]
(st/emit! (ptk/data-event :dropdown/open {:id "file-menu"}))
(let [stream (->> st/stream
(rx/filter (ptk/type? :dropdown/open))
(rx/map deref)
(rx/filter #(not= "file-menu" (:id %)))
(rx/take 1))
subs (rx/subs! nil nil on-close stream)]
(fn []
(rx/dispose! subs))))
(let [sub-options (let [sub-options
(concat (concat
(for [project current-projects] (for [project current-projects]
@ -328,7 +331,7 @@
:handler on-delete})])] :handler on-delete})])]
[:> context-menu* [:> context-menu*
{:on-close on-menu-close {:on-close on-close
:fixed (or (not= top 0) (not= left 0)) :fixed (or (not= top 0) (not= left 0))
:show true :show true
:min-width true :min-width true

View File

@ -128,7 +128,7 @@
:left (- (:x (:menu-pos @local)) 180) :left (- (:x (:menu-pos @local)) 180)
:top (:y (:menu-pos @local)) :top (:y (:menu-pos @local))
:on-edit on-edit :on-edit on-edit
:on-menu-close on-menu-close :on-close on-menu-close
:on-import on-import}])]])) :on-import on-import}])]]))
(mf/defc files-section* (mf/defc files-section*

View File

@ -439,7 +439,7 @@
:can-edit can-edit :can-edit can-edit
:navigate true :navigate true
:on-edit on-edit :on-edit on-edit
:on-menu-close on-menu-close :on-close on-menu-close
:origin origin :origin origin
:parent-id (dm/str file-id "-action-menu")}]])]]]]])) :parent-id (dm/str file-id "-action-menu")}]])]]]]]))

View File

@ -17,11 +17,12 @@
[app.main.ui.dashboard.import :as udi] [app.main.ui.dashboard.import :as udi]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(mf/defc project-menu* (mf/defc project-menu*
{::mf/props :obj} [{:keys [project show on-edit on-close top left on-import]}]
[{:keys [project show on-edit on-menu-close top left on-import]}]
(let [top (or top 0) (let [top (or top 0)
left (or left 0) left (or left 0)
@ -42,7 +43,8 @@
(with-meta project {:on-success on-duplicate-success})))) (with-meta project {:on-success on-duplicate-success}))))
toggle-pin toggle-pin
#(st/emit! (dd/toggle-project-pin project)) (fn []
(st/emit! (dd/toggle-project-pin project)))
on-move-success on-move-success
(fn [team-id] (fn [team-id]
@ -63,25 +65,23 @@
(dcm/go-to-dashboard-recent :team-id team-id)))) (dcm/go-to-dashboard-recent :team-id team-id))))
on-delete on-delete
#(st/emit! (fn []
(modal/show (st/emit!
{:type :confirm (modal/show {:type :confirm
:title (tr "modals.delete-project-confirm.title") :title (tr "modals.delete-project-confirm.title")
:message (tr "modals.delete-project-confirm.message") :message (tr "modals.delete-project-confirm.message")
:accept-label (tr "modals.delete-project-confirm.accept") :accept-label (tr "modals.delete-project-confirm.accept")
:on-accept delete-fn})) :on-accept delete-fn})))
file-input (mf/use-ref nil) file-input
(mf/use-ref nil)
on-import-files on-import-files
(mf/use-callback (fn [] (dom/click! (mf/ref-val file-input)))
(fn []
(dom/click (mf/ref-val file-input))))
on-finish-import on-finish-import
(mf/use-callback (mf/use-fn
(fn [] (fn [] (when (fn? on-import) (on-import))))
(when (fn? on-import) (on-import))))
options options
[(when-not (:is-default project) [(when-not (:is-default project)
@ -116,9 +116,21 @@
:id "project-delete" :id "project-delete"
:handler on-delete})]] :handler on-delete})]]
(mf/with-effect [show on-close]
(when ^boolean show
(st/emit! (ptk/data-event :dropdown/open {:id "project-menu"}))
(let [stream (->> st/stream
(rx/filter (ptk/type? :dropdown/open))
(rx/map deref)
(rx/filter #(not= "project-menu" (:id %)))
(rx/take 1))
subs (rx/subs! nil nil on-close stream)]
(fn []
(rx/dispose! subs)))))
[:* [:*
[:> context-menu* [:> context-menu*
{:on-close on-menu-close {:on-close on-close
:show show :show show
:fixed (or (not= top 0) (not= left 0)) :fixed (or (not= top 0) (not= left 0))
:min-width true :min-width true

View File

@ -271,7 +271,7 @@
:left (+ 24 (:x (:menu-pos @local))) :left (+ 24 (:x (:menu-pos @local)))
:top (:y (:menu-pos @local)) :top (:y (:menu-pos @local))
:on-edit on-edit-open :on-edit on-edit-open
:on-menu-close on-menu-close :on-close on-menu-close
:on-import on-import}])]]] :on-import on-import}])]]]
[:div {:class (stl/css :grid-container) :ref rowref} [:div {:class (stl/css :grid-container) :ref rowref}

View File

@ -8,7 +8,6 @@
(:require-macros [app.main.style :as stl]) (:require-macros [app.main.style :as stl])
(:require (:require
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.spec :as us]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.main.data.auth :as da] [app.main.data.auth :as da]
@ -21,7 +20,7 @@
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.router :as rt] [app.main.router :as rt]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.dropdown-menu :refer [dropdown-menu [app.main.ui.components.dropdown-menu :refer [dropdown-menu*
dropdown-menu-item*]] dropdown-menu-item*]]
[app.main.ui.components.link :refer [link]] [app.main.ui.components.link :refer [link]]
[app.main.ui.dashboard.comments :refer [comments-icon* comments-section]] [app.main.ui.dashboard.comments :refer [comments-icon* comments-section]]
@ -37,7 +36,6 @@
[app.util.object :as obj] [app.util.object :as obj]
[app.util.timers :as ts] [app.util.timers :as ts]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[cljs.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[goog.functions :as f] [goog.functions :as f]
[potok.v2.core :as ptk] [potok.v2.core :as ptk]
@ -194,7 +192,7 @@
:left (:x (:menu-pos local)) :left (:x (:menu-pos local))
:top (:y (:menu-pos local)) :top (:y (:menu-pos local))
:on-edit on-edit-open :on-edit on-edit-open
:on-menu-close on-menu-close}]])) :on-close on-menu-close}]]))
(mf/defc sidebar-search (mf/defc sidebar-search
[{:keys [search-term team-id] :as props}] [{:keys [search-term team-id] :as props}]
@ -273,47 +271,24 @@
:on-click on-clear-click} :on-click on-clear-click}
search-icon])])) search-icon])]))
(mf/defc teams-selector-dropdown-items (mf/defc teams-selector-dropdown*
{::mf/wrap-props false} {::mf/wrap-props false}
[{:keys [team profile teams] :as props}] [{:keys [team profile teams] :rest props}]
(let [on-create-clicked (let [on-create-click
(mf/use-fn (mf/use-fn #(st/emit! (modal/show :team-form {})))
#(st/emit! (modal/show :team-form {})))
team-selected on-team-click
(mf/use-fn (mf/use-fn
(fn [event] (fn [event]
(let [team-id (-> (dom/get-current-target event) (let [team-id (-> (dom/get-current-target event)
(dom/get-data "value") (dom/get-data "value")
(uuid/parse))] (uuid/parse))]
(st/emit! (dcm/go-to-dashboard-recent :team-id team-id))))) (st/emit! (dcm/go-to-dashboard-recent :team-id team-id)))))]
handle-select-default [:> dropdown-menu* props
(mf/use-fn
(mf/deps profile team-selected)
(fn [event]
(when (kbd/enter? event)
(team-selected (:default-team-id profile) event))))
handle-select-team [:> dropdown-menu-item* {:on-click on-team-click
(mf/use-fn
(mf/deps team-selected)
(fn [event]
(when (kbd/enter? event)
(team-selected event))))
handle-creation-key-down
(mf/use-fn
(mf/deps on-create-clicked)
(fn [event]
(when (kbd/enter? event)
(on-create-clicked event))))]
[:*
[:> dropdown-menu-item* {:on-click team-selected
:data-value (:default-team-id profile) :data-value (:default-team-id profile)
:on-key-down handle-select-default
:id "teams-selector-default-team"
:class (stl/css :team-dropdown-item)} :class (stl/css :team-dropdown-item)}
[:span {:class (stl/css :penpot-icon)} i/logo-icon] [:span {:class (stl/css :penpot-icon)} i/logo-icon]
@ -322,12 +297,10 @@
tick-icon)] tick-icon)]
(for [team-item (remove :is-default (vals teams))] (for [team-item (remove :is-default (vals teams))]
[:> dropdown-menu-item* {:on-click team-selected [:> dropdown-menu-item* {:on-click on-team-click
:data-value (:id team-item) :data-value (:id team-item)
:on-key-down handle-select-team
:id (str "teams-selector-" (:id team-item))
:class (stl/css :team-dropdown-item) :class (stl/css :team-dropdown-item)
:key (str "teams-selector-" (:id team-item))} :key (str (:id team-item))}
[:img {:src (cf/resolve-team-photo-url team-item) [:img {:src (cf/resolve-team-photo-url team-item)
:class (stl/css :team-picture) :class (stl/css :team-picture)
:alt (:name team-item)}] :alt (:name team-item)}]
@ -342,21 +315,14 @@
(when (= (:id team-item) (:id team)) (when (= (:id team-item) (:id team))
tick-icon)]) tick-icon)])
[:hr {:role "separator" [:hr {:role "separator" :class (stl/css :team-separator)}]
:class (stl/css :team-separator)}] [:> dropdown-menu-item* {:on-click on-create-click
[:> dropdown-menu-item* {:on-click on-create-clicked
:on-key-down handle-creation-key-down
:id "teams-selector-create-team"
:class (stl/css :team-dropdown-item :action)} :class (stl/css :team-dropdown-item :action)}
[:span {:class (stl/css :icon-wrapper)} add-icon] [:span {:class (stl/css :icon-wrapper)} add-icon]
[:span {:class (stl/css :team-text)} (tr "dashboard.create-new-team")]]])) [:span {:class (stl/css :team-text)} (tr "dashboard.create-new-team")]]]))
(s/def ::member-id ::us/uuid) (mf/defc team-options-dropdown*
(s/def ::leave-modal-form [{:keys [team profile] :rest props}]
(s/keys :req-un [::member-id]))
(mf/defc team-options-dropdown
[{:keys [team profile] :as props}]
(let [go-members #(st/emit! (dcm/go-to-dashboard-members)) (let [go-members #(st/emit! (dcm/go-to-dashboard-members))
go-invitations #(st/emit! (dcm/go-to-dashboard-invitations)) go-invitations #(st/emit! (dcm/go-to-dashboard-invitations))
go-webhooks #(st/emit! (dcm/go-to-dashboard-webhooks)) go-webhooks #(st/emit! (dcm/go-to-dashboard-webhooks))
@ -449,218 +415,125 @@
:title (tr "modals.delete-team-confirm.title") :title (tr "modals.delete-team-confirm.title")
:message (tr "modals.delete-team-confirm.message") :message (tr "modals.delete-team-confirm.message")
:accept-label (tr "modals.delete-team-confirm.accept") :accept-label (tr "modals.delete-team-confirm.accept")
:on-accept delete-fn}))) :on-accept delete-fn})))]
[:> dropdown-menu* props
handle-members
(mf/use-fn
(mf/deps go-members)
(fn [event]
(when (kbd/enter? event)
(go-members))))
handle-invitations
(mf/use-fn
(mf/deps go-invitations)
(fn [event]
(when (kbd/enter? event)
(go-invitations))))
handle-webhooks
(mf/use-fn
(mf/deps go-webhooks)
(fn [event]
(when (kbd/enter? event)
(go-webhooks))))
handle-settings
(mf/use-fn
(mf/deps go-settings)
(fn [event]
(when (kbd/enter? event)
(go-settings))))
handle-rename
(mf/use-fn
(mf/deps on-rename-clicked)
(fn [event]
(when (kbd/enter? event)
(on-rename-clicked))))
handle-leave-and-close
(mf/use-fn
(mf/deps leave-and-close)
(fn [event]
(when (kbd/enter? event)
(leave-and-close))))
handle-leave-as-owner-clicked
(mf/use-fn
(mf/deps on-leave-as-owner-clicked)
(fn [event]
(when (kbd/enter? event)
(on-leave-as-owner-clicked))))
handle-on-leave-clicked
(mf/use-fn
(mf/deps on-leave-clicked)
(fn [event]
(when (kbd/enter? event)
(on-leave-clicked))))
handle-on-delete-clicked
(mf/use-fn
(mf/deps on-delete-clicked)
(fn [event]
(when (kbd/enter? event)
(on-delete-clicked))))]
[:*
[:> dropdown-menu-item* {:on-click go-members [:> dropdown-menu-item* {:on-click go-members
:on-key-down handle-members :class (stl/css :team-options-item)
:className (stl/css :team-options-item) :data-testid "team-members"}
:id "teams-options-members"
:data-testid "team-members"}
(tr "labels.members")] (tr "labels.members")]
[:> dropdown-menu-item* {:on-click go-invitations [:> dropdown-menu-item* {:on-click go-invitations
:on-key-down handle-invitations :class (stl/css :team-options-item)
:className (stl/css :team-options-item) :data-testid "team-invitations"}
:id "teams-options-invitations"
:data-testid "team-invitations"}
(tr "labels.invitations")] (tr "labels.invitations")]
(when (contains? cf/flags :webhooks) (when (contains? cf/flags :webhooks)
[:> dropdown-menu-item* {:on-click go-webhooks [:> dropdown-menu-item* {:on-click go-webhooks
:on-key-down handle-webhooks :class (stl/css :team-options-item)}
:className (stl/css :team-options-item)
:id "teams-options-webhooks"}
(tr "labels.webhooks")]) (tr "labels.webhooks")])
[:> dropdown-menu-item* {:on-click go-settings [:> dropdown-menu-item* {:on-click go-settings
:on-key-down handle-settings :class (stl/css :team-options-item)
:className (stl/css :team-options-item) :data-testid "team-settings"}
:id "teams-options-settings"
:data-testid "team-settings"}
(tr "labels.settings")] (tr "labels.settings")]
[:hr {:class (stl/css :team-option-separator)}] [:hr {:class (stl/css :team-option-separator)}]
(when can-rename? (when can-rename?
[:> dropdown-menu-item* {:on-click on-rename-clicked [:> dropdown-menu-item* {:on-click on-rename-clicked
:on-key-down handle-rename :class (stl/css :team-options-item)
:id "teams-options-rename" :data-testid "rename-team"}
:className (stl/css :team-options-item)
:data-testid "rename-team"}
(tr "labels.rename")]) (tr "labels.rename")])
(cond (cond
(= (count members) 1) (= (count members) 1)
[:> dropdown-menu-item* {:on-click leave-and-close [:> dropdown-menu-item* {:on-click leave-and-close
:on-key-down handle-leave-and-close :class (stl/css :team-options-item)}
:className (stl/css :team-options-item)
:id "teams-options-leave-team"}
(tr "dashboard.leave-team")] (tr "dashboard.leave-team")]
(get-in team [:permissions :is-owner]) (get-in team [:permissions :is-owner])
[:> dropdown-menu-item* {:on-click on-leave-as-owner-clicked [:> dropdown-menu-item* {:on-click on-leave-as-owner-clicked
:on-key-down handle-leave-as-owner-clicked :class (stl/css :team-options-item)
:id "teams-options-leave-team" :data-testid "leave-team"}
:className (stl/css :team-options-item)
:data-testid "leave-team"}
(tr "dashboard.leave-team")] (tr "dashboard.leave-team")]
(> (count members) 1) (> (count members) 1)
[:> dropdown-menu-item* {:on-click on-leave-clicked [:> dropdown-menu-item* {:on-click on-leave-clicked
:on-key-down handle-on-leave-clicked :class (stl/css :team-options-item)}
:className (stl/css :team-options-item)
:id "teams-options-leave-team"}
(tr "dashboard.leave-team")]) (tr "dashboard.leave-team")])
(when (get-in team [:permissions :is-owner]) (when (get-in team [:permissions :is-owner])
[:> dropdown-menu-item* {:on-click on-delete-clicked [:> dropdown-menu-item* {:on-click on-delete-clicked
:on-key-down handle-on-delete-clicked :class (stl/css :team-options-item :warning)
:id "teams-options-delete-team" :data-testid "delete-team"}
:className (stl/css :team-options-item :warning)
:data-testid "delete-team"}
(tr "dashboard.delete-team")])])) (tr "dashboard.delete-team")])]))
(mf/defc sidebar-team-switch (mf/defc sidebar-team-switch
[{:keys [team profile] :as props}] [{:keys [team profile] :as props}]
(let [teams (mf/deref refs/teams) (let [teams (mf/deref refs/teams)
teams-without-default (into {} (filter (fn [[_ v]] (= false (:is-default v))) teams))
team-ids (map #(str "teams-selector-" %) (keys teams-without-default))
ids (concat ["teams-selector-default-team"] team-ids ["teams-selector-create-team"])
show-team-opts-ddwn? (mf/use-state false)
show-teams-ddwn? (mf/use-state false)
can-rename? (or (get-in team [:permissions :is-owner]) (get-in team [:permissions :is-admin]))
options-ids ["teams-options-members"
"teams-options-invitations"
(when (contains? cf/flags :webhooks)
"teams-options-webhooks")
"teams-options-settings"
(when can-rename?
"teams-options-rename")
"teams-options-leave-team"
(when (get-in team [:permissions :is-owner])
"teams-options-delete-team")]
subscription
(get team :subscription)
;; _ (prn "--------------- sidebar-team-switch") subscription-type
;; _ (app.common.pprint/pprint teams) (get-subscription-type subscription)
handle-show-team-click show-team-options-menu*
(fn [event] (mf/use-state false)
(dom/stop-propagation event)
(swap! show-teams-ddwn? not)
(reset! show-team-opts-ddwn? false))
handle-show-team-keydown show-team-options-menu?
(fn [event] (deref show-team-options-menu*)
(when (or (kbd/space? event) (kbd/enter? event))
(dom/prevent-default event)
(reset! show-teams-ddwn? true)
(reset! show-team-opts-ddwn? false)
(ts/schedule-on-idle
(fn []
(let [first-element (dom/get-element (first ids))]
(when first-element
(dom/focus! first-element)))))))
close-team-opts-ddwn show-teams-menu*
(mf/use-state false)
show-teams-menu?
(deref show-teams-menu*)
on-show-teams-click
(mf/use-fn (mf/use-fn
#(reset! show-team-opts-ddwn? false)) (fn [event]
(dom/stop-propagation event)
(swap! show-teams-menu* not)))
handle-show-opts-click on-show-teams-keydown
(fn [event] (mf/use-fn
(dom/stop-propagation event) (fn [event]
(swap! show-team-opts-ddwn? not) (when (or (kbd/space? event)
(reset! show-teams-ddwn? false)) (kbd/enter? event))
(dom/prevent-default event)
(dom/stop-propagation event)
(some-> (dom/get-current-target event)
(dom/click!)))))
handle-show-opts-keydown close-team-options-menu
(fn [event] (mf/use-fn #(reset! show-team-options-menu* false))
(when (or (kbd/space? event) (kbd/enter? event))
(dom/prevent-default event)
(reset! show-team-opts-ddwn? true)
(reset! show-teams-ddwn? false)
(ts/schedule-on-idle
(fn []
(let [first-element (dom/get-element (first options-ids))]
(when first-element
(dom/focus! first-element)))))))
handle-close-team on-show-options-click
(fn [] (mf/use-fn
(reset! show-teams-ddwn? false)) (fn [event]
subscription (:subscription team) (dom/stop-propagation event)
subscription-type (get-subscription-type subscription)] (swap! show-team-options-menu* not)))
on-show-options-keydown
(mf/use-fn
(fn [event]
(when (or (kbd/space? event)
(kbd/enter? event))
(dom/prevent-default event)
(dom/stop-propagation event)
(some-> (dom/get-current-target event)
(dom/click!)))))
close-teams-menu
(mf/use-fn #(reset! show-teams-menu* false))]
[:div {:class (stl/css :sidebar-team-switch)} [:div {:class (stl/css :sidebar-team-switch)}
[:div {:class (stl/css :switch-content)} [:div {:class (stl/css :switch-content)}
[:button {:class (stl/css :current-team) [:button {:class (stl/css :current-team)
:on-click handle-show-team-click :on-click on-show-teams-click
:on-key-down handle-show-team-keydown} :on-key-down on-show-teams-keydown}
(cond (cond
(:is-default team) (:is-default team)
[:div {:class (stl/css :team-name)} [:div {:class (stl/css :team-name)}
@ -691,29 +564,28 @@
(when-not (:is-default team) (when-not (:is-default team)
[:button {:class (stl/css :switch-options) [:button {:class (stl/css :switch-options)
:on-click handle-show-opts-click :on-click on-show-options-click
:aria-label "team-management" :aria-label "team-management"
:tab-index "0" :tab-index "0"
:on-key-down handle-show-opts-keydown} :on-key-down on-show-options-keydown}
menu-icon])] menu-icon])]
;; Teams Dropdown ;; Teams Dropdown
[:& dropdown-menu {:show @show-teams-ddwn? [:> teams-selector-dropdown* {:show show-teams-menu?
:on-close handle-close-team :on-close close-teams-menu
:ids ids :id "team-list"
:list-class (stl/css :dropdown :teams-dropdown)} :class (stl/css :dropdown :teams-dropdown)
[:& teams-selector-dropdown-items {:ids ids :team team
:team team :profile profile
:profile profile :teams teams}]
:teams teams}]]
[:& dropdown-menu {:show @show-team-opts-ddwn? [:> team-options-dropdown* {:show show-team-options-menu?
:on-close close-team-opts-ddwn :on-close close-team-options-menu
:ids options-ids :id "team-options"
:list-class (stl/css :dropdown :options-dropdown)} :class (stl/css :dropdown :options-dropdown)
[:& team-options-dropdown {:team team :team team
:profile profile}]]])) :profile profile}]]))
(mf/defc sidebar-content* (mf/defc sidebar-content*
{::mf/private true {::mf/private true
@ -806,7 +678,7 @@
(mf/use-layout-effect (mf/use-layout-effect
(mf/deps pinned-projects) (mf/deps pinned-projects)
(fn [] (fn []
(let [dom (mf/ref-val container) (let [dom (mf/ref-val container)
client-height (obj/get dom "clientHeight") client-height (obj/get dom "clientHeight")
scroll-height (obj/get dom "scrollHeight")] scroll-height (obj/get dom "scrollHeight")]
(reset! overflow* (> scroll-height client-height))))) (reset! overflow* (> scroll-height client-height)))))
@ -878,15 +750,17 @@
(mf/defc profile-section* (mf/defc profile-section*
{::mf/props :obj} {::mf/props :obj}
[{:keys [profile team]}] [{:keys [profile team]}]
(let [show* (mf/use-state false) (let [show-profile-menu* (mf/use-state false)
show (deref show*) show-profile-menu? (deref show-profile-menu*)
photo (cf/resolve-profile-photo-url profile)
photo
(cf/resolve-profile-photo-url profile)
on-click on-click
(mf/use-fn (mf/use-fn
(fn [section event] (fn [section event]
(dom/stop-propagation event) (dom/stop-propagation event)
(reset! show* false) (reset! show-profile-menu* false)
(if (keyword? section) (if (keyword? section)
(st/emit! (rt/nav section)) (st/emit! (rt/nav section))
(st/emit! section)))) (st/emit! section))))
@ -917,24 +791,16 @@
(mf/use-fn (mf/use-fn
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(swap! show* not))) (swap! show-profile-menu* not)))
handle-key-down handle-key-down
(mf/use-fn (mf/use-fn
(fn [event] (fn [event]
(when (kbd/enter? event) (when (kbd/enter? event)
(reset! show* true)))) (reset! show-profile-menu* true))))
on-close on-close
(fn [event] (mf/use-fn #(reset! show-profile-menu* false))
(dom/stop-propagation event)
(reset! show* false))
handle-key-down-profile
(mf/use-fn
(fn [event]
(when (kbd/enter? event)
(on-click :settings-profile event))))
handle-click-url handle-click-url
(mf/use-fn (mf/use-fn
@ -943,40 +809,13 @@
(dom/get-data "url"))] (dom/get-data "url"))]
(dom/open-new-window url)))) (dom/open-new-window url))))
handle-keydown-url
(mf/use-fn
(fn [event]
(let [url (-> (dom/get-current-target event)
(dom/get-data "url"))]
(when (kbd/enter? event)
(dom/open-new-window url)))))
handle-show-release-notes
(mf/use-fn
(mf/deps show-release-notes)
(fn [event]
(when (kbd/enter? event)
(show-release-notes))))
handle-feedback-click handle-feedback-click
(mf/use-fn #(on-click :settings-feedback %)) (mf/use-fn #(on-click :settings-feedback %))
handle-feedback-keydown
(mf/use-fn
(fn [event]
(when (kbd/enter? event)
(on-click :settings-feedback event))))
handle-logout-click handle-logout-click
(mf/use-fn (mf/use-fn
#(on-click (da/logout) %)) #(on-click (da/logout) %))
handle-logout-keydown
(mf/use-fn
(fn [event]
(when (kbd/enter? event)
(on-click (da/logout) event))))
handle-set-profile handle-set-profile
(mf/use-fn (mf/use-fn
#(on-click :settings-profile %)) #(on-click :settings-profile %))
@ -997,7 +836,8 @@
:on-click on-power-up-click} :on-click on-power-up-click}
[:div {:class (stl/css :penpot-free)} [:div {:class (stl/css :penpot-free)}
[:span (tr "dashboard.upgrade-plan.penpot-free")] [:span (tr "dashboard.upgrade-plan.penpot-free")]
[:span {:class (stl/css :no-limits)} (tr "dashboard.upgrade-plan.no-limits")]] [:span {:class (stl/css :no-limits)}
(tr "dashboard.upgrade-plan.no-limits")]]
[:div {:class (stl/css :power-up)} [:div {:class (stl/css :power-up)}
(tr "subscription.dashboard.upgrade-plan.power-up")]]) (tr "subscription.dashboard.upgrade-plan.power-up")]])
@ -1020,85 +860,67 @@
:alt (:fullname profile)}] :alt (:fullname profile)}]
[:span {:class (stl/css :profile-fullname)} (:fullname profile)]] [:span {:class (stl/css :profile-fullname)} (:fullname profile)]]
[:& dropdown-menu {:on-close on-close [:> dropdown-menu* {:on-close on-close
:show show :show show-profile-menu?
:list-class (stl/css :profile-dropdown)} :id "profile-menu"
[:li {:tab-index (if show "0" "-1") :class (stl/css :profile-dropdown)}
:class (stl/css :profile-dropdown-item) [:> dropdown-menu-item* {:class (stl/css :profile-dropdown-item)
:on-click handle-set-profile :on-click handle-set-profile
:on-key-down handle-key-down-profile :data-testid "profile-profile-opt"}
:data-testid "profile-profile-opt"}
(tr "labels.your-account")] (tr "labels.your-account")]
[:li {:class (stl/css :profile-separator)}] [:li {:class (stl/css :profile-separator)}]
[:li {:class (stl/css :profile-dropdown-item) [:> dropdown-menu-item* {:class (stl/css :profile-dropdown-item)
:tab-index (if show "0" "-1") :data-url "https://help.penpot.app"
:data-url "https://help.penpot.app" :on-click handle-click-url
:on-click handle-click-url :data-testid "help-center-profile-opt"}
:on-key-down handle-keydown-url
:data-testid "help-center-profile-opt"}
(tr "labels.help-center")] (tr "labels.help-center")]
[:li {:tab-index (if show "0" "-1") [:> dropdown-menu-item* {:class (stl/css :profile-dropdown-item)
:class (stl/css :profile-dropdown-item) :data-url "https://community.penpot.app"
:data-url "https://community.penpot.app" :on-click handle-click-url}
:on-click handle-click-url
:on-key-down handle-keydown-url}
(tr "labels.community")] (tr "labels.community")]
[:li {:tab-index (if show "0" "-1") [:> dropdown-menu-item* {:class (stl/css :profile-dropdown-item)
:class (stl/css :profile-dropdown-item) :data-url "https://www.youtube.com/c/Penpot"
:data-url "https://www.youtube.com/c/Penpot" :on-click handle-click-url}
:on-click handle-click-url
:on-key-down handle-keydown-url}
(tr "labels.tutorials")] (tr "labels.tutorials")]
[:li {:tab-index (if show "0" "-1") [:> dropdown-menu-item* {:tab-index "0"
:class (stl/css :profile-dropdown-item) :class (stl/css :profile-dropdown-item)
:on-click show-release-notes :on-click show-release-notes}
:on-key-down handle-show-release-notes}
(tr "labels.release-notes")] (tr "labels.release-notes")]
[:li {:class (stl/css :profile-separator)}] [:li {:class (stl/css :profile-separator)}]
[:li {:class (stl/css :profile-dropdown-item) [:> dropdown-menu-item* {:class (stl/css :profile-dropdown-item)
:tab-index (if show "0" "-1") :data-url "https://penpot.app/libraries-templates"
:data-url "https://penpot.app/libraries-templates" :on-click handle-click-url
:on-click handle-click-url :data-testid "libraries-templates-profile-opt"}
:on-key-down handle-keydown-url
:data-testid "libraries-templates-profile-opt"}
(tr "labels.libraries-and-templates")] (tr "labels.libraries-and-templates")]
[:li {:tab-index (if show "0" "-1") [:> dropdown-menu-item* {:class (stl/css :profile-dropdown-item)
:class (stl/css :profile-dropdown-item) :data-url "https://github.com/penpot/penpot"
:data-url "https://github.com/penpot/penpot" :on-click handle-click-url}
:on-click handle-click-url
:on-key-down handle-keydown-url}
(tr "labels.github-repo")] (tr "labels.github-repo")]
[:li {:tab-index (if show "0" "-1") [:> dropdown-menu-item* {:class (stl/css :profile-dropdown-item)
:class (stl/css :profile-dropdown-item) :data-url "https://penpot.app/terms"
:data-url "https://penpot.app/terms" :on-click handle-click-url}
:on-click handle-click-url
:on-key-down handle-keydown-url}
(tr "auth.terms-of-service")] (tr "auth.terms-of-service")]
[:li {:class (stl/css :profile-separator)}] [:li {:class (stl/css :profile-separator)}]
(when (contains? cf/flags :user-feedback) (when (contains? cf/flags :user-feedback)
[:li {:class (stl/css :profile-dropdown-item) [:> dropdown-menu-item* {:class (stl/css :profile-dropdown-item)
:tab-index (if show "0" "-1") :on-click handle-feedback-click
:on-click handle-feedback-click :data-testid "feedback-profile-opt"}
:on-key-down handle-feedback-keydown
:data-testid "feedback-profile-opt"}
(tr "labels.give-feedback")]) (tr "labels.give-feedback")])
[:li {:class (stl/css :profile-dropdown-item :item-with-icon) [:> dropdown-menu-item* {:class (stl/css :profile-dropdown-item :item-with-icon)
:tab-index (if show "0" "-1") :on-click handle-logout-click
:on-click handle-logout-click :data-testid "logout-profile-opt"}
:on-key-down handle-logout-keydown
:data-testid "logout-profile-opt"}
exit-icon exit-icon
(tr "labels.logout")]] (tr "labels.logout")]]

View File

@ -304,7 +304,7 @@
[:div {:class (stl/css :rol-selector)} [:div {:class (stl/css :rol-selector)}
[:span {:class (stl/css :rol-label)} (tr role)]]) [:span {:class (stl/css :rol-label)} (tr role)]])
[:& dropdown {:show @show? :on-close on-hide} [:& dropdown {:show @show? :on-close on-hide :dropdown-id (str "member-role-" (:id member))}
[:ul {:class (stl/css :roles-dropdown) [:ul {:class (stl/css :roles-dropdown)
:role "listbox"} :role "listbox"}
[:li {:on-click on-set-viewer [:li {:on-click on-set-viewer
@ -341,7 +341,7 @@
:on-click on-show} :on-click on-show}
menu-icon] menu-icon]
[:& dropdown {:show @show? :on-close on-hide} [:& dropdown {:show @show? :on-close on-hide :dropdown-id (str "member-actions-" (:id member))}
[:ul {:class (stl/css :actions-dropdown)} [:ul {:class (stl/css :actions-dropdown)}
(when is-you? (when is-you?
[:li {:on-click on-leave [:li {:on-click on-leave
@ -592,7 +592,7 @@
[:div {:class (stl/css :rol-selector)} [:div {:class (stl/css :rol-selector)}
[:span {:class (stl/css :rol-label)} label]]) [:span {:class (stl/css :rol-label)} label]])
[:& dropdown {:show @show? :on-close on-hide} [:& dropdown {:show @show? :on-close on-hide :dropdown-id "invitation-role-selector"}
[:ul {:class (stl/css :roles-dropdown)} [:ul {:class (stl/css :roles-dropdown)}
[:li {:data-role "admin" [:li {:data-role "admin"
:class (stl/css :rol-dropdown-item) :class (stl/css :rol-dropdown-item)
@ -692,7 +692,7 @@
:on-click on-show} :on-click on-show}
menu-icon] menu-icon]
[:& dropdown {:show @show? :on-close on-hide} [:& dropdown {:show @show? :on-close on-hide :dropdown-id "invitation-actions"}
[:ul {:class (stl/css :actions-dropdown :invitations-dropdown)} [:ul {:class (stl/css :actions-dropdown :invitations-dropdown)}
[:li {:on-click on-copy [:li {:on-click on-copy
:class (stl/css :action-dropdown-item)} :class (stl/css :action-dropdown-item)}
@ -982,7 +982,7 @@
[:button {:class (stl/css :menu-btn) [:button {:class (stl/css :menu-btn)
:on-click on-show} :on-click on-show}
menu-icon] menu-icon]
[:& dropdown {:show @show? :on-close on-hide} [:& dropdown {:show @show? :on-close on-hide :dropdown-id "webhook-actions"}
[:ul {:class (stl/css :webhook-actions-dropdown)} [:ul {:class (stl/css :webhook-actions-dropdown)}
[:li {:on-click on-edit [:li {:on-click on-edit
:class (stl/css :webhook-dropdown-item)} (tr "labels.edit")] :class (stl/css :webhook-dropdown-item)} (tr "labels.edit")]

View File

@ -28,7 +28,7 @@
[app.main.features :as features] [app.main.features :as features]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.dropdown-menu :refer [dropdown-menu dropdown-menu-item*]] [app.main.ui.components.dropdown-menu :refer [dropdown-menu* dropdown-menu-item*]]
[app.main.ui.context :as ctx] [app.main.ui.context :as ctx]
[app.main.ui.dashboard.subscription :refer [main-menu-power-up* get-subscription-type]] [app.main.ui.dashboard.subscription :refer [main-menu-power-up* get-subscription-type]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
@ -93,11 +93,12 @@
(st/emit! (modal/show {:type :onboarding})) (st/emit! (modal/show {:type :onboarding}))
(st/emit! (modal/show {:type :release-notes :version version}))))))] (st/emit! (modal/show {:type :release-notes :version version}))))))]
[:& dropdown-menu {:show true [:> dropdown-menu* {:show true
:on-close on-close ;; :id "workspace-help-menu"
:list-class (stl/css-case :sub-menu true :on-close on-close
:help-info plugins? :class (stl/css-case :sub-menu true
:help-info-old (not plugins?))} :help-info plugins?
:help-info-old (not plugins?))}
[:> dropdown-menu-item* {:class (stl/css :submenu-item) [:> dropdown-menu-item* {:class (stl/css :submenu-item)
:on-click nav-to-helpc-center :on-click nav-to-helpc-center
:on-key-down (fn [event] :on-key-down (fn [event]
@ -182,10 +183,11 @@
[{:keys [layout profile toggle-flag on-close toggle-theme]}] [{:keys [layout profile toggle-flag on-close toggle-theme]}]
(let [show-nudge-options (mf/use-fn #(modal/show! {:type :nudge-option}))] (let [show-nudge-options (mf/use-fn #(modal/show! {:type :nudge-option}))]
[:& dropdown-menu {:show true [:> dropdown-menu* {:show true
:list-class (stl/css-case :sub-menu true ;; :id "workspace-preferences-menu"
:preferences true) :class (stl/css-case :sub-menu true
:on-close on-close} :preferences true)
:on-close on-close}
[:> dropdown-menu-item* {:on-click toggle-flag [:> dropdown-menu-item* {:on-click toggle-flag
:class (stl/css :submenu-item) :class (stl/css :submenu-item)
:on-key-down (fn [event] :on-key-down (fn [event]
@ -312,10 +314,11 @@
(-> (dw/toggle-layout-flag :textpalette) (-> (dw/toggle-layout-flag :textpalette)
(vary-meta assoc ::ev/origin "workspace-menu")))))] (vary-meta assoc ::ev/origin "workspace-menu")))))]
[:& dropdown-menu {:show true [:> dropdown-menu* {:show true
:list-class (stl/css-case :sub-menu true ;; :id "workspace-view-menu"
:view true) :class (stl/css-case :sub-menu true
:on-close on-close} :view true)
:on-close on-close}
[:> dropdown-menu-item* {:class (stl/css :submenu-item) [:> dropdown-menu-item* {:class (stl/css :submenu-item)
:on-click toggle-flag :on-click toggle-flag
@ -430,10 +433,11 @@
perms (mf/use-ctx ctx/permissions) perms (mf/use-ctx ctx/permissions)
can-edit (:can-edit perms)] can-edit (:can-edit perms)]
[:& dropdown-menu {:show true [:> dropdown-menu* {:show true
:list-class (stl/css-case :sub-menu true ;; :id "workspace-edit-menu"
:edit true) :class (stl/css-case :sub-menu true
:on-close on-close} :edit true)
:on-close on-close}
[:> dropdown-menu-item* {:class (stl/css :submenu-item) [:> dropdown-menu-item* {:class (stl/css :submenu-item)
:on-click select-all :on-click select-all
@ -592,10 +596,11 @@
(when (kbd/enter? event) (when (kbd/enter? event)
(on-export-frames event))))] (on-export-frames event))))]
[:& dropdown-menu {:show true [:> dropdown-menu* {:show true
:list-class (stl/css-case :sub-menu true ;; :id "workspace-file-menu"
:file true) :class (stl/css-case :sub-menu true
:on-close on-close} :file true)
:on-close on-close}
(if ^boolean shared? (if ^boolean shared?
(when can-edit (when can-edit
@ -690,9 +695,10 @@
(let [plugins (preg/plugins-list) (let [plugins (preg/plugins-list)
user-can-edit? (:can-edit (deref refs/permissions)) user-can-edit? (:can-edit (deref refs/permissions))
permissions-peek (deref refs/plugins-permissions-peek)] permissions-peek (deref refs/plugins-permissions-peek)]
[:& dropdown-menu {:show true [:> dropdown-menu* {:show true
:list-class (stl/css-case :sub-menu true :plugins true) ;; :id "workspace-plugins-menu"
:on-close on-close} :class (stl/css-case :sub-menu true :plugins true)
:on-close on-close}
[:> dropdown-menu-item* {:on-click open-plugins [:> dropdown-menu-item* {:on-click open-plugins
:class (stl/css :submenu-item) :class (stl/css :submenu-item)
:on-key-down (fn [event] :on-key-down (fn [event]
@ -840,9 +846,10 @@
:on-click open-menu :on-click open-menu
:icon "menu"}] :icon "menu"}]
[:& dropdown-menu {:show show-menu? [:> dropdown-menu* {:show show-menu?
:on-close close-menu :id "workspace-menu"
:list-class (stl/css :menu)} :on-close close-menu
:class (stl/css :menu)}
[:> dropdown-menu-item* {:class (stl/css :menu-item) [:> dropdown-menu-item* {:class (stl/css :menu-item)
:on-click on-menu-click :on-click on-menu-click
:on-key-down (fn [event] :on-key-down (fn [event]

View File

@ -11,7 +11,7 @@
[app.config :as cf] [app.config :as cf]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.ui.components.dropdown-menu :refer [dropdown-menu [app.main.ui.components.dropdown-menu :refer [dropdown-menu*
dropdown-menu-item*]] dropdown-menu-item*]]
[app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.components.title-bar :refer [title-bar]]
[app.main.ui.context :as ctx] [app.main.ui.context :as ctx]
@ -123,9 +123,10 @@
:icon "import-export" :icon "import-export"
:variant "secondary"} :variant "secondary"}
(tr "workspace.tokens.tools")] (tr "workspace.tokens.tools")]
[:& dropdown-menu {:show show-menu? [:> dropdown-menu* {:show show-menu?
:on-close close-menu :on-close close-menu
:list-class (stl/css :import-export-menu)} :id "tokens-menu"
:class (stl/css :import-export-menu)}
(when can-edit? (when can-edit?
[:> dropdown-menu-item* {:class (stl/css :import-export-menu-item) [:> dropdown-menu-item* {:class (stl/css :import-export-menu-item)
:on-click on-modal-show} :on-click on-modal-show}

View File

@ -469,6 +469,11 @@
(when (some? node) (when (some? node)
(.focus node))) (.focus node)))
(defn click!
[^js node]
(when (some? node)
(.click node)))
(defn focus? (defn focus?
[^js node] [^js node]
(and node (and node
@ -899,4 +904,4 @@
;; In theory We could disable this only for the workspace. However gets too unreliable. ;; In theory We could disable this only for the workspace. However gets too unreliable.
;; It is better to be safe and disable for the dashboard as well. ;; It is better to be safe and disable for the dashboard as well.
(set! (.. js/document -documentElement -style -overscrollBehaviorX) "none") (set! (.. js/document -documentElement -style -overscrollBehaviorX) "none")
(set! (.. js/document -body -style -overscrollBehaviorX) "none")) (set! (.. js/document -body -style -overscrollBehaviorX) "none"))