From 0ea3ea332f4340a71eccbc1782d30217f995c4ac Mon Sep 17 00:00:00 2001 From: Xaviju Date: Wed, 6 May 2026 12:40:23 +0200 Subject: [PATCH] :tada: Display autocomplete combobox on token creation (#9109) Co-authored-by: Eva Marco --- common/src/app/common/types/token.cljc | 9 ++- .../playwright/ui/specs/tokens/apply.spec.js | 21 +++-- .../playwright/ui/specs/tokens/crud.spec.js | 27 +++++-- .../playwright/ui/specs/tokens/helpers.js | 23 +++++- .../ui/specs/tokens/remapping.spec.js | 76 ++++++++++++++++--- .../playwright/ui/specs/tokens/tree.spec.js | 36 ++++++++- .../src/app/main/ui/settings/options.scss | 2 +- .../src/app/main/ui/workspace/sidebar.cljs | 8 +- .../management/forms/controls/combobox.cljs | 7 +- .../management/forms/form_container.cljs | 33 ++++++-- .../tokens/management/forms/generic_form.cljs | 31 ++++++-- .../tokens/management/forms/modals.cljs | 72 +++++++++--------- 12 files changed, 262 insertions(+), 83 deletions(-) diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc index 1ece712296..31657983d3 100644 --- a/common/src/app/common/types/token.cljc +++ b/common/src/app/common/types/token.cljc @@ -554,10 +554,17 @@ :opacity [:opacity] :stroke-width [:stroke-width :dimensions] :font-size [:font-size] + :font-weight [:font-weight] + :text-decoration [:text-decoration] + :text-case [:text-case] :letter-spacing [:letter-spacing] + :dimensions [:dimensions] :fill [:color] :stroke-color [:color] - :typography [:typography]}) + :typography [:typography] + :number [:number] + :sizing [:sizing :dimensions] + :spacing [:spacing :dimensions]}) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; HELPERS for tokens application diff --git a/frontend/playwright/ui/specs/tokens/apply.spec.js b/frontend/playwright/ui/specs/tokens/apply.spec.js index 64f49733ce..8b09827d0f 100644 --- a/frontend/playwright/ui/specs/tokens/apply.spec.js +++ b/frontend/playwright/ui/specs/tokens/apply.spec.js @@ -1386,8 +1386,14 @@ test.describe("Numeric Input and Token Integration Tests", () => { flags: ["enable-token-combobox", "enable-feature-token-input"], }); // Create a token with a reference value in other set. - await createToken(page, "Dimensions", "reference-token", "Value", "{card.padding}"); - + await createToken( + page, + "Dimensions", + "reference-token", + "Value", + "combobox", + "{card.padding}", + ); // Apply this token to a shape await page.getByRole("tab", { name: "Layers" }).click(); @@ -1410,11 +1416,16 @@ test.describe("Numeric Input and Token Integration Tests", () => { }); await expect(measuresSection).toBeVisible(); - await expect(measuresSection.getByRole('button', { name: 'reference-token' })).toBeVisible(); + await expect( + measuresSection.getByRole("button", { name: "reference-token" }), + ).toBeVisible(); // Deactivate token set where reference token exist to make token broken - await tokenThemesSetsSidebar.getByRole('button', { name: 'theme' }).getByRole('checkbox').click(); - + await tokenThemesSetsSidebar + .getByRole("button", { name: "theme" }) + .getByRole("checkbox") + .click(); + // Check if token pill show broken reference state const brokenPill = measuresSection.getByRole("button", { name: "is not in any active set", diff --git a/frontend/playwright/ui/specs/tokens/crud.spec.js b/frontend/playwright/ui/specs/tokens/crud.spec.js index 4bfd1c6a4b..4f3d62f975 100644 --- a/frontend/playwright/ui/specs/tokens/crud.spec.js +++ b/frontend/playwright/ui/specs/tokens/crud.spec.js @@ -1794,22 +1794,33 @@ test("User duplicate color token", async ({ page }) => { test("User disables the current set but token still have resolved values shown in the sidebar", async ({ page, }) => { - const { tokenThemesSetsSidebar, tokensSidebar } = await setupEmptyTokensFileRender(page); + const { tokenThemesSetsSidebar, tokensSidebar } = + await setupEmptyTokensFileRender(page); // Create color token - await createToken(page, "Color", "color.primary", "Value", "#ff0000"); + await createToken( + page, + "Color", + "color.primary", + "Value", + "textbox", + "#ff0000", + ); await unfoldTokenType(tokensSidebar, "color"); // Deactivate current set - await tokenThemesSetsSidebar - .getByRole("checkbox") - .click(); + await tokenThemesSetsSidebar.getByRole("checkbox").click(); // Tokens tab panel should have a token with the color #ff0000 and correct resolved value in the tooltip - const colorTokenPill = tokensSidebar.getByRole("button", { name: "#ff0000 color.primary" }); + const colorTokenPill = tokensSidebar.getByRole("button", { + 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(); // Force title attribute to be attached to the button + await expect(colorTokenPill).toHaveAttribute( + "title", + /Resolved value: #ff0000/, + ); }); test.describe("Tokens tab - edition", () => { diff --git a/frontend/playwright/ui/specs/tokens/helpers.js b/frontend/playwright/ui/specs/tokens/helpers.js index 937a268242..d3f47a98da 100644 --- a/frontend/playwright/ui/specs/tokens/helpers.js +++ b/frontend/playwright/ui/specs/tokens/helpers.js @@ -332,7 +332,26 @@ const unfoldTokenType = async (tokensTabPanel, type) => { } }; -const createToken = async (page, type, name, textFieldName, value) => { +/** + * Creates a token from the Tokens sidebar modal. + * + * @param {import("@playwright/test").Page} page - Playwright page instance. + * @param {string} type - Token category label shown in UI (e.g. "Color", "Typography", "Shadow"). + * @param {string} name - Token name to create. + * @param {string} textFieldName - Accessible label of the value textbox in the modal (e.g. "Value", "Color", "Font size"). + * @param {"textbox" | "combobox"} textFieldType - Type of the token value field, wether it's a simple text field or a combo box, to properly fill the value. + * @param {string} value - Token value to set in the modal. + * @returns {Promise} + */ + +const createToken = async ( + page, + type, + name, + textFieldName, + textFieldType, + value, +) => { const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); const { tokensUpdateCreateModal } = await setupTokensFileRender(page, { @@ -348,7 +367,7 @@ const createToken = async (page, type, name, textFieldName, value) => { const nameField = tokensUpdateCreateModal.getByLabel("Name"); await nameField.fill(name); - const colorField = tokensUpdateCreateModal.getByRole("textbox", { + const colorField = tokensUpdateCreateModal.getByRole(textFieldType, { name: textFieldName, }); await colorField.fill(value); diff --git a/frontend/playwright/ui/specs/tokens/remapping.spec.js b/frontend/playwright/ui/specs/tokens/remapping.spec.js index 55472cb4a1..3c733e9a2c 100644 --- a/frontend/playwright/ui/specs/tokens/remapping.spec.js +++ b/frontend/playwright/ui/specs/tokens/remapping.spec.js @@ -106,7 +106,14 @@ test.describe("Remapping a single token", () => { }); // Create base shadow token - await createToken(page, "Shadow", "base-shadow", "Color", "#000000"); + await createToken( + page, + "Shadow", + "base-shadow", + "Color", + "textbox", + "#000000", + ); // Create derived shadow token that references base-shadow await createCompositeDerivedToken( @@ -150,7 +157,14 @@ test.describe("Remapping a single token", () => { } = await setupTokensFileRender(page, { flags: ["enable-token-shadow"] }); // Create base shadow token - await createToken(page, "Shadow", "primary-shadow", "Color", "#000000"); + await createToken( + page, + "Shadow", + "primary-shadow", + "Color", + "textbox", + "#000000", + ); // Create derived shadow token that references base await createCompositeDerivedToken( @@ -257,7 +271,14 @@ test.describe("Remapping a single token", () => { const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); // Create base typography token - await createToken(page, "Typography", "base-text", "Font size", "16"); + await createToken( + page, + "Typography", + "base-text", + "Font size", + "textbox", + "16", + ); // Create derived typography token await createCompositeDerivedToken( @@ -301,7 +322,14 @@ test.describe("Remapping a single token", () => { const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); // Create base typography token - await createToken(page, "Typography", "body-style", "Font size", "16"); + await createToken( + page, + "Typography", + "body-style", + "Font size", + "textbox", + "16", + ); // Create derived typography token await tokensTabPanel @@ -536,7 +564,14 @@ test.describe("Remapping a single token", () => { }); // Create base shadow token - await createToken(page, "Shadow", "base-shadow", "Color", "#000000"); + await createToken( + page, + "Shadow", + "base-shadow", + "Color", + "textbox", + "#000000", + ); // Create derived shadow token that references base-shadow await createCompositeDerivedToken( @@ -575,7 +610,14 @@ test.describe("Remapping a single token", () => { }); // Create base shadow token - await createToken(page, "Shadow", "base-shadow", "Color", "#000000"); + await createToken( + page, + "Shadow", + "base-shadow", + "Color", + "textbox", + "#000000", + ); // Create derived shadow token that references base-shadow await createCompositeDerivedToken( @@ -615,8 +657,22 @@ test.describe("Remapping group of tokens", () => { const rightSidebar = workspacePage.rightSidebar; // Create multiple tokens in a group - await createToken(page, "Color", "light.primary", "Value", "#FFFFFF"); - await createToken(page, "Color", "light.secondary", "Value", "#EEEEEE"); + await createToken( + page, + "Color", + "light.primary", + "Value", + "textbox", + "#FFFFFF", + ); + await createToken( + page, + "Color", + "light.secondary", + "Value", + "textbox", + "#EEEEEE", + ); // Verify that the node and child token are visible before deletion const lightNode = tokensSidebar.getByRole("button", { @@ -687,7 +743,9 @@ test.describe("Remapping group of tokens", () => { await expect(lighterNode).toBeVisible(); // Verify that the applied token reference has been updated in the right sidebar for the selected shape - const fillSection = rightSidebar.getByRole("region", { name: "Fill section" }); + const fillSection = rightSidebar.getByRole("region", { + name: "Fill section", + }); await expect(fillSection).toBeVisible(); const tokenReference = fillSection.getByLabel("lighter.primary", { diff --git a/frontend/playwright/ui/specs/tokens/tree.spec.js b/frontend/playwright/ui/specs/tokens/tree.spec.js index 243a539432..5ee9ced175 100644 --- a/frontend/playwright/ui/specs/tokens/tree.spec.js +++ b/frontend/playwright/ui/specs/tokens/tree.spec.js @@ -32,8 +32,22 @@ test.describe("Tokens - node tree", () => { const { tokensSidebar } = await setupTokensFileRender(page); // Create multiple tokens in a group - await createToken(page, "Color", "dark.primary", "Value", "#000000"); - await createToken(page, "Color", "dark.secondary", "Value", "#111111"); + await createToken( + page, + "Color", + "dark.primary", + "Value", + "textbox", + "#000000", + ); + await createToken( + page, + "Color", + "dark.secondary", + "Value", + "textbox", + "#111111", + ); // Verify that the node and child token are visible before deletion const darkNode = tokensSidebar.getByRole("button", { @@ -88,8 +102,22 @@ test.describe("Tokens - node tree", () => { const { tokensSidebar } = await setupTokensFileRender(page); // Create multiple tokens in a group - await createToken(page, "Color", "dark.primary", "Value", "#000000"); - await createToken(page, "Color", "dark.secondary", "Value", "#111111"); + await createToken( + page, + "Color", + "dark.primary", + "Value", + "textbox", + "#000000", + ); + await createToken( + page, + "Color", + "dark.secondary", + "Value", + "textbox", + "#111111", + ); // Verify that the node and child token are visible before deletion const darkNode = tokensSidebar.getByRole("button", { diff --git a/frontend/src/app/main/ui/settings/options.scss b/frontend/src/app/main/ui/settings/options.scss index abe949897f..566b6270df 100644 --- a/frontend/src/app/main/ui/settings/options.scss +++ b/frontend/src/app/main/ui/settings/options.scss @@ -25,8 +25,8 @@ grid-auto-rows: auto; gap: var(--sp-s); width: $sz-500; - margin-block: var(--sp-xxxl) $sz-120; /* FIXME: this should be a proper token */ padding-block-start: var(--sp-xxxl); + margin: var(--sp-xxxl) $sz-120; /* FIXME: this should be a proper token */ border-block-start: $b-1 solid var(--color-background-quaternary); color: var(--color-foreground-primary); } diff --git a/frontend/src/app/main/ui/workspace/sidebar.cljs b/frontend/src/app/main/ui/workspace/sidebar.cljs index b1bfe74db9..828d9b2732 100644 --- a/frontend/src/app/main/ui/workspace/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar.cljs @@ -389,6 +389,10 @@ resolved-active-tokens (sd/use-resolved-tokens* active-tokens) + tokenscript-resolved-active-tokens + (mf/with-memo [active-tokens tokenscript?] + (when tokenscript? (ts/resolve-tokens active-tokens))) + tokenscript-resolved-active-tokens-force-set (mf/with-memo [active-tokens-force-set tokenscript?] (when tokenscript? (ts/resolve-tokens active-tokens-force-set))) @@ -415,4 +419,6 @@ :file-id file-id :page-id page-id :tokens-lib tokens-lib - :active-tokens resolved-active-tokens}]])) + :active-tokens (if tokenscript? + tokenscript-resolved-active-tokens + resolved-active-tokens)}]])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs index 137551c260..59e3b409a3 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs @@ -99,11 +99,10 @@ container (hooks/use-portal-container) - raw-tokens-by-type (mf/use-ctx muc/active-tokens-by-type) - + resolved-tokens-by-type (mf/use-ctx muc/active-tokens-by-type) filtered-tokens-by-type - (mf/with-memo [raw-tokens-by-type token-type] - (csu/filter-tokens-for-input raw-tokens-by-type token-type)) + (mf/with-memo [resolved-tokens-by-type token-type] + (csu/filter-tokens-for-input resolved-tokens-by-type token-type)) visible-options (mf/with-memo [filtered-tokens-by-type token] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs index 0e6d55df03..c0476454e9 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs @@ -35,13 +35,32 @@ (mf/with-memo [token-path tokens-in-selected-set] (-> (ctob/tokens-tree tokens-in-selected-set) (d/dissoc-in token-path))) + props - (mf/spread-props props {:token-type token-type - :tokens-tree-in-selected-set tokens-tree-in-selected-set - :token token}) - text-case-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-case-value-enter")}) - text-decoration-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-decoration-value-enter")}) - font-weight-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.font-weight-value-enter")}) + (if (contains? cf/flags :token-combobox) + (mf/spread-props props {:token-type token-type + :tokens-tree-in-selected-set tokens-tree-in-selected-set + :selected-token-set-id (mf/deref refs/selected-token-set-id) + :token token + :input-component token.controls/value-combobox*}) + (mf/spread-props props {:token-type token-type + :tokens-tree-in-selected-set tokens-tree-in-selected-set + :selected-token-set-id (mf/deref refs/selected-token-set-id) + :token token})) + text-case-props (if (contains? cf/flags :token-combobox) + (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-case-value-enter") + :input-component token.controls/value-combobox*}) + (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-case-value-enter")})) + text-decoration-props (if (contains? cf/flags :token-combobox) + (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-decoration-value-enter") + :input-component token.controls/value-combobox*}) + (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-decoration-value-enter")})) + + font-weight-props (if (contains? cf/flags :token-combobox) + (mf/spread-props props {:input-component token.controls/value-combobox* + :input-value-placeholder (tr "workspace.tokens.font-weight-value-enter")}) + (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.font-weight-value-enter")})) + border-radius-props (if (contains? cf/flags :token-combobox) (mf/spread-props props {:input-component token.controls/value-combobox*}) props)] @@ -55,4 +74,4 @@ :text-decoration [:> generic/form* text-decoration-props] :font-weight [:> generic/form* font-weight-props] :border-radius [:> generic/form* border-radius-props] - [:> generic/form* props]))) \ No newline at end of file + [:> generic/form* props]))) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs index a4cc813bbc..18761aa103 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs @@ -10,9 +10,12 @@ [app.common.files.tokens :as cfo] [app.common.schema :as sm] [app.common.types.tokens-lib :as ctob] + [app.config :as cf] [app.main.constants :refer [max-input-length]] [app.main.data.helpers :as dh] [app.main.data.modal :as modal] + [app.main.data.style-dictionary :as sd] + [app.main.data.tokenscript :as ts] [app.main.data.workspace.tokens.application :as dwta] [app.main.data.workspace.tokens.errors :as wte] [app.main.data.workspace.tokens.library-edit :as dwtl] @@ -83,23 +86,41 @@ token-title (str/lower (:title token-properties)) - tokens (mf/deref refs/workspace-all-tokens-map) + ;; All tokens in the lib, as a map name -> token, flattened + ;; including tokens in inactive sets. + tokens-tree (mf/deref refs/workspace-all-tokens-map) + ;; A map name -> token, tokens only in actual set. tokens-in-selected-set (mf/deref refs/workspace-all-tokens-in-selected-set) + ;; Make actual set tokens take precedence over tokens in other sets. tokens - (mf/with-memo [tokens tokens-in-selected-set token] + (mf/with-memo [tokens-tree tokens-in-selected-set token] ;; Ensure that the resolved value uses the currently editing token ;; even if the name has been overriden by a token with the same name ;; in another set below. - (cond-> (merge tokens tokens-in-selected-set) + (cond-> (merge tokens-tree tokens-in-selected-set) (and (:name token) (:value token)) (assoc (:name token) token))) + tokenscript? (contains? cf/flags :tokenscript) + + ;; A map name-> token with resolved-values, resolved with style dictionary + resolved-active-tokens + (sd/use-resolved-tokens* tokens-tree) + + ;; A map name-> token with resolved-values, resolved with tokescript + tokenscript-resolved-active-tokens + (mf/with-memo [tokens-tree tokenscript?] + (when tokenscript? (ts/resolve-tokens tokens-tree))) + + ;; A map which keys are token types and values are vectors of tokens of that type, with resolved values. active-tokens-by-type - (mf/with-memo [tokens] - (delay (ctob/group-by-type tokens))) + (mf/with-memo [resolved-active-tokens tokenscript-resolved-active-tokens tokenscript?] + (delay (ctob/group-by-type (if tokenscript? + tokenscript-resolved-active-tokens + resolved-active-tokens)))) schema (mf/with-memo [tokens-tree-in-selected-set active-tab] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/modals.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/modals.cljs index 9356d45725..6d0ae946da 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/modals.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/modals.cljs @@ -118,27 +118,15 @@ [properties] [:& token-update-create-modal properties]) -(mf/defc color-modal - {::mf/register modal/components - ::mf/register-as :tokens/color} - [properties] - [:& token-update-create-modal properties]) - -(mf/defc stroke-width-modal - {::mf/register modal/components - ::mf/register-as :tokens/stroke-width} - [properties] - [:& token-update-create-modal properties]) - (mf/defc box-shadow-modal {::mf/register modal/components ::mf/register-as :tokens/shadow} [properties] [:& token-update-create-modal properties]) -(mf/defc sizing-modal +(mf/defc color-modal {::mf/register modal/components - ::mf/register-as :tokens/sizing} + ::mf/register-as :tokens/color} [properties] [:& token-update-create-modal properties]) @@ -148,6 +136,30 @@ [properties] [:& token-update-create-modal properties]) +(mf/defc font-familiy-modal + {::mf/register modal/components + ::mf/register-as :tokens/font-family} + [properties] + [:& token-update-create-modal properties]) + +(mf/defc font-size-modal + {::mf/register modal/components + ::mf/register-as :tokens/font-size} + [properties] + [:& token-update-create-modal properties]) + +(mf/defc font-weight-modal + {::mf/register modal/components + ::mf/register-as :tokens/font-weight} + [properties] + [:& token-update-create-modal properties]) + +(mf/defc letter-spacing-modal + {::mf/register modal/components + ::mf/register-as :tokens/letter-spacing} + [properties] + [:& token-update-create-modal properties]) + (mf/defc number-modal {::mf/register modal/components ::mf/register-as :tokens/number} @@ -172,6 +184,12 @@ [properties] [:& token-update-create-modal properties]) +(mf/defc sizing-modal + {::mf/register modal/components + ::mf/register-as :tokens/sizing} + [properties] + [:& token-update-create-modal properties]) + (mf/defc spacing-modal {::mf/register modal/components ::mf/register-as :tokens/spacing} @@ -184,27 +202,9 @@ [properties] [:& token-update-create-modal properties]) -(mf/defc typography-modal +(mf/defc stroke-width-modal {::mf/register modal/components - ::mf/register-as :tokens/typography} - [properties] - [:& token-update-create-modal properties]) - -(mf/defc font-size-modal - {::mf/register modal/components - ::mf/register-as :tokens/font-size} - [properties] - [:& token-update-create-modal properties]) - -(mf/defc letter-spacing-modal - {::mf/register modal/components - ::mf/register-as :tokens/letter-spacing} - [properties] - [:& token-update-create-modal properties]) - -(mf/defc font-familiy-modal - {::mf/register modal/components - ::mf/register-as :tokens/font-family} + ::mf/register-as :tokens/stroke-width} [properties] [:& token-update-create-modal properties]) @@ -220,8 +220,8 @@ [properties] [:& token-update-create-modal properties]) -(mf/defc font-weight-modal +(mf/defc typography-modal {::mf/register modal/components - ::mf/register-as :tokens/font-weight} + ::mf/register-as :tokens/typography} [properties] [:& token-update-create-modal properties])