mirror of
https://github.com/penpot/penpot.git
synced 2026-09-30 07:46:16 +00:00
♻️ Add DS tooltip to token pills" (#11900)
* 🐛 Fix can-edit permission wiring for token pills * ✨ Use DS tooltip component in token pill * 🎉 Add playwright test * 🐛 Fix tests
This commit is contained in:
parent
7e3f779d8c
commit
20c818661d
@ -1924,11 +1924,11 @@ test("User disables the current set but token still have resolved values shown i
|
||||
name: "#ff0000 color.primary",
|
||||
});
|
||||
await expect(colorTokenPill).toHaveCount(1);
|
||||
await colorTokenPill.hover(); // Force title attribute to be attached to the button
|
||||
await expect(colorTokenPill).toHaveAttribute(
|
||||
"title",
|
||||
/Resolved value: #ff0000/,
|
||||
);
|
||||
await colorTokenPill.hover();
|
||||
|
||||
const tooltip = page.getByRole("tooltip");
|
||||
await expect(tooltip).toBeVisible();
|
||||
await expect(tooltip).toContainText("Resolved value: #ff0000");
|
||||
});
|
||||
|
||||
test.describe("User can't create groups that clash with token names", () => {
|
||||
|
||||
@ -337,3 +337,38 @@ test("User can see an error on token pill and token modal form when token has an
|
||||
});
|
||||
await expect(submitButton).toBeEnabled();
|
||||
});
|
||||
|
||||
test("User sees a formatted tooltip when hovering a token pill", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { tokensSidebar } = await setupTokensFileRender(page);
|
||||
|
||||
await unfoldTokenType(tokensSidebar, "color");
|
||||
|
||||
const colorToken = tokensSidebar.getByRole("button", {
|
||||
name: "colors.blue.100",
|
||||
});
|
||||
await expect(colorToken).toBeVisible();
|
||||
|
||||
// No tooltip should be rendered before hovering the pill
|
||||
await expect(page.getByRole("tooltip")).not.toBeVisible();
|
||||
|
||||
await colorToken.hover();
|
||||
|
||||
const tooltip = page.getByRole("tooltip");
|
||||
await expect(tooltip).toBeVisible();
|
||||
|
||||
// The token name is shown next to a "Name:" label, and is bold
|
||||
await expect(tooltip).toContainText("Name:");
|
||||
const tokenNameEl = tooltip.getByText("colors.blue.100", { exact: true });
|
||||
await expect(tokenNameEl).toBeVisible();
|
||||
await expect(tokenNameEl).toHaveCSS("font-weight", "700");
|
||||
|
||||
// The original and resolved values are shown on their own lines
|
||||
await expect(tooltip).toContainText("Original value:");
|
||||
await expect(tooltip).toContainText("Resolved value:");
|
||||
|
||||
// Moving away hides the tooltip again
|
||||
await page.mouse.move(0, 0);
|
||||
await expect(page.getByRole("tooltip")).not.toBeVisible();
|
||||
});
|
||||
|
||||
@ -160,6 +160,7 @@ $arrow-side: 12px;
|
||||
grid-area: content;
|
||||
block-size: fit-content;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.tooltip-trigger {
|
||||
|
||||
@ -95,6 +95,9 @@
|
||||
can-edit-tokens?
|
||||
(mf/use-ctx ctx/can-edit-tokens?)
|
||||
|
||||
can-edit-file?
|
||||
(mf/use-ctx ctx/can-edit?)
|
||||
|
||||
is-selected-inside-layout (d/nilv is-selected-inside-layout false)
|
||||
|
||||
tokens
|
||||
@ -195,4 +198,6 @@
|
||||
:tokens-lib tokens-lib
|
||||
:on-token-pill-click on-token-pill-click
|
||||
:on-pill-context-menu on-pill-context-menu
|
||||
:on-node-context-menu on-node-context-menu}])]))
|
||||
:on-node-context-menu on-node-context-menu
|
||||
:can-edit can-edit-file?
|
||||
:can-edit-tokens can-edit-tokens?}])]))
|
||||
|
||||
@ -18,9 +18,9 @@
|
||||
[app.main.data.workspace.tokens.color :as dwtc]
|
||||
[app.main.data.workspace.tokens.errors :as wte]
|
||||
[app.main.data.workspace.tokens.format :as dwtf]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.ds.foundations.utilities.token.token-status :refer [token-status-icon*]]
|
||||
[app.main.ui.ds.tooltip :refer [tooltip*]]
|
||||
[app.main.ui.ds.utilities.swatch :refer [swatch*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
@ -127,11 +127,14 @@
|
||||
(tr "labels.all"))
|
||||
grouped-values (group-by dimensions-dictionary app-token-keys)
|
||||
|
||||
base-title (dm/str "Token: " name "\n"
|
||||
(tr "workspace.tokens.original-value" (dwtf/format-token-value value)) "\n"
|
||||
(tr "workspace.tokens.resolved-value" (dwtf/format-token-value resolved-value))
|
||||
(when (= (:type token) :number)
|
||||
(dm/str "\n" (tr "workspace.tokens.more-options"))))]
|
||||
name-line
|
||||
(mf/html [:* (dm/str (tr "workspace.tokens.token-name") ": ")
|
||||
[:span {:class (stl/css :token-pill-tooltip-name)} name]])
|
||||
|
||||
rest-of-title (dm/str (tr "workspace.tokens.original-value" (dwtf/format-token-value value)) "\n"
|
||||
(tr "workspace.tokens.resolved-value" (dwtf/format-token-value resolved-value))
|
||||
(when (= (:type token) :number)
|
||||
(dm/str "\n" (tr "workspace.tokens.more-options"))))]
|
||||
|
||||
(cond
|
||||
;; If there are errors, show the appropriate message
|
||||
@ -146,15 +149,21 @@
|
||||
|
||||
;; If the token is applied and the user is a is-viewer, show the details
|
||||
(and is-applied? is-viewer)
|
||||
(->> [base-title
|
||||
(tr "workspace.tokens.applied-to")
|
||||
(if (= :dimensions type)
|
||||
(translate-and-format grouped-values)
|
||||
(str "- " title ": " applied-to))]
|
||||
(str/join "\n"))
|
||||
(mf/html
|
||||
[:*
|
||||
name-line
|
||||
[:div rest-of-title]
|
||||
[:div (tr "workspace.tokens.applied-to")]
|
||||
[:div (if (= :dimensions type)
|
||||
(translate-and-format grouped-values)
|
||||
(str "- " title ": " applied-to))]])
|
||||
|
||||
;; Otherwise only show the base title
|
||||
:else base-title)))
|
||||
:else
|
||||
(mf/html
|
||||
[:*
|
||||
name-line
|
||||
[:div rest-of-title]]))))
|
||||
|
||||
;; FIXME: the token thould already have precalculated references, so
|
||||
;; we don't need to perform this regex operation on each rerender
|
||||
@ -188,11 +197,17 @@
|
||||
|
||||
(mf/defc token-pill*
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [on-click token on-context-menu selected-shapes is-selected-inside-layout active-theme-tokens]}]
|
||||
[{:keys [on-click token on-context-menu selected-shapes is-selected-inside-layout active-theme-tokens can-edit]}]
|
||||
(let [{:keys [name value type]} token
|
||||
resolved-token (get active-theme-tokens (:name token))
|
||||
errors (:errors resolved-token)
|
||||
|
||||
pill-ref (mf/use-ref nil)
|
||||
pill-id (mf/use-id)
|
||||
|
||||
tooltip-content* (mf/use-state "")
|
||||
tooltip-content (deref tooltip-content*)
|
||||
|
||||
has-selected? (pos? (count selected-shapes))
|
||||
is-reference? (cfo/is-reference? token)
|
||||
contains-path? (str/includes? name ".")
|
||||
@ -219,10 +234,8 @@
|
||||
(not half-applied?)
|
||||
(not (attributes-match-selection? selected-shapes attributes {:selected-inside-layout? is-selected-inside-layout})))
|
||||
|
||||
;; FIXME: move to context or props
|
||||
can-edit? (:can-edit (deref refs/permissions))
|
||||
|
||||
is-viewer? (not can-edit?)
|
||||
is-viewer? (not can-edit)
|
||||
|
||||
ref-not-in-active-set
|
||||
(if (contains? cf/flags :tokenscript)
|
||||
@ -268,77 +281,89 @@
|
||||
|
||||
on-context-menu
|
||||
(mf/use-fn
|
||||
(mf/deps can-edit? on-context-menu token)
|
||||
(mf/deps can-edit on-context-menu token)
|
||||
(fn [e]
|
||||
(dom/stop-propagation e)
|
||||
(when can-edit?
|
||||
(when can-edit
|
||||
(on-context-menu e token))))
|
||||
|
||||
on-click
|
||||
(mf/use-fn
|
||||
(mf/deps errors? on-click)
|
||||
(mf/deps errors? on-click can-edit)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(when (and can-edit? (not (seq errors)) on-click)
|
||||
(when (and can-edit (not (seq errors)) on-click)
|
||||
(on-click event))))
|
||||
|
||||
on-hover
|
||||
(mf/use-fn
|
||||
(mf/deps selected-shapes is-viewer? active-theme-tokens token half-applied? no-valid-value ref-not-in-active-set name-collision errors)
|
||||
(fn [event]
|
||||
(let [node (dom/get-current-target event)
|
||||
theme-token (get active-theme-tokens name)
|
||||
(fn [_]
|
||||
(let [theme-token (get active-theme-tokens name)
|
||||
title (generate-tooltip is-viewer? (first selected-shapes) theme-token token
|
||||
half-applied? no-valid-value ref-not-in-active-set name-collision errors)]
|
||||
(dom/set-attribute! node "title" title))))]
|
||||
(reset! tooltip-content* title))))
|
||||
|
||||
[:button {:class (stl/css-case
|
||||
:token-pill true
|
||||
:token-pill-no-icon (and (not status-icon?) (not errors?))
|
||||
:token-pill-default can-edit?
|
||||
:token-pill-disabled disabled?
|
||||
:token-pill-applied (and can-edit? applied?)
|
||||
:token-pill-invalid (and can-edit? errors?)
|
||||
:token-pill-invalid-applied (and applied? errors? can-edit?)
|
||||
;; `tooltip-content` may hold rich (hiccup) content, so it's wrapped
|
||||
;; in a function to satisfy the tooltip's `:content` schema; this
|
||||
;; wrapper is cheap, the actual (expensive) computation still only
|
||||
;; happens once, inside `on-hover`.
|
||||
tooltip-content-fn
|
||||
(mf/use-fn
|
||||
(mf/deps tooltip-content)
|
||||
(fn [] tooltip-content))]
|
||||
|
||||
:token-pill-viewer is-viewer?
|
||||
:token-pill-applied-viewer (and is-viewer?
|
||||
applied?)
|
||||
:token-pill-invalid-viewer (and is-viewer?
|
||||
errors?)
|
||||
:token-pill-invalid-applied-viewer (and is-viewer?
|
||||
applied?))
|
||||
:id (str "token-pill-" (:id token))
|
||||
:type "button"
|
||||
:on-focus on-hover
|
||||
[:> tooltip* {:content tooltip-content-fn
|
||||
:trigger-ref pill-ref
|
||||
:id pill-id}
|
||||
[:button {:ref pill-ref
|
||||
:class (stl/css-case
|
||||
:token-pill true
|
||||
:token-pill-no-icon (and (not status-icon?) (not errors?))
|
||||
:token-pill-default can-edit
|
||||
:token-pill-disabled disabled?
|
||||
:token-pill-applied (and can-edit applied?)
|
||||
:token-pill-invalid (and can-edit errors?)
|
||||
:token-pill-invalid-applied (and applied? errors? can-edit)
|
||||
|
||||
:on-click on-click
|
||||
:on-mouse-enter on-hover
|
||||
:token-pill-viewer is-viewer?
|
||||
:token-pill-applied-viewer (and is-viewer?
|
||||
applied?)
|
||||
:token-pill-invalid-viewer (and is-viewer?
|
||||
errors?)
|
||||
:token-pill-invalid-applied-viewer (and is-viewer?
|
||||
applied?))
|
||||
:id (str "token-pill-" (:id token))
|
||||
:type "button"
|
||||
:on-focus on-hover
|
||||
|
||||
:on-context-menu on-context-menu}
|
||||
(cond
|
||||
errors?
|
||||
[:> icon*
|
||||
{:icon-id i/broken-link
|
||||
:class (stl/css :token-pill-icon)
|
||||
:aria-label (if name-collision
|
||||
(wte/resolve-error-message (first errors))
|
||||
(tr "workspace.tokens.missing-reference"))}]
|
||||
:on-click on-click
|
||||
:on-mouse-enter on-hover
|
||||
|
||||
color
|
||||
[:> swatch* {:background color
|
||||
:size "small"}]
|
||||
:on-context-menu on-context-menu}
|
||||
(cond
|
||||
errors?
|
||||
[:> icon*
|
||||
{:icon-id i/broken-link
|
||||
:class (stl/css :token-pill-icon)
|
||||
:aria-label (if name-collision
|
||||
(wte/resolve-error-message (first errors))
|
||||
(tr "workspace.tokens.missing-reference"))}]
|
||||
|
||||
status-icon?
|
||||
[:> token-status-icon*
|
||||
{:icon-id token-status-id
|
||||
:class (stl/css :token-pill-icon)}])
|
||||
color
|
||||
[:> swatch* {:background color
|
||||
:size "small"}]
|
||||
|
||||
(if contains-path?
|
||||
(let [[_ last-part] (cpn/split-by-last-period name)]
|
||||
[:span {:class (stl/css :divided-name-wrapper)
|
||||
:aria-label name}
|
||||
[:span {:class (stl/css :last-name-wrapper)} last-part]])
|
||||
[:span {:class (stl/css :name-wrapper)
|
||||
:aria-label name}
|
||||
name])]))
|
||||
status-icon?
|
||||
[:> token-status-icon*
|
||||
{:icon-id token-status-id
|
||||
:class (stl/css :token-pill-icon)}])
|
||||
|
||||
(if contains-path?
|
||||
(let [[_ last-part] (cpn/split-by-last-period name)]
|
||||
[:span {:class (stl/css :divided-name-wrapper)
|
||||
:aria-label name}
|
||||
[:span {:class (stl/css :last-name-wrapper)} last-part]])
|
||||
[:span {:class (stl/css :name-wrapper)
|
||||
:aria-label name}
|
||||
name])]]))
|
||||
|
||||
@ -192,3 +192,7 @@
|
||||
--token-pill-accent: var(--color-foreground-error);
|
||||
}
|
||||
}
|
||||
|
||||
.token-pill-tooltip-name {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@ -12,7 +12,6 @@
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.ds.layers.layer-button :refer [layer-button*]]
|
||||
[app.main.ui.workspace.tokens.management.token-pill :refer [token-pill*]]
|
||||
[rumext.v2 :as mf]))
|
||||
@ -29,7 +28,8 @@
|
||||
[:tokens-lib {:optional true} :any]
|
||||
[:on-token-pill-click {:optional true} fn?]
|
||||
[:on-pill-context-menu {:optional true} fn?]
|
||||
[:on-node-context-menu {:optional true} fn?]])
|
||||
[:on-node-context-menu {:optional true} fn?]
|
||||
[:can-edit {:optional true} :boolean]])
|
||||
|
||||
(mf/defc folder-node*
|
||||
{::mf/schema schema:folder-node}
|
||||
@ -43,7 +43,8 @@
|
||||
tokens-lib
|
||||
on-token-pill-click
|
||||
on-pill-context-menu
|
||||
on-node-context-menu]}]
|
||||
on-node-context-menu
|
||||
can-edit]}]
|
||||
(let [full-path (str (name type) "." (:path node))
|
||||
is-folder-expanded (not (contains? (set (or folded-token-paths [])) full-path))
|
||||
children (:children node)
|
||||
@ -100,6 +101,7 @@
|
||||
:on-pill-context-menu on-pill-context-menu
|
||||
:on-node-context-menu on-node-context-menu
|
||||
:tokens-lib tokens-lib
|
||||
:can-edit can-edit
|
||||
:selected-token-set-id selected-token-set-id}]]
|
||||
(let [id (:id (:leaf child))
|
||||
token (ctob/get-token tokens-lib selected-token-set-id id)]
|
||||
@ -110,6 +112,7 @@
|
||||
:is-selected-inside-layout is-selected-inside-layout
|
||||
:active-theme-tokens active-theme-tokens
|
||||
:on-click on-token-pill-click
|
||||
:can-edit can-edit
|
||||
:on-context-menu on-pill-context-menu}]))))])]))
|
||||
|
||||
(def ^:private schema:token-tree
|
||||
@ -124,7 +127,9 @@
|
||||
[:selected-token-set-id {:optional true} :any]
|
||||
[:on-token-pill-click {:optional true} fn?]
|
||||
[:on-pill-context-menu {:optional true} fn?]
|
||||
[:on-node-context-menu {:optional true} fn?]])
|
||||
[:on-node-context-menu {:optional true} fn?]
|
||||
[:can-edit {:optional true} :boolean]
|
||||
[:can-edit-tokens {:optional true} :boolean]])
|
||||
|
||||
(mf/defc token-tree*
|
||||
{::mf/schema schema:token-tree}
|
||||
@ -138,19 +143,18 @@
|
||||
selected-token-set-id
|
||||
on-token-pill-click
|
||||
on-pill-context-menu
|
||||
on-node-context-menu]}]
|
||||
on-node-context-menu
|
||||
can-edit
|
||||
can-edit-tokens]}]
|
||||
(let [separator "."
|
||||
raw-tree
|
||||
(mf/with-memo [tokens]
|
||||
(cpn/build-tree-root tokens separator))
|
||||
|
||||
can-edit-file?
|
||||
(mf/use-ctx ctx/can-edit?)
|
||||
|
||||
on-node-context-menu (mf/use-fn
|
||||
(mf/deps can-edit-file? on-node-context-menu)
|
||||
(mf/deps can-edit-tokens on-node-context-menu)
|
||||
(fn [event node]
|
||||
(when can-edit-file?
|
||||
(when can-edit-tokens
|
||||
(on-node-context-menu event node))))
|
||||
|
||||
ordered-nodes (mf/with-memo [raw-tree]
|
||||
@ -175,6 +179,7 @@
|
||||
:is-selected-inside-layout is-selected-inside-layout
|
||||
:active-theme-tokens active-theme-tokens
|
||||
:on-click on-token-pill-click
|
||||
:can-edit can-edit
|
||||
:on-context-menu on-pill-context-menu}])
|
||||
;; Render segment folder
|
||||
[:ul {:class (stl/css :node-parent)
|
||||
@ -189,4 +194,5 @@
|
||||
:on-node-context-menu on-node-context-menu
|
||||
:on-pill-context-menu on-pill-context-menu
|
||||
:tokens-lib tokens-lib
|
||||
:can-edit can-edit
|
||||
:selected-token-set-id selected-token-set-id}]]))]))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user