diff --git a/frontend/playwright/ui/specs/tokens/crud.spec.js b/frontend/playwright/ui/specs/tokens/crud.spec.js index 99ec33ee46..d37a18c1ed 100644 --- a/frontend/playwright/ui/specs/tokens/crud.spec.js +++ b/frontend/playwright/ui/specs/tokens/crud.spec.js @@ -919,6 +919,7 @@ test.describe("Tokens - creation", () => { test("User creates shadow token", async ({ page }) => { const emptyNameError = "Name should be at least 1 character"; + const emptyFieldError = "This field cannot be empty"; const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = await setupEmptyTokensFileRender(page, { @@ -997,6 +998,14 @@ test.describe("Tokens - creation", () => { await expect(emptyNameErrorNode).toBeVisible(); await expect(submitButton).toBeDisabled(); + // 5. Empty fill -> disabled + error message + await offsetXField.fill("3"); // Fill should be touched in order to show the error message + await offsetXField.fill(""); + const emptyFieldErrorNode = + tokensUpdateCreateModal.getByText(emptyFieldError); + + await expect(emptyFieldErrorNode).toBeVisible(); + // // ------- SUCCESSFUL FIELDS ------- // @@ -1102,6 +1111,28 @@ test.describe("Tokens - creation", () => { await expect( tokensTabPanel.getByRole("button", { name: "my-token-2" }), ).toBeEnabled(); + + // + // ------- THIRD TOKEN WITH EMPTY BLUR AND SPREAD ------- + // + await addTokenButton.click(); + + await nameField.fill("my-token-3"); + await colorField.fill("red"); + + // 1. Empty blur and spread + + await blurField.fill(""); + + await spreadField.fill(""); + + await expect(submitButton).toBeEnabled(); + await submitButton.click(); + + await unfoldTokenType(tokensTabPanel, "shadow"); + await expect( + tokensTabPanel.getByRole("button", { name: "my-token-3" }), + ).toBeEnabled(); }); test("User cant submit empty typography token or reference", async ({ @@ -1691,13 +1722,21 @@ test.describe("Tokens - creation", () => { }); }); -test("User cannot create token with a conflicting name in other set", async ({ page }) => { - const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } = - await setupTokensFileRender(page); +test("User cannot create token with a conflicting name in other set", async ({ + page, +}) => { + const { + tokensUpdateCreateModal, + tokenThemesSetsSidebar, + tokensSidebar, + tokenContextMenuForToken, + } = await setupTokensFileRender(page); await expect(tokensSidebar).toBeVisible(); - await tokenThemesSetsSidebar.getByRole('button', { name: 'light', exact: true }).click(); + await tokenThemesSetsSidebar + .getByRole("button", { name: "light", exact: true }) + .click(); const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" }); await tokensTabPanel @@ -1721,8 +1760,11 @@ test("User cannot create token with a conflicting name in other set", async ({ p await nameField.fill("accent.default"); // An error message should appear and submit button should be disabled - await expect(tokensUpdateCreateModal.getByText('A token already exists at the path: accent.default')) - .toBeVisible() + await expect( + tokensUpdateCreateModal.getByText( + "A token already exists at the path: accent.default", + ), + ).toBeVisible(); await expect(submitButton).toBeDisabled(); @@ -1730,8 +1772,11 @@ test("User cannot create token with a conflicting name in other set", async ({ p await nameField.fill("colors.red"); // An error message should appear and submit button should be disabled - await expect(tokensUpdateCreateModal.getByText('A token already exists at the path: colors.red')) - .toBeVisible() + await expect( + tokensUpdateCreateModal.getByText( + "A token already exists at the path: colors.red", + ), + ).toBeVisible(); await expect(submitButton).toBeDisabled(); 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 3defb347d3..d2da1c89c3 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 @@ -311,21 +311,39 @@ (swap! form (fn [state] (-> state (assoc-in [:data :value value-subfield index field] (if trim? (str/trim value) value)) + (assoc-in [:touched :value value-subfield index field] true) + (update :errors dissoc :value) + (update :extra-errors dissoc :value) (update :errors clean-errors) (update :extra-errors clean-errors))))))) + (mf/defc indexed-color-input* [{:keys [name tokens token index value-subfield] :rest props}] (let [form (mf/use-ctx fc/context) input-name name token-name (get-in @form [:data :name] nil) - error - (get-in @form [:errors :value value-subfield index input-name]) + + touched? + (get-in @form [:touched :value value-subfield index input-name]) value (get-in @form [:data :value value-subfield index input-name] "") + ;; Resolution error for this specific field + indexed-error + (get-in @form [:errors :value value-subfield index input-name]) + + ;; Empty-field error: scoped to this layer so each shadow layer is + ;; evaluated independently from the others. + empty-error + (when (str/blank? value) + {:message (tr "errors.tokens.empty-field")}) + + error + (when touched? (or indexed-error empty-error)) + color-resolved (get-in @form [:data :value value-subfield index :color-result] "") 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 3e20c5a2e1..172d712a8b 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 @@ -383,22 +383,40 @@ (swap! form (fn [state] (-> state (assoc-in [:data :value value-subfield index field] (if trim? (str/trim value) value)) + (assoc-in [:touched :value value-subfield index field] true) + (update :errors dissoc :value) + (update :extra-errors dissoc :value) (update :errors clean-errors) (update :extra-errors clean-errors))))))) (mf/defc input-indexed* - [{:keys [name tokens token index value-subfield] :rest props}] + [{:keys [name tokens token index value-subfield nillable] :rest props}] (let [form (mf/use-ctx fc/context) input-name name token-name (get-in @form [:data :name] nil) + nillable (d/nilv nillable false) - error - (get-in @form [:errors :value value-subfield index input-name]) + touched? + (get-in @form [:touched :value value-subfield index input-name]) value-from-form (get-in @form [:data :value value-subfield index input-name] "") + ;; Resolution error for this specific field (e.g. missing reference) + indexed-error + (get-in @form [:errors :value value-subfield index input-name]) + + ;; Empty-field error: derived purely from the local field value so that + ;; each shadow layer is evaluated independently. + empty-error + (when (and (not nillable) + (str/blank? value-from-form)) + {:message (tr "errors.tokens.empty-field")}) + + error + (when touched? (or indexed-error empty-error)) + resolve-stream (mf/with-memo [token index input-name] (if-let [value (get-in token [:value value-subfield index input-name])] @@ -428,7 +446,7 @@ props (if error (mf/spread-props props {:hint-type "error" - :hint-message (:message error)}) + :hint-message (or (:message error) (tr "errors.field-missing"))}) props) props 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 c928cc785a..1a245fe8f9 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 @@ -57,10 +57,13 @@ (update :token-value (fn [value] (->> (or value []) (mapv (fn [shadow] - (d/update-when shadow :inset #(cond - (boolean? %) % - (= "true" %) true - :else false))))))) + (-> shadow + (d/update-when :inset #(cond + (boolean? %) % + (= "true" %) true + :else false)) + (update :blur #(if (str/blank? %) "0" %)) + (update :spread #(if (str/blank? %) "0" %)))))))) (assoc :validators [check-empty-shadow-token check-shadow-token-self-reference]))] @@ -160,6 +163,7 @@ {:aria-label (tr "workspace.tokens.shadow-blur") :placeholder (tr "workspace.tokens.shadow-blur") :name :blur + :nillable true :slot-start (mf/html [:span {:class (stl/css :visible-label)} (str (tr "workspace.tokens.shadow-blur") ":")]) :token blur-token @@ -172,6 +176,7 @@ {:aria-label (tr "workspace.tokens.shadow-spread") :placeholder (tr "workspace.tokens.shadow-spread") :name :spread + :nillable true :slot-start (mf/html [:span {:class (stl/css :visible-label)} (str (tr "workspace.tokens.shadow-spread") ":")]) :token spread-token @@ -310,15 +315,14 @@ ref-valid? (and reference (not (str/blank? reference))) shadows (get value :shadow) - ;; To be a valid shadow it must contain one on each valid values + ;; To be a valid shadow it must contain one on each valid values. + ;; blur and spread are optional and default to "0" when blank. valid-composite-shadow? (and (seq shadows) (every? - (fn [{:keys [offset-x offset-y blur spread color]}] + (fn [{:keys [offset-x offset-y color]}] (and (not (str/blank? offset-x)) (not (str/blank? offset-y)) - (not (str/blank? blur)) - (not (str/blank? spread)) (not (str/blank? color)))) shadows))] @@ -333,7 +337,11 @@ (vector? value) {:reference nil - :shadow value} + :shadow (mapv (fn [shadow] + (-> shadow + (update :blur #(if (str/blank? %) "0" %)) + (update :spread #(if (str/blank? %) "0" %)))) + value)} :else {:reference nil diff --git a/frontend/translations/en.po b/frontend/translations/en.po index a28e60343c..73740718e0 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -8461,6 +8461,10 @@ msgstr "Edit %s token" msgid "errors.tokens.empty-input" msgstr "Token value cannot be empty" +#: src/app/main/data/workspace/tokens/errors.cljs +msgid "errors.tokens.empty-field" +msgstr "This field cannot be empty" + #: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:241 msgid "workspace.tokens.enter-token-name" msgstr "Enter %s token name" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 4a22fb7a9b..bfe6852555 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -8221,6 +8221,10 @@ msgstr "Editar token de %s" msgid "errors.tokens.empty-input" msgstr "El valor del token no puede estar vacío" +#: src/app/main/data/workspace/tokens/errors.cljs +msgid "errors.tokens.empty-field" +msgstr "Este campo no puede estar vacío" + #: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:241 msgid "workspace.tokens.enter-token-name" msgstr "Introduce un nombre para el token %s"