From 2a1f76ad1a2bd8c42e85d7c4c65f357a3c635674 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Fri, 22 Nov 2024 13:54:41 +0100 Subject: [PATCH] :recycle: Fix context menu --- .../src/app/main/ui/components/select.cljs | 5 + .../ui/workspace/sidebar/assets/common.cljs | 5 +- .../ui/workspace/tokens/context_menu.cljs | 144 ++++++++++++------ .../ui/workspace/tokens/context_menu.scss | 94 +++++------- .../app/main/ui/workspace/tokens/sidebar.cljs | 16 +- .../app/main/ui/workspace/tokens/sidebar.scss | 30 ++-- frontend/translations/en.po | 12 ++ frontend/translations/es.po | 12 ++ 8 files changed, 199 insertions(+), 119 deletions(-) diff --git a/frontend/src/app/main/ui/components/select.cljs b/frontend/src/app/main/ui/components/select.cljs index 0187499e6..fba040fbf 100644 --- a/frontend/src/app/main/ui/components/select.cljs +++ b/frontend/src/app/main/ui/components/select.cljs @@ -86,6 +86,11 @@ (mf/with-effect [default-value] (swap! state* assoc :current-value default-value)) + (mf/with-effect [is-open?] + (when (and (not= 0 (mf/ref-val dropdown-direction-change*)) (= false is-open?)) + (reset! dropdown-direction* "down") + (mf/set-ref-val! dropdown-direction-change* 0))) + (mf/with-effect [is-open? dropdown-element*] (let [dropdown-element (mf/ref-val dropdown-element*)] (when (and (= 0 (mf/ref-val dropdown-direction-change*)) dropdown-element) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs index e4c31b57b..353ef6b59 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs @@ -129,7 +129,7 @@ (mf/defc asset-section {::mf/wrap-props false} - [{:keys [children file-id title section assets-count icon open?]}] + [{:keys [children file-id title section assets-count icon open? on-click]}] (let [children (-> (array/normalize-to-array children) (array/without-nils)) @@ -159,7 +159,8 @@ [:div {:class (stl/css-case :asset-section true :opened (and (< 0 assets-count) - open?))} + open?)) + :on-click on-click} [:& title-bar {:collapsable (< 0 assets-count) :collapsed (not open?) diff --git a/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs index bc6132253..2f716fd3f 100644 --- a/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs @@ -8,17 +8,19 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.types.tokens-lib :as ctob] [app.main.data.modal :as modal] [app.main.data.tokens :as dt] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] - [app.main.ui.icons :as i] + [app.main.ui.ds.foundations.assets.icon :refer [icon*]] [app.main.ui.workspace.tokens.changes :as wtch] [app.main.ui.workspace.tokens.token :as wtt] [app.main.ui.workspace.tokens.token-types :as wtty] [app.util.dom :as dom] + [app.util.i18n :refer [tr]] [app.util.timers :as timers] [okulary.core :as l] [rumext.v2 :as mf])) @@ -58,7 +60,7 @@ all-action (let [props {:attributes attributes :token token :shape-ids shape-ids}] - {:title "All" + {:title (tr "labels.all") :selected? all-selected? :action #(if all-selected? (st/emit! (wtch/unapply-token props)) @@ -96,7 +98,7 @@ vertical-padding-selected? (and (not all-selected?) (every? selected-pred vertical-attributes)) - padding-items [{:title "All" + padding-items [{:title (tr "labels.all") :selected? all-selected? :action (fn [] (let [props {:attributes all-padding-attrs @@ -199,6 +201,7 @@ {:title "Sizing" :submenu :sizing} :separator {:title "Border Radius" :submenu :border-radius}] + [:separator] (stroke-width context-data) [:separator] (generic-attribute-actions #{:x} "X" (assoc context-data :on-update-shape wtch/update-shape-position)) @@ -206,11 +209,8 @@ (defn default-actions [{:keys [token selected-token-set-path]}] (let [{:keys [modal]} (wtty/get-token-properties token)] - [{:title "Delete Token" - :action #(st/emit! (dt/delete-token (ctob/set-path->set-name selected-token-set-path) (:name token)))} - {:title "Duplicate Token" - :action #(st/emit! (dt/duplicate-token (:name token)))} - {:title "Edit Token" + [{:title (tr "workspace.token.edit") + :no-selectable true :action (fn [event] (let [{:keys [key fields]} modal] (st/emit! dt/hide-token-context-menu) @@ -222,9 +222,11 @@ :action "edit" :selected-token-set-path selected-token-set-path :token token})))} - {:title "Duplicate Token" + {:title (tr "workspace.token.duplicate") + :no-selectable true :action #(st/emit! (dt/duplicate-token (:name token)))} - {:title "Delete Token" + {:title (tr "workspace.token.delete") + :no-selectable true :action #(st/emit! (-> selected-token-set-path ctob/prefixed-set-path-string->set-name-string (dt/delete-token (:name token))))}])) @@ -237,6 +239,12 @@ (when (seq attribute-actions) [:separator]) (default-actions context-data)))) +(defn submenu-actions-selection-actions [{:keys [type token] :as context-data}] + (let [with-actions (get shape-attribute-actions-map (or type (:type token))) + attribute-actions (if with-actions (with-actions context-data) [])] + (concat + attribute-actions))) + ;; Components ------------------------------------------------------------------ (def tokens-menu-ref @@ -249,69 +257,93 @@ (mf/defc menu-entry {::mf/props :obj} - [{:keys [title value on-click selected? children submenu-offset]}] + [{:keys [title value on-click selected? children submenu-offset submenu-direction no-selectable]}] (let [submenu-ref (mf/use-ref nil) hovering? (mf/use-ref false) on-pointer-enter - (mf/use-callback + (mf/use-fn (fn [] (mf/set-ref-val! hovering? true) (when-let [submenu-node (mf/ref-val submenu-ref)] (dom/set-css-property! submenu-node "display" "block")))) + on-pointer-leave - (mf/use-callback + (mf/use-fn (fn [] (mf/set-ref-val! hovering? false) (when-let [submenu-node (mf/ref-val submenu-ref)] (timers/schedule 50 #(when-not (mf/ref-val hovering?) (dom/set-css-property! submenu-node "display" "none")))))) + set-dom-node - (mf/use-callback + (mf/use-fn (fn [dom] (let [submenu-node (mf/ref-val submenu-ref)] - (when (and (some? dom) (some? submenu-node)) - (dom/set-css-property! submenu-node "top" (str (.-offsetTop dom) "px"))))))] - [:li - {:class (stl/css :context-menu-item) - :ref set-dom-node - :data-value value - :on-click on-click - :on-pointer-enter on-pointer-enter - :on-pointer-leave on-pointer-leave} + (when (and (some? dom) (some? submenu-node) (= submenu-direction "up")) + (dom/set-css-property! submenu-node "top" "unset")) + (when (and (some? dom) (some? submenu-node) (= submenu-direction "down")) + (dom/set-css-property! submenu-node "top" (dm/str (.-offsetTop dom) "px"))))))] + + (mf/use-effect + (mf/deps submenu-direction) + (fn [] + (let [submenu-node (mf/ref-val submenu-ref)] + (when (= submenu-direction "up") + (dom/set-css-property! submenu-node "top" "unset"))))) + + [:li {:class (stl/css :context-menu-item) + :ref set-dom-node + :data-value value + :on-click on-click + :on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave} (when selected? - [:span {:class (stl/css :icon-wrapper)} - [:span {:class (stl/css :selected-icon)} i/tick]]) - [:span {:class (stl/css :title)} title] + [:> icon* {:id "tick" :size "s" :class (stl/css :icon-wrapper)}]) + [:span {:class (stl/css-case :item-text true + :item-with-icon-space (and + (not selected?) + (not no-selectable)))} + title] (when children [:* - [:span {:class (stl/css :submenu-icon)} i/arrow] + [:> icon* {:id "arrow" :size "s"}] [:ul {:class (stl/css :token-context-submenu) + :data-direction submenu-direction :ref submenu-ref + ;; Under review: This distances are arbitrary, + ;; https://tree.taiga.io/project/penpot/task/9627 :style {:display "none" - :top 0 - :left (str submenu-offset "px")} + :--dist (if (= submenu-direction "down") + "-80px" + "80px") + :left (dm/str submenu-offset "px")} :on-context-menu prevent-default} children]])])) (mf/defc menu-tree - [{:keys [selected-shapes] :as context-data}] + [{:keys [selected-shapes submenu-offset submenu-direction type] :as context-data}] (let [entries (if (seq selected-shapes) - (selection-actions context-data) + (if (some? type) + (submenu-actions-selection-actions context-data) + (selection-actions context-data)) (default-actions context-data))] - (for [[index {:keys [title action selected? submenu] :as entry}] (d/enumerate entries)] - [:* {:key (str title " " index)} + (for [[index {:keys [title action selected? submenu no-selectable] :as entry}] (d/enumerate entries)] + [:* {:key (dm/str title " " index)} (cond (= :separator entry) [:li {:class (stl/css :separator)}] submenu [:& menu-entry {:title title - :submenu-offset (:submenu-offset context-data)} + :no-selectable true + :submenu-direction submenu-direction + :submenu-offset submenu-offset} [:& menu-tree (assoc context-data :type submenu)]] :else [:& menu-entry {:title title :on-click action + :no-selectable no-selectable :selected? selected?}])]))) (mf/defc token-context-menu-tree - [{:keys [width] :as mdata}] + [{:keys [width direction] :as mdata}] (let [objects (mf/deref refs/workspace-page-objects) selected (mf/deref refs/selected-shapes) selected-shapes (into [] (keep (d/getf objects)) selected) @@ -320,27 +352,51 @@ selected-token-set-path (mf/deref refs/workspace-selected-token-set-path)] [:ul {:class (stl/css :context-list)} [:& menu-tree {:submenu-offset width + :submenu-direction direction :token token :selected-token-set-path selected-token-set-path :selected-shapes selected-shapes}]])) (mf/defc token-context-menu [] - (let [mdata (mf/deref tokens-menu-ref) - top (+ (get-in mdata [:position :y]) 5) - left (+ (get-in mdata [:position :x]) 5) - width (mf/use-state 0) - dropdown-ref (mf/use-ref)] + (let [mdata (mf/deref tokens-menu-ref) + is-open? (boolean mdata) + width (mf/use-state 0) + dropdown-ref (mf/use-ref) + dropdown-direction* (mf/use-state "down") + dropdown-direction (deref dropdown-direction*) + dropdown-direction-change* (mf/use-ref 0) + top (+ (get-in mdata [:position :y]) 5) + left (+ (get-in mdata [:position :x]) 5)] + (mf/use-effect - (mf/deps mdata) + (mf/deps is-open?) (fn [] (when-let [node (mf/ref-val dropdown-ref)] (reset! width (.-offsetWidth node))))) - [:& dropdown {:show (boolean mdata) + + (mf/with-effect [is-open?] + (when (and (not= 0 (mf/ref-val dropdown-direction-change*)) (= false is-open?)) + (reset! dropdown-direction* "down") + (mf/set-ref-val! dropdown-direction-change* 0))) + + (mf/with-effect [is-open? dropdown-ref] + (let [dropdown-element (mf/ref-val dropdown-ref)] + (when (and (= 0 (mf/ref-val dropdown-direction-change*)) dropdown-element) + (let [is-outside? (dom/is-element-outside? dropdown-element)] + (reset! dropdown-direction* (if is-outside? "up" "down")) + (mf/set-ref-val! dropdown-direction-change* (inc (mf/ref-val dropdown-direction-change*))))))) + + [:& dropdown {:show is-open? :on-close #(st/emit! dt/hide-token-context-menu)} [:div {:class (stl/css :token-context-menu) :ref dropdown-ref - :style {:top top :left left} + :data-direction dropdown-direction + :style {:--bottom (if (= dropdown-direction "up") + "40px" + "unset") + :--top (dm/str top "px") + :left (dm/str left "px")} :on-context-menu prevent-default} (when mdata - [:& token-context-menu-tree (assoc mdata :offset @width)])]])) + [:& token-context-menu-tree (assoc mdata :width @width :direction dropdown-direction)])]])) diff --git a/frontend/src/app/main/ui/workspace/tokens/context_menu.scss b/frontend/src/app/main/ui/workspace/tokens/context_menu.scss index c1d6cc573..2d3aeab92 100644 --- a/frontend/src/app/main/ui/workspace/tokens/context_menu.scss +++ b/frontend/src/app/main/ui/workspace/tokens/context_menu.scss @@ -4,6 +4,7 @@ // // Copyright (c) KALEIDOS INC +@use "../../ds/typography.scss" as *; @import "refactor/common-refactor.scss"; .token-context-menu { @@ -11,6 +12,14 @@ z-index: $z-index-4; } +.token-context-menu[data-direction="up"] { + bottom: var(--bottom); +} + +.token-context-menu[data-direction="down"] { + top: var(--top); +} + .context-list, .token-context-submenu { @include menuShadow; @@ -18,15 +27,18 @@ width: $s-240; padding: $s-4; border-radius: $br-8; - border: $s-2 solid var(--panel-border-color); - background-color: var(--menu-background-color); + border: $s-2 solid var(--color-background-quaternary); + background-color: var(--color-background-tertiary); max-height: 100vh; overflow-y: auto; +} - li { - @include bodySmallTypography; - color: var(--menu-foreground-color); - } +.token-context-submenu[data-direction="up"] { + bottom: var(--dist); +} + +.token-context-submenu[data-direction="down"] { + top: var(--dist); } .token-context-submenu { @@ -36,68 +48,46 @@ } .separator { - @include bodySmallTypography; margin: $s-6; border-block-start: $s-1 solid var(--panel-border-color); } .context-menu-item { + --context-menu-item-bg-color: none; + --context-menu-item-fg-color: var(--color-foreground-primary); + --context-menu-item-border-color: none; + @include use-typography("body-small"); display: flex; align-items: center; height: $s-28; width: 100%; - padding: $s-6; + padding: $s-8; border-radius: $br-8; + color: var(--context-menu-item-fg-color); + background-color: var(--context-menu-item-bg-color); + border: $s-1 solid var(--context-menu-item-border-color); cursor: pointer; - - .title { - flex-grow: 1; - @include bodySmallTypography; - color: var(--menu-foreground-color); - margin-left: calc(($s-32 + $s-28) / 2); - } - - .icon-wrapper { - display: grid; - grid-template-columns: 1fr 1fr; - } - - .icon-wrapper + .title { - margin-left: $s-6; - } - - .selected-icon { - svg { - @extend .button-icon-small; - stroke: var(--menu-foreground-color); - } - } - - .submenu-icon { - margin-left: $s-2; - svg { - @extend .button-icon-small; - stroke: var(--menu-foreground-color); - } - } - &:hover { - background-color: var(--menu-background-color-hover); - .title { - color: var(--menu-foreground-color-hover); - } - .shortcut { - color: var(--menu-shortcut-foreground-color-hover); - } + --context-menu-item-bg-color: var(--color-background-quaternary); } &:focus { - border: 1px solid var(--menu-border-color-focus); - background-color: var(--menu-background-color-focus); + --context-menu-item-bg-color: var(--menu-background-color-focus); + --context-menu-item-border-color: var(--color-background-tertiary); } - &[disabled] { - pointer-events: none; - opacity: 0.6; + &[aria-selected="true"] { + --context-menu-item-bg-color: var(--color-background-quaternary); } } + +.item-text { + flex-grow: 1; +} + +.item-with-icon-space { + padding-left: $s-20; +} +.icon-wrapper { + margin-right: $s-4; +} diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs index ba5c84950..edf8eca75 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs @@ -79,9 +79,10 @@ (fn [event token] (dom/prevent-default event) (dom/stop-propagation event) - (st/emit! (dt/show-token-context-menu {:type :token - :position (dom/get-client-position event) - :token-name (:name token)})))) + (st/emit! (dt/show-token-context-menu + {:type :token + :position (dom/get-client-position event) + :token-name (:name token)})))) on-toggle-open-click (mf/use-fn (mf/deps open? tokens) @@ -249,7 +250,8 @@ [:& token-context-menu] [:& title-bar {:all-clickable true :title "TOKENS"}] - (for [{:keys [token-key token-type-props tokens]} (concat (:filled token-groups) (:empty token-groups))] + (for [{:keys [token-key token-type-props tokens]} (concat (:filled token-groups) + (:empty token-groups))] [:& token-component {:key token-key :type token-key :selected-shapes selected-shapes @@ -276,6 +278,10 @@ (reset! show-menu* false))) input-ref (mf/use-ref) + on-option-click + (mf/use-fn + #(.click (mf/ref-val input-ref))) + on-import (fn [event] (let [file (-> event .-target .-files (aget 0))] @@ -313,7 +319,7 @@ :on-close close-menu :list-class (stl/css :import-export-menu)} [:> dropdown-menu-item* {:class (stl/css :import-export-menu-item) - :on-click #(.click (mf/ref-val input-ref))} + :on-click on-option-click} (tr "labels.import")] [:> dropdown-menu-item* {:class (stl/css :import-export-menu-item) diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.scss b/frontend/src/app/main/ui/workspace/tokens/sidebar.scss index 62010417d..1241b2a67 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.scss +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.scss @@ -39,8 +39,8 @@ } .themes-header { + @include use-typography("headline-small"); display: block; - @include headlineSmallTypography; margin-bottom: $s-8; padding-left: $s-8; color: var(--title-foreground-color); @@ -105,6 +105,19 @@ box-shadow: var(--el-shadow-dark); } +.import-export-button { + @extend .button-secondary; + display: flex; + align-items: center; + justify-content: end; + padding: $s-6 $s-8; + text-transform: uppercase; + gap: $s-8; + background-color: var(--color-background-primary); + + box-shadow: var(--el-shadow-dark); +} + .import-export-menu { @extend .menu-dropdown; top: -#{$s-6}; @@ -117,23 +130,8 @@ .import-export-menu-item { @extend .menu-item-base; cursor: pointer; - .open-arrow { - @include flexCenter; - svg { - @extend .button-icon; - stroke: var(--icon-foreground); - } - } &:hover { color: var(--menu-foreground-color-hover); - .open-arrow { - svg { - stroke: var(--menu-foreground-color-hover); - } - } - .shortcut-key { - color: var(--menu-shortcut-foreground-color-hover); - } } } diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 884636cdd..53db08b4d 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -6742,6 +6742,18 @@ msgstr "There are no sets yet." msgid "workspace.token.no-sets-create" msgstr "There are no sets defined yet. Create one first." +#: src/app/main/ui/workspace/tokens/context_menu.cljs +msgid "workspace.token.delete" +msgstr "Delete token" + +#: src/app/main/ui/workspace/tokens/context_menu.cljs +msgid "workspace.token.duplicate" +msgstr "Duplicate token" + +#: src/app/main/ui/workspace/tokens/context_menu.cljs +msgid "workspace.token.edit" +msgstr "Edit token" + msgid "workspace.versions.button.save" msgstr "Save version" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index a123a2de6..fbf7d0c48 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -6746,6 +6746,18 @@ msgstr "Crea uno." msgid "workspace.token.no-sets-create" msgstr "Aun no hay sets definidos. Crea uno primero" +#: src/app/main/ui/workspace/tokens/context_menu.cljs +msgid "workspace.token.delete" +msgstr "Eliminar token" + +#: src/app/main/ui/workspace/tokens/context_menu.cljs +msgid "workspace.token.duplicate" +msgstr "Duplicar token" + +#: src/app/main/ui/workspace/tokens/context_menu.cljs +msgid "workspace.token.edit" +msgstr "Editar token" + msgid "workspace.versions.button.save" msgstr "Guardar versiĆ³n"