0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-20 05:34:23 -05:00

♻️ Fix context menu

This commit is contained in:
Eva Marco 2024-11-22 13:54:41 +01:00
parent 6284f42a70
commit 2a1f76ad1a
8 changed files with 199 additions and 119 deletions

View file

@ -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)

View file

@ -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?)

View file

@ -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)])]]))

View file

@ -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;
}

View file

@ -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)

View file

@ -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);
}
}
}

View file

@ -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"

View file

@ -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"