diff --git a/CHANGES.md b/CHANGES.md index 2189481aad..3bf7904ba6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 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 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) diff --git a/frontend/src/app/main/ui/components/dropdown_menu.cljs b/frontend/src/app/main/ui/components/dropdown_menu.cljs index 8f9daef577..3ccf804f6b 100644 --- a/frontend/src/app/main/ui/components/dropdown_menu.cljs +++ b/frontend/src/app/main/ui/components/dropdown_menu.cljs @@ -8,45 +8,44 @@ (:require [app.common.data :as d] [app.config :as cfg] + [app.main.store :as st] [app.util.dom :as dom] [app.util.globals :as globals] [app.util.keyboard :as kbd] - [app.util.object :as obj] + [beicon.v2.core :as rx] [goog.events :as events] - [goog.object :as gobj] + [potok.v2.core :as ptk] [rumext.v2 :as mf]) (:import goog.events.EventType)) (mf/defc dropdown-menu-item* - {::mf/wrap-props false} - [props] - (let [props (-> (obj/clone props) - (obj/set! "role" "menuitem"))] + [{:keys [can-focus] :rest props}] + (let [can-focus (d/nilv can-focus true) + tab-index (if can-focus "0" "-1") + props (mf/spread-props props {:role "menuitem" :tab-index tab-index})] [:> :li props])) -(mf/defc dropdown-menu' - {::mf/wrap-props false} - [props] - (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) +(mf/defc internal-dropdown-menu* + {::mf/private true} + [{:keys [on-close children class id]}] - ;; MacOS ctrl+click sends two events: context-menu and click. - ;; 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))] - (when (and (not mac-ctrl-click?) - (not (.-data-no-close ^js target))) - (if ref - (let [parent (mf/ref-val ref)] - (when-not (or (not parent) (.contains parent target)) - (on-close))) - (on-close))))) + (assert (fn? on-close) "missing `on-close` prop") + + (let [on-click + (mf/use-fn + (mf/deps on-close) + (fn [event] + (let [target (dom/get-target event) + ;; MacOS ctrl+click sends two events: context-menu and click. + ;; 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))] + (when (and (not mac-ctrl-click?) + (not (.-data-no-close ^js target)) + (fn? on-close)) + (on-close))))) + + container + (mf/use-ref) on-keyup (fn [event] @@ -55,35 +54,49 @@ on-key-down (fn [event] - (let [first-id (dom/get-element (first ids)) - first-element (dom/get-element first-id) - len (count ids)] + (when-let [container (mf/ref-val container)] + (let [entries (vec (dom/query-all container "[role=menuitem]"))] - (when (kbd/home? event) - (when first-element - (dom/focus! first-element))) + (cond + (kbd/up-arrow? event) + (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) - (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)))) + (dom/focus! target)) - (when (kbd/down-arrow? event) - (let [actual-selected (dom/get-active) - actual-id (dom/get-attribute actual-selected "id") - actual-index (d/index-of ids actual-id) - next-id (if (= (- len 1) actual-index) - (first ids) - (get ids (+ 1 actual-index) (first ids))) - node-item (dom/get-element next-id)] - (dom/focus! node-item))) + (kbd/down-arrow? event) + (let [selected (dom/get-active) + index (d/index-of-pred entries #(identical? % selected)) + target (if (nil? index) + (first entries) + (or (get entries (inc index)) (first entries)))] + (dom/focus! target)) - (when (kbd/tab? event) - (on-close))))] + (kbd/enter? event) + (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 [] (let [keys [(events/listen globals/document EventType.CLICK on-click) @@ -93,21 +106,10 @@ #(doseq [key keys] (events/unlistenByKey key)))) - [:ul {:class list-class :role "menu"} children])) + [:ul {:class class :role "menu" :ref container} children])) -(mf/defc dropdown-menu - {::mf/props :obj} - [props] - (assert (fn? (gobj/get props "on-close")) "missing `on-close` prop") - (assert (boolean? (gobj/get props "show")) "missing `show` prop") +(mf/defc dropdown-menu* + [{:keys [show] :as props}] + (when show + [:> internal-dropdown-menu* props])) - (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}))))) diff --git a/frontend/src/app/main/ui/dashboard/comments.cljs b/frontend/src/app/main/ui/dashboard/comments.cljs index ab9d800f6a..5d08caecb9 100644 --- a/frontend/src/app/main/ui/dashboard/comments.cljs +++ b/frontend/src/app/main/ui/dashboard/comments.cljs @@ -84,7 +84,7 @@ ::ev/origin "dashboard"}))))) [: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 :header)} [:h3 {:class (stl/css :header-title)} (tr "dashboard.notifications")] diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs index 6e10ede276..2d8a0c0271 100644 --- a/frontend/src/app/main/ui/dashboard/file_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs @@ -21,6 +21,7 @@ [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [beicon.v2.core :as rx] + [potok.v2.core :as ptk] [rumext.v2 :as mf])) (defn- get-project-name @@ -55,11 +56,11 @@ projects)) (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 (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") (let [is-lib-page? (= :libraries origin) @@ -161,13 +162,12 @@ (on-move-accept params team-id project-id)))))) 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 - (mf/use-fn - (mf/deps files) - (fn [_] - (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 (fn [event] @@ -186,32 +186,35 @@ :count-libraries file-count}))) on-export-files - (mf/use-fn - (mf/deps files) - (fn [format] - (st/emit! (with-meta (fexp/export-files files format) - {::ev/origin "dashboard"})))) + (fn [format] + (st/emit! (with-meta (fexp/export-files files format) + {::ev/origin "dashboard"}))) on-export-binary-files - (mf/use-fn - (mf/deps on-export-files) - (partial on-export-files :binfile-v1)) + (partial on-export-files :binfile-v1) on-export-binary-files-v3 - (mf/use-fn - (mf/deps on-export-files) - (partial on-export-files :binfile-v3)) + (partial on-export-files :binfile-v3) on-export-standard-files - (mf/use-fn - (mf/deps on-export-files) - (partial on-export-files :legacy-zip))] + (partial on-export-files :legacy-zip)] (mf/with-effect [] (->> (rp/cmd! :get-all-projects) (rx/map group-by-team) (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 (concat (for [project current-projects] @@ -328,7 +331,7 @@ :handler on-delete})])] [:> context-menu* - {:on-close on-menu-close + {:on-close on-close :fixed (or (not= top 0) (not= left 0)) :show true :min-width true diff --git a/frontend/src/app/main/ui/dashboard/files.cljs b/frontend/src/app/main/ui/dashboard/files.cljs index f1d3d0a314..89dafd69c6 100644 --- a/frontend/src/app/main/ui/dashboard/files.cljs +++ b/frontend/src/app/main/ui/dashboard/files.cljs @@ -128,7 +128,7 @@ :left (- (:x (:menu-pos @local)) 180) :top (:y (:menu-pos @local)) :on-edit on-edit - :on-menu-close on-menu-close + :on-close on-menu-close :on-import on-import}])]])) (mf/defc files-section* diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index fd1f92fdf0..6f8dd68479 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -439,7 +439,7 @@ :can-edit can-edit :navigate true :on-edit on-edit - :on-menu-close on-menu-close + :on-close on-menu-close :origin origin :parent-id (dm/str file-id "-action-menu")}]])]]]]])) diff --git a/frontend/src/app/main/ui/dashboard/project_menu.cljs b/frontend/src/app/main/ui/dashboard/project_menu.cljs index 367fca95ef..4c2d34b7e7 100644 --- a/frontend/src/app/main/ui/dashboard/project_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/project_menu.cljs @@ -17,11 +17,12 @@ [app.main.ui.dashboard.import :as udi] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] + [beicon.v2.core :as rx] + [potok.v2.core :as ptk] [rumext.v2 :as mf])) (mf/defc project-menu* - {::mf/props :obj} - [{:keys [project show on-edit on-menu-close top left on-import]}] + [{:keys [project show on-edit on-close top left on-import]}] (let [top (or top 0) left (or left 0) @@ -42,7 +43,8 @@ (with-meta project {:on-success on-duplicate-success})))) toggle-pin - #(st/emit! (dd/toggle-project-pin project)) + (fn [] + (st/emit! (dd/toggle-project-pin project))) on-move-success (fn [team-id] @@ -63,25 +65,23 @@ (dcm/go-to-dashboard-recent :team-id team-id)))) on-delete - #(st/emit! - (modal/show - {:type :confirm - :title (tr "modals.delete-project-confirm.title") - :message (tr "modals.delete-project-confirm.message") - :accept-label (tr "modals.delete-project-confirm.accept") - :on-accept delete-fn})) + (fn [] + (st/emit! + (modal/show {:type :confirm + :title (tr "modals.delete-project-confirm.title") + :message (tr "modals.delete-project-confirm.message") + :accept-label (tr "modals.delete-project-confirm.accept") + :on-accept delete-fn}))) - file-input (mf/use-ref nil) + file-input + (mf/use-ref nil) 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 - (mf/use-callback - (fn [] - (when (fn? on-import) (on-import)))) + (mf/use-fn + (fn [] (when (fn? on-import) (on-import)))) options [(when-not (:is-default project) @@ -116,9 +116,21 @@ :id "project-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* - {:on-close on-menu-close + {:on-close on-close :show show :fixed (or (not= top 0) (not= left 0)) :min-width true diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs index 538bf9c472..5ca9dd7a6c 100644 --- a/frontend/src/app/main/ui/dashboard/projects.cljs +++ b/frontend/src/app/main/ui/dashboard/projects.cljs @@ -271,7 +271,7 @@ :left (+ 24 (:x (:menu-pos @local))) :top (:y (:menu-pos @local)) :on-edit on-edit-open - :on-menu-close on-menu-close + :on-close on-menu-close :on-import on-import}])]]] [:div {:class (stl/css :grid-container) :ref rowref} diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 4c6a3c6191..f00c638dc4 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -8,7 +8,6 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.data.macros :as dm] - [app.common.spec :as us] [app.common.uuid :as uuid] [app.config :as cf] [app.main.data.auth :as da] @@ -21,7 +20,7 @@ [app.main.refs :as refs] [app.main.router :as rt] [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*]] [app.main.ui.components.link :refer [link]] [app.main.ui.dashboard.comments :refer [comments-icon* comments-section]] @@ -37,7 +36,6 @@ [app.util.object :as obj] [app.util.timers :as ts] [beicon.v2.core :as rx] - [cljs.spec.alpha :as s] [cuerdas.core :as str] [goog.functions :as f] [potok.v2.core :as ptk] @@ -194,7 +192,7 @@ :left (:x (:menu-pos local)) :top (:y (:menu-pos local)) :on-edit on-edit-open - :on-menu-close on-menu-close}]])) + :on-close on-menu-close}]])) (mf/defc sidebar-search [{:keys [search-term team-id] :as props}] @@ -273,47 +271,24 @@ :on-click on-clear-click} search-icon])])) -(mf/defc teams-selector-dropdown-items +(mf/defc teams-selector-dropdown* {::mf/wrap-props false} - [{:keys [team profile teams] :as props}] - (let [on-create-clicked - (mf/use-fn - #(st/emit! (modal/show :team-form {}))) + [{:keys [team profile teams] :rest props}] + (let [on-create-click + (mf/use-fn #(st/emit! (modal/show :team-form {}))) - team-selected + on-team-click (mf/use-fn (fn [event] (let [team-id (-> (dom/get-current-target event) (dom/get-data "value") (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 - (mf/use-fn - (mf/deps profile team-selected) - (fn [event] - (when (kbd/enter? event) - (team-selected (:default-team-id profile) event)))) + [:> dropdown-menu* props - handle-select-team - (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 + [:> dropdown-menu-item* {:on-click on-team-click :data-value (:default-team-id profile) - :on-key-down handle-select-default - :id "teams-selector-default-team" :class (stl/css :team-dropdown-item)} [:span {:class (stl/css :penpot-icon)} i/logo-icon] @@ -322,12 +297,10 @@ tick-icon)] (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) - :on-key-down handle-select-team - :id (str "teams-selector-" (:id team-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) :class (stl/css :team-picture) :alt (:name team-item)}] @@ -342,21 +315,14 @@ (when (= (:id team-item) (:id team)) tick-icon)]) - [:hr {:role "separator" - :class (stl/css :team-separator)}] - [:> dropdown-menu-item* {:on-click on-create-clicked - :on-key-down handle-creation-key-down - :id "teams-selector-create-team" + [:hr {:role "separator" :class (stl/css :team-separator)}] + [:> dropdown-menu-item* {:on-click on-create-click :class (stl/css :team-dropdown-item :action)} [:span {:class (stl/css :icon-wrapper)} add-icon] [:span {:class (stl/css :team-text)} (tr "dashboard.create-new-team")]]])) -(s/def ::member-id ::us/uuid) -(s/def ::leave-modal-form - (s/keys :req-un [::member-id])) - -(mf/defc team-options-dropdown - [{:keys [team profile] :as props}] +(mf/defc team-options-dropdown* + [{:keys [team profile] :rest props}] (let [go-members #(st/emit! (dcm/go-to-dashboard-members)) go-invitations #(st/emit! (dcm/go-to-dashboard-invitations)) go-webhooks #(st/emit! (dcm/go-to-dashboard-webhooks)) @@ -449,218 +415,125 @@ :title (tr "modals.delete-team-confirm.title") :message (tr "modals.delete-team-confirm.message") :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 - :on-key-down handle-members - :className (stl/css :team-options-item) - :id "teams-options-members" - :data-testid "team-members"} + :class (stl/css :team-options-item) + :data-testid "team-members"} (tr "labels.members")] [:> dropdown-menu-item* {:on-click go-invitations - :on-key-down handle-invitations - :className (stl/css :team-options-item) - :id "teams-options-invitations" - :data-testid "team-invitations"} + :class (stl/css :team-options-item) + :data-testid "team-invitations"} (tr "labels.invitations")] (when (contains? cf/flags :webhooks) - [:> dropdown-menu-item* {:on-click go-webhooks - :on-key-down handle-webhooks - :className (stl/css :team-options-item) - :id "teams-options-webhooks"} + [:> dropdown-menu-item* {:on-click go-webhooks + :class (stl/css :team-options-item)} (tr "labels.webhooks")]) [:> dropdown-menu-item* {:on-click go-settings - :on-key-down handle-settings - :className (stl/css :team-options-item) - :id "teams-options-settings" - :data-testid "team-settings"} + :class (stl/css :team-options-item) + :data-testid "team-settings"} (tr "labels.settings")] [:hr {:class (stl/css :team-option-separator)}] (when can-rename? [:> dropdown-menu-item* {:on-click on-rename-clicked - :on-key-down handle-rename - :id "teams-options-rename" - :className (stl/css :team-options-item) - :data-testid "rename-team"} + :class (stl/css :team-options-item) + :data-testid "rename-team"} (tr "labels.rename")]) (cond (= (count members) 1) - [:> dropdown-menu-item* {:on-click leave-and-close - :on-key-down handle-leave-and-close - :className (stl/css :team-options-item) - :id "teams-options-leave-team"} + [:> dropdown-menu-item* {:on-click leave-and-close + :class (stl/css :team-options-item)} (tr "dashboard.leave-team")] (get-in team [:permissions :is-owner]) [:> dropdown-menu-item* {:on-click on-leave-as-owner-clicked - :on-key-down handle-leave-as-owner-clicked - :id "teams-options-leave-team" - :className (stl/css :team-options-item) - :data-testid "leave-team"} + :class (stl/css :team-options-item) + :data-testid "leave-team"} (tr "dashboard.leave-team")] (> (count members) 1) - [:> dropdown-menu-item* {:on-click on-leave-clicked - :on-key-down handle-on-leave-clicked - :className (stl/css :team-options-item) - :id "teams-options-leave-team"} + [:> dropdown-menu-item* {:on-click on-leave-clicked + :class (stl/css :team-options-item)} (tr "dashboard.leave-team")]) (when (get-in team [:permissions :is-owner]) [:> dropdown-menu-item* {:on-click on-delete-clicked - :on-key-down handle-on-delete-clicked - :id "teams-options-delete-team" - :className (stl/css :team-options-item :warning) - :data-testid "delete-team"} + :class (stl/css :team-options-item :warning) + :data-testid "delete-team"} (tr "dashboard.delete-team")])])) (mf/defc sidebar-team-switch [{:keys [team profile] :as props}] - (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")] + (let [teams (mf/deref refs/teams) + subscription + (get team :subscription) - ;; _ (prn "--------------- sidebar-team-switch") - ;; _ (app.common.pprint/pprint teams) + subscription-type + (get-subscription-type subscription) - handle-show-team-click - (fn [event] - (dom/stop-propagation event) - (swap! show-teams-ddwn? not) - (reset! show-team-opts-ddwn? false)) + show-team-options-menu* + (mf/use-state false) - handle-show-team-keydown - (fn [event] - (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))))))) + show-team-options-menu? + (deref show-team-options-menu*) - 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 - #(reset! show-team-opts-ddwn? false)) + (fn [event] + (dom/stop-propagation event) + (swap! show-teams-menu* not))) - handle-show-opts-click - (fn [event] - (dom/stop-propagation event) - (swap! show-team-opts-ddwn? not) - (reset! show-teams-ddwn? false)) + on-show-teams-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!))))) - handle-show-opts-keydown - (fn [event] - (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))))))) + close-team-options-menu + (mf/use-fn #(reset! show-team-options-menu* false)) - handle-close-team - (fn [] - (reset! show-teams-ddwn? false)) - subscription (:subscription team) - subscription-type (get-subscription-type subscription)] + on-show-options-click + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (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 :switch-content)} [:button {:class (stl/css :current-team) - :on-click handle-show-team-click - :on-key-down handle-show-team-keydown} + :on-click on-show-teams-click + :on-key-down on-show-teams-keydown} (cond (:is-default team) [:div {:class (stl/css :team-name)} @@ -691,29 +564,28 @@ (when-not (:is-default team) [:button {:class (stl/css :switch-options) - :on-click handle-show-opts-click + :on-click on-show-options-click :aria-label "team-management" :tab-index "0" - :on-key-down handle-show-opts-keydown} + :on-key-down on-show-options-keydown} menu-icon])] ;; Teams Dropdown - [:& dropdown-menu {:show @show-teams-ddwn? - :on-close handle-close-team - :ids ids - :list-class (stl/css :dropdown :teams-dropdown)} - [:& teams-selector-dropdown-items {:ids ids - :team team - :profile profile - :teams teams}]] + [:> teams-selector-dropdown* {:show show-teams-menu? + :on-close close-teams-menu + :id "team-list" + :class (stl/css :dropdown :teams-dropdown) + :team team + :profile profile + :teams teams}] - [:& dropdown-menu {:show @show-team-opts-ddwn? - :on-close close-team-opts-ddwn - :ids options-ids - :list-class (stl/css :dropdown :options-dropdown)} - [:& team-options-dropdown {:team team - :profile profile}]]])) + [:> team-options-dropdown* {:show show-team-options-menu? + :on-close close-team-options-menu + :id "team-options" + :class (stl/css :dropdown :options-dropdown) + :team team + :profile profile}]])) (mf/defc sidebar-content* {::mf/private true @@ -806,7 +678,7 @@ (mf/use-layout-effect (mf/deps pinned-projects) (fn [] - (let [dom (mf/ref-val container) + (let [dom (mf/ref-val container) client-height (obj/get dom "clientHeight") scroll-height (obj/get dom "scrollHeight")] (reset! overflow* (> scroll-height client-height))))) @@ -878,15 +750,17 @@ (mf/defc profile-section* {::mf/props :obj} [{:keys [profile team]}] - (let [show* (mf/use-state false) - show (deref show*) - photo (cf/resolve-profile-photo-url profile) + (let [show-profile-menu* (mf/use-state false) + show-profile-menu? (deref show-profile-menu*) + + photo + (cf/resolve-profile-photo-url profile) on-click (mf/use-fn (fn [section event] (dom/stop-propagation event) - (reset! show* false) + (reset! show-profile-menu* false) (if (keyword? section) (st/emit! (rt/nav section)) (st/emit! section)))) @@ -917,24 +791,16 @@ (mf/use-fn (fn [event] (dom/stop-propagation event) - (swap! show* not))) + (swap! show-profile-menu* not))) handle-key-down (mf/use-fn (fn [event] (when (kbd/enter? event) - (reset! show* true)))) + (reset! show-profile-menu* true)))) on-close - (fn [event] - (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)))) + (mf/use-fn #(reset! show-profile-menu* false)) handle-click-url (mf/use-fn @@ -943,40 +809,13 @@ (dom/get-data "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 (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 (mf/use-fn #(on-click (da/logout) %)) - handle-logout-keydown - (mf/use-fn - (fn [event] - (when (kbd/enter? event) - (on-click (da/logout) event)))) - handle-set-profile (mf/use-fn #(on-click :settings-profile %)) @@ -997,7 +836,8 @@ :on-click on-power-up-click} [:div {:class (stl/css :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)} (tr "subscription.dashboard.upgrade-plan.power-up")]]) @@ -1020,85 +860,67 @@ :alt (:fullname profile)}] [:span {:class (stl/css :profile-fullname)} (:fullname profile)]] - [:& dropdown-menu {:on-close on-close - :show show - :list-class (stl/css :profile-dropdown)} - [:li {:tab-index (if show "0" "-1") - :class (stl/css :profile-dropdown-item) - :on-click handle-set-profile - :on-key-down handle-key-down-profile - :data-testid "profile-profile-opt"} + [:> dropdown-menu* {:on-close on-close + :show show-profile-menu? + :id "profile-menu" + :class (stl/css :profile-dropdown)} + [:> dropdown-menu-item* {:class (stl/css :profile-dropdown-item) + :on-click handle-set-profile + :data-testid "profile-profile-opt"} (tr "labels.your-account")] [:li {:class (stl/css :profile-separator)}] - [:li {:class (stl/css :profile-dropdown-item) - :tab-index (if show "0" "-1") - :data-url "https://help.penpot.app" - :on-click handle-click-url - :on-key-down handle-keydown-url - :data-testid "help-center-profile-opt"} + [:> dropdown-menu-item* {:class (stl/css :profile-dropdown-item) + :data-url "https://help.penpot.app" + :on-click handle-click-url + :data-testid "help-center-profile-opt"} (tr "labels.help-center")] - [:li {:tab-index (if show "0" "-1") - :class (stl/css :profile-dropdown-item) - :data-url "https://community.penpot.app" - :on-click handle-click-url - :on-key-down handle-keydown-url} + [:> dropdown-menu-item* {:class (stl/css :profile-dropdown-item) + :data-url "https://community.penpot.app" + :on-click handle-click-url} (tr "labels.community")] - [:li {:tab-index (if show "0" "-1") - :class (stl/css :profile-dropdown-item) - :data-url "https://www.youtube.com/c/Penpot" - :on-click handle-click-url - :on-key-down handle-keydown-url} + [:> dropdown-menu-item* {:class (stl/css :profile-dropdown-item) + :data-url "https://www.youtube.com/c/Penpot" + :on-click handle-click-url} (tr "labels.tutorials")] - [:li {:tab-index (if show "0" "-1") - :class (stl/css :profile-dropdown-item) - :on-click show-release-notes - :on-key-down handle-show-release-notes} + [:> dropdown-menu-item* {:tab-index "0" + :class (stl/css :profile-dropdown-item) + :on-click show-release-notes} (tr "labels.release-notes")] [:li {:class (stl/css :profile-separator)}] - [:li {:class (stl/css :profile-dropdown-item) - :tab-index (if show "0" "-1") - :data-url "https://penpot.app/libraries-templates" - :on-click handle-click-url - :on-key-down handle-keydown-url - :data-testid "libraries-templates-profile-opt"} + [:> dropdown-menu-item* {:class (stl/css :profile-dropdown-item) + :data-url "https://penpot.app/libraries-templates" + :on-click handle-click-url + :data-testid "libraries-templates-profile-opt"} (tr "labels.libraries-and-templates")] - [:li {:tab-index (if show "0" "-1") - :class (stl/css :profile-dropdown-item) - :data-url "https://github.com/penpot/penpot" - :on-click handle-click-url - :on-key-down handle-keydown-url} + [:> dropdown-menu-item* {:class (stl/css :profile-dropdown-item) + :data-url "https://github.com/penpot/penpot" + :on-click handle-click-url} (tr "labels.github-repo")] - [:li {:tab-index (if show "0" "-1") - :class (stl/css :profile-dropdown-item) - :data-url "https://penpot.app/terms" - :on-click handle-click-url - :on-key-down handle-keydown-url} + [:> dropdown-menu-item* {:class (stl/css :profile-dropdown-item) + :data-url "https://penpot.app/terms" + :on-click handle-click-url} (tr "auth.terms-of-service")] [:li {:class (stl/css :profile-separator)}] (when (contains? cf/flags :user-feedback) - [:li {:class (stl/css :profile-dropdown-item) - :tab-index (if show "0" "-1") - :on-click handle-feedback-click - :on-key-down handle-feedback-keydown - :data-testid "feedback-profile-opt"} + [:> dropdown-menu-item* {:class (stl/css :profile-dropdown-item) + :on-click handle-feedback-click + :data-testid "feedback-profile-opt"} (tr "labels.give-feedback")]) - [:li {:class (stl/css :profile-dropdown-item :item-with-icon) - :tab-index (if show "0" "-1") - :on-click handle-logout-click - :on-key-down handle-logout-keydown - :data-testid "logout-profile-opt"} + [:> dropdown-menu-item* {:class (stl/css :profile-dropdown-item :item-with-icon) + :on-click handle-logout-click + :data-testid "logout-profile-opt"} exit-icon (tr "labels.logout")]] diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 1ff0c5f3be..650de506cb 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -304,7 +304,7 @@ [:div {:class (stl/css :rol-selector)} [: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) :role "listbox"} [:li {:on-click on-set-viewer @@ -341,7 +341,7 @@ :on-click on-show} 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)} (when is-you? [:li {:on-click on-leave @@ -592,7 +592,7 @@ [:div {:class (stl/css :rol-selector)} [: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)} [:li {:data-role "admin" :class (stl/css :rol-dropdown-item) @@ -692,7 +692,7 @@ :on-click on-show} 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)} [:li {:on-click on-copy :class (stl/css :action-dropdown-item)} @@ -982,7 +982,7 @@ [:button {:class (stl/css :menu-btn) :on-click on-show} 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)} [:li {:on-click on-edit :class (stl/css :webhook-dropdown-item)} (tr "labels.edit")] diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs index 3ea6523e64..f517bf34a8 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.cljs +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -28,7 +28,7 @@ [app.main.features :as features] [app.main.refs :as refs] [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.dashboard.subscription :refer [main-menu-power-up* get-subscription-type]] [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 :release-notes :version version}))))))] - [:& dropdown-menu {:show true - :on-close on-close - :list-class (stl/css-case :sub-menu true - :help-info plugins? - :help-info-old (not plugins?))} + [:> dropdown-menu* {:show true + ;; :id "workspace-help-menu" + :on-close on-close + :class (stl/css-case :sub-menu true + :help-info plugins? + :help-info-old (not plugins?))} [:> dropdown-menu-item* {:class (stl/css :submenu-item) :on-click nav-to-helpc-center :on-key-down (fn [event] @@ -182,10 +183,11 @@ [{:keys [layout profile toggle-flag on-close toggle-theme]}] (let [show-nudge-options (mf/use-fn #(modal/show! {:type :nudge-option}))] - [:& dropdown-menu {:show true - :list-class (stl/css-case :sub-menu true - :preferences true) - :on-close on-close} + [:> dropdown-menu* {:show true + ;; :id "workspace-preferences-menu" + :class (stl/css-case :sub-menu true + :preferences true) + :on-close on-close} [:> dropdown-menu-item* {:on-click toggle-flag :class (stl/css :submenu-item) :on-key-down (fn [event] @@ -312,10 +314,11 @@ (-> (dw/toggle-layout-flag :textpalette) (vary-meta assoc ::ev/origin "workspace-menu")))))] - [:& dropdown-menu {:show true - :list-class (stl/css-case :sub-menu true - :view true) - :on-close on-close} + [:> dropdown-menu* {:show true + ;; :id "workspace-view-menu" + :class (stl/css-case :sub-menu true + :view true) + :on-close on-close} [:> dropdown-menu-item* {:class (stl/css :submenu-item) :on-click toggle-flag @@ -430,10 +433,11 @@ perms (mf/use-ctx ctx/permissions) can-edit (:can-edit perms)] - [:& dropdown-menu {:show true - :list-class (stl/css-case :sub-menu true - :edit true) - :on-close on-close} + [:> dropdown-menu* {:show true + ;; :id "workspace-edit-menu" + :class (stl/css-case :sub-menu true + :edit true) + :on-close on-close} [:> dropdown-menu-item* {:class (stl/css :submenu-item) :on-click select-all @@ -592,10 +596,11 @@ (when (kbd/enter? event) (on-export-frames event))))] - [:& dropdown-menu {:show true - :list-class (stl/css-case :sub-menu true - :file true) - :on-close on-close} + [:> dropdown-menu* {:show true + ;; :id "workspace-file-menu" + :class (stl/css-case :sub-menu true + :file true) + :on-close on-close} (if ^boolean shared? (when can-edit @@ -690,9 +695,10 @@ (let [plugins (preg/plugins-list) user-can-edit? (:can-edit (deref refs/permissions)) permissions-peek (deref refs/plugins-permissions-peek)] - [:& dropdown-menu {:show true - :list-class (stl/css-case :sub-menu true :plugins true) - :on-close on-close} + [:> dropdown-menu* {:show true + ;; :id "workspace-plugins-menu" + :class (stl/css-case :sub-menu true :plugins true) + :on-close on-close} [:> dropdown-menu-item* {:on-click open-plugins :class (stl/css :submenu-item) :on-key-down (fn [event] @@ -840,9 +846,10 @@ :on-click open-menu :icon "menu"}] - [:& dropdown-menu {:show show-menu? - :on-close close-menu - :list-class (stl/css :menu)} + [:> dropdown-menu* {:show show-menu? + :id "workspace-menu" + :on-close close-menu + :class (stl/css :menu)} [:> dropdown-menu-item* {:class (stl/css :menu-item) :on-click on-menu-click :on-key-down (fn [event] diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs index a83c936870..7bffc618af 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs @@ -11,7 +11,7 @@ [app.config :as cf] [app.main.data.modal :as modal] [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*]] [app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.context :as ctx] @@ -123,9 +123,10 @@ :icon "import-export" :variant "secondary"} (tr "workspace.tokens.tools")] - [:& dropdown-menu {:show show-menu? - :on-close close-menu - :list-class (stl/css :import-export-menu)} + [:> dropdown-menu* {:show show-menu? + :on-close close-menu + :id "tokens-menu" + :class (stl/css :import-export-menu)} (when can-edit? [:> dropdown-menu-item* {:class (stl/css :import-export-menu-item) :on-click on-modal-show} diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index d05a8439cc..d8c50d61b1 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -469,6 +469,11 @@ (when (some? node) (.focus node))) +(defn click! + [^js node] + (when (some? node) + (.click node))) + (defn focus? [^js node] (and node @@ -899,4 +904,4 @@ ;; 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. (set! (.. js/document -documentElement -style -overscrollBehaviorX) "none") - (set! (.. js/document -body -style -overscrollBehaviorX) "none")) \ No newline at end of file + (set! (.. js/document -body -style -overscrollBehaviorX) "none"))