From 857aa4175c0c6ac8e1ea2680a03bd86851058dac Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Wed, 20 May 2026 17:38:16 +0200 Subject: [PATCH] :bug: Fix Numeric inputs rejects values with leading whitespaces --- .../playwright/ui/specs/numeric-input.spec.js | 80 +++++++++++++++++++ .../main/ui/ds/controls/numeric_input.cljs | 58 +++++++------- 2 files changed, 110 insertions(+), 28 deletions(-) create mode 100644 frontend/playwright/ui/specs/numeric-input.spec.js diff --git a/frontend/playwright/ui/specs/numeric-input.spec.js b/frontend/playwright/ui/specs/numeric-input.spec.js new file mode 100644 index 0000000000..11faa57b4d --- /dev/null +++ b/frontend/playwright/ui/specs/numeric-input.spec.js @@ -0,0 +1,80 @@ +import { test, expect } from "@playwright/test"; +import { WasmWorkspacePage } from "../pages/WasmWorkspacePage"; + +test.beforeEach(async ({ page }) => { + await WasmWorkspacePage.init(page); + await WasmWorkspacePage.mockConfigFlags(page, ["enable-feature-token-input"]); +}); + +test("BUG 14226: Numeric inputs in the design panel reject values with leading whitespace", async ({ + page, +}) => { + const workspacePage = new WasmWorkspacePage(page); + await workspacePage.setupEmptyFile(page); + await workspacePage.mockRPC( + /get\-file\?/, + "workspace/get-file-copy-paste.json", + ); + await workspacePage.mockRPC( + "get-file-fragment?file-id=*&fragment-id=*", + "workspace/get-file-copy-paste-fragment.json", + ); + + await workspacePage.goToWorkspace({ + fileId: "870f9f10-87b5-8137-8005-934804124660", + pageId: "870f9f10-87b5-8137-8005-934804124661", + }); + + // Select first shape + await page.getByTestId("layer-item").getByRole("button").first().click(); + await workspacePage.layers.getByTestId("layer-row").nth(0).click(); + + // Check if measures section is visible + const measuresSection = workspacePage.rightSidebar.getByRole("region", { + name: "shape-measures-section", + }); + await expect(measuresSection).toBeVisible(); + + // Width + const widthInput = measuresSection.getByRole("textbox", { + name: "Width", + exact: true, + }); + await expect(widthInput).toHaveValue("360"); + + await widthInput.fill("100"); + await widthInput.press("Enter"); + await expect(widthInput).toHaveValue("100"); + + await widthInput.fill(" 100"); + await widthInput.press("Enter"); + await expect(widthInput).toHaveValue("100"); + + await widthInput.fill(" 100 "); + await widthInput.press("Enter"); + await expect(widthInput).toHaveValue("100"); + + await widthInput.fill("100 "); + await widthInput.press("Enter"); + await expect(widthInput).toHaveValue("100"); + + await widthInput.fill("98+2"); + await widthInput.press("Enter"); + await expect(widthInput).toHaveValue("100"); + + await widthInput.fill("98 + 2"); + await widthInput.press("Enter"); + await expect(widthInput).toHaveValue("100"); + + await widthInput.fill(" 98 + 2 "); + await widthInput.press("Enter"); + await expect(widthInput).toHaveValue("100"); + + await widthInput.fill(" 98+2 "); + await widthInput.press("Enter"); + await expect(widthInput).toHaveValue("100"); + + await widthInput.fill(" asdasdasdasd "); + await widthInput.press("Enter"); + await expect(widthInput).toHaveValue("100"); +}); diff --git a/frontend/src/app/main/ui/ds/controls/numeric_input.cljs b/frontend/src/app/main/ui/ds/controls/numeric_input.cljs index 8a20573119..aed2b769d4 100644 --- a/frontend/src/app/main/ui/ds/controls/numeric_input.cljs +++ b/frontend/src/app/main/ui/ds/controls/numeric_input.cljs @@ -291,33 +291,35 @@ (mf/use-fn (mf/deps on-change update-input value nillable min max) (fn [raw-value] - (if-let [parsed (parse-value raw-value (mf/ref-val last-value*) min max nillable)] - (when-not (= parsed (mf/ref-val last-value*)) - (mf/set-ref-val! last-value* parsed) - (reset! token-applied-name* nil) - (when (fn? on-change) - (on-change parsed)) - - (mf/set-ref-val! raw-value* (fmt/format-number parsed)) - (update-input (fmt/format-number parsed))) - - (if (and nillable (empty? raw-value)) + (let [raw-value (str/trim (str raw-value))] + (if-let [parsed (parse-value raw-value (mf/ref-val last-value*) min max nillable)] (do - (mf/set-ref-val! last-value* nil) - (mf/set-ref-val! raw-value* "") - (reset! token-applied-name* nil) - (update-input "") - (when (fn? on-change) - (on-change nil))) + (when-not (= parsed (mf/ref-val last-value*)) + (mf/set-ref-val! last-value* parsed) + (reset! token-applied-name* nil) + (when (fn? on-change) + (on-change parsed))) - (let [fallback-value (or (mf/ref-val last-value*) default)] - (mf/set-ref-val! raw-value* fallback-value) - (mf/set-ref-val! last-value* fallback-value) - (reset! token-applied-name* nil) - (update-input (fmt/format-number fallback-value)) + (mf/set-ref-val! raw-value* (fmt/format-number parsed)) + (update-input (fmt/format-number parsed))) - (when (and (fn? on-change) (not= fallback-value (str value))) - (on-change fallback-value))))))) + (if (and nillable (empty? raw-value)) + (do + (mf/set-ref-val! last-value* nil) + (mf/set-ref-val! raw-value* "") + (reset! token-applied-name* nil) + (update-input "") + (when (fn? on-change) + (on-change nil))) + + (let [fallback-value (or (mf/ref-val last-value*) default)] + (mf/set-ref-val! raw-value* fallback-value) + (mf/set-ref-val! last-value* fallback-value) + (reset! token-applied-name* nil) + (update-input (fmt/format-number fallback-value)) + + (when (and (fn? on-change) (not= fallback-value (str value))) + (on-change fallback-value)))))))) apply-token (mf/use-fn @@ -466,7 +468,7 @@ (dom/prevent-default event) (handle-focus-change options focused-id* new-index (mf/ref-val nodes-ref))) - (let [parsed (parse-value (mf/ref-val raw-value*) (mf/ref-val last-value*) min max nillable) + (let [parsed (parse-value (str/trim (mf/ref-val raw-value*)) (mf/ref-val last-value*) min max nillable) current-value (or parsed default) new-val (increment current-value step min max)] (dom/prevent-default event) @@ -479,7 +481,7 @@ (dom/prevent-default event) (handle-focus-change options focused-id* new-index (mf/ref-val nodes-ref))) - (let [parsed (parse-value (mf/ref-val raw-value*) (mf/ref-val last-value*) min max nillable) + (let [parsed (parse-value (str/trim (mf/ref-val raw-value*)) (mf/ref-val last-value*) min max nillable) current-value (or parsed default) new-val (decrement current-value step min max)] (dom/prevent-default event) @@ -508,7 +510,7 @@ (let [inc? (->> (dom/get-delta-position event) :y (neg?)) - parsed (parse-value (mf/ref-val raw-value*) (mf/ref-val last-value*) min max nillable) + parsed (parse-value (str/trim (mf/ref-val raw-value*)) (mf/ref-val last-value*) min max nillable) current-value (or parsed default) new-val (if inc? (increment current-value step min max) @@ -527,7 +529,7 @@ has-token (some? (deref token-applied-name*))] (when-not (or is-focused has-token) (let [client-x (.-clientX event) - parsed (parse-value (mf/ref-val raw-value*) (mf/ref-val last-value*) min max nillable) + parsed (parse-value (str/trim (mf/ref-val raw-value*)) (mf/ref-val last-value*) min max nillable) start-val (or parsed default 0)] (mf/set-ref-val! drag-state* :maybe-dragging) (mf/set-ref-val! drag-start-x* client-x)