diff --git a/frontend/playwright/ui/specs/numeric-input.spec.js b/frontend/playwright/ui/specs/numeric-input.spec.js index 49d3d1ca6b..499d9deae6 100644 --- a/frontend/playwright/ui/specs/numeric-input.spec.js +++ b/frontend/playwright/ui/specs/numeric-input.spec.js @@ -133,7 +133,7 @@ test("BUG 10001: Negative margins are allowed on the numeric input", async ({ }); await expect(verticalMarginInput).toBeVisible(); - + await verticalMarginInput.fill("-10"); await verticalMarginInput.press("Enter"); await expect(verticalMarginInput).toHaveValue("-10"); @@ -230,3 +230,54 @@ test("BUG 10638 - Invalid padding input on multi-selection must not persist stri expect(seenLayoutValues).not.toEqual([]); expect(badLayoutValues).toEqual([]); }); + +test("BUG 12227: Numeric inputs only show the scrub cursor when they hold a single value", 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", + }); + + await page.getByTestId("layer-item").getByRole("button").first().click(); + await workspacePage.layers.getByTestId("layer-row").nth(0).click(); + + const measuresSection = workspacePage.rightSidebar.getByRole("region", { + name: "shape-measures-section", + }); + await expect(measuresSection).toBeVisible(); + + const widthInput = measuresSection.getByRole("textbox", { + name: "Width", + exact: true, + }); + await expect(widthInput).toHaveValue("360"); + + // The wrapper around the input is the element that carries the cursor. + const widthWrapper = measuresSection + .getByTestId("numeric-input-wrapper") + .filter({ has: page.getByRole("textbox", { name: "Width", exact: true }) }); + + // A single committed value can be scrubbed. + await expect(widthWrapper).toHaveCSS("cursor", "ew-resize"); + + // Selecting a second shape with a different width mixes the values. + // The field then has no single value, so it must not look scrubbable. + await workspacePage.layers + .getByTestId("layer-row") + .nth(1) + .click({ modifiers: ["Shift"] }); + await expect(widthInput).toHaveAttribute("placeholder", "Mixed"); + await expect(widthWrapper).not.toHaveCSS("cursor", "ew-resize"); +}); 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 56b24b18b6..7595f8873c 100644 --- a/frontend/src/app/main/ui/ds/controls/numeric_input.cljs +++ b/frontend/src/app/main/ui/ds/controls/numeric_input.cljs @@ -245,6 +245,10 @@ is-token-applied? (and (some? token-applied-name) (not= :multiple token-applied-name)) + scrubbable? (and (some? value) + (not disabled) + (not is-token-applied?)) + focused-id* (mf/use-state nil) focused-id (deref focused-id*) @@ -572,24 +576,22 @@ on-scrub-pointer-down (mf/use-fn - (mf/deps disabled is-open is-multiple? ref min max nillable default is-token-applied?) + (mf/deps is-open ref min max nillable default scrubbable?) (fn [event] - (when-not (or disabled is-open is-multiple? is-token-applied?) - (let [has-token (some? (deref token-applied-name*))] - (when-not has-token - (let [client-x (.-clientX event) - 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) - (mf/set-ref-val! drag-start-val* start-val) - (dom/capture-pointer event))))))) + (when (and (not is-open) scrubbable?) + (let [client-x (.-clientX event) + 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) + (mf/set-ref-val! drag-start-val* start-val) + (dom/capture-pointer event))))) on-scrub-pointer-move (mf/use-fn - (mf/deps apply-value update-input step min max on-change-start is-token-applied?) + (mf/deps apply-value update-input step min max on-change-start scrubbable?) (fn [event] - (when-not is-token-applied? + (when scrubbable? (let [state (mf/ref-val drag-state*)] (when (or (= state :maybe-dragging) (= state :dragging)) (let [client-x (.-clientX event) @@ -857,9 +859,9 @@ (mf/with-effect [handle-unmount] handle-unmount) [:div {:class [class (stl/css-case :input-wrapper true - :resizable (and (not is-token-applied?) - (not disabled)))] + :scrubbable scrubbable?)] :ref wrapper-ref + :data-testid "numeric-input-wrapper" :on-pointer-down on-scrub-pointer-down :on-pointer-move on-scrub-pointer-move :on-pointer-up on-scrub-pointer-up diff --git a/frontend/src/app/main/ui/ds/controls/numeric_input.scss b/frontend/src/app/main/ui/ds/controls/numeric_input.scss index 3f9769912c..dd79749e67 100644 --- a/frontend/src/app/main/ui/ds/controls/numeric_input.scss +++ b/frontend/src/app/main/ui/ds/controls/numeric_input.scss @@ -22,7 +22,7 @@ inline-size: 100%; position: relative; - &.resizable { + &.scrubbable { cursor: ew-resize; }