diff --git a/frontend/playwright/ui/specs/tokens.spec.js b/frontend/playwright/ui/specs/tokens.spec.js index 941e901f71..f8502b5ecb 100644 --- a/frontend/playwright/ui/specs/tokens.spec.js +++ b/frontend/playwright/ui/specs/tokens.spec.js @@ -118,6 +118,132 @@ const checkInputFieldWithoutError = async ( expect(await inputLocator.getAttribute("aria-describedby")).toBeNull(); }; +async function testTokenCreationFlow( + page, + { + tokenLabel, + namePlaceholder, + valuePlaceholder, + validValue, + invalidValue, + selfReferenceValue, + missingReferenceValue, + secondValidValue, + resolvedValueText, + secondResolvedValueText, + }, +) { + const invalidValueError = "Invalid token value"; + const emptyNameError = "Name should be at least 1 character"; + const selfReferenceError = "Token has self reference"; + const missingReferenceError = "Missing token references"; + + const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = + await setupEmptyTokensFile(page); + + // Open modal + const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); + + const addTokenButton = tokensTabPanel.getByRole("button", { + name: `Add Token: ${tokenLabel}`, + }); + + await addTokenButton.click(); + await expect(tokensUpdateCreateModal).toBeVisible(); + + // Placeholder checks + await expect( + tokensUpdateCreateModal.getByPlaceholder(namePlaceholder), + ).toBeVisible(); + await expect( + tokensUpdateCreateModal.getByPlaceholder(valuePlaceholder), + ).toBeVisible(); + + const nameField = tokensUpdateCreateModal.getByLabel("Name"); + const valueField = tokensUpdateCreateModal.getByLabel("Value"); + const submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + + // 1. Name filled + empty value → disabled + await nameField.fill("my-token"); + await expect(submitButton).toBeDisabled(); + + // 2. Invalid value → disabled + error message + await valueField.fill(invalidValue); + + const invalidValueErrorNode = + tokensUpdateCreateModal.getByText(invalidValueError); + + await expect(invalidValueErrorNode).toBeVisible(); + await expect(submitButton).toBeDisabled(); + + // 3. Empty name → disabled + error message + await nameField.fill(""); + + const emptyNameErrorNode = tokensUpdateCreateModal.getByText(emptyNameError); + + await expect(emptyNameErrorNode).toBeVisible(); + await expect(submitButton).toBeDisabled(); + + // 4. Self reference → disabled + error message + await nameField.fill("my-token"); + await valueField.fill(selfReferenceValue); + + const selfRefErrorNode = + tokensUpdateCreateModal.getByText(selfReferenceError); + + await expect(selfRefErrorNode).toBeVisible(); + await expect(submitButton).toBeDisabled(); + + // 5. Missing reference → disabled + error message + await valueField.fill(missingReferenceValue); + + const missingRefErrorNode = tokensUpdateCreateModal.getByText( + missingReferenceError, + ); + + await expect(missingRefErrorNode).toBeVisible(); + await expect(submitButton).toBeDisabled(); + + // + // ------- SUCCESSFUL CREATION ------- + // + + // 6. Basic valid value → enabled + await valueField.fill(validValue); + await expect( + tokensUpdateCreateModal.getByText(resolvedValueText), + ).toBeVisible(); + await expect(submitButton).toBeEnabled(); + + await submitButton.click(); + + await expect( + tokensTabPanel.getByRole("button", { name: "my-token" }), + ).toBeEnabled(); + + // + // ------- SECOND TOKEN WITH VALID REFERENCE ------- + // + + await addTokenButton.click(); + + await nameField.fill("my-token-2"); + await valueField.fill(secondValidValue); + + await expect( + tokensUpdateCreateModal.getByText(secondResolvedValueText), + ).toBeVisible(); + await expect(submitButton).toBeEnabled(); + + await submitButton.click(); + + await expect( + tokensTabPanel.getByRole("button", { name: "my-token-2" }), + ).toBeEnabled(); +} + test.describe("Tokens: Tokens Tab", () => { test("Clicking tokens tab button opens tokens sidebar tab", async ({ page, @@ -130,40 +256,218 @@ test.describe("Tokens: Tokens Tab", () => { await expect(tokensTabPanel).toHaveText(/Themes/); }); + test("User creates border radius token", async ({ page }) => { + await testTokenCreationFlow(page, { + tokenLabel: "Border Radius", + namePlaceholder: "Enter border radius token name", + valuePlaceholder: "Enter a value or alias with {alias}", + invalidValue: "red", + validValue: "2 + 3", + selfReferenceValue: "{my-token}", + missingReferenceValue: "{missing-token}", + secondValidValue: "{my-token} - 2", + resolvedValueText: "Resolved value: 5", + secondResolvedValueText: "Resolved value: 3", + }); + }); + + test("User creates dimensions token", async ({ page }) => { + await testTokenCreationFlow(page, { + tokenLabel: "Dimensions", + namePlaceholder: "Enter dimensions token name", + valuePlaceholder: "Enter a value or alias with {alias}", + invalidValue: "red", + validValue: "2 + 3", + selfReferenceValue: "{my-token}", + missingReferenceValue: "{missing-token}", + secondValidValue: "{my-token} - 2", + resolvedValueText: "Resolved value: 5", + secondResolvedValueText: "Resolved value: 3", + }); + }); + + test("User creates font size token", async ({ page }) => { + await testTokenCreationFlow(page, { + tokenLabel: "Font Size", + namePlaceholder: "Enter font size token name", + valuePlaceholder: "Enter a value or alias with {alias}", + invalidValue: "red", + validValue: "2 + 3", + selfReferenceValue: "{my-token}", + missingReferenceValue: "{missing-token}", + secondValidValue: "{my-token} - 2", + resolvedValueText: "Resolved value: 5", + secondResolvedValueText: "Resolved value: 3", + }); + }); + + test("User creates letter spacing token", async ({ page }) => { + await testTokenCreationFlow(page, { + tokenLabel: "Letter spacing", + namePlaceholder: "Enter letter spacing token name", + valuePlaceholder: "Enter a value or alias with {alias}", + invalidValue: "red", + validValue: "2 + 3", + selfReferenceValue: "{my-token}", + missingReferenceValue: "{missing-token}", + secondValidValue: "{my-token} - 2", + resolvedValueText: "Resolved value: 5", + secondResolvedValueText: "Resolved value: 3", + }); + }); + + test("User creates number token", async ({ page }) => { + await testTokenCreationFlow(page, { + tokenLabel: "Number", + namePlaceholder: "Enter number token name", + valuePlaceholder: "Enter a value or alias with {alias}", + invalidValue: "red", + validValue: "2 + 3", + selfReferenceValue: "{my-token}", + missingReferenceValue: "{missing-token}", + secondValidValue: "{my-token} - 2", + resolvedValueText: "Resolved value: 5", + secondResolvedValueText: "Resolved value: 3", + }); + }); + + test("User creates rotation token", async ({ page }) => { + await testTokenCreationFlow(page, { + tokenLabel: "Rotation", + namePlaceholder: "Enter rotation token name", + valuePlaceholder: "Enter a value or alias with {alias}", + invalidValue: "red", + validValue: "2 + 3", + selfReferenceValue: "{my-token}", + missingReferenceValue: "{missing-token}", + secondValidValue: "{my-token} - 2", + resolvedValueText: "Resolved value: 5", + secondResolvedValueText: "Resolved value: 3", + }); + }); + + test("User creates sizing token", async ({ page }) => { + await testTokenCreationFlow(page, { + tokenLabel: "Sizing", + namePlaceholder: "Enter sizing token name", + valuePlaceholder: "Enter a value or alias with {alias}", + invalidValue: "red", + validValue: "2 + 3", + selfReferenceValue: "{my-token}", + missingReferenceValue: "{missing-token}", + secondValidValue: "{my-token} - 2", + resolvedValueText: "Resolved value: 5", + secondResolvedValueText: "Resolved value: 3", + }); + }); + + test("User creates spacing token", async ({ page }) => { + await testTokenCreationFlow(page, { + tokenLabel: "Spacing", + namePlaceholder: "Enter spacing token name", + valuePlaceholder: "Enter a value or alias with {alias}", + invalidValue: "red", + validValue: "2 + 3", + selfReferenceValue: "{my-token}", + missingReferenceValue: "{missing-token}", + secondValidValue: "{my-token} - 2", + resolvedValueText: "Resolved value: 5", + secondResolvedValueText: "Resolved value: 3", + }); + }); + + test("User creates stroke width token", async ({ page }) => { + await testTokenCreationFlow(page, { + tokenLabel: "Stroke width", + namePlaceholder: "Enter stroke width token name", + valuePlaceholder: "Enter a value or alias with {alias}", + invalidValue: "red", + validValue: "2 + 3", + selfReferenceValue: "{my-token}", + missingReferenceValue: "{missing-token}", + secondValidValue: "{my-token} - 2", + resolvedValueText: "Resolved value: 5", + secondResolvedValueText: "Resolved value: 3", + }); + }); + test("User creates color token and auto created set show up in the sidebar", async ({ page, }) => { + const invalidValueError = "Invalid color value"; + const emptyNameError = "Name should be at least 1 character"; + const selfReferenceError = "Token has self reference"; + const missingReferenceError = "Missing token references"; const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = await setupEmptyTokensFile(page); const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); - await tokensTabPanel - .getByRole("button", { name: "Add Token: Color" }) - .click(); + const addTokenButton = tokensTabPanel.getByRole("button", { + name: `Add Token: Color`, + }); - // Create color token with mouse + await addTokenButton.click(); await expect(tokensUpdateCreateModal).toBeVisible(); + // Placeholder checks + await expect( + tokensUpdateCreateModal.getByPlaceholder("Enter color token name"), + ).toBeVisible(); + await expect( + tokensUpdateCreateModal.getByPlaceholder( + "Enter a value or alias with {alias}", + ), + ).toBeVisible(); + const nameField = tokensUpdateCreateModal.getByLabel("Name"); const valueField = tokensUpdateCreateModal.getByLabel("Value"); - - await nameField.click(); - await nameField.fill("color.primary"); - - // try invalid value - await valueField.click(); - - await valueField.fill("1"); - await expect( - tokensUpdateCreateModal.getByText("Invalid color value: 1"), - ).toBeVisible(); - - // valid value - await valueField.fill("red"); - const submitButton = tokensUpdateCreateModal.getByRole("button", { name: "Save", }); + + // 1. Name filled + empty value → disabled + await nameField.click(); + await nameField.fill("color.primary"); + await expect(submitButton).toBeDisabled(); + + // 2. Invalid value → disabled + error message + await valueField.fill("1"); + const invalidValueErrorNode = + tokensUpdateCreateModal.getByText(invalidValueError); + await expect(invalidValueErrorNode).toBeVisible(); + await expect(submitButton).toBeDisabled(); + + // 3. Empty name → disabled + error message + await nameField.fill(""); + + const emptyNameErrorNode = + tokensUpdateCreateModal.getByText(emptyNameError); + + await expect(emptyNameErrorNode).toBeVisible(); + await expect(submitButton).toBeDisabled(); + + // 4. Self reference → disabled + error message + await nameField.fill("color.primary"); + await valueField.fill("{color.primary}"); + + const selfRefErrorNode = + tokensUpdateCreateModal.getByText(selfReferenceError); + + await expect(selfRefErrorNode).toBeVisible(); + await expect(submitButton).toBeDisabled(); + + // 5. Missing reference → disabled + error message + await valueField.fill("{missing-reference}"); + + const missingRefErrorNode = tokensUpdateCreateModal.getByText( + missingReferenceError, + ); + + await expect(missingRefErrorNode).toBeVisible(); + await expect(submitButton).toBeDisabled(); + + // valid value + await valueField.fill("red"); await expect(submitButton).toBeEnabled(); await submitButton.click(); @@ -212,62 +516,992 @@ test.describe("Tokens: Tokens Tab", () => { name: "Global", }), ).toHaveAttribute("aria-checked", "true"); + + // Check color picker + await tokensTabPanel + .getByRole("button", { name: "Add Token: Color" }) + .click(); + await expect(tokensUpdateCreateModal).toBeVisible(); + + await nameField.click(); + await nameField.fill("color.tertiary"); + await nameField.press("Tab"); + const colorSwatch = tokensUpdateCreateModal.getByTestId( + "token-form-color-bullet", + ); + await colorSwatch.click(); + const rampSelector = tokensUpdateCreateModal.getByTestId( + "value-saturation-selector", + ); + await expect(rampSelector).toBeVisible(); + await rampSelector.click({ position: { x: 50, y: 50 } }); + + await expect( + tokensUpdateCreateModal.getByText("Resolved value:"), + ).toBeVisible(); + + await expect(submitButton).toBeEnabled(); + + // Check color with opacity + const sliderOpacity = tokensUpdateCreateModal.getByTestId("slider-opacity"); + await sliderOpacity.click({ position: { x: 50, y: 0 } }); + await expect( + tokensUpdateCreateModal.getByText("Resolved value: rgba("), + ).toBeVisible(); + + await expect(submitButton).toBeEnabled(); + + // Check hslv + await colorSwatch.click(); + await expect(rampSelector).not.toBeVisible(); + await valueField.fill("hsv(1,1,1)"); + await expect( + tokensUpdateCreateModal.getByText("Resolved value: #ff0400"), + ).toBeVisible(); + + await expect(submitButton).toBeEnabled(); }); - test("User creates dimensions token and auto created set show up in the sidebar", async ({ - page, - }) => { + test("User creates a font family token", async ({ page }) => { + const emptyNameError = "Name should be at least 1 character"; + const selfReferenceError = "Token has self reference"; + const missingReferenceError = "Missing token references"; + const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = await setupEmptyTokensFile(page); + // Open modal const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); - await tokensTabPanel - .getByRole("button", { name: "Add token: Dimensions" }) - .click(); + const addTokenButton = tokensTabPanel.getByRole("button", { + name: `Add Token: Font Family`, + }); + + await addTokenButton.click(); await expect(tokensUpdateCreateModal).toBeVisible(); + // Placeholder checks + await expect( + tokensUpdateCreateModal.getByPlaceholder("Enter font family token name"), + ).toBeVisible(); + await expect( + tokensUpdateCreateModal.getByPlaceholder( + "Enter a value or alias with {alias}", + ), + ).toBeVisible(); + const nameField = tokensUpdateCreateModal.getByLabel("Name"); const valueField = tokensUpdateCreateModal.getByLabel("Value"); - - await nameField.click(); - await nameField.fill("dimension.spacing.small"); - - // try invalid value first - await valueField.click(); - - await valueField.fill("red"); - await expect( - tokensUpdateCreateModal.getByText("Invalid token value: red"), - ).toBeVisible(); - - // valid value - await valueField.fill("4px"); - await expect( - tokensUpdateCreateModal.getByText("Resolved value: 4"), - ).toBeVisible(); - const submitButton = tokensUpdateCreateModal.getByRole("button", { name: "Save", }); + + // 1. Name filled + empty value → disabled + await nameField.fill("my-token"); + await expect(submitButton).toBeDisabled(); + + // 2. Empty name → disabled + error message + await nameField.fill(""); + + const emptyNameErrorNode = + tokensUpdateCreateModal.getByText(emptyNameError); + + await expect(emptyNameErrorNode).toBeVisible(); + await expect(submitButton).toBeDisabled(); + + // 4. Self reference → disabled + error message + await nameField.fill("my-token"); + await valueField.fill("{my-token}"); + + const selfRefErrorNode = + tokensUpdateCreateModal.getByText(selfReferenceError); + + await expect(selfRefErrorNode).toBeVisible(); + await expect(submitButton).toBeDisabled(); + + // 5. Missing reference → disabled + error message + await valueField.fill("{missing-token}"); + + const missingRefErrorNode = tokensUpdateCreateModal.getByText( + missingReferenceError, + ); + + await expect(missingRefErrorNode).toBeVisible(); + await expect(submitButton).toBeDisabled(); + + // + // ------- SUCCESSFUL CREATION ------- + // + + // 6. Basic valid value → enabled + await valueField.fill("Times new roman"); + await expect( + tokensUpdateCreateModal.getByText("Resolved value: Times new roman"), + ).toBeVisible(); + await expect(submitButton).toBeEnabled(); + + await submitButton.click(); + + await expect( + tokensTabPanel.getByRole("button", { name: "my-token" }), + ).toBeEnabled(); + + // + // ------- SECOND TOKEN DROPDOWN OPTION ------- + // + + await addTokenButton.click(); + + await nameField.fill("my-token-2"); + const selectDropdown = tokensUpdateCreateModal.getByRole("button", { + name: "Select font family", + }); + await selectDropdown.click(); + + const fontOption = tokensUpdateCreateModal.getByText("ABeeZee"); + await expect(fontOption).toBeVisible(); + + await fontOption.click(); + + await expect( + tokensUpdateCreateModal.getByText("Resolved value: ABeeZee"), + ).toBeVisible(); + await selectDropdown.click(); + + const searchField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Search font", + }); + await searchField.fill("alme"); + const fontOption2 = tokensUpdateCreateModal.getByText("Almendra Display"); + await expect(fontOption2).toBeVisible(); + await fontOption2.click(); + + await expect( + tokensUpdateCreateModal.getByText("Resolved value: Almendra Display"), + ).toBeVisible(); + await expect(submitButton).toBeEnabled(); await submitButton.click(); await expect( - tokensTabPanel.getByText("dimension.spacing.small"), + tokensTabPanel.getByRole("button", { name: "my-token-2" }), + ).toBeEnabled(); + + // + // ------- THIRD TOKEN REFERENCE OPTION ------- + // + + await addTokenButton.click(); + + await nameField.fill("my-token-3"); + await valueField.fill("{my-token}"); + + await expect(submitButton).toBeEnabled(); + + await submitButton.click(); + + await expect( + tokensTabPanel.getByRole("button", { name: "my-token-3" }), + ).toBeEnabled(); + }); + + test("User creates font weight token", async ({ page }) => { + const invalidValueError = + "Invalid font weight value: use numeric values (100-950) or standard names (thin, light, regular, bold, etc.) optionally followed by 'Italic'"; + const emptyNameError = "Name should be at least 1 character"; + const selfReferenceError = "Token has self reference"; + const missingReferenceError = "Missing token references"; + + const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = + await setupEmptyTokensFile(page); + + // Open modal + const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); + + const addTokenButton = tokensTabPanel.getByRole("button", { + name: `Add Token: Font Weight`, + }); + + await addTokenButton.click(); + await expect(tokensUpdateCreateModal).toBeVisible(); + + // Placeholder checks + await expect( + tokensUpdateCreateModal.getByPlaceholder("Enter font weight token name"), + ).toBeVisible(); + await expect( + tokensUpdateCreateModal.getByPlaceholder( + "Font weight (300, Bold Italic...) or an {alias}", + ), ).toBeVisible(); - // Global set has been auto created and is active + const nameField = tokensUpdateCreateModal.getByLabel("Name"); + const valueField = tokensUpdateCreateModal.getByLabel("Value"); + const submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + + // 1. Name filled + empty value → disabled + await nameField.fill("my-token"); + await expect(submitButton).toBeDisabled(); + + // 2. Invalid value → disabled + error message + await valueField.fill("red"); + + const invalidValueErrorNode = + tokensUpdateCreateModal.getByText(invalidValueError); + + await expect(invalidValueErrorNode).toBeVisible(); + await expect(submitButton).toBeDisabled(); + + // 3. Empty name → disabled + error message + await nameField.fill(""); + + const emptyNameErrorNode = + tokensUpdateCreateModal.getByText(emptyNameError); + + await expect(emptyNameErrorNode).toBeVisible(); + await expect(submitButton).toBeDisabled(); + + // 4. Self reference → disabled + error message + await nameField.fill("my-token"); + await valueField.fill("{my-token}"); + + const selfRefErrorNode = + tokensUpdateCreateModal.getByText(selfReferenceError); + + await expect(selfRefErrorNode).toBeVisible(); + await expect(submitButton).toBeDisabled(); + + // 5. Missing reference → disabled + error message + await valueField.fill("{missing-reference}"); + + const missingRefErrorNode = tokensUpdateCreateModal.getByText( + missingReferenceError, + ); + + await expect(missingRefErrorNode).toBeVisible(); + await expect(submitButton).toBeDisabled(); + + // + // ------- SUCCESSFUL CREATION ------- + // + + // 6. Basic valid value → enabled + await valueField.fill("300"); await expect( - tokenThemesSetsSidebar.getByRole("button", { - name: "Global", - }), - ).toHaveCount(1); + tokensUpdateCreateModal.getByText("Resolved value: 300"), + ).toBeVisible(); + await expect(submitButton).toBeEnabled(); + + await submitButton.click(); + await expect( - tokenThemesSetsSidebar.getByRole("button", { - name: "Global", - }), - ).toHaveAttribute("aria-checked", "true"); + tokensTabPanel.getByRole("button", { name: "my-token" }), + ).toBeEnabled(); + + // + // ------- SECOND TOKEN WITH VALID REFERENCE ------- + // + + await addTokenButton.click(); + + await nameField.fill("my-token-2"); + await valueField.fill("{my-token} + 200"); + + await expect( + tokensUpdateCreateModal.getByText("Resolved value: 500"), + ).toBeVisible(); + await expect(submitButton).toBeEnabled(); + + await submitButton.click(); + + await expect( + tokensTabPanel.getByRole("button", { name: "my-token-2" }), + ).toBeEnabled(); + + // + // ------- THIRD TOKEN WITH NAMED FONT WEIGHT ------- + // + + await addTokenButton.click(); + + await nameField.fill("my-token-3"); + await valueField.fill("bold"); + + await expect( + tokensUpdateCreateModal.getByText("Resolved value: bold"), + ).toBeVisible(); + await expect(submitButton).toBeEnabled(); + + await submitButton.click(); + + await expect( + tokensTabPanel.getByRole("button", { name: "my-token-3" }), + ).toBeEnabled(); + }); + + test("User creates text case token", async ({ page }) => { + const invalidValueError = + "Invalid token value: only none, Uppercase, Lowercase or Capitalize are accepted"; + const emptyNameError = "Name should be at least 1 character"; + const selfReferenceError = "Token has self reference"; + const missingReferenceError = "Missing token references"; + + const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = + await setupEmptyTokensFile(page); + + // Open modal + const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); + + const addTokenButton = tokensTabPanel.getByRole("button", { + name: `Add Token: Text Case`, + }); + + await addTokenButton.click(); + await expect(tokensUpdateCreateModal).toBeVisible(); + + // Placeholder checks + await expect( + tokensUpdateCreateModal.getByPlaceholder("Enter text case token name"), + ).toBeVisible(); + await expect( + tokensUpdateCreateModal.getByPlaceholder( + "none | uppercase | lowercase | capitalize or {alias}", + ), + ).toBeVisible(); + + const nameField = tokensUpdateCreateModal.getByLabel("Name"); + const valueField = tokensUpdateCreateModal.getByLabel("Value"); + const submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + + // 1. Name filled + empty value → disabled + await nameField.fill("my-token"); + await expect(submitButton).toBeDisabled(); + + // 2. Invalid value → disabled + error message + await valueField.fill("red"); + + const invalidValueErrorNode = + tokensUpdateCreateModal.getByText(invalidValueError); + + await expect(invalidValueErrorNode).toBeVisible(); + await expect(submitButton).toBeDisabled(); + + // 3. Empty name → disabled + error message + await nameField.fill(""); + + const emptyNameErrorNode = + tokensUpdateCreateModal.getByText(emptyNameError); + + await expect(emptyNameErrorNode).toBeVisible(); + await expect(submitButton).toBeDisabled(); + + // 4. Self reference → disabled + error message + await nameField.fill("my-token"); + await valueField.fill("{my-token}"); + + const selfRefErrorNode = + tokensUpdateCreateModal.getByText(selfReferenceError); + + await expect(selfRefErrorNode).toBeVisible(); + await expect(submitButton).toBeDisabled(); + + // 5. Missing reference → disabled + error message + await valueField.fill("{missing-reference}"); + + const missingRefErrorNode = tokensUpdateCreateModal.getByText( + missingReferenceError, + ); + + await expect(missingRefErrorNode).toBeVisible(); + await expect(submitButton).toBeDisabled(); + + // + // ------- SUCCESSFUL CREATION ------- + // + + // 6. Basic valid value → enabled + await valueField.fill("uppercase"); + await expect( + tokensUpdateCreateModal.getByText("Resolved value: uppercase"), + ).toBeVisible(); + await expect(submitButton).toBeEnabled(); + + await submitButton.click(); + + await expect( + tokensTabPanel.getByRole("button", { name: "my-token" }), + ).toBeEnabled(); + + // + // ------- SECOND TOKEN WITH VALID REFERENCE ------- + // + + await addTokenButton.click(); + + await nameField.fill("my-token-2"); + await valueField.fill("{my-token}"); + + await expect( + tokensUpdateCreateModal.getByText("Resolved value: uppercase"), + ).toBeVisible(); + await expect(submitButton).toBeEnabled(); + + await submitButton.click(); + + await expect( + tokensTabPanel.getByRole("button", { name: "my-token-2" }), + ).toBeEnabled(); + }); + + test("User creates text decoration token", async ({ page }) => { + const invalidValueError = + "Invalid token value: only none, underline and strike-through are accepted"; + const emptyNameError = "Name should be at least 1 character"; + const selfReferenceError = "Token has self reference"; + const missingReferenceError = "Missing token references"; + + const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = + await setupEmptyTokensFile(page); + + // Open modal + const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); + + const addTokenButton = tokensTabPanel.getByRole("button", { + name: `Add Token: Text Decoration`, + }); + + await addTokenButton.click(); + await expect(tokensUpdateCreateModal).toBeVisible(); + + // Placeholder checks + await expect( + tokensUpdateCreateModal.getByPlaceholder( + "Enter text decoration token name", + ), + ).toBeVisible(); + await expect( + tokensUpdateCreateModal.getByPlaceholder( + "none | underline | strike-through or {alias}", + ), + ).toBeVisible(); + + const nameField = tokensUpdateCreateModal.getByLabel("Name"); + const valueField = tokensUpdateCreateModal.getByLabel("Value"); + const submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + + // 1. Name filled + empty value → disabled + await nameField.fill("my-token"); + await expect(submitButton).toBeDisabled(); + + // 2. Invalid value → disabled + error message + await valueField.fill("red"); + + const invalidValueErrorNode = + tokensUpdateCreateModal.getByText(invalidValueError); + + await expect(invalidValueErrorNode).toBeVisible(); + await expect(submitButton).toBeDisabled(); + + // 3. Empty name → disabled + error message + await nameField.fill(""); + + const emptyNameErrorNode = + tokensUpdateCreateModal.getByText(emptyNameError); + + await expect(emptyNameErrorNode).toBeVisible(); + await expect(submitButton).toBeDisabled(); + + // 4. Self reference → disabled + error message + await nameField.fill("my-token"); + await valueField.fill("{my-token}"); + + const selfRefErrorNode = + tokensUpdateCreateModal.getByText(selfReferenceError); + + await expect(selfRefErrorNode).toBeVisible(); + await expect(submitButton).toBeDisabled(); + + // 5. Missing reference → disabled + error message + await valueField.fill("{missing-reference}"); + + const missingRefErrorNode = tokensUpdateCreateModal.getByText( + missingReferenceError, + ); + + await expect(missingRefErrorNode).toBeVisible(); + await expect(submitButton).toBeDisabled(); + + // + // ------- SUCCESSFUL CREATION ------- + // + + // 6. Basic valid value → enabled + await valueField.fill("none"); + await expect( + tokensUpdateCreateModal.getByText("Resolved value: none"), + ).toBeVisible(); + await expect(submitButton).toBeEnabled(); + + await submitButton.click(); + + await expect( + tokensTabPanel.getByRole("button", { name: "my-token" }), + ).toBeEnabled(); + + // + // ------- SECOND TOKEN WITH VALID REFERENCE ------- + // + + await addTokenButton.click(); + + await nameField.fill("my-token-2"); + await valueField.fill("{my-token}"); + + await expect( + tokensUpdateCreateModal.getByText("Resolved value: none"), + ).toBeVisible(); + await expect(submitButton).toBeEnabled(); + + await submitButton.click(); + + await expect( + tokensTabPanel.getByRole("button", { name: "my-token-2" }), + ).toBeEnabled(); + }); + + test("User creates shadow token", async ({ page }) => { + const emptyNameError = "Name should be at least 1 character"; + + const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = + await setupEmptyTokensFile(page, {flags: ["enable-token-shadow"]}); + + // Open modal + const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); + + const addTokenButton = tokensTabPanel.getByRole("button", { + name: `Add Token: Shadow`, + }); + + await addTokenButton.click(); + await expect(tokensUpdateCreateModal).toBeVisible(); + + await expect( + tokensUpdateCreateModal.getByPlaceholder( + "Enter a value or alias with {alias}", + ), + ).toBeVisible(); + + const nameField = tokensUpdateCreateModal.getByLabel("Name"); + const colorField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Color", + }); + const offsetXField = tokensUpdateCreateModal.getByRole("textbox", { + name: "X", + }); + const offsetYField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Y", + }); + const blurField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Blur", + }); + const spreadField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Spread", + }); + const submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + + // 1. Check default values + await expect(offsetXField).toHaveValue("4"); + await expect(offsetYField).toHaveValue("4"); + await expect(blurField).toHaveValue("4"); + await expect(spreadField).toHaveValue("0"); + + // 2. Name filled + empty value → disabled + await nameField.fill("my-token"); + await expect(submitButton).toBeDisabled(); + + // 3. Invalid color → disabled + error message + await colorField.fill("1"); + + await expect( + tokensUpdateCreateModal.getByText("Invalid color value: 1"), + ).toBeVisible(); + + await expect(submitButton).toBeDisabled(); + + await colorField.fill("{missing-reference}"); + + await expect( + tokensUpdateCreateModal.getByText( + "Missing token references: missing-reference", + ), + ).toBeVisible(); + + // 4. Empty name → disabled + error message + await nameField.fill(""); + + const emptyNameErrorNode = + tokensUpdateCreateModal.getByText(emptyNameError); + + await expect(emptyNameErrorNode).toBeVisible(); + await expect(submitButton).toBeDisabled(); + + // + // ------- SUCCESSFUL FIELDS ------- + // + + // 5. Valid color → resolved + + await colorField.fill("red"); + await expect( + tokensUpdateCreateModal.getByText("Resolved value: #ff0000"), + ).toBeVisible(); + const colorSwatch = tokensUpdateCreateModal.getByTestId( + "token-form-color-bullet", + ); + await colorSwatch.click(); + const rampSelector = tokensUpdateCreateModal.getByTestId( + "value-saturation-selector", + ); + await expect(rampSelector).toBeVisible(); + await rampSelector.click({ position: { x: 50, y: 50 } }); + + await expect( + tokensUpdateCreateModal.getByText("Resolved value:"), + ).toBeVisible(); + + const sliderOpacity = tokensUpdateCreateModal.getByTestId("slider-opacity"); + await sliderOpacity.click({ position: { x: 50, y: 0 } }); + await expect( + tokensUpdateCreateModal.getByRole("textbox", { name: "Color" }), + ).toHaveValue(/rgba\s*\([^)]*\)/); + + // 6. Valid offset → resolved + await offsetXField.fill("3 + 3"); + + await expect( + tokensUpdateCreateModal.getByText("Resolved value: 6"), + ).toBeVisible(); + + await offsetYField.fill("3 + 7"); + + await expect( + tokensUpdateCreateModal.getByText("Resolved value: 10"), + ).toBeVisible(); + + // 7. Valid blur → resolved + + await blurField.fill("3 + 1"); + await expect( + tokensUpdateCreateModal.getByText("Resolved value: 4"), + ).toBeVisible(); + + // 8. Valid spread → resolved + + await blurField.fill("3 - 3"); + await expect( + tokensUpdateCreateModal.getByText("Resolved value: 0"), + ).toBeVisible(); + + await nameField.fill("my-token"); + await expect(submitButton).toBeEnabled(); + await submitButton.click(); + + await expect( + tokensTabPanel.getByRole("button", { name: "my-token" }), + ).toBeEnabled(); + + // + // ------- SECOND TOKEN WITH VALID REFERENCE ------- + // + await addTokenButton.click(); + + await nameField.fill("my-token-2"); + const referenceToggle = + tokensUpdateCreateModal.getByTestId("reference-opt"); + const compositeToggle = + tokensUpdateCreateModal.getByTestId("composite-opt"); + await referenceToggle.click(); + + const referenceInput = tokensUpdateCreateModal.getByPlaceholder( + "Enter a token shadow alias", + ); + await expect(referenceInput).toBeVisible(); + + await compositeToggle.click(); + await expect(colorField).toBeVisible(); + + await referenceToggle.click(); + const referenceField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Reference", + }); + await referenceField.fill("{my-token}"); + await expect( + tokensUpdateCreateModal.getByText( + "Resolved value: - X: 6 - Y: 10 - Blur: 0 - Spread: 0 ", + ), + ).toBeVisible(); + + await expect(submitButton).toBeEnabled(); + await submitButton.click(); + await expect( + tokensTabPanel.getByRole("button", { name: "my-token-2" }), + ).toBeEnabled(); + }); + + test("User creates typography token", async ({ page }) => { + const emptyNameError = "Name should be at least 1 character"; + const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = + await setupEmptyTokensFile(page); + + // Open modal + const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); + + const addTokenButton = tokensTabPanel.getByRole("button", { + name: `Add Token: Typography`, + }); + + await addTokenButton.click(); + await expect(tokensUpdateCreateModal).toBeVisible(); + + const nameField = tokensUpdateCreateModal.getByLabel("Name"); + const fontFamilyField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Font family", + }); + await expect(fontFamilyField).toBeVisible(); + const fontSizeField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Font size", + }); + await expect(fontSizeField).toBeVisible(); + const fontWeightField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Font weight", + }); + await expect(fontWeightField).toBeVisible(); + const lineHeightField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Line height", + }); + await expect(lineHeightField).toBeVisible(); + const letterSpacingField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Letter spacing", + }); + await expect(letterSpacingField).toBeVisible(); + + const textCaseField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Text case", + }); + await expect(textCaseField).toBeVisible(); + const textDecorationField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Text decoration", + }); + await expect(textDecorationField).toBeVisible(); + + const submitButton = tokensUpdateCreateModal.getByRole("button", { + name: "Save", + }); + + // 1. Name filled + empty value → disabled + await nameField.fill("my-token"); + await expect(submitButton).toBeDisabled(); + + // 2. Empty name → disabled + error message + await nameField.fill(""); + + const emptyNameErrorNode = + tokensUpdateCreateModal.getByText(emptyNameError); + + await expect(emptyNameErrorNode).toBeVisible(); + await expect(submitButton).toBeDisabled(); + await nameField.fill("my-token"); + + // + // ------- CHECK FIELDS ------- + // + + // 3. Font family + + const fontFamilyPlaceholder = tokensUpdateCreateModal.getByPlaceholder( + "Font family or list of fonts separated by comma (,)", + ); + await expect(fontFamilyPlaceholder).toBeVisible(); + await fontFamilyField.fill("red"); + await expect( + tokensUpdateCreateModal.getByText("Resolved value: red"), + ).toBeVisible(); + + const selectDropdown = tokensUpdateCreateModal.getByRole("button", { + name: "Select font family", + }); + await selectDropdown.click(); + + const fontOption = tokensUpdateCreateModal.getByText("ABeeZee"); + await expect(fontOption).toBeVisible(); + + await fontOption.click(); + + await expect( + tokensUpdateCreateModal.getByText("Resolved value: ABeeZee"), + ).toBeVisible(); + + await selectDropdown.click(); + + const searchField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Search font", + }); + await searchField.fill("alme"); + const fontOption2 = tokensUpdateCreateModal.getByText("Almendra Display"); + await expect(fontOption2).toBeVisible(); + await fontOption2.click(); + await expect( + tokensUpdateCreateModal.getByText("Resolved value: Almendra Display"), + ).toBeVisible(); + + // 4. Font Size + const fontSizePlaceholder = tokensUpdateCreateModal.getByPlaceholder( + "Font size or {alias}", + ); + await expect(fontSizePlaceholder).toBeVisible(); + await fontSizeField.fill("red"); + await expect( + tokensUpdateCreateModal.getByText("Invalid token value: red"), + ).toBeVisible(); + await fontSizeField.fill("24 + 2"); + + await expect( + tokensUpdateCreateModal.getByText("Resolved value: 26"), + ).toBeVisible(); + + // 4. Font Weight + const fontWeightPlaceholder = tokensUpdateCreateModal.getByPlaceholder( + "Font weight (300, Bold Italic...) or an {alias}", + ); + await expect(fontWeightPlaceholder).toBeVisible(); + await fontWeightField.fill("2"); + await expect( + tokensUpdateCreateModal.getByText( + "Invalid font weight value: use numeric values (100-950) or standard names (thin, light, regular, bold, etc.)", + ), + ).toBeVisible(); + await fontWeightField.fill("100 + 200"); + + await expect( + tokensUpdateCreateModal.getByText("Resolved value: 300"), + ).toBeVisible(); + + await fontWeightField.fill("bold"); + + // 5. Line height + + const lineHegithPlaceholdewr = tokensUpdateCreateModal.getByPlaceholder( + "Line height (multiplier, px, %) or {alias}", + ); + await expect(lineHegithPlaceholdewr).toBeVisible(); + await fontSizeField.fill(""); + await lineHeightField.fill("2"); + await expect( + tokensUpdateCreateModal.getByText( + "Line Height depends on Font Size. Add a Font Size to get the resolved value.", + ), + ).toBeVisible(); + + await fontSizeField.fill("24 + 2"); + await lineHeightField.fill("2"); + + // 6. Text case + const textCasePlaceholder = tokensUpdateCreateModal.getByPlaceholder( + "none | uppercase | lowercase | capitalize or {alias}", + ); + await expect(textCasePlaceholder).toBeVisible(); + await textCaseField.fill("200"); + await expect( + tokensUpdateCreateModal.getByText( + "Invalid token value: only none, Uppercase, Lowercase or Capitalize are accepted", + ), + ).toBeVisible(); + + await textCaseField.fill("none"); + + // 7. Letter spacing + const letterSpacingPlaceholder = tokensUpdateCreateModal.getByPlaceholder( + "Letter spacing or {alias}", + ); + await expect(letterSpacingPlaceholder).toBeVisible(); + await letterSpacingField.fill("green"); + await expect( + tokensUpdateCreateModal.getByText("Invalid token value: green"), + ).toBeVisible(); + + await letterSpacingField.fill("2.3"); + + // 7. Text decoration + + const textDecorationPlaceholder = tokensUpdateCreateModal.getByPlaceholder( + "none | underline | strike-through or {alias}", + ); + await expect(textDecorationPlaceholder).toBeVisible(); + + await textDecorationField.fill("200"); + await expect( + tokensUpdateCreateModal.getByText( + "Invalid token value: only none, underline and strike-through are accepted", + ), + ).toBeVisible(); + + await textDecorationField.fill("underline"); + + await expect(submitButton).toBeEnabled(); + await submitButton.click(); + + await expect( + tokensTabPanel.getByRole("button", { name: "my-token" }), + ).toBeEnabled(); + + // + // ------- SECOND TOKEN WITH VALID REFERENCE ------- + // + await addTokenButton.click(); + + await nameField.fill("my-token-2"); + + const referenceToggle = + tokensUpdateCreateModal.getByTestId("reference-opt"); + const compositeToggle = + tokensUpdateCreateModal.getByTestId("composite-opt"); + + await referenceToggle.click(); + + const referenceInput = tokensUpdateCreateModal.getByPlaceholder( + "Enter a token typography alias", + ); + await expect(referenceInput).toBeVisible(); + + await compositeToggle.click(); + await expect( + tokensUpdateCreateModal.getByPlaceholder("Enter typography token name"), + ).toBeVisible(); + + await referenceToggle.click(); + const referenceField = tokensUpdateCreateModal.getByRole("textbox", { + name: "Reference", + }); + await referenceField.fill("{my-token}"); + await expect( + tokensUpdateCreateModal.getByText( + "Resolved value: - Font Family: Almendra Display - Font Weight: bold - Font Size: 26 - Text Case: none - Letter Spacing: 2.3 - Text Decoration: underline - Line Height: 2", + ), + ).toBeVisible(); + + await expect(submitButton).toBeEnabled(); + await submitButton.click(); + await expect( + tokensTabPanel.getByRole("button", { name: "my-token-2" }), + ).toBeEnabled(); }); test("User edits token and auto created set show up in the sidebar", async ({ diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs index 654429dd2a..ce18196c22 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs @@ -43,8 +43,8 @@ ;; 2) Indexed Color Input ;; - Used when the token’s value stores an array of items (e.g. inside ;; shadows, where each shadow layer has its own :color field). -;; - The input writes to a nested subfield: -;; [:value :color] +;; - The input writes to a nested value-subfield: +;; [:value :color] ;; - Only that specific color entry is validated. ;; - Other properties (offsets, blur, inset, etc.) remain untouched. ;; @@ -436,19 +436,21 @@ (some? error) (let [error' (:message error)] - (swap! form assoc-in [:extra-errors :value value-subfield index input-name] {:message error'}) - (swap! form assoc-in [:data :value value-subfield index :color-result] "") - (reset! hint* {:message error' :type "error"})) + (do + (swap! form assoc-in [:extra-errors :value value-subfield index input-name] {:message error'}) + (swap! form assoc-in [:data :value value-subfield index :color-result] "") + (reset! hint* {:message error' :type "error"}))) :else (let [message (tr "workspace.tokens.resolved-value" (dwtf/format-token-value value)) input-value (get-in @form [:data :value value-subfield index input-name] "")] - (swap! form update :errors dissoc :value) - (swap! form update :extra-errors dissoc :value) - (swap! form assoc-in [:data :value value-subfield index :color-result] (dwtf/format-token-value value)) - (if (= input-value (str value)) - (reset! hint* {}) - (reset! hint* {:message message :type "hint"})))))))] + (do + (swap! form update :errors dissoc :value) + (swap! form update :extra-errors dissoc :value) + (swap! form assoc-in [:data :value value-subfield index :color-result] (dwtf/format-token-value value)) + (if (= input-value (str value)) + (reset! hint* {}) + (reset! hint* {:message message :type "hint"}))))))))] (fn [] (rx/dispose! subs)))) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs index 420c2c1dd6..ba6a8348c2 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs @@ -41,7 +41,7 @@ ;; 2) Composite Font Picker ;; - Used inside typography tokens, where `:value` is a map (e.g. contains ;; :font-family, :font-weight, :letter-spacing, etc.). -;; - The input writes to the specific subfield `[:value :font-family]`. +;; - The input writes to the specific value-subfield `[:value :font-family]`. ;; - Only this field is validated and updated—other typography fields remain ;; untouched. ;; diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs index 29a8145095..f4eadff944 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs @@ -48,7 +48,7 @@ ;; 2) COMPOSITE INPUTS ;; ---------------------------------------------------------- ;; Used when the token contains a set of *named fields* inside :value. -;; The UI must write into a specific subfield inside the :value map. +;; The UI must write into a specific value-subfield inside the :value map. ;; ;; Example: typography tokens ;; {:value {:font-family "Inter" @@ -64,7 +64,7 @@ ;; * Validation rules apply per-field. ;; ;; In practice: -;; - The component knows which subfield to update. +;; - The component knows which value-subfield to update. ;; - The form accumulates multiple fields into a single map under :value. ;; ;; diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/select.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/select.cljs index 4bbede130a..9e7009fe8c 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/select.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/select.cljs @@ -13,10 +13,10 @@ ;; --- Select Input (Indexed) -------------------------------------------------- ;; ;; This input type is part of the indexed system, used for fields that exist -;; inside an array of maps stored in a subfield of :value. +;; inside an array of maps stored in a value-subfield of :value. ;; ;; - Writes to a nested location: -;; [:value ] +;; [:value ] ;; - Each item in the array has its own select input, independent of others. ;; - Validation ensures the selected value is valid for that field. ;; - Changing one item does not affect the other items in the array. 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 098f738010..cb1f3a1902 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 @@ -230,16 +230,29 @@ {:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])] [:div {:class (stl/css :input-row)} - [:> input-component - {:placeholder (or input-value-placeholder (tr "workspace.tokens.token-value-enter")) - :label (tr "workspace.tokens.token-value") - :name :value - :form form - :token token - :tokens tokens - :tab active-tab - :subfield value-subfield - :toggle on-toggle-tab}]] + (case type + :indexed + [:> input-component + {:token token + :tokens tokens + :tab active-tab + :value-subfield value-subfield + :handle-toggle on-toggle-tab}] + + :composite + [:> input-component + {:token token + :tokens tokens + :tab active-tab + :handle-toggle on-toggle-tab}] + + [:> input-component + {:placeholder (or input-value-placeholder + (tr "workspace.tokens.token-value-enter")) + :label (tr "workspace.tokens.token-value") + :name :value + :token token + :tokens tokens}])] [:div {:class (stl/css :input-row)} [:> fc/form-input* {:id "token-description" diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs index 880d957167..594f911a48 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs @@ -208,19 +208,20 @@ :tokens tokens}]]) (mf/defc tabs-wrapper* - [{:keys [token tokens form tab toggle subfield] :rest props}] - (let [on-add-shadow-block + [{:keys [token tokens tab handle-toggle value-subfield] :rest props}] + (let [form (mf/use-ctx forms/context) + on-add-shadow-block (mf/use-fn - (mf/deps subfield) + (mf/deps value-subfield) (fn [] - (swap! form update-in [:data :value subfield] conj default-token-shadow))) + (swap! form update-in [:data :value value-subfield] conj default-token-shadow))) remove-shadow-block (mf/use-fn - (mf/deps subfield) + (mf/deps value-subfield) (fn [index event] (dom/prevent-default event) - (swap! form update-in [:data :value subfield] #(d/remove-at-index % index))))] + (swap! form update-in [:data :value value-subfield] #(d/remove-at-index % index))))] [:* [:div {:class (stl/css :title-bar)} @@ -232,7 +233,7 @@ :icon i/add}] [:& radio-buttons {:class (stl/css :listing-options) :selected (d/name tab) - :on-change toggle + :on-change handle-toggle :name "reference-composite-tab"} [:& radio-button {:icon i/layers :value "composite" @@ -247,7 +248,7 @@ [:> composite-form* {:token token :tokens tokens :remove-shadow-block remove-shadow-block - :value-subfield subfield}] + :value-subfield value-subfield}] [:> reference-form* {:token token :tokens tokens}])])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.cljs index 1db82e390d..474d6cb5b5 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.cljs @@ -181,13 +181,13 @@ :tokens tokens}]]) (mf/defc tabs-wrapper* - [{:keys [token tokens tab toggle] :rest props}] + [{:keys [token tokens tab handle-toggle] :rest props}] [:* [:div {:class (stl/css :title-bar)} [:div {:class (stl/css :title)} (tr "labels.typography")] [:& radio-buttons {:class (stl/css :listing-options) :selected (d/name tab) - :on-change toggle + :on-change handle-toggle :name "reference-composite-tab"} [:& radio-button {:icon i/layers :value "composite"