From 0c75fe7067b10343f98dc1b855ce08f85ba00e0e Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Fri, 31 Oct 2025 16:24:36 +0200 Subject: [PATCH 01/14] feat: add scroll event --- packages/main/src/StepInput.ts | 16 ++++++++++++++++ packages/main/src/StepInputTemplate.tsx | 1 + 2 files changed, 17 insertions(+) diff --git a/packages/main/src/StepInput.ts b/packages/main/src/StepInput.ts index 87b527641399..4aa7575f098e 100644 --- a/packages/main/src/StepInput.ts +++ b/packages/main/src/StepInput.ts @@ -422,6 +422,22 @@ class StepInput extends UI5Element implements IFormInputElement { this._onInputChange(); } + _onMouseWheel(e: WheelEvent) { + if (this.disabled || this.readonly) { + return; + } + + // Prevent page scroll when component is focused + if (!this._isFocused) { + e.preventDefault(); + } + + // Determine scroll direction and modify value accordingly + const isScrollUp = e.deltaY < 0; + const modifier = isScrollUp ? this.step : -this.step; + this._modifyValue(modifier, true); + } + _setButtonState() { this._decIconDisabled = this.min !== undefined && this.value <= this.min; this._incIconDisabled = this.max !== undefined && this.value >= this.max; diff --git a/packages/main/src/StepInputTemplate.tsx b/packages/main/src/StepInputTemplate.tsx index 75f33862e56c..775b861f91eb 100644 --- a/packages/main/src/StepInputTemplate.tsx +++ b/packages/main/src/StepInputTemplate.tsx @@ -13,6 +13,7 @@ export default function StepInputTemplate(this: StepInput) { onKeyDown={this._onkeydown} onFocusIn={this._onfocusin} onFocusOut={this._onfocusout} + onWheel={this._onMouseWheel} > {/* Decrement Icon */} {!this.readonly && From 69b5eb019e533e73289d21e2e07fb229086e6be5 Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Tue, 4 Nov 2025 10:25:16 +0200 Subject: [PATCH 02/14] feat: add thousand seperator --- packages/localization/src/NumberFormat.ts | 8 +++ packages/localization/used-modules.txt | 3 +- packages/main/src/StepInput.ts | 71 ++++++++++++++++++++--- packages/main/test/pages/StepInput.html | 22 ++++--- 4 files changed, 86 insertions(+), 18 deletions(-) create mode 100644 packages/localization/src/NumberFormat.ts diff --git a/packages/localization/src/NumberFormat.ts b/packages/localization/src/NumberFormat.ts new file mode 100644 index 000000000000..27167ae1b5b5 --- /dev/null +++ b/packages/localization/src/NumberFormat.ts @@ -0,0 +1,8 @@ +import type NumberFormatT from "sap/ui/core/format/NumberFormat"; +// @ts-ignore +import NumberFormatNative from "./sap/ui/core/format/NumberFormat.js"; + +const NumberFormatWrapped = NumberFormatNative as typeof NumberFormatT; +class NumberFormat extends NumberFormatWrapped {} + +export default NumberFormat; diff --git a/packages/localization/used-modules.txt b/packages/localization/used-modules.txt index a62e0e5f3598..9c5a8b4e7f0c 100644 --- a/packages/localization/used-modules.txt +++ b/packages/localization/used-modules.txt @@ -48,4 +48,5 @@ sap/ui/core/date/Persian.js sap/ui/core/date/UI5Date.js sap/ui/core/date/UniversalDate.js sap/ui/core/format/TimezoneUtil.js -sap/ui/core/format/DateFormat.js \ No newline at end of file +sap/ui/core/format/DateFormat.js +sap/ui/core/format/NumberFormat.js \ No newline at end of file diff --git a/packages/main/src/StepInput.ts b/packages/main/src/StepInput.ts index 4aa7575f098e..b3a22673ddd7 100644 --- a/packages/main/src/StepInput.ts +++ b/packages/main/src/StepInput.ts @@ -38,6 +38,7 @@ import "@ui5/webcomponents-icons/dist/add.js"; import type Input from "./Input.js"; import type { InputAccInfo, InputEventDetail } from "./Input.js"; import InputType from "./types/InputType.js"; +import NumberFormat from "@ui5/webcomponents-localization/dist/NumberFormat.js"; // Styles import StepInputCss from "./generated/themes/StepInput.css.js"; @@ -249,6 +250,15 @@ class StepInput extends UI5Element implements IFormInputElement { @property() accessibleNameRef?: string; + /** + * Defines whether to display thousands separator. + * @default false + * @public + * @since 2.16.0 + */ + @property({ type: Boolean }) + showThousandsSeparator = false; + @property({ noAttribute: true }) _decIconDisabled = false; @@ -293,6 +303,8 @@ class StepInput extends UI5Element implements IFormInputElement { _initialValueState?: `${ValueState}`; + _formatter?: NumberFormat; + @i18n("@ui5/webcomponents") static i18nBundle: I18nBundle; @@ -329,7 +341,7 @@ class StepInput extends UI5Element implements IFormInputElement { } get type() { - return InputType.Number; + return this.showThousandsSeparator ? InputType.Text : InputType.Number; } // icons-related @@ -356,14 +368,14 @@ class StepInput extends UI5Element implements IFormInputElement { get _displayValue() { if ((this.value === 0) || (Number.isInteger(this.value))) { - return this.value.toFixed(this.valuePrecision); + return this._formatNumber(this.value); } if (this.input && this.value === Number(this.input.value)) { // For the cases where the number is fractional and is ending with 0s. return this.input.value; } - return this.value.toString(); + return this._formatNumber(this.value); } get accInfo(): InputAccInfo { @@ -385,6 +397,17 @@ class StepInput extends UI5Element implements IFormInputElement { this._setButtonState(); } + get formatter(): NumberFormat { + if (!this._formatter) { + this._formatter = NumberFormat.getFloatInstance({ + decimals: this.valuePrecision, + groupingEnabled: this.showThousandsSeparator, + }); + } + + return this._formatter; + } + get input(): Input { return this.shadowRoot!.querySelector("[ui5-input]")!; } @@ -427,12 +450,10 @@ class StepInput extends UI5Element implements IFormInputElement { return; } - // Prevent page scroll when component is focused if (!this._isFocused) { e.preventDefault(); } - // Determine scroll direction and modify value accordingly const isScrollUp = e.deltaY < 0; const modifier = isScrollUp ? this.step : -this.step; this._modifyValue(modifier, true); @@ -501,7 +522,7 @@ class StepInput extends UI5Element implements IFormInputElement { value = this._preciseValue(value); if (value !== this.value) { this.value = value; - this.input.value = value.toFixed(this.valuePrecision); + this.input.value = this._formatNumber(value); this._validate(); this._setButtonState(); this.focused = true; @@ -514,6 +535,37 @@ class StepInput extends UI5Element implements IFormInputElement { } } + /** + * Formats a number with thousands separator based on current locale + * @private + */ + _formatNumber(value: number): string { + if (!this.showThousandsSeparator) { + return value.toFixed(this.valuePrecision); + } + + // Use Intl.NumberFormat for locale-aware formatting + return new Intl.NumberFormat(undefined, { + minimumFractionDigits: this.valuePrecision, + maximumFractionDigits: this.valuePrecision, + useGrouping: true, + }).format(value); + } + + /** + * Parses formatted number string back to numeric value + * @private + */ + _parseNumber(formattedValue: string): number { + if (!this.showThousandsSeparator) { + return Number(formattedValue); + } + + // Remove thousands separators and parse + const cleanValue = formattedValue.replace(/[,\s]/g, ""); + return Number(cleanValue); + } + _incValue() { if (this._incIconClickable && !this.disabled && !this.readonly) { this._modifyValue(this.step, true); @@ -529,6 +581,9 @@ class StepInput extends UI5Element implements IFormInputElement { } get _isValueWithCorrectPrecision() { + if (this.showThousandsSeparator) { + return true; + } // gets either "." or "," as delimiter which is based on locale, and splits the number by it const delimiter = this.input?.value?.includes(".") ? "." : ","; const numberParts = this.input?.value?.split(delimiter); @@ -540,7 +595,7 @@ class StepInput extends UI5Element implements IFormInputElement { _onInputChange() { this._setDefaultInputValueIfNeeded(); - const inputValue = Number(this.input.value); + const inputValue = this._parseNumber(this.input.value); if (this._isValueChanged(inputValue)) { this._updateValueAndValidate(inputValue); } @@ -548,7 +603,7 @@ class StepInput extends UI5Element implements IFormInputElement { _setDefaultInputValueIfNeeded() { if (this.input.value === "") { - const defaultValue = (this.min || 0).toFixed(this.valuePrecision); + const defaultValue = this._formatNumber(this.min || 0); this.input.value = defaultValue; this.innerInput.value = defaultValue; // we need to update inner input value as well, to avoid empty input scenario } diff --git a/packages/main/test/pages/StepInput.html b/packages/main/test/pages/StepInput.html index 21a5ed3d911a..b970d62e0b17 100644 --- a/packages/main/test/pages/StepInput.html +++ b/packages/main/test/pages/StepInput.html @@ -25,11 +25,15 @@ - -

StepInput

- Event [change] :: N/A
- -
+ + + + +

StepInput in Cozy

StepInput in Cozy >
-
+ - --> From a7578e5c0b93d874b00f3beb154874ee3cefc7c3 Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Tue, 4 Nov 2025 16:45:35 +0200 Subject: [PATCH 03/14] feat: format display value --- packages/main/src/StepInput.ts | 59 ++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/packages/main/src/StepInput.ts b/packages/main/src/StepInput.ts index b3a22673ddd7..d3139a168f2d 100644 --- a/packages/main/src/StepInput.ts +++ b/packages/main/src/StepInput.ts @@ -17,6 +17,7 @@ import { isPageDownShift, isEscape, isEnter, + isMinus, } from "@ui5/webcomponents-base/dist/Keys.js"; import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js"; import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js"; @@ -101,6 +102,8 @@ type StepInputValueStateChangeEventDetail = { renderer: jsxRenderer, styles: StepInputCss, template: StepInputTemplate, + languageAware: true, + cldr: true, }) /** * Fired when the input operation has finished by pressing Enter or on focusout. @@ -540,16 +543,7 @@ class StepInput extends UI5Element implements IFormInputElement { * @private */ _formatNumber(value: number): string { - if (!this.showThousandsSeparator) { - return value.toFixed(this.valuePrecision); - } - - // Use Intl.NumberFormat for locale-aware formatting - return new Intl.NumberFormat(undefined, { - minimumFractionDigits: this.valuePrecision, - maximumFractionDigits: this.valuePrecision, - useGrouping: true, - }).format(value); + return this.formatter.format(value); } /** @@ -557,13 +551,7 @@ class StepInput extends UI5Element implements IFormInputElement { * @private */ _parseNumber(formattedValue: string): number { - if (!this.showThousandsSeparator) { - return Number(formattedValue); - } - - // Remove thousands separators and parse - const cleanValue = formattedValue.replace(/[,\s]/g, ""); - return Number(cleanValue); + return this.formatter.parse(formattedValue) as number; } _incValue() { @@ -594,11 +582,12 @@ class StepInput extends UI5Element implements IFormInputElement { _onInputChange() { this._setDefaultInputValueIfNeeded(); - const inputValue = this._parseNumber(this.input.value); if (this._isValueChanged(inputValue)) { - this._updateValueAndValidate(inputValue); + this._updateValueAndValidate(Number.isNaN(inputValue) ? this.min || 0 : inputValue); } + + this.innerInput.value = this._formatNumber(this._parseNumber(this.innerInput.value)); } _setDefaultInputValueIfNeeded() { @@ -619,7 +608,8 @@ class StepInput extends UI5Element implements IFormInputElement { || this.value !== inputValue || inputValue === 0 || !isValueWithCorrectPrecision - || isPrecisionCorrectButValueStateError; + || isPrecisionCorrectButValueStateError + || Number.isNaN(inputValue); } _updateValueAndValidate(inputValue: number) { @@ -672,9 +662,38 @@ class StepInput extends UI5Element implements IFormInputElement { } else if (!isUpCtrl(e) && !isDownCtrl(e) && !isUpShift(e) && !isDownShift(e)) { preventDefault = false; } + + const cursorPosition = this.input.getDomRef()!.querySelector("input")!.selectionStart; + const inputValue = this.innerInput.value; + const typedValue = `${inputValue.substring(0, cursorPosition!)}${e.key}${inputValue.substring(cursorPosition!)}`; + + if (!this._isNavigationKey(e.key)) { + const parsedValue = this._parseNumber(typedValue); + + if (Number.isNaN(parsedValue) || /,{2,}/.test(typedValue)) { + preventDefault = true; + } + } + if (preventDefault) { e.preventDefault(); } + + if (cursorPosition === 0 && isMinus(e)) { + this._updateValueAndValidate(this._parseNumber(typedValue)); + } + } + + _isNavigationKey(key: string) { + const navigationKeys = [ + "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", + "Home", "End", "Tab", "Escape", "Enter", + "Backspace", "Delete", "Control", "Alt", "Shift", + "Meta", "CapsLock", "NumLock", "ScrollLock", + "PageUp", "PageDown", "Insert", + ]; + + return navigationKeys.includes(key) || key.startsWith("F"); } _decSpin() { From decbbda9a3e18faa15ae00366f62b9ef6ead3695 Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Wed, 5 Nov 2025 11:56:28 +0200 Subject: [PATCH 04/14] feat: format value --- packages/main/src/StepInput.ts | 5 ++++- packages/main/test/pages/StepInput.html | 12 ++++++------ .../main/StepInput/ValuePrecision/sample.html | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/main/src/StepInput.ts b/packages/main/src/StepInput.ts index d3139a168f2d..3cecba4991fd 100644 --- a/packages/main/src/StepInput.ts +++ b/packages/main/src/StepInput.ts @@ -669,7 +669,6 @@ class StepInput extends UI5Element implements IFormInputElement { if (!this._isNavigationKey(e.key)) { const parsedValue = this._parseNumber(typedValue); - if (Number.isNaN(parsedValue) || /,{2,}/.test(typedValue)) { preventDefault = true; } @@ -682,6 +681,10 @@ class StepInput extends UI5Element implements IFormInputElement { if (cursorPosition === 0 && isMinus(e)) { this._updateValueAndValidate(this._parseNumber(typedValue)); } + + if (this.type === InputType.Number) { + this.innerInput.value = this._formatNumber(this._parseNumber(this.innerInput.value)); + } } _isNavigationKey(key: string) { diff --git a/packages/main/test/pages/StepInput.html b/packages/main/test/pages/StepInput.html index b970d62e0b17..73e764250fbd 100644 --- a/packages/main/test/pages/StepInput.html +++ b/packages/main/test/pages/StepInput.html @@ -30,8 +30,8 @@ value-precision="2" show-thousands-separator> - +

StepInput

+ Event [change] :: N/A

StepInput in Cozy

@@ -41,7 +41,7 @@

StepInput in Cozy

>
- + - + diff --git a/packages/website/docs/_samples/main/StepInput/ValuePrecision/sample.html b/packages/website/docs/_samples/main/StepInput/ValuePrecision/sample.html index ca33525c7c66..c2d861e8b4ae 100644 --- a/packages/website/docs/_samples/main/StepInput/ValuePrecision/sample.html +++ b/packages/website/docs/_samples/main/StepInput/ValuePrecision/sample.html @@ -7,7 +7,7 @@ Sample -
+
From e48be5815b8b86a14b5665885e6f6357329592b3 Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Tue, 2 Dec 2025 15:08:19 +0200 Subject: [PATCH 05/14] refactor: refactor code --- packages/main/src/StepInput.ts | 59 +++++++++---------------- packages/main/test/pages/StepInput.html | 3 +- 2 files changed, 22 insertions(+), 40 deletions(-) diff --git a/packages/main/src/StepInput.ts b/packages/main/src/StepInput.ts index 3cecba4991fd..1bc77577d1eb 100644 --- a/packages/main/src/StepInput.ts +++ b/packages/main/src/StepInput.ts @@ -253,15 +253,6 @@ class StepInput extends UI5Element implements IFormInputElement { @property() accessibleNameRef?: string; - /** - * Defines whether to display thousands separator. - * @default false - * @public - * @since 2.16.0 - */ - @property({ type: Boolean }) - showThousandsSeparator = false; - @property({ noAttribute: true }) _decIconDisabled = false; @@ -344,7 +335,7 @@ class StepInput extends UI5Element implements IFormInputElement { } get type() { - return this.showThousandsSeparator ? InputType.Text : InputType.Number; + return InputType.Text; } // icons-related @@ -404,7 +395,7 @@ class StepInput extends UI5Element implements IFormInputElement { if (!this._formatter) { this._formatter = NumberFormat.getFloatInstance({ decimals: this.valuePrecision, - groupingEnabled: this.showThousandsSeparator, + groupingEnabled: true, }); } @@ -453,7 +444,7 @@ class StepInput extends UI5Element implements IFormInputElement { return; } - if (!this._isFocused) { + if (this._isFocused) { e.preventDefault(); } @@ -569,9 +560,6 @@ class StepInput extends UI5Element implements IFormInputElement { } get _isValueWithCorrectPrecision() { - if (this.showThousandsSeparator) { - return true; - } // gets either "." or "," as delimiter which is based on locale, and splits the number by it const delimiter = this.input?.value?.includes(".") ? "." : ","; const numberParts = this.input?.value?.split(delimiter); @@ -585,9 +573,8 @@ class StepInput extends UI5Element implements IFormInputElement { const inputValue = this._parseNumber(this.input.value); if (this._isValueChanged(inputValue)) { this._updateValueAndValidate(Number.isNaN(inputValue) ? this.min || 0 : inputValue); + this.innerInput.value = this.input.value; } - - this.innerInput.value = this._formatNumber(this._parseNumber(this.innerInput.value)); } _setDefaultInputValueIfNeeded() { @@ -663,15 +650,14 @@ class StepInput extends UI5Element implements IFormInputElement { preventDefault = false; } - const cursorPosition = this.input.getDomRef()!.querySelector("input")!.selectionStart; - const inputValue = this.innerInput.value; - const typedValue = `${inputValue.substring(0, cursorPosition!)}${e.key}${inputValue.substring(cursorPosition!)}`; + if(e.key && e.key.length !== 1) { + return; + } - if (!this._isNavigationKey(e.key)) { - const parsedValue = this._parseNumber(typedValue); - if (Number.isNaN(parsedValue) || /,{2,}/.test(typedValue)) { - preventDefault = true; - } + const { parsedValue: parsedValue, cursorPosition, stringValue: typedValue } = this._getValueOnkeyDown(e); + + if (Number.isNaN(parsedValue) || /,{2,}/.test(typedValue)) { + preventDefault = true; } if (preventDefault) { @@ -681,22 +667,19 @@ class StepInput extends UI5Element implements IFormInputElement { if (cursorPosition === 0 && isMinus(e)) { this._updateValueAndValidate(this._parseNumber(typedValue)); } - - if (this.type === InputType.Number) { - this.innerInput.value = this._formatNumber(this._parseNumber(this.innerInput.value)); - } } - _isNavigationKey(key: string) { - const navigationKeys = [ - "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", - "Home", "End", "Tab", "Escape", "Enter", - "Backspace", "Delete", "Control", "Alt", "Shift", - "Meta", "CapsLock", "NumLock", "ScrollLock", - "PageUp", "PageDown", "Insert", - ]; + _getValueOnkeyDown(e: KeyboardEvent) { + const cursorPosition = this.input.getDomRef()!.querySelector("input")!.selectionStart; + const inputValue = this.innerInput.value; + const typedValue = `${inputValue.substring(0, cursorPosition!)}${e.key}${inputValue.substring(cursorPosition!)}`; + const parsedValue = this._parseNumber(typedValue); - return navigationKeys.includes(key) || key.startsWith("F"); + return { + stringValue: typedValue, + parsedValue: parsedValue, + cursorPosition + } } _decSpin() { diff --git a/packages/main/test/pages/StepInput.html b/packages/main/test/pages/StepInput.html index 73e764250fbd..dbdfc413c9c2 100644 --- a/packages/main/test/pages/StepInput.html +++ b/packages/main/test/pages/StepInput.html @@ -27,8 +27,7 @@ + value-precision="2">

StepInput

Event [change] :: N/A
From e809f51bd236cb264ce0a572e3c1d3f03fd453d6 Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Wed, 3 Dec 2025 10:48:15 +0200 Subject: [PATCH 06/14] test: add test suites --- packages/main/cypress/specs/StepInput.cy.tsx | 80 +++++++++++++++++-- .../support/commands/StepInput.commands.ts | 47 ++++++++++- 2 files changed, 117 insertions(+), 10 deletions(-) diff --git a/packages/main/cypress/specs/StepInput.cy.tsx b/packages/main/cypress/specs/StepInput.cy.tsx index 7339f9d975f0..60891ad2174f 100644 --- a/packages/main/cypress/specs/StepInput.cy.tsx +++ b/packages/main/cypress/specs/StepInput.cy.tsx @@ -622,6 +622,36 @@ describe("StepInput events", () => { }); }); +describe("StepInput thousand separator formatting", () => { + it("should display value with thousand separator", () => { + cy.mount( + + ); + + cy.get("[ui5-step-input]") + .ui5StepInputGetInnerInput() + .should($input => { + const val = $input.val(); + // Accepts both comma and dot as separator depending on locale + expect(val).to.match(/12[,.]345/); + }); + }); + + it("should parse formatted value correctly", () => { + cy.mount( + + ); + + cy.get("[ui5-step-input]") + .ui5StepInputGetInnerInput() + .should($input => { + const val = $input.val() as string; + const num = Number(val.replace(/[^\d]/g, "")); + expect(num).to.equal(12345); + }); + }); +}); + describe("StepInput property propagation", () => { it("should propagate 'placeholder' property to inner input", () => { cy.mount( @@ -632,31 +662,33 @@ describe("StepInput property propagation", () => { .ui5StepInputCheckInnerInputProperty("placeholder", "Enter number"); }); - it("should propagate 'min' property to inner input", () => { + it("should not propagate 'min' property to inner input", () => { cy.mount( ); + // min should not be propogated because step input uses input with type="text" cy.get("[ui5-step-input]") - .ui5StepInputCheckInnerInputProperty("min", "0"); + .ui5StepInputCheckInnerInputProperty("min", "0", false); }); - it("should propagate 'max' property to inner input", () => { + it("should not propagate 'max' property to inner input", () => { cy.mount( ); + // min should not be propogated because step input uses input with type="text" cy.get("[ui5-step-input]") - .ui5StepInputCheckInnerInputProperty("max", "10"); + .ui5StepInputCheckInnerInputProperty("max", "10", false); }); - it("should propagate 'step' property to inner input", () => { + it("should not propagate 'step' property to inner input", () => { cy.mount( ); cy.get("[ui5-step-input]") - .ui5StepInputCheckInnerInputProperty("step", "2"); + .ui5StepInputCheckInnerInputProperty("step", "2", false); }); it("should propagate 'disabled' property to inner input", () => { @@ -685,6 +717,42 @@ describe("StepInput property propagation", () => { cy.get("[ui5-step-input]") .ui5StepInputCheckInnerInputProperty("value", "5"); }); + + it("should increase value on mouse wheel up", () => { + cy.mount( + + ); + + cy.get("[ui5-step-input]") + .as("stepInput"); + + cy.get("@stepInput") + .ui5StepInputScrollToChangeValue(7, false); + }); + + it("should decrease value on mouse wheel down", () => { + cy.mount( + + ); + + cy.get("[ui5-step-input]") + .as("stepInput"); + + cy.get("@stepInput") + .ui5StepInputScrollToChangeValue(3, true); + }); + + it("should not change value when readonly", () => { + cy.mount( + + ); + + cy.get("[ui5-step-input]") + .as("stepInput"); + + cy.get("@stepInput") + .ui5StepInputScrollToChangeValue(5, true); + }); }); describe("Validation inside form", () => { diff --git a/packages/main/cypress/support/commands/StepInput.commands.ts b/packages/main/cypress/support/commands/StepInput.commands.ts index 264e5a773893..58ba36d8a59c 100644 --- a/packages/main/cypress/support/commands/StepInput.commands.ts +++ b/packages/main/cypress/support/commands/StepInput.commands.ts @@ -44,7 +44,7 @@ Cypress.Commands.add("ui5StepInputAttachHandler", { prevSubject: true }, (subje }); }); -Cypress.Commands.add("ui5StepInputCheckInnerInputProperty", { prevSubject: true }, (subject, propName: string, expectedValue: any) => { +Cypress.Commands.add("ui5StepInputGetInnerInput", { prevSubject: true }, (subject) => { cy.wrap(subject) .as("stepInput") .should("be.visible"); @@ -56,8 +56,16 @@ Cypress.Commands.add("ui5StepInputCheckInnerInputProperty", { prevSubject: true .find("input") .as("innerInput"); - cy.get("@innerInput") - .should("have.prop", propName, expectedValue); + return cy.get("@innerInput"); +}); + +Cypress.Commands.add("ui5StepInputCheckInnerInputProperty", { prevSubject: true }, (subject, propName: string, expectedValue: any, shouldBePropagated: boolean = true) => { + cy.get(subject) + .ui5StepInputGetInnerInput() + .then($innerInput => { + const condition = shouldBePropagated ? "have.prop" : "not.have.prop"; + cy.wrap($innerInput).should(condition, propName, expectedValue); + }); }); Cypress.Commands.add("ui5StepInputTypeNumber", { prevSubject: true }, (subject, value: number) => { @@ -75,14 +83,45 @@ Cypress.Commands.add("ui5StepInputTypeNumber", { prevSubject: true }, (subject, .realPress("Enter"); }); +Cypress.Commands.add("ui5StepInputScrollToChangeValue", { prevSubject: true }, (subject, expectedValue: number, decreaseValue: boolean) => { + const deltaY = decreaseValue ? 100 : -100; + + cy.wrap(subject) + .as("stepInput") + .should("be.visible"); + + cy.get("@stepInput") + .realClick(); + + cy.get("@stepInput") + .should("be.focused"); + + cy.get("@stepInput") + .shadow() + .find(".ui5-step-input-root") + .then($el => { + const wheelEvent = new WheelEvent("wheel", { deltaY, bubbles: true, cancelable: true }); + $el[0].dispatchEvent(wheelEvent); + }); + + cy.realPress("Tab"); // To trigger change event + + cy.get("@stepInput") + .should("have.prop", "value", expectedValue); +}); + + + declare global { namespace Cypress { interface Chainable { ui5StepInputChangeValueWithArrowKeys(expectedValue: number, decreaseValue?: boolean): Chainable ui5StepInputChangeValueWithButtons(expectedValue: number, decreaseValue?: boolean): Chainable ui5StepInputAttachHandler(eventName: string, stubName: string): Chainable - ui5StepInputCheckInnerInputProperty(propName: string, expectedValue: any): Chainable + ui5StepInputGetInnerInput(): Chainable> + ui5StepInputCheckInnerInputProperty(propName: string, expectedValue: any, shouldBePropagated?: boolean): Chainable ui5StepInputTypeNumber(value: number): Chainable + ui5StepInputScrollToChangeValue(expectedValue: number, decreaseValue: boolean): Chainable } } } \ No newline at end of file From 9a9b678d72836994a6797ebf5a6730e7676e1ea8 Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Wed, 3 Dec 2025 10:56:01 +0200 Subject: [PATCH 07/14] refactor: enhance sample --- packages/main/test/pages/StepInput.html | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/main/test/pages/StepInput.html b/packages/main/test/pages/StepInput.html index dbdfc413c9c2..9b21b32b736f 100644 --- a/packages/main/test/pages/StepInput.html +++ b/packages/main/test/pages/StepInput.html @@ -25,10 +25,6 @@ - -

StepInput

Event [change] :: N/A
@@ -171,6 +167,14 @@

'input' event prevented

'change' event result

+
+

StepInput with large value and precision (thousands separator)

+ + +

Form validation

From 7a31a1fdac958db2bf15316bc0a80ab29f40e4fa Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Wed, 3 Dec 2025 11:04:21 +0200 Subject: [PATCH 08/14] fix: lint errors --- packages/main/src/StepInput.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/main/src/StepInput.ts b/packages/main/src/StepInput.ts index 1bc77577d1eb..d28bb102f2cf 100644 --- a/packages/main/src/StepInput.ts +++ b/packages/main/src/StepInput.ts @@ -650,11 +650,11 @@ class StepInput extends UI5Element implements IFormInputElement { preventDefault = false; } - if(e.key && e.key.length !== 1) { + if (e.key && e.key.length !== 1) { return; } - const { parsedValue: parsedValue, cursorPosition, stringValue: typedValue } = this._getValueOnkeyDown(e); + const { parsedValue, cursorPosition, stringValue: typedValue } = this._getValueOnkeyDown(e); if (Number.isNaN(parsedValue) || /,{2,}/.test(typedValue)) { preventDefault = true; @@ -672,14 +672,14 @@ class StepInput extends UI5Element implements IFormInputElement { _getValueOnkeyDown(e: KeyboardEvent) { const cursorPosition = this.input.getDomRef()!.querySelector("input")!.selectionStart; const inputValue = this.innerInput.value; - const typedValue = `${inputValue.substring(0, cursorPosition!)}${e.key}${inputValue.substring(cursorPosition!)}`; - const parsedValue = this._parseNumber(typedValue); + const stringValue = `${inputValue.substring(0, cursorPosition!)}${e.key}${inputValue.substring(cursorPosition!)}`; + const parsedValue = this._parseNumber(stringValue); return { - stringValue: typedValue, - parsedValue: parsedValue, - cursorPosition - } + stringValue, + parsedValue, + cursorPosition, + }; } _decSpin() { From c0b0dead9d8334729eb9aa9e64c689f2f08e98aa Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Tue, 9 Dec 2025 17:04:56 +0200 Subject: [PATCH 09/14] refactor: address review comments --- packages/main/src/StepInput.ts | 42 +++++++++++++------------ packages/main/test/pages/StepInput.html | 2 +- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/packages/main/src/StepInput.ts b/packages/main/src/StepInput.ts index d28bb102f2cf..c91763287170 100644 --- a/packages/main/src/StepInput.ts +++ b/packages/main/src/StepInput.ts @@ -98,13 +98,14 @@ type StepInputValueStateChangeEventDetail = { */ @customElement({ tag: "ui5-step-input", + cldr: true, formAssociated: true, renderer: jsxRenderer, styles: StepInputCss, template: StepInputTemplate, languageAware: true, - cldr: true, }) + /** * Fired when the input operation has finished by pressing Enter or on focusout. * @public @@ -395,7 +396,7 @@ class StepInput extends UI5Element implements IFormInputElement { if (!this._formatter) { this._formatter = NumberFormat.getFloatInstance({ decimals: this.valuePrecision, - groupingEnabled: true, + groupingEnabled: true }); } @@ -561,7 +562,8 @@ class StepInput extends UI5Element implements IFormInputElement { get _isValueWithCorrectPrecision() { // gets either "." or "," as delimiter which is based on locale, and splits the number by it - const delimiter = this.input?.value?.includes(".") ? "." : ","; + // @ts-ignore oFormatOptions is a private API of NumberFormat but we need it here to get the decimal separator + const delimiter = this.formatter?.oFormatOptions?.decimalSeparator || "."; const numberParts = this.input?.value?.split(delimiter); const decimalPartLength = numberParts?.length > 1 ? numberParts[1].length : 0; @@ -654,32 +656,32 @@ class StepInput extends UI5Element implements IFormInputElement { return; } - const { parsedValue, cursorPosition, stringValue: typedValue } = this._getValueOnkeyDown(e); - - if (Number.isNaN(parsedValue) || /,{2,}/.test(typedValue)) { - preventDefault = true; - } + const cursorPosition = this._getCursorPosition(); + const inputValue = this.innerInput.value; + const typedValue = this._getValueOnkeyDown(e, inputValue, cursorPosition!); + const parsedValue = this._parseNumber(typedValue); + const isValidTypedValue = this._isTypedValueValid(typedValue, parsedValue); - if (preventDefault) { + if (preventDefault || !isValidTypedValue) { e.preventDefault(); + return; } if (cursorPosition === 0 && isMinus(e)) { - this._updateValueAndValidate(this._parseNumber(typedValue)); + this._updateValueAndValidate(parsedValue); } } - _getValueOnkeyDown(e: KeyboardEvent) { - const cursorPosition = this.input.getDomRef()!.querySelector("input")!.selectionStart; - const inputValue = this.innerInput.value; - const stringValue = `${inputValue.substring(0, cursorPosition!)}${e.key}${inputValue.substring(cursorPosition!)}`; - const parsedValue = this._parseNumber(stringValue); + _getCursorPosition() { + return this.input.getDomRef()!.querySelector("input")!.selectionStart; + } - return { - stringValue, - parsedValue, - cursorPosition, - }; + _getValueOnkeyDown(e: KeyboardEvent,inputValue: string ,cursorPosition?: number) { + return `${inputValue.substring(0, cursorPosition!)}${e.key}${inputValue.substring(cursorPosition!)}`; + } + + _isTypedValueValid(typedValue: string, parsedValue: number) { + return !Number.isNaN(parsedValue) && !/, {2,}/.test(typedValue); } _decSpin() { diff --git a/packages/main/test/pages/StepInput.html b/packages/main/test/pages/StepInput.html index 9b21b32b736f..949924ad1151 100644 --- a/packages/main/test/pages/StepInput.html +++ b/packages/main/test/pages/StepInput.html @@ -184,7 +184,7 @@

Form validation

min="5" max="10" step="0.05" - value="6" + value="0" value-precision="2" required id="formStepInput"> From c74eaf88c635416f1e75c42c7c24f9ecb7923b08 Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Wed, 10 Dec 2025 11:27:58 +0200 Subject: [PATCH 10/14] refactor: address review comments --- packages/main/src/StepInput.ts | 10 ++++------ packages/main/test/pages/StepInput.html | 1 - 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/main/src/StepInput.ts b/packages/main/src/StepInput.ts index c91763287170..27e1a139d01e 100644 --- a/packages/main/src/StepInput.ts +++ b/packages/main/src/StepInput.ts @@ -105,7 +105,6 @@ type StepInputValueStateChangeEventDetail = { template: StepInputTemplate, languageAware: true, }) - /** * Fired when the input operation has finished by pressing Enter or on focusout. * @public @@ -396,7 +395,7 @@ class StepInput extends UI5Element implements IFormInputElement { if (!this._formatter) { this._formatter = NumberFormat.getFloatInstance({ decimals: this.valuePrecision, - groupingEnabled: true + groupingEnabled: true, }); } @@ -564,7 +563,7 @@ class StepInput extends UI5Element implements IFormInputElement { // gets either "." or "," as delimiter which is based on locale, and splits the number by it // @ts-ignore oFormatOptions is a private API of NumberFormat but we need it here to get the decimal separator const delimiter = this.formatter?.oFormatOptions?.decimalSeparator || "."; - const numberParts = this.input?.value?.split(delimiter); + const numberParts = this.input?.value?.split(delimiter as string); const decimalPartLength = numberParts?.length > 1 ? numberParts[1].length : 0; return decimalPartLength === this.valuePrecision; @@ -664,7 +663,6 @@ class StepInput extends UI5Element implements IFormInputElement { if (preventDefault || !isValidTypedValue) { e.preventDefault(); - return; } if (cursorPosition === 0 && isMinus(e)) { @@ -676,8 +674,8 @@ class StepInput extends UI5Element implements IFormInputElement { return this.input.getDomRef()!.querySelector("input")!.selectionStart; } - _getValueOnkeyDown(e: KeyboardEvent,inputValue: string ,cursorPosition?: number) { - return `${inputValue.substring(0, cursorPosition!)}${e.key}${inputValue.substring(cursorPosition!)}`; + _getValueOnkeyDown(e: KeyboardEvent, inputValue: string, cursorPosition?: number) { + return `${inputValue.substring(0, cursorPosition)}${e.key}${inputValue.substring(cursorPosition!)}`; } _isTypedValueValid(typedValue: string, parsedValue: number) { diff --git a/packages/main/test/pages/StepInput.html b/packages/main/test/pages/StepInput.html index 949924ad1151..9e148722545a 100644 --- a/packages/main/test/pages/StepInput.html +++ b/packages/main/test/pages/StepInput.html @@ -184,7 +184,6 @@

Form validation

min="5" max="10" step="0.05" - value="0" value-precision="2" required id="formStepInput"> From bab220d7b5a872d80107f8f60e8bfa53c131fb24 Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Wed, 10 Dec 2025 14:09:49 +0200 Subject: [PATCH 11/14] fix: remove grouping in NumberFormatter --- packages/main/src/StepInput.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/main/src/StepInput.ts b/packages/main/src/StepInput.ts index 27e1a139d01e..f639aa65d4e4 100644 --- a/packages/main/src/StepInput.ts +++ b/packages/main/src/StepInput.ts @@ -394,8 +394,7 @@ class StepInput extends UI5Element implements IFormInputElement { get formatter(): NumberFormat { if (!this._formatter) { this._formatter = NumberFormat.getFloatInstance({ - decimals: this.valuePrecision, - groupingEnabled: true, + decimals: this.valuePrecision }); } From 3ef8e156d0bb5dbced5e0dd65192f00a2850ec91 Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Wed, 10 Dec 2025 14:46:01 +0200 Subject: [PATCH 12/14] fix: format display value only when it is valid --- packages/main/src/StepInput.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/main/src/StepInput.ts b/packages/main/src/StepInput.ts index f639aa65d4e4..a66cb0f7599d 100644 --- a/packages/main/src/StepInput.ts +++ b/packages/main/src/StepInput.ts @@ -361,15 +361,16 @@ class StepInput extends UI5Element implements IFormInputElement { } get _displayValue() { + const value = this.input?.value && !this._isValueWithCorrectPrecision ? this.input.value : this._formatNumber(this.value); if ((this.value === 0) || (Number.isInteger(this.value))) { - return this._formatNumber(this.value); + return value } if (this.input && this.value === Number(this.input.value)) { // For the cases where the number is fractional and is ending with 0s. return this.input.value; } - return this._formatNumber(this.value); + return value; } get accInfo(): InputAccInfo { From 0fe46976e5f54ee27a70be2481186dcec906585b Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Wed, 10 Dec 2025 14:53:29 +0200 Subject: [PATCH 13/14] chore: add comment in code --- packages/main/src/StepInput.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/main/src/StepInput.ts b/packages/main/src/StepInput.ts index a66cb0f7599d..7b4d68472d59 100644 --- a/packages/main/src/StepInput.ts +++ b/packages/main/src/StepInput.ts @@ -361,6 +361,7 @@ class StepInput extends UI5Element implements IFormInputElement { } get _displayValue() { + // For the cases when there is set value precision but the input value is not with correct precision we don't need to format it const value = this.input?.value && !this._isValueWithCorrectPrecision ? this.input.value : this._formatNumber(this.value); if ((this.value === 0) || (Number.isInteger(this.value))) { return value From a9918e816ee14af2dd95f4e413010341ef5e83e1 Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Fri, 12 Dec 2025 14:52:14 +0200 Subject: [PATCH 14/14] fix: lint errors --- packages/main/src/StepInput.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/main/src/StepInput.ts b/packages/main/src/StepInput.ts index b228312b495c..1c38297ebf01 100644 --- a/packages/main/src/StepInput.ts +++ b/packages/main/src/StepInput.ts @@ -364,7 +364,7 @@ class StepInput extends UI5Element implements IFormInputElement { // For the cases when there is set value precision but the input value is not with correct precision we don't need to format it const value = this.input?.value && !this._isValueWithCorrectPrecision ? this.input.value : this._formatNumber(this.value); if ((this.value === 0) || (Number.isInteger(this.value))) { - return value + return value; } if (this.input && this.value === Number(this.input.value)) { // For the cases where the number is fractional and is ending with 0s. @@ -396,7 +396,7 @@ class StepInput extends UI5Element implements IFormInputElement { get formatter(): NumberFormat { if (!this._formatter) { this._formatter = NumberFormat.getFloatInstance({ - decimals: this.valuePrecision + decimals: this.valuePrecision, }); }