🎉 Display autocomplete combobox on token creation (#9109)

Co-authored-by: Eva Marco <evamarcod@gmail.com>
This commit is contained in:
Xaviju 2026-05-06 12:40:23 +02:00 committed by GitHub
parent e65ce8bdeb
commit 0ea3ea332f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 262 additions and 83 deletions

View File

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

View File

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

View File

@ -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", () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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])))
[:> generic/form* props])))

View File

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

View File

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