diff --git a/packages/main/cypress/specs/DynamicDateRange.cy.tsx b/packages/main/cypress/specs/DynamicDateRange.cy.tsx index 2d10e919ab014..93beaf69560ba 100644 --- a/packages/main/cypress/specs/DynamicDateRange.cy.tsx +++ b/packages/main/cypress/specs/DynamicDateRange.cy.tsx @@ -311,6 +311,8 @@ describe("DynamicDateRange Last/Next Options", () => { .should("exist"); cy.get("@stepInput") + .shadow() + .find("[ui5-number-input]") .shadow() .find("[ui5-input]") .shadow() @@ -361,6 +363,8 @@ describe("DynamicDateRange Last/Next Options", () => { .as("stepInput"); cy.get("@stepInput") + .shadow() + .find("[ui5-number-input]") .shadow() .find("[ui5-input]") .shadow() diff --git a/packages/main/cypress/specs/NumberInput.cy.tsx b/packages/main/cypress/specs/NumberInput.cy.tsx new file mode 100644 index 0000000000000..346c1fa67464e --- /dev/null +++ b/packages/main/cypress/specs/NumberInput.cy.tsx @@ -0,0 +1,876 @@ +import NumberInput from "../../src/NumberInput.js"; +import Label from "../../src/Label.js"; +import { setLanguage } from "@ui5/webcomponents-base/dist/config/Language.js"; +import "../../src/Assets.js"; + +const decreaseValue = true; + +describe("NumberInput keyboard interaction tests", () => { + it("should increase the value with 'ArrowUp' only if it is less than 'max'", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .realClick(); + + cy.get("@numberInput") + .ui5NumberInputChangeValueWithArrowKeys(5); + + cy.get("@numberInput") + .ui5NumberInputChangeValueWithArrowKeys(5); + }); + + it("should decrease the value with 'ArrowDown' only if it is more than 'min'", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .realClick(); + + cy.get("@numberInput") + .ui5NumberInputChangeValueWithArrowKeys(5, decreaseValue); + + cy.get("@numberInput") + .ui5NumberInputChangeValueWithArrowKeys(5, decreaseValue); + }); + + it("should set the value to the 'max' with 'Shift+PageUp'", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .realClick() + .should("be.focused"); + + cy.realPress(['Shift', 'PageUp']); + + cy.get("@numberInput") + .should("have.prop", "value", 5); + }); + + it("should set the value to the 'min' with 'Shift+PageDown'", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .realClick() + .should("be.focused"); + + cy.realPress(['Shift', 'PageDown']); + + cy.get("@numberInput") + .should("have.prop", "value", 0); + }); + + it("should set the value to the 'max' with 'Ctrl+Shift+ArrowUp'", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .realClick() + .should("be.focused"); + + cy.realPress(['Control', 'Shift', 'ArrowUp']); + + cy.get("@numberInput") + .should("have.prop", "value", 5); + }); + + it("should set the value to the 'min' with 'Ctrl+Shift+ArrowDown'", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .realClick() + .should("be.focused"); + + cy.realPress(['Control', 'Shift', 'ArrowDown']); + + cy.get("@numberInput") + .should("have.prop", "value", 0); + }); + + it("should restore the previous value with 'Escape'", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .realClick(); + + cy.get("@numberInput") + .ui5NumberInputChangeValueWithArrowKeys(6); + + cy.realPress("Escape"); + + cy.get("@numberInput") + .should("have.prop", "value", 5); + }); + + it("should update the value when typed in input", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .realClick({ "clickCount": 2 }) + .should("be.focused"); + + cy.realType("23"); + cy.realPress("Enter"); + + cy.get("@numberInput") + .should("have.prop", "value", 23); + }); + + it("should reset the value to 0 if input is deleted", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .realClick({ "clickCount": 2 }) + .should("be.focused"); + + cy.realPress("Backspace"); + cy.realPress("Enter"); + + cy.get("@numberInput") + .should("have.prop", "value", 0); + }); +}); + +describe("NumberInput misc interaction tests", () => { + it("should not round value when 'valuePrecision' is set", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .realClick() + .should("be.focused"); + + cy.get("@numberInput") + .ui5NumberInputChangeValueWithArrowKeys(30.999); + }); + + it("should round value when 'valuePrecision' is set to default", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .realClick() + .should("be.focused"); + + cy.get("@numberInput") + .ui5NumberInputChangeValueWithArrowKeys(31); + }); + + it("should set 'valueState' to 'Negative' when the value is not compliant", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .realClick({ "clickCount": 2 }) + .should("be.focused"); + + cy.realType("23.034"); + + cy.realPress("Enter"); + + cy.get("@numberInput") + .should("have.prop", "valueState", "Negative"); + }); +}); + +describe("NumberInput events", () => { + it("should not change value state when 'value-state-change' event is prevented", () => { + const valueState = "Positive"; + + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .then($input => { + $input.get(0).addEventListener("value-state-change", e => { + e.preventDefault(); + }); + }); + + cy.get("@numberInput") + .ui5NumberInputAttachHandler("value-state-change", "stateChange"); + + cy.get("@numberInput") + .realClick(); + + cy.get("@numberInput") + .ui5NumberInputChangeValueWithArrowKeys(1); + + cy.get("@stateChange") + .should("have.been.calledOnce"); + + cy.get("@numberInput") + .should("have.prop", "valueState", valueState); + }); + + it("should prevent input event", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .then($input => { + $input.get(0).addEventListener("input", e => { + e.preventDefault(); + (e.target as NumberInput).value = 30; + }); + }); + + cy.get("@numberInput") + .realClick() + .should("be.focused"); + + cy.realPress("1"); + + cy.get("@numberInput") + .ui5NumberInputCheckInnerInputProperty("value", "30"); + }); + + it("should not fire 'change' when navigating with 'ArrowUp'/'ArrowDown' keys", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .ui5NumberInputAttachHandler("ui5-change", "change"); + + cy.get("@numberInput") + .realClick(); + + cy.get("@numberInput") + .ui5NumberInputChangeValueWithArrowKeys(1); + + cy.get("@change") + .should("have.not.been.called"); + + cy.get("@numberInput") + .ui5NumberInputChangeValueWithArrowKeys(0, decreaseValue); + + cy.get("@change") + .should("have.not.been.called"); + }); + + it("should fire 'change' after 'Enter' is pressed", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .ui5NumberInputAttachHandler("ui5-change", "change"); + + cy.get("@numberInput") + .realClick(); + + cy.get("@numberInput") + .ui5NumberInputChangeValueWithArrowKeys(1); + + cy.realPress("Enter"); + + cy.get("@change") + .should("have.been.calledOnce"); + }); + + it("should not fire 'change' when previous value is restored with 'Escape'", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .ui5NumberInputAttachHandler("ui5-change", "change"); + + cy.get("@numberInput") + .realClick(); + + cy.get("@numberInput") + .ui5NumberInputChangeValueWithArrowKeys(1); + + cy.realPress("Escape"); + + cy.get("@numberInput") + .should("have.prop", "value", 0); + + cy.get("@change") + .should("not.have.been.called"); + }); + + it("should fire 'change' after focus out", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .ui5NumberInputAttachHandler("ui5-change", "change"); + + cy.get("@numberInput") + .realClick(); + + cy.get("@numberInput") + .ui5NumberInputChangeValueWithArrowKeys(1); + + cy.realPress("Tab"); + + cy.get("@change") + .should("have.been.calledOnce"); + + cy.get("@numberInput") + .should("have.prop", "value", 1); + }); + + it("should fire 'change' when 'Enter' is pressed after manual input", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .ui5NumberInputAttachHandler("ui5-change", "change"); + + cy.get("@numberInput") + .realClick({ "clickCount": 2 }) + .should("be.focused"); + + cy.realType("23"); + + cy.get("@change") + .should("not.have.been.called"); + + cy.realPress("Enter"); + + cy.get("@numberInput") + .should("have.prop", "value", 23); + + cy.get("@change") + .should("have.been.calledOnce"); + }); + + it("should fire 'change' after focusing out of input", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .ui5NumberInputAttachHandler("ui5-change", "change"); + + cy.get("@numberInput") + .realClick({ "clickCount": 2 }) + .should("be.focused"); + + cy.realType("23"); + + cy.get("@change") + .should("not.have.been.called"); + + cy.realPress("Tab"); + + cy.get("@numberInput") + .should("have.prop", "value", 23); + + cy.get("@change") + .should("have.been.calledOnce"); + }); + + it("should fire 'change' after input is deleted and focused out", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .ui5NumberInputAttachHandler("ui5-change", "change"); + + cy.get("@numberInput") + .realClick({ "clickCount": 2 }) + .should("be.focused"); + + cy.realPress("Backspace"); + + cy.get("@change") + .should("not.have.been.called"); + + cy.realPress("Tab"); + + cy.get("@numberInput") + .should("have.prop", "value", 0); + + cy.get("@change") + .should("have.been.calledOnce"); + }); + + it("should fire 'change' after value property is programmatically set and then changed with arrow keys", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .ui5NumberInputAttachHandler("ui5-change", "change"); + + cy.get("@numberInput") + .invoke("prop", "value", 4); + + cy.get("@change") + .should("not.have.been.called"); + + cy.get("@numberInput") + .realClick() + .should("be.focused"); + + cy.get("@numberInput") + .ui5NumberInputChangeValueWithArrowKeys(5); + + cy.realPress("Enter"); + + cy.get("@change") + .should("have.been.calledOnce"); + }); +}); + +describe("NumberInput thousand separator formatting", () => { + it("should display value with thousand separator", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .ui5NumberInputGetInnerInput() + .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-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .ui5NumberInputGetInnerInput() + .should($input => { + const val = $input.val() as string; + const num = Number(val.replace(/[^\d]/g, "")); + expect(num).to.equal(12345); + }); + + cy.get("@numberInput") + .realClick({ "clickCount": 2 }) + .should("be.focused"); + + cy.realType("1,0000"); + cy.realPress("Enter"); + + cy.get("@numberInput") + .ui5NumberInputGetInnerInput() + .should($input => { + const val = $input.val() as string; + expect(val).to.equal("10,000"); + }); + + cy.get("@numberInput") + .should("have.prop", "value", 10000); + }); + + it("should update input value when language is changed", () => { + cy.wrap({ setLanguage }) + .then(async ({ setLanguage }) => { + await setLanguage("en"); + }); + + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .ui5NumberInputGetInnerInput() + .should($input => { + const val = $input.val() as string; + expect(val).to.equal("10,000.56"); + }); + + cy.wrap({ setLanguage }) + .then(async ({ setLanguage }) => { + await setLanguage("de"); + }) + .then(() => { + cy.get("@numberInput") + .ui5NumberInputGetInnerInput() + .should($input => { + const val = $input.val() as string; + expect(val).to.equal("10.000,56"); + }); + }); + + cy.wrap({ setLanguage }) + .then(async ({ setLanguage }) => { + await setLanguage("en"); + }); + }); +}); + +describe("NumberInput property propagation", () => { + it("should propagate 'placeholder' property to inner input", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .ui5NumberInputCheckInnerInputProperty("placeholder", "Enter number"); + }); + + it("should not propagate 'min' property to inner input", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .ui5NumberInputCheckInnerInputProperty("min", "0", false); + }); + + it("should not propagate 'max' property to inner input", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .ui5NumberInputCheckInnerInputProperty("max", "10", false); + }); + + it("should not propagate 'step' property to inner input", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .ui5NumberInputCheckInnerInputProperty("step", "2", false); + }); + + it("should propagate 'disabled' property to inner input", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .ui5NumberInputCheckInnerInputProperty("disabled", true); + }); + + it("should propagate 'readonly' property to inner input", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .ui5NumberInputCheckInnerInputProperty("readonly", true); + }); + + it("should propagate 'value' property to inner input", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .ui5NumberInputCheckInnerInputProperty("value", "5"); + }); + + it("should increase value on mouse wheel up", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .ui5NumberInputScrollToChangeValue(7, false); + }); + + it("should decrease value on mouse wheel down", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .ui5NumberInputScrollToChangeValue(3, true); + }); + + it("should not change value when readonly", () => { + cy.mount( + + ); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .ui5NumberInputScrollToChangeValue(5, true); + }); +}); + +describe("Validation inside form", () => { + it("has correct validity for patternMissmatch", () => { + cy.mount( +
+ + +
+ ); + + cy.get("form") + .then($item => { + $item.get(0).addEventListener("submit", (e) => e.preventDefault()); + $item.get(0).addEventListener("submit", cy.stub().as("submit")); + }); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .ui5NumberInputTypeNumber(2.34); + + cy.get("#submitBtn") + .realClick(); + + cy.get("@submit") + .should("have.not.been.called"); + + cy.get("@numberInput") + .ui5AssertValidityState({ + formValidity: { patternMismatch: true }, + validity: { patternMismatch: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#numberInput:invalid") + .should("exist", "NumberInput without formatted value should have :invalid CSS class"); + + cy.get("@numberInput") + .ui5NumberInputTypeNumber(2.345); + + cy.get("@numberInput") + .ui5AssertValidityState({ + formValidity: { patternMismatch: false }, + validity: { patternMismatch: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#numberInput:invalid") + .should("not.exist", "NumberInput with formatted value should not have :invalid CSS class"); + }); + + it("has correct validity for rangeUnderflow", () => { + cy.mount( +
+ + +
+ ); + + cy.get("form") + .then($item => { + $item.get(0).addEventListener("submit", (e) => e.preventDefault()); + $item.get(0).addEventListener("submit", cy.stub().as("submit")); + }); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .ui5NumberInputTypeNumber(2); + + cy.get("#submitBtn") + .realClick(); + + cy.get("@submit") + .should("have.not.been.called"); + + cy.get("@numberInput") + .ui5AssertValidityState({ + formValidity: { rangeUnderflow: true }, + validity: { rangeUnderflow: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#numberInput:invalid") + .should("exist", "NumberInput with value lower than min should have :invalid CSS class"); + + cy.get("@numberInput") + .ui5NumberInputTypeNumber(4); + + cy.get("@numberInput") + .ui5AssertValidityState({ + formValidity: { rangeUnderflow: false }, + validity: { rangeUnderflow: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#numberInput:invalid") + .should("not.exist", "NumberInput with value higher than min should not have :invalid CSS class"); + }); + + it("has correct validity for rangeOverflow", () => { + cy.mount( +
+ + +
+ ); + + cy.get("form") + .then($item => { + $item.get(0).addEventListener("submit", (e) => e.preventDefault()); + $item.get(0).addEventListener("submit", cy.stub().as("submit")); + }); + + cy.get("[ui5-number-input]") + .as("numberInput"); + + cy.get("@numberInput") + .ui5NumberInputTypeNumber(4); + + cy.get("#submitBtn") + .realClick(); + + cy.get("@submit") + .should("have.not.been.called"); + + cy.get("@numberInput") + .ui5AssertValidityState({ + formValidity: { rangeOverflow: true }, + validity: { rangeOverflow: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#numberInput:invalid") + .should("exist", "NumberInput with value above max should have :invalid CSS class"); + + cy.get("@numberInput") + .ui5NumberInputTypeNumber(2); + + cy.get("@numberInput") + .ui5AssertValidityState({ + formValidity: { rangeOverflow: false }, + validity: { rangeOverflow: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#numberInput:invalid") + .should("not.exist", "NumberInput with value lower than max should not have :invalid CSS class"); + }); +}); + +describe("Accessibility", () => { + it("should have correct aria-label when associated with a label via 'for' attribute", () => { + const labelText = "Quantity"; + + cy.mount( + <> + + + + ); + + cy.get("[ui5-number-input]") + .shadow() + .find("[ui5-input]") + .shadow() + .find("input") + .should("have.attr", "aria-label", labelText); + }); +}); diff --git a/packages/main/cypress/specs/StepInput.cy.tsx b/packages/main/cypress/specs/StepInput.cy.tsx index d589a5a2624bc..11fea79863fbd 100644 --- a/packages/main/cypress/specs/StepInput.cy.tsx +++ b/packages/main/cypress/specs/StepInput.cy.tsx @@ -1,178 +1,7 @@ import StepInput from "../../src/StepInput.js"; -import Label from "../../src/Label.js"; -import { setLanguage } from "@ui5/webcomponents-base/dist/config/Language.js"; -import "../../src/Assets.js"; const decreaseValue = true; -describe("StepInput keyboard interaction tests", () => { - it("should increase the value with 'ArrowUp' only if it is less than 'max'", () => { - cy.mount( - - ); - - cy.get("[ui5-step-input]") - .as("stepInput"); - - cy.get("@stepInput") - .realClick(); - - cy.get("@stepInput") - .ui5StepInputChangeValueWithArrowKeys(5); - - cy.get("@stepInput") - .ui5StepInputChangeValueWithArrowKeys(5); - }); - - it("should decreases the value with 'ArrowDown' only if it is more than 'min'", () => { - cy.mount( - - ); - - cy.get("[ui5-step-input]") - .as("stepInput"); - - cy.get("@stepInput") - .realClick(); - - cy.get("@stepInput") - .ui5StepInputChangeValueWithArrowKeys(5, decreaseValue); - - cy.get("@stepInput") - .ui5StepInputChangeValueWithArrowKeys(5, decreaseValue); - }); - - it("should set the value to the 'max' with 'Shift+PageUp'", () => { - cy.mount( - - ); - - cy.get("[ui5-step-input]") - .as("stepInput"); - - cy.get("@stepInput") - .realClick() - .should("be.focused"); - - cy.realPress(['Shift', 'PageUp']); - - cy.get("@stepInput") - .should("have.prop", "value", 5); - }); - - it("should set the value to the 'min' with 'Shift+PageDown'", () => { - cy.mount( - - ); - - cy.get("[ui5-step-input]") - .as("stepInput"); - - cy.get("@stepInput") - .realClick() - .should("be.focused"); - - cy.realPress(['Shift', 'PageDown']); - - cy.get("@stepInput") - .should("have.prop", "value", 0); - }); - - it("should set the value to the 'max' with 'Ctrl+Shift+ArrowUp'", () => { - cy.mount( - - ); - - cy.get("[ui5-step-input]") - .as("stepInput"); - - cy.get("@stepInput") - .realClick() - .should("be.focused"); - - cy.realPress(['Control', 'Shift', 'ArrowUp']); - - cy.get("@stepInput") - .should("have.prop", "value", 5); - }); - - it("should set the value to the 'min' with 'Ctrl+Shift+ArrowDown'", () => { - cy.mount( - - ); - - cy.get("[ui5-step-input]") - .as("stepInput"); - - cy.get("@stepInput") - .realClick() - .should("be.focused"); - - cy.realPress(['Control', 'Shift', 'ArrowDown']); - - cy.get("@stepInput") - .should("have.prop", "value", 0); - }); - - it("should restore the previous value with 'Escape'", () => { - cy.mount( - - ); - - cy.get("[ui5-step-input]") - .as("stepInput"); - - cy.get("@stepInput") - .realClick(); - - cy.get("@stepInput") - .ui5StepInputChangeValueWithArrowKeys(6); - - cy.realPress("Escape"); - - cy.get("@stepInput") - .should("have.prop", "value", 5); - }); - - it("should update the value when typed in input", () => { - cy.mount( - - ); - - cy.get("[ui5-step-input]") - .as("stepInput"); - - cy.get("@stepInput") - .realClick({ "clickCount": 2 }) - .should("be.focused"); - - cy.realType("23"); - cy.realPress("Enter"); - - cy.get("@stepInput") - .should("have.prop", "value", 23); - }); - - it("should reset the value to 0 if input is deleted", () => { - cy.mount( - - ); - - cy.get("[ui5-step-input]") - .as("stepInput"); - - cy.get("@stepInput") - .realClick({ "clickCount": 2 }) - .should("be.focused"); - - cy.realPress("Backspace"); - cy.realPress("Enter"); - - cy.get("@stepInput") - .should("have.prop", "value", 0); - }); -}); - describe("StepInput button interaction tests", () => { it("should increase the value by clicking the 'Increase' button only if it is less than 'max'", () => { cy.mount( @@ -203,34 +32,8 @@ describe("StepInput button interaction tests", () => { cy.get("@stepInput") .ui5StepInputChangeValueWithButtons(0, decreaseValue) }); -}); - -describe("StepInput misc interaction tests", () => { - it("should not round value when 'valuePrecision' is set", () => { - cy.mount( - - ); - - cy.get("[ui5-step-input]") - .as("stepInput"); - - cy.get("@stepInput") - .ui5StepInputChangeValueWithButtons(30.999); - }); - it("should round value when 'valuePrecision' is set to default", () => { - cy.mount( - - ); - - cy.get("[ui5-step-input]") - .as("stepInput"); - - cy.get("@stepInput") - .ui5StepInputChangeValueWithButtons(31); - }); - - it("should set 'valueState' to 'Negative' when the value is not compliant", () => { + it("should fire 'change' when using 'Increase' button", () => { cy.mount( ); @@ -239,81 +42,56 @@ describe("StepInput misc interaction tests", () => { .as("stepInput"); cy.get("@stepInput") - .realClick({ "clickCount": 2 }) - .should("be.focused"); - - cy.realType("23.034"); - - cy.realPress("Enter"); - - cy.get("@stepInput") - .should("have.prop", "valueState", "Negative"); - }); -}); - -describe("StepInput events", () => { - it("should not change value state when 'value-state-change' event is prevented", () => { - const valueState = "Positive"; - - cy.mount( - - ); - - cy.get("[ui5-step-input]") - .as("stepInput"); - - cy.get("@stepInput") - .then($input => { - $input.get(0).addEventListener("value-state-change", e => { - e.preventDefault(); - }); - }); + .ui5StepInputAttachHandler("ui5-change", "change"); cy.get("@stepInput") - .ui5StepInputAttachHandler("value-state-change", "stateChange"); + .shadow() + .find("[ui5-number-input]") + .shadow() + .find(".ui5-step-inc") + .as("increaseButton"); - cy.get("@stepInput") + cy.get("@increaseButton") .realClick(); - cy.get("@stepInput") - .ui5StepInputChangeValueWithArrowKeys(1); - - cy.get("@stateChange") + cy.get("@change") .should("have.been.calledOnce"); cy.get("@stepInput") - .should("have.prop", "valueState", valueState); + .should("have.prop", "value", 1); }); - it("should prevent input event", () => { + it("should fire 'change' when using 'Decrease' button", () => { cy.mount( - + ); cy.get("[ui5-step-input]") .as("stepInput"); cy.get("@stepInput") - .then($input => { - $input.get(0).addEventListener("input", e => { - e.preventDefault(); - (e.target as StepInput).value = 30; - }); - }); + .ui5StepInputAttachHandler("ui5-change", "change"); cy.get("@stepInput") - .realClick() - .should("be.focused"); + .shadow() + .find("[ui5-number-input]") + .shadow() + .find(".ui5-step-dec") + .as("decreaseButton"); - cy.realPress("1"); + cy.get("@decreaseButton") + .realClick(); + + cy.get("@change") + .should("have.been.calledOnce"); cy.get("@stepInput") - .ui5StepInputCheckInnerInputProperty("value", "30"); + .should("have.prop", "value", 4); }); - it("should not fire 'change' when navigating with 'ArrowUp'/'ArrowDown' keys", () => { + it("should fire 'change' when clicking 'Increase' button only if it is less than 'max'", () => { cy.mount( - + ); cy.get("[ui5-step-input]") @@ -323,24 +101,21 @@ describe("StepInput events", () => { .ui5StepInputAttachHandler("ui5-change", "change"); cy.get("@stepInput") - .realClick(); - - cy.get("@stepInput") - .ui5StepInputChangeValueWithArrowKeys(1); + .ui5StepInputChangeValueWithButtons(5) cy.get("@change") - .should("have.not.been.called"); + .should("have.been.calledOnce"); cy.get("@stepInput") - .ui5StepInputChangeValueWithArrowKeys(0, decreaseValue); + .ui5StepInputChangeValueWithButtons(5) cy.get("@change") - .should("have.not.been.called"); + .should("have.been.calledOnce"); }); - it("should fire 'change' after 'Enter' is pressed", () => { + it("should fire 'change' when clicking 'Decrease' button only if it is more than 'min'", () => { cy.mount( - + ); cy.get("[ui5-step-input]") @@ -350,20 +125,21 @@ describe("StepInput events", () => { .ui5StepInputAttachHandler("ui5-change", "change"); cy.get("@stepInput") - .realClick(); + .ui5StepInputChangeValueWithButtons(0, decreaseValue) - cy.get("@stepInput") - .ui5StepInputChangeValueWithArrowKeys(1); + cy.get("@change") + .should("have.been.calledOnce"); - cy.realPress("Enter"); + cy.get("@stepInput") + .ui5StepInputChangeValueWithButtons(0, decreaseValue) cy.get("@change") .should("have.been.calledOnce"); }); - it("should not fire 'change' when previous value is restored with 'Escape'", () => { + it("should fire 'change' after value property is programmatically set and then changed with buttons", () => { cy.mount( - + ); cy.get("[ui5-step-input]") @@ -373,147 +149,85 @@ describe("StepInput events", () => { .ui5StepInputAttachHandler("ui5-change", "change"); cy.get("@stepInput") - .realClick(); - - cy.get("@stepInput") - .ui5StepInputChangeValueWithArrowKeys(1); - - cy.realPress("Escape"); - - cy.get("@stepInput") - .should("have.prop", "value", 0); + .invoke("prop", "value", 4); cy.get("@change") .should("not.have.been.called"); - }); - - it("should fire 'change' after focus out", () => { - cy.mount( - - ); - - cy.get("[ui5-step-input]") - .as("stepInput"); - - cy.get("@stepInput") - .ui5StepInputAttachHandler("ui5-change", "change"); - - cy.get("@stepInput") - .realClick(); cy.get("@stepInput") - .ui5StepInputChangeValueWithArrowKeys(1); - - cy.realPress("Tab"); + .ui5StepInputChangeValueWithButtons(5); cy.get("@change") .should("have.been.calledOnce"); - - cy.get("@stepInput") - .should("have.prop", "value", 1); }); - it("should fire 'change' when using 'Increase' button'", () => { + it("buttons are hidden when 'readonly' is set", () => { cy.mount( - + ); cy.get("[ui5-step-input]") - .as("stepInput"); - - cy.get("@stepInput") - .ui5StepInputAttachHandler("ui5-change", "change"); + .shadow() + .find("[ui5-number-input]") + .shadow() + .find(".ui5-step-dec") + .should("not.exist"); - cy.get("@stepInput") + cy.get("[ui5-step-input]") + .shadow() + .find("[ui5-number-input]") .shadow() .find(".ui5-step-inc") - .as("increaseButton"); - - cy.get("@increaseButton") - .realClick(); - - cy.get("@change") - .should("have.been.calledOnce"); - - cy.get("@stepInput") - .should("have.prop", "value", 1); + .should("not.exist"); }); - it("should fire 'change' when using 'Decrease' button'", () => { + it("buttons are visually disabled when component is 'disabled'", () => { cy.mount( - + ); cy.get("[ui5-step-input]") - .as("stepInput"); - - cy.get("@stepInput") - .ui5StepInputAttachHandler("ui5-change", "change"); - - cy.get("@stepInput") .shadow() - .find(".ui5-step-dec") - .as("decreaseButton"); - - cy.get("@decreaseButton") - .realClick(); - - cy.get("@change") - .should("have.been.calledOnce"); + .find("[ui5-number-input]") + .shadow() + .find(".ui5-step-dec [ui5-icon]") + .should("not.have.class", "ui5-number-input-icon--clickable"); - cy.get("@stepInput") - .should("have.prop", "value", 4); + cy.get("[ui5-step-input]") + .shadow() + .find("[ui5-number-input]") + .shadow() + .find(".ui5-step-inc [ui5-icon]") + .should("not.have.class", "ui5-number-input-icon--clickable"); }); - it("should fire 'change' when clicking 'Increase' button only if it is less than 'max'", () => { + it("should not round value when 'valuePrecision' is set", () => { cy.mount( - + ); cy.get("[ui5-step-input]") .as("stepInput"); cy.get("@stepInput") - .ui5StepInputAttachHandler("ui5-change", "change"); - - cy.get("@stepInput") - .ui5StepInputChangeValueWithButtons(5) - - cy.get("@change") - .should("have.been.calledOnce"); - - cy.get("@stepInput") - .ui5StepInputChangeValueWithButtons(5) - - cy.get("@change") - .should("have.been.calledOnce"); + .ui5StepInputChangeValueWithButtons(30.999); }); - it("should fire 'change' when clicking 'Decrease' button only if it is more than 'min'", () => { + it("should round value when 'valuePrecision' is set to default", () => { cy.mount( - + ); cy.get("[ui5-step-input]") .as("stepInput"); cy.get("@stepInput") - .ui5StepInputAttachHandler("ui5-change", "change"); - - cy.get("@stepInput") - .ui5StepInputChangeValueWithButtons(0, decreaseValue) - - cy.get("@change") - .should("have.been.calledOnce"); - - cy.get("@stepInput") - .ui5StepInputChangeValueWithButtons(0, decreaseValue) - - cy.get("@change") - .should("have.been.calledOnce"); + .ui5StepInputChangeValueWithButtons(31); }); +}); - it("should fire 'change' when 'Enter' is pressed after manual input", () => { +describe("StepInput events", () => { + it("should fire 'input' event when typing", () => { cy.mount( ); @@ -522,298 +236,173 @@ describe("StepInput events", () => { .as("stepInput"); cy.get("@stepInput") - .ui5StepInputAttachHandler("ui5-change", "change"); + .ui5StepInputAttachHandler("ui5-input", "input"); cy.get("@stepInput") - .realClick({ "clickCount": 2 }) + .shadow() + .find("[ui5-number-input]") + .realClick() .should("be.focused"); - cy.realType("23"); - - cy.get("@change") - .should("not.have.been.called"); - - cy.realPress("Enter"); - - cy.get("@stepInput") - .should("have.prop", "value", 23); + cy.realType("5"); - cy.get("@change") - .should("have.been.calledOnce"); + cy.get("@input") + .should("have.been.called"); }); - it("should fire 'change' after focusing out of input", () => { + it("should prevent 'input' event when prevented on StepInput", () => { cy.mount( - + ); cy.get("[ui5-step-input]") .as("stepInput"); cy.get("@stepInput") - .ui5StepInputAttachHandler("ui5-change", "change"); + .then($el => { + $el.get(0).addEventListener("input", e => { + e.preventDefault(); + ($el.get(0) as StepInput).value = 30; + }); + }); cy.get("@stepInput") - .realClick({ "clickCount": 2 }) + .shadow() + .find("[ui5-number-input]") + .realClick() .should("be.focused"); - cy.realType("23"); - - cy.get("@change") - .should("not.have.been.called"); - - cy.realPress("Tab"); + cy.realPress("1"); cy.get("@stepInput") - .should("have.prop", "value", 23); - - cy.get("@change") - .should("have.been.calledOnce"); + .should("have.prop", "value", 30); }); - it("should fire 'change' after input is deleted and focused out", () => { + it("should fire 'value-state-change' event when value goes out of range", () => { cy.mount( - + ); cy.get("[ui5-step-input]") .as("stepInput"); cy.get("@stepInput") - .ui5StepInputAttachHandler("ui5-change", "change"); + .ui5StepInputAttachHandler("ui5-value-state-change", "stateChange"); cy.get("@stepInput") - .realClick({ "clickCount": 2 }) + .shadow() + .find("[ui5-number-input]") + .realClick({ clickCount: 2 }) .should("be.focused"); - cy.realPress("Backspace"); - - cy.get("@change") - .should("not.have.been.called"); + cy.realType("2"); cy.realPress("Tab"); - cy.get("@stepInput") - .should("have.prop", "value", 0); - - cy.get("@change") + cy.get("@stateChange") .should("have.been.calledOnce"); - }); - - it("should fire 'change' after value propety is programatically set and then changed with +/- keys", () => { - cy.mount( - - ); - - cy.get("[ui5-step-input]") - .as("stepInput"); - - cy.get("@stepInput") - .ui5StepInputAttachHandler("ui5-change", "change"); - - cy.get("@stepInput") - .invoke("prop", "value", 4); - - cy.get("@change") - .should("not.have.been.called"); cy.get("@stepInput") - .ui5StepInputChangeValueWithButtons(5); - - cy.get("@change") - .should("have.been.calledOnce"); + .should("have.prop", "valueState", "Negative"); }); -}); -describe("StepInput thousand separator formatting", () => { - it("should display value with thousand separator", () => { - cy.mount( - - ); + it("should not change 'valueState' when 'value-state-change' event is prevented", () => { + const valueState = "Positive"; - 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.mount( + ); cy.get("[ui5-step-input]") .as("stepInput"); cy.get("@stepInput") - .ui5StepInputGetInnerInput() - .should($input => { - const val = $input.val() as string; - const num = Number(val.replace(/[^\d]/g, "")); - expect(num).to.equal(12345); - }); + .then($el => { + $el.get(0).addEventListener("value-state-change", e => { + e.preventDefault(); + }); + }); cy.get("@stepInput") - .realClick({ "clickCount": 2 }) - .should("be.focused"); - - cy.realType("1,0000"); - cy.realPress("Enter"); + .ui5StepInputAttachHandler("ui5-value-state-change", "stateChange"); cy.get("@stepInput") - .ui5StepInputGetInnerInput() - .should($input => { - const val = $input.val() as string; - expect(val).to.equal("10,000"); - }); + .shadow() + .find("[ui5-number-input]") + .realClick({ clickCount: 2 }) + .should("be.focused"); - cy.get("@stepInput") - .should("have.prop", "value", 10000); - }); + cy.realType("2"); - it("should update input value when language is changed", () => { - cy.wrap({ setLanguage }) - .then(async ({ setLanguage }) => { - await setLanguage("en"); - }); + cy.realPress("Tab"); - cy.mount( - - ); + cy.get("@stateChange") + .should("have.been.calledOnce"); - cy.get("[ui5-step-input]") - .as("stepInput"); cy.get("@stepInput") - .ui5StepInputGetInnerInput() - .should($input => { - const val = $input.val() as string; - expect(val).to.equal("10,000.56"); - }); - - cy.wrap({ setLanguage }) - .then(async ({ setLanguage }) => { - await setLanguage("de"); - }) - .then(() => { - cy.get("@stepInput") - .ui5StepInputGetInnerInput() - .should($input => { - const val = $input.val() as string; - expect(val).to.equal("10.000,56"); - }); - }); - - cy.wrap({ setLanguage }) - .then(async ({ setLanguage }) => { - await setLanguage("en"); - }); + .should("have.prop", "valueState", valueState); }); -}); -describe("StepInput property propagation", () => { - it("should propagate 'placeholder' property to inner input", () => { + it("should sync 'valueState' to outer element on 'change'", () => { cy.mount( - + ); cy.get("[ui5-step-input]") - .ui5StepInputCheckInnerInputProperty("placeholder", "Enter number"); - }); + .as("stepInput"); - it("should not propagate 'min' property to inner input", () => { - cy.mount( - - ); + cy.get("@stepInput") + .shadow() + .find("[ui5-number-input]") + .realClick({ clickCount: 2 }) + .should("be.focused"); - // min should not be propogated because step input uses input with type="text" - cy.get("[ui5-step-input]") - .ui5StepInputCheckInnerInputProperty("min", "0", false); - }); + cy.realType("2"); - it("should not propagate 'max' property to inner input", () => { - cy.mount( - - ); + cy.realPress("Tab"); - // min should not be propogated because step input uses input with type="text" - cy.get("[ui5-step-input]") - .ui5StepInputCheckInnerInputProperty("max", "10", false); - }); + cy.get("@stepInput") + .should("have.prop", "valueState", "Negative"); - it("should not propagate 'step' property to inner input", () => { - cy.mount( - - ); + cy.get("@stepInput") + .shadow() + .find("[ui5-number-input]") + .realClick({ clickCount: 2 }) + .should("be.focused"); - cy.get("[ui5-step-input]") - .ui5StepInputCheckInnerInputProperty("step", "2", false); - }); + cy.realType("5"); - it("should propagate 'disabled' property to inner input", () => { - cy.mount( - - ); + cy.realPress("Tab"); - cy.get("[ui5-step-input]") - .ui5StepInputCheckInnerInputProperty("disabled", true); + cy.get("@stepInput") + .should("have.prop", "valueState", "None"); }); - it("should propagate 'readonly' property to inner input", () => { + it("should submit form when 'Enter' is pressed inside the input", () => { cy.mount( - +
+ + +
); - cy.get("[ui5-step-input]") - .ui5StepInputCheckInnerInputProperty("readonly", true); - }); - - it("should propagate 'value' property to inner input", () => { - cy.mount( - - ); + cy.get("form") + .then($item => { + $item.get(0).addEventListener("submit", (e) => e.preventDefault()); + $item.get(0).addEventListener("submit", cy.stub().as("submit")); + }); 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( - - ); + .shadow() + .find("[ui5-number-input]") + .realClick() + .should("be.focused"); - cy.get("[ui5-step-input]") - .as("stepInput"); + cy.realPress("Enter"); - cy.get("@stepInput") - .ui5StepInputScrollToChangeValue(5, true); - }); + cy.get("@submit") + .should("have.been.calledOnce"); + }); }); describe("Validation inside form", () => { @@ -835,7 +424,9 @@ describe("Validation inside form", () => { .as("stepInput"); cy.get("@stepInput") - .ui5StepInputTypeNumber(2.34); + .shadow() + .find("[ui5-number-input]") + .ui5NumberInputTypeNumber(2.34); cy.get("#submitBtn") .realClick(); @@ -855,7 +446,9 @@ describe("Validation inside form", () => { .should("exist", "StepInput without formatted value should have :invalid CSS class"); cy.get("@stepInput") - .ui5StepInputTypeNumber(2.345); + .shadow() + .find("[ui5-number-input]") + .ui5NumberInputTypeNumber(2.345); cy.get("@stepInput") .ui5AssertValidityState({ @@ -864,6 +457,7 @@ describe("Validation inside form", () => { checkValidity: true, reportValidity: true }); + cy.get("#stepInput:invalid") .should("not.exist", "StepInput with formatted value should not have :invalid CSS class"); }); @@ -886,7 +480,9 @@ describe("Validation inside form", () => { .as("stepInput"); cy.get("@stepInput") - .ui5StepInputTypeNumber(2); + .shadow() + .find("[ui5-number-input]") + .ui5NumberInputTypeNumber(2); cy.get("#submitBtn") .realClick(); @@ -906,7 +502,9 @@ describe("Validation inside form", () => { .should("exist", "StepInput with value lower than min should have :invalid CSS class"); cy.get("@stepInput") - .ui5StepInputTypeNumber(4); + .shadow() + .find("[ui5-number-input]") + .ui5NumberInputTypeNumber(4); cy.get("@stepInput") .ui5AssertValidityState({ @@ -938,7 +536,9 @@ describe("Validation inside form", () => { .as("stepInput"); cy.get("@stepInput") - .ui5StepInputTypeNumber(4); + .shadow() + .find("[ui5-number-input]") + .ui5NumberInputTypeNumber(4); cy.get("#submitBtn") .realClick(); @@ -955,10 +555,12 @@ describe("Validation inside form", () => { }); cy.get("#stepInput:invalid") - .should("exist", "StepInput without value lower than min should have :invalid CSS class"); + .should("exist", "StepInput with value above max should have :invalid CSS class"); cy.get("@stepInput") - .ui5StepInputTypeNumber(2); + .shadow() + .find("[ui5-number-input]") + .ui5NumberInputTypeNumber(2); cy.get("@stepInput") .ui5AssertValidityState({ @@ -972,23 +574,3 @@ describe("Validation inside form", () => { .should("not.exist", "StepInput with value lower than max should not have :invalid CSS class"); }); }); - -describe("Accessibility", () => { - it("should have correct aria-label when associated with a label via 'for' attribute", () => { - const labelText = "Quantity"; - - cy.mount( - <> - - - - ); - - cy.get("[ui5-step-input]") - .shadow() - .find("[ui5-input]") - .shadow() - .find("input") - .should("have.attr", "aria-label", labelText); - }); -}); \ No newline at end of file diff --git a/packages/main/cypress/support/commands.ts b/packages/main/cypress/support/commands.ts index 541f9abe882d8..c03d9f24fe335 100644 --- a/packages/main/cypress/support/commands.ts +++ b/packages/main/cypress/support/commands.ts @@ -55,6 +55,7 @@ import "./commands/DatePicker.commands.js"; import "./commands/Menu.commands.js"; import "./commands/SegmentedButton.commands.js"; import "./commands/StepInput.commands.js"; +import "./commands/NumberInput.commands.js"; import "./commands/Switch.commands.js"; import "./commands/TabContainer.commands.js"; import "./commands/TimeSelectionClocks.commands.js"; diff --git a/packages/main/cypress/support/commands/NumberInput.commands.ts b/packages/main/cypress/support/commands/NumberInput.commands.ts new file mode 100644 index 0000000000000..1c8b9c3cfb10c --- /dev/null +++ b/packages/main/cypress/support/commands/NumberInput.commands.ts @@ -0,0 +1,105 @@ +Cypress.Commands.add("ui5NumberInputChangeValueWithArrowKeys", { prevSubject: true }, (subject, expectedValue: number, decreaseValue?: boolean) => { + const key = decreaseValue ? "ArrowDown" : "ArrowUp"; + + cy.wrap(subject) + .as("numberInput") + .should("be.visible") + .should("be.focused"); + + cy.realPress(key); + + cy.get("@numberInput") + .should("have.prop", "value", expectedValue); +}); + +Cypress.Commands.add("ui5NumberInputAttachHandler", { prevSubject: true }, (subject, eventName: string, stubName: string) => { + const changeStub = cy.stub().as(stubName); + + cy.wrap(subject) + .as("numberInput") + .should("be.visible"); + + cy.get("@numberInput") + .then($el => { + $el[0].addEventListener(eventName, changeStub); + }); +}); + +Cypress.Commands.add("ui5NumberInputGetInnerInput", { prevSubject: true }, (subject) => { + cy.wrap(subject) + .as("numberInput") + .should("be.visible"); + + cy.get("@numberInput") + .shadow() + .find("[ui5-input]") + .shadow() + .find("input") + .as("innerInput"); + + return cy.get("@innerInput"); +}); + +Cypress.Commands.add("ui5NumberInputCheckInnerInputProperty", { prevSubject: true }, (subject, propName: string, expectedValue: any, shouldBePropagated: boolean = true) => { + cy.get(subject) + .ui5NumberInputGetInnerInput() + .then($innerInput => { + const condition = shouldBePropagated ? "have.prop" : "not.have.prop"; + cy.wrap($innerInput).should(condition, propName, expectedValue); + }); +}); + +Cypress.Commands.add("ui5NumberInputTypeNumber", { prevSubject: true }, (subject, value: number) => { + cy.wrap(subject) + .as("numberInput") + .should("be.visible"); + + cy.get("@numberInput") + .shadow() + .find("[ui5-input]") + .shadow() + .find("input") + .clear() + .realType(value.toString()) + .realPress("Enter"); +}); + +Cypress.Commands.add("ui5NumberInputScrollToChangeValue", { prevSubject: true }, (subject, expectedValue: number, decreaseValue: boolean) => { + const deltaY = decreaseValue ? 100 : -100; + + cy.wrap(subject) + .as("numberInput") + .should("be.visible"); + + cy.get("@numberInput") + .realClick(); + + cy.get("@numberInput") + .should("be.focused"); + + cy.get("@numberInput") + .shadow() + .find(".ui5-number-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("@numberInput") + .should("have.prop", "value", expectedValue); +}); + +declare global { + namespace Cypress { + interface Chainable { + ui5NumberInputChangeValueWithArrowKeys(expectedValue: number, decreaseValue?: boolean): Chainable + ui5NumberInputAttachHandler(eventName: string, stubName: string): Chainable + ui5NumberInputGetInnerInput(): Chainable> + ui5NumberInputCheckInnerInputProperty(propName: string, expectedValue: any, shouldBePropagated?: boolean): Chainable + ui5NumberInputTypeNumber(value: number): Chainable + ui5NumberInputScrollToChangeValue(expectedValue: number, decreaseValue: boolean): Chainable + } + } +} diff --git a/packages/main/cypress/support/commands/StepInput.commands.ts b/packages/main/cypress/support/commands/StepInput.commands.ts index 58ba36d8a59c0..f4932be7686f6 100644 --- a/packages/main/cypress/support/commands/StepInput.commands.ts +++ b/packages/main/cypress/support/commands/StepInput.commands.ts @@ -1,18 +1,4 @@ -Cypress.Commands.add("ui5StepInputChangeValueWithArrowKeys", { prevSubject: true }, (subject, expectedValue: number, decreaseValue?: boolean) => { - const key = decreaseValue ? "ArrowDown" : "ArrowUp"; - - cy.wrap(subject) - .as("stepInput") - .should("be.visible") - .should("be.focused"); - - cy.realPress(key); - - cy.get("@stepInput") - .should("have.prop", "value", expectedValue); -}); - -Cypress.Commands.add("ui5StepInputChangeValueWithButtons", { prevSubject: true }, (subject, expectedValue: number, decreaseValue?: boolean) => { +Cypress.Commands.add("ui5StepInputChangeValueWithButtons", { prevSubject: true }, (subject, expectedValue: number, decreaseValue?: boolean) => { const buttonClass = decreaseValue ? ".ui5-step-dec" : ".ui5-step-inc"; cy.wrap(subject) @@ -20,6 +6,8 @@ Cypress.Commands.add("ui5StepInputChangeValueWithButtons", { prevSubject: true } .should("be.visible"); cy.get("@stepInput") + .shadow() + .find("[ui5-number-input]") .shadow() .find(buttonClass) .as("button"); @@ -28,10 +16,10 @@ Cypress.Commands.add("ui5StepInputChangeValueWithButtons", { prevSubject: true } .realClick(); cy.get("@stepInput") - .should("have.prop", "value", expectedValue); + .should("have.prop", "value", expectedValue); }); -Cypress.Commands.add("ui5StepInputAttachHandler", { prevSubject: true }, (subject, eventName: string, stubName: string) => { +Cypress.Commands.add("ui5StepInputAttachHandler", { prevSubject: true }, (subject, eventName: string, stubName: string) => { const changeStub = cy.stub().as(stubName); cy.wrap(subject) @@ -44,84 +32,11 @@ Cypress.Commands.add("ui5StepInputAttachHandler", { prevSubject: true }, (subje }); }); -Cypress.Commands.add("ui5StepInputGetInnerInput", { prevSubject: true }, (subject) => { - cy.wrap(subject) - .as("stepInput") - .should("be.visible"); - - cy.get("@stepInput") - .shadow() - .find("[ui5-input]") - .shadow() - .find("input") - .as("innerInput"); - - 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) => { - cy.wrap(subject) - .as("stepInput") - .should("be.visible"); - - cy.get("@stepInput") - .shadow() - .find("[ui5-input]") - .shadow() - .find("input") - .clear() - .realType(value.toString()) - .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 - 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 +} diff --git a/packages/main/src/NumberInput.ts b/packages/main/src/NumberInput.ts new file mode 100644 index 0000000000000..b2861f45e2d19 --- /dev/null +++ b/packages/main/src/NumberInput.ts @@ -0,0 +1,865 @@ +import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; +import type { Slot } from "@ui5/webcomponents-base/dist/UI5Element.js"; +import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; +import property from "@ui5/webcomponents-base/dist/decorators/property.js"; +import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js"; +import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js"; +import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; +import { + isUp, + isDown, + isUpCtrl, + isDownCtrl, + isUpShift, + isDownShift, + isUpShiftCtrl, + isDownShiftCtrl, + isPageUpShift, + 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"; +import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js"; +import { getEffectiveAriaLabelText, getAssociatedLabelForTexts } from "@ui5/webcomponents-base/dist/util/AccessibilityTextsHelper.js"; +import type { Timeout } from "@ui5/webcomponents-base/dist/types.js"; +import type { IFormInputElement } from "@ui5/webcomponents-base/dist/features/InputElementsFormSupport.js"; +import { submitForm } from "@ui5/webcomponents-base/dist/features/InputElementsFormSupport.js"; +import NumberInputTemplate from "./NumberInputTemplate.js"; +import { + NUMBERINPUT_DEC_ICON_TITLE, + NUMBERINPUT_INC_ICON_TITLE, + NUMBERINPUT_PATTER_MISSMATCH, + NUMBERINPUT_RANGEOVERFLOW, + NUMBERINPUT_RANGEUNDERFLOW, +} from "./generated/i18n/i18n-defaults.js"; +import "@ui5/webcomponents-icons/dist/less.js"; +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 NumberInputCss from "./generated/themes/NumberInput.css.js"; +import getCachedLocaleDataInstance from "@ui5/webcomponents-localization/dist/getCachedLocaleDataInstance.js"; +import getLocale from "@ui5/webcomponents-base/dist/locale/getLocale.js"; +import { attachLanguageChange, detachLanguageChange } from "@ui5/webcomponents-base/dist/locale/languageChange.js"; + +// Spin variables +const INITIAL_WAIT_TIMEOUT = 500; // milliseconds +const ACCELERATION = 0.8; +const MIN_WAIT_TIMEOUT = 50; // milliseconds +const INITIAL_SPEED = 120; // milliseconds + +type NumberInputValueStateChangeEventDetail = { + valueState: `${ValueState}`, + valid: boolean, +} + +/** + * @class + * + * ### Overview + * + * The `ui5-number-input` is a numeric input field. It allows users to enter, edit, and select numeric values. + * Optionally, it can display increment/decrement buttons (controlled via the internal `_showStepButtons` property). + * + * ### Usage + * + * The default step is 1 but the app developer can set a different one. + * + * App developers can set a maximum and minimum value for the `NumberInput`. + * The increase/decrease button and the up/down keyboard navigation become disabled when + * the value reaches the max/min or a new value is entered from the input which is greater/less than the max/min. + * + * #### When to use: + * + * - To enter or adjust numeric values. + * - To adjust values for a specific step. + * + * #### When not to use: + * + * - To enter a static number (for example, postal code, phone number, or ID). In this case, + * use the regular `ui5-input` instead. + * - To enter dates and times. In this case, use date/time related components instead. + * + * ### ES6 Module Import + * + * `import "@ui5/webcomponents/dist/NumberInput.js";` + * @constructor + * @extends UI5Element + * @since 2.24.0 + * @public + */ +@customElement({ + tag: "ui5-number-input", + cldr: true, + formAssociated: true, + renderer: jsxRenderer, + styles: NumberInputCss, + template: NumberInputTemplate, + languageAware: true, +}) +/** + * Fired when the input operation has finished by pressing Enter or on focusout. + * @public + */ +@event("change", { + bubbles: true, +}) +/** + * Fired when the value of the component changes at each keystroke. + * @public + * @since 2.6.0 + */ +@event("input", { + cancelable: true, + bubbles: true, +}) +/** + * Fired before the value state of the component is updated internally. + * The event is preventable, meaning that if it's default action is + * prevented, the component will not update the value state. + * @since 1.23.0 + * @public + * @param {string} valueState The new `valueState` that will be set. + * @param {boolean} valid Indicator if the value is in between the min and max value. + */ +@event("value-state-change", { + bubbles: true, + cancelable: true, +}) +@event("_request-submit", { + bubbles: true, +}) +class NumberInput extends UI5Element implements IFormInputElement { + eventDetails!: { + change: void + input: InputEventDetail + "value-state-change": NumberInputValueStateChangeEventDetail + "_request-submit": void + } + + /** + * Defines a value of the component. + * @default 0 + * @public + */ + @property({ type: Number }) + value = 0; + + /** + * Defines a minimum value of the component. + * @default undefined + * @public + */ + @property({ type: Number }) + min?: number; + + /** + * Defines a maximum value of the component. + * @default undefined + * @public + */ + @property({ type: Number }) + max?: number; + + /** + * Defines a step of increasing/decreasing the value of the component. + * @default 1 + * @public + */ + @property({ type: Number }) + step: number = 1; + + /** + * Defines the value state of the component. + * @default "None" + * @public + */ + @property() + valueState: `${ValueState}` = "None"; + + /** + * Defines whether the component is required. + * @default false + * @public + */ + @property({ type: Boolean }) + required = false; + + /** + * Determines whether the component is displayed as disabled. + * @default false + * @public + */ + @property({ type: Boolean }) + disabled = false; + + /** + * Determines whether the component is displayed as read-only. + * @default false + * @public + */ + @property({ type: Boolean }) + readonly = false; + + /** + * Defines a short hint, intended to aid the user with data entry when the + * component has no value. + * + * **Note:** When no placeholder is set, the format pattern is displayed as a placeholder. + * Passing an empty string as the value of this property will make the component appear empty - without placeholder or format pattern. + * @default undefined + * @public + */ + @property() + placeholder?: string; + + /** + * Determines the name by which the component will be identified upon submission in an HTML form. + * + * **Note:** This property is only applicable within the context of an HTML Form element. + * @default undefined + * @public + */ + @property() + name?: string; + + /** + * Determines the number of digits after the decimal point of the component. + * @default 0 + * @public + */ + @property({ type: Number }) + valuePrecision = 0; + + /** + * Defines the accessible ARIA name of the component. + * @default undefined + * @public + * @since 1.0.0-rc.15 + */ + @property() + accessibleName?: string; + + /** + * Receives id(or many ids) of the elements that label the component. + * @default undefined + * @public + * @since 1.0.0-rc.15 + */ + @property() + accessibleNameRef?: string; + + /** + * Controls whether the increment/decrement step buttons are shown. + * @default false + * @private + */ + @property({ type: Boolean, noAttribute: true }) + _showStepButtons = false; + + @property({ noAttribute: true }) + _externalAriaLabel?: string; + + @property({ noAttribute: true }) + _decIconDisabled = false; + + @property({ noAttribute: true }) + _incIconDisabled = false; + + @property({ type: Boolean }) + focused = false; + + @property({ noAttribute: true }) + _inputFocused = false; + + @property({ noAttribute: true }) + _previousValue: number | undefined; + + @property({ noAttribute: true }) + _waitTimeout: number = INITIAL_WAIT_TIMEOUT; + + @property({ noAttribute: true }) + _speed: number = INITIAL_SPEED; + + @property({ noAttribute: true }) + _btnDown?: boolean; + + @property({ noAttribute: true }) + _spinTimeoutId?: Timeout; + + @property({ noAttribute: true }) + _spinStarted = false; + + /** + * Defines the value state message that will be displayed as pop up under the component. + * + * **Note:** If not specified, a default text (in the respective language) will be displayed. + * + * **Note:** The `valueStateMessage` would be displayed, + * when the component is in `Information`, `Critical` or `Negative` value state. + * @public + */ + @slot() + valueStateMessage!: Slot; + + _initialValueState?: `${ValueState}`; + + _formatter?: NumberFormat; + + _languageChangeHandler?: (lang: string) => Promise; + + _languageChanged?: boolean = false; + + _delimiter?: string; + + _groupSeparator?: string; + + @i18n("@ui5/webcomponents") + static i18nBundle: I18nBundle; + + async formElementAnchor() { + return (await this.getFocusDomRefAsync() as UI5Element)?.getFocusDomRefAsync(); + } + + get formValidityMessage() { + const validity = this.formValidity; + + if (validity.patternMismatch) { + return NumberInput.i18nBundle.getText(NUMBERINPUT_PATTER_MISSMATCH, this.valuePrecision); + } + if (validity.rangeUnderflow) { + return NumberInput.i18nBundle.getText(NUMBERINPUT_RANGEUNDERFLOW, this.min as number); + } + if (validity.rangeOverflow) { + return NumberInput.i18nBundle.getText(NUMBERINPUT_RANGEOVERFLOW, this.max as number); + } + + return ""; + } + + get formValidity(): ValidityStateFlags { + return { + patternMismatch: this.value !== 0 && !this._isValueWithCorrectPrecision, + rangeOverflow: this.max !== undefined && this.value >= this.max, + rangeUnderflow: this.min !== undefined && this.value <= this.min, + }; + } + + get formFormattedValue(): FormData | string | null { + return this.value.toString(); + } + + get type() { + return InputType.Text; + } + + // icons-related + + get decIconTitle() { + return NumberInput.i18nBundle.getText(NUMBERINPUT_DEC_ICON_TITLE); + } + + get incIconTitle() { + return NumberInput.i18nBundle.getText(NUMBERINPUT_INC_ICON_TITLE); + } + + get _decIconClickable() { + return !this._decIconDisabled && !this.readonly && !this.disabled; + } + + get _incIconClickable() { + return !this._incIconDisabled && !this.readonly && !this.disabled; + } + + get _isFocused() { + return this.focused; + } + + get _displayValue() { + if (this._languageChanged) { + this._languageChanged = false; + this.valueState = ValueState.None; // to reset the value state visual + return this._formatNumber(this.value); + } + // 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; + } + + if (this.input && this.value === this._parseNumber(this.input.value)) { // For the cases where the number is fractional and is ending with 0s. + return this.input.value; + } + + return value; + } + + get accInfo(): InputAccInfo { + return { + "ariaRequired": this.required, + "ariaLabel": getEffectiveAriaLabelText(this) || this._externalAriaLabel || getAssociatedLabelForTexts(this), + }; + } + + get inputAttributes() { + return { + min: this.min === undefined ? undefined : this.min, + max: this.max === undefined ? undefined : this.max, + step: this.step, + }; + } + + onBeforeRendering() { + this._setButtonState(); + } + + onEnterDOM() { + this._setupLanguageChangeHandler(); + } + + onExitDOM() { + this._cleanupLanguageChangeHandler(); + } + + _setupLanguageChangeHandler() { + if (this._languageChangeHandler) { + return; + } + + this._languageChangeHandler = () => { + this._formatter = undefined; + this._languageChanged = true; + this._delimiter = undefined; + this._groupSeparator = undefined; + + return Promise.resolve(); + }; + attachLanguageChange(this._languageChangeHandler); + } + + _cleanupLanguageChangeHandler() { + if (this._languageChangeHandler) { + detachLanguageChange(this._languageChangeHandler); + this._languageChangeHandler = undefined; + } + } + + get formatter(): NumberFormat { + if (!this._formatter) { + this._formatter = NumberFormat.getFloatInstance({ + decimals: this.valuePrecision, + }); + } + + return this._formatter; + } + + get delimiter() { + if (!this._delimiter) { + const localeData = getCachedLocaleDataInstance(getLocale()); + this._delimiter = localeData.getNumberSymbol("decimal") || "."; + } + + return this._delimiter; + } + + get groupSeparator() { + if (!this._groupSeparator) { + const localeData = getCachedLocaleDataInstance(getLocale()); + this._groupSeparator = localeData.getNumberSymbol("group") || ","; + } + + return this._groupSeparator; + } + + get input(): Input { + return this.shadowRoot!.querySelector("[ui5-input]")!; + } + + get innerInput(): HTMLInputElement { + return this.input.shadowRoot!.querySelector("input")!; + } + + get inputOuter() { + return this.shadowRoot!.querySelector(".ui5-number-input-input")!; + } + + _onButtonFocusOut() { + setTimeout(() => { + if (!this._inputFocused && !this.shadowRoot!.activeElement) { + this.inputOuter.removeAttribute("focused"); + } + }, 0); + } + + _onInput(e: CustomEvent) { + const prevented = !this.fireDecoratorEvent("input", { inputType: e.detail.inputType }); + + if (prevented) { + e.preventDefault(); + } + } + + _onInputFocusIn() { + this._inputFocused = true; + } + + _onInputFocusOut() { + this._inputFocused = false; + this._onInputChange(); + } + + _onMouseWheel(e: WheelEvent) { + if (this.disabled || this.readonly || !this._isFocused) { + return; + } + + e.preventDefault(); + + 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; + } + + _validate() { + if (this._initialValueState === undefined) { + this._initialValueState = this.valueState; + } + + this._updateValueState(); + } + + _updateValueState() { + const isWithinRange = (this.min === undefined || this._parseNumber(this.input.value) >= this.min) + && (this.max === undefined || this._parseNumber(this.input.value) <= this.max); + const isValueWithCorrectPrecision = this._isValueWithCorrectPrecision; + const previousValueState = this.valueState; + const isValid = isWithinRange && isValueWithCorrectPrecision; + + this.valueState = isValid ? ValueState.None : ValueState.Negative; + + const eventPrevented = !this.fireDecoratorEvent("value-state-change", { + valueState: this.valueState, + valid: isValid, + }); + + if (eventPrevented) { + this.valueState = previousValueState; + } + } + + _preciseValue(value: number) { + const pow = 10 ** this.valuePrecision; + return Math.round(value * pow) / pow; + } + + _fireChangeEvent() { + if (this._previousValue !== this.value) { + this._previousValue = this.value; + this.fireDecoratorEvent("change"); + } + } + + /** + * Value modifier - modifies the value of the component, validates the new value and enables/disables increment and + * decrement buttons according to the value and min/max values (if set). Fires `change` event when requested + * @private + * @param modifier modifies the value of the component with the given modifier (positive or negative) + * @param fireChangeEvent if `true`, fires `change` event when the value is changed + */ + _modifyValue(modifier: number, fireChangeEvent = false) { + let value; + value = this.value + modifier; + if (this.min !== undefined && value < this.min) { + value = this.min; + } + if (this.max !== undefined && value > this.max) { + value = this.max; + } + value = this._preciseValue(value); + if (value !== this.value) { + this.value = value; + this.input.value = this._formatNumber(value); + this._validate(); + this._setButtonState(); + this.focused = true; + this.inputOuter.setAttribute("focused", ""); + if (fireChangeEvent) { + this._fireChangeEvent(); + } else { + this.input.focus(); + } + } + } + + /** + * Formats a number with thousands separator based on current locale + * @private + */ + _formatNumber(value: number): string { + return this.formatter.format(value); + } + + /** + * Parses formatted number string back to numeric value + * @private + */ + _parseNumber(formattedValue: string): number { + return this.formatter.parse(formattedValue) as number; + } + + _incValue() { + if (this._incIconClickable && !this.disabled && !this.readonly) { + this._modifyValue(this.step, true); + this._previousValue = this.value; + } + } + + _decValue() { + if (this._decIconClickable && !this.disabled && !this.readonly) { + this._modifyValue(-this.step, true); + this._previousValue = this.value; + } + } + + get _isValueWithCorrectPrecision() { + const delimiter = this.delimiter; + // check if the value will be displayed with correct precision + // _displayValue has special formatting logic + if (this.valuePrecision === 0 && !this.input?.value.includes(delimiter) && ((this.value === 0) || (Number.isInteger(this.value)))) { + // integers and zero will be formatted with toFixed, so they're always valid + return true; + } + const numberParts = this.input?.value?.split(delimiter); + const decimalPartLength = numberParts?.length > 1 ? numberParts[1].length : 0; + + return decimalPartLength === this.valuePrecision; + } + + _onInputChange() { + this._setDefaultInputValueIfNeeded(); + const updatedValue = this._removeGroupSeparators(this.input.value); + const inputValue = this._parseNumber(updatedValue); + if (this._isValueChanged(inputValue)) { + this._updateValueAndValidate(Number.isNaN(inputValue) ? this.min || 0 : inputValue); + this.innerInput.value = this.input.value; + } + } + + _setDefaultInputValueIfNeeded() { + if (this.input.value === "") { + 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 + } + } + + _isValueChanged(inputValue: number) { + const isValueWithCorrectPrecision = this._isValueWithCorrectPrecision; + const isWithinRange = (this.min === undefined || inputValue >= this.min) && (this.max === undefined || inputValue <= this.max); + // Treat values as distinct when the precision was just corrected (e.g., from 3.4000 to 3.40) while + // the value was previously invalid due to precision — but only when the value is in range. Without + // this guard the condition also triggers for range violations, causing a redundant second _validate call. + const isPrecisionCorrectButValueStateError = isValueWithCorrectPrecision && isWithinRange && this.valueState === ValueState.Negative; + + return this.value !== this._previousValue + || this.value !== inputValue + || inputValue === 0 + || !isValueWithCorrectPrecision + || isPrecisionCorrectButValueStateError + || Number.isNaN(inputValue); + } + + _updateValueAndValidate(inputValue: number) { + this.value = inputValue; + this._validate(); + this._setButtonState(); + this._fireChangeEvent(); + } + + _onfocusin() { + this.focused = true; + this._previousValue = this.value; + } + + _onfocusout() { + this.focused = false; + this._previousValue = undefined; + } + + _onInputRequestSubmit() { + if (this._internals.form) { + submitForm(this); + } else { + this.fireDecoratorEvent("_request-submit"); + } + } + + _onkeydown(e: KeyboardEvent) { + let preventDefault = true; + if (this.disabled || this.readonly) { + return; + } + + if (isEnter(e)) { + this._onInputChange(); + return; + } + + if (isUp(e)) { + // step up + this._modifyValue(this.step); + } else if (isDown(e)) { + // step down + this._modifyValue(-this.step); + } else if (isEscape(e)) { + // return previous value + if (this._previousValue === undefined) { + this._previousValue = this.value; + } + this.value = this._previousValue; + this.input.value = this.value.toFixed(this.valuePrecision); + } else if (this.max !== undefined && (isPageUpShift(e) || isUpShiftCtrl(e))) { + // step to max + this._modifyValue(this.max - this.value); + } else if (this.min !== undefined && (isPageDownShift(e) || isDownShiftCtrl(e))) { + // step to min + this._modifyValue(this.min - this.value); + } else if (!isUpCtrl(e) && !isDownCtrl(e) && !isUpShift(e) && !isDownShift(e)) { + preventDefault = false; + } + + if (e.key && e.key.length !== 1) { + return; + } + + const caretPosition = this._getCaretPosition(); + const inputValue = this.innerInput.value; + const typedValue = this._getValueOnkeyDown(e, inputValue, caretPosition!); + const parsedValue = this._parseNumber(typedValue); + const isValidTypedValue = this._isInputValueValid(typedValue, parsedValue); + + if (preventDefault || !isValidTypedValue) { + e.preventDefault(); + } + + if (caretPosition === 0 && isMinus(e)) { + this._updateValueAndValidate(parsedValue); + } + } + + _getCaretPosition() { + return this.input.getDomRef()!.querySelector("input")!.selectionStart; + } + + _getValueOnkeyDown(e: KeyboardEvent, inputValue: string, cursorPosition?: number) { + const typedValue = `${inputValue.substring(0, cursorPosition)}${e.key}${inputValue.substring(cursorPosition!)}`; + const updatedValue = this._removeGroupSeparators(typedValue); + return updatedValue; + } + + _removeGroupSeparators(value: string) { + const groupSeparator = this.groupSeparator; + return value.replaceAll(groupSeparator, ""); + } + + _isInputValueValid(typedValue: string, parsedValue: number) { + return !Number.isNaN(parsedValue) && !/, {2,}/.test(typedValue); + } + + _decSpin(e: MouseEvent) { + if (this._isFocused || this._decIconDisabled) { + e.preventDefault(); + } + if (!this._decIconDisabled) { + this._spinValue(false, true); + } else { + this.input.focus(); + } + } + + _incSpin(e: MouseEvent) { + if (this._isFocused || this._incIconDisabled) { + e.preventDefault(); + } + if (!this._incIconDisabled) { + this._spinValue(true, true); + } else { + this.input.focus(); + } + } + + /** + * Calculates the time which should be waited until _spinValue function is called. + */ + _calcWaitTimeout() { + this._speed *= ACCELERATION; + this._waitTimeout = ((this._waitTimeout - this._speed) < MIN_WAIT_TIMEOUT ? MIN_WAIT_TIMEOUT : (this._waitTimeout - this._speed)); + return this._waitTimeout; + } + + /** + * Called when the increment or decrement button is pressed and held to set new value. + * @private + * @param increment - is this the increment button or not so the values should be spin accordingly up or down + * @param resetVariables - whether to reset the spin-related variables or not + */ + _spinValue(increment: boolean, resetVariables = false) { + if (resetVariables) { + this._waitTimeout = INITIAL_WAIT_TIMEOUT; + this._speed = INITIAL_SPEED; + this._btnDown = true; + } + this._spinTimeoutId = setTimeout(() => { + if (this._btnDown) { + this._spinStarted = true; + this._modifyValue(increment ? this.step : -this.step); + this._setButtonState(); + if ((!this._incIconDisabled && increment) || (!this._decIconDisabled && !increment)) { + this._spinValue(increment); + } else { + this._resetSpin(); + this._fireChangeEvent(); + } + } + }, this._calcWaitTimeout()); + } + + /** + * Resets spin process + */ + _resetSpin() { + clearTimeout(this._spinTimeoutId); + this._btnDown = false; + this._spinStarted = false; + } + + /** + * Resets spin process when mouse outs + or - buttons + */ + _resetSpinOut() { + if (this._btnDown) { + this._resetSpin(); + this._fireChangeEvent(); + } + } +} + +NumberInput.define(); + +export default NumberInput; +export type { + NumberInputValueStateChangeEventDetail, +}; diff --git a/packages/main/src/NumberInputTemplate.tsx b/packages/main/src/NumberInputTemplate.tsx new file mode 100644 index 0000000000000..10d1cb61b433b --- /dev/null +++ b/packages/main/src/NumberInputTemplate.tsx @@ -0,0 +1,94 @@ +import type NumberInput from "./NumberInput.js"; +import Icon from "./Icon.js"; +import Input from "./Input.js"; + +import less from "@ui5/webcomponents-icons/dist/less.js"; +import add from "@ui5/webcomponents-icons/dist/add.js"; + +export default function NumberInputTemplate(this: NumberInput) { + return ( +
+ {/* Decrement Icon */} + {this._showStepButtons && !this.readonly && +
+ +
+ } + + {/* INPUT */} + + {this.valueStateMessage.length > 0 && + + } + + + {/* Increment Icon */} + {this._showStepButtons && !this.readonly && +
+ +
+ } +
+ ); +} diff --git a/packages/main/src/StepInput.ts b/packages/main/src/StepInput.ts index e125e14433e85..d35e61896cc69 100644 --- a/packages/main/src/StepInput.ts +++ b/packages/main/src/StepInput.ts @@ -5,60 +5,26 @@ import property from "@ui5/webcomponents-base/dist/decorators/property.js"; import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js"; import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js"; import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; -import { - isUp, - isDown, - isUpCtrl, - isDownCtrl, - isUpShift, - isDownShift, - isUpShiftCtrl, - isDownShiftCtrl, - isPageUpShift, - 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"; -import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js"; -import { getEffectiveAriaLabelText, getAssociatedLabelForTexts } from "@ui5/webcomponents-base/dist/util/AccessibilityTextsHelper.js"; -import type { Timeout } from "@ui5/webcomponents-base/dist/types.js"; import type { IFormInputElement } from "@ui5/webcomponents-base/dist/features/InputElementsFormSupport.js"; import { submitForm } from "@ui5/webcomponents-base/dist/features/InputElementsFormSupport.js"; -import StepInputTemplate from "./StepInputTemplate.js"; +import type ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js"; +import { getAssociatedLabelForTexts } from "@ui5/webcomponents-base/dist/util/AccessibilityTextsHelper.js"; +import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js"; +import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js"; import { - STEPINPUT_DEC_ICON_TITLE, - STEPINPUT_INC_ICON_TITLE, - STEPINPUT_PATTER_MISSMATCH, - STEPINPUT_RANGEOVERFLOW, - STEPINPUT_RANGEUNDERFLOW, + NUMBERINPUT_PATTER_MISSMATCH, + NUMBERINPUT_RANGEOVERFLOW, + NUMBERINPUT_RANGEUNDERFLOW, } from "./generated/i18n/i18n-defaults.js"; -import "@ui5/webcomponents-icons/dist/less.js"; -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"; +import StepInputTemplate from "./StepInputTemplate.js"; +import type { InputEventDetail } from "./Input.js"; +import type NumberInput from "./NumberInput.js"; +import type { NumberInputValueStateChangeEventDetail } from "./NumberInput.js"; // Styles import StepInputCss from "./generated/themes/StepInput.css.js"; -import getCachedLocaleDataInstance from "@ui5/webcomponents-localization/dist/getCachedLocaleDataInstance.js"; -import getLocale from "@ui5/webcomponents-base/dist/locale/getLocale.js"; -import { attachLanguageChange, detachLanguageChange } from "@ui5/webcomponents-base/dist/locale/languageChange.js"; - -// Spin variables -const INITIAL_WAIT_TIMEOUT = 500; // milliseconds -const ACCELERATION = 0.8; -const MIN_WAIT_TIMEOUT = 50; // milliseconds -const INITIAL_SPEED = 120; // milliseconds - -type StepInputValueStateChangeEventDetail = { - valueState: `${ValueState}`, - valid: boolean, -} + +type StepInputValueStateChangeEventDetail = NumberInputValueStateChangeEventDetail; /** * @class @@ -213,9 +179,6 @@ class StepInput extends UI5Element implements IFormInputElement { /** * Defines a short hint, intended to aid the user with data entry when the * component has no value. - * - * **Note:** When no placeholder is set, the format pattern is displayed as a placeholder. - * Passing an empty string as the value of this property will make the component appear empty - without placeholder or format pattern. * @default undefined * @public */ @@ -224,8 +187,6 @@ class StepInput extends UI5Element implements IFormInputElement { /** * Determines the name by which the component will be identified upon submission in an HTML form. - * - * **Note:** This property is only applicable within the context of an HTML Form element. * @default undefined * @public */ @@ -258,63 +219,20 @@ class StepInput extends UI5Element implements IFormInputElement { @property() accessibleNameRef?: string; - @property({ noAttribute: true }) - _decIconDisabled = false; - - @property({ noAttribute: true }) - _incIconDisabled = false; - - @property({ type: Boolean }) - focused = false; - - @property({ noAttribute: true }) - _inputFocused = false; - - @property({ noAttribute: true }) - _previousValue: number | undefined; - - @property({ noAttribute: true }) - _waitTimeout: number = INITIAL_WAIT_TIMEOUT; - - @property({ noAttribute: true }) - _speed: number = INITIAL_SPEED; - - @property({ noAttribute: true }) - _btnDown?: boolean; - - @property({ noAttribute: true }) - _spinTimeoutId?: Timeout; - - @property({ noAttribute: true }) - _spinStarted = false; - /** * Defines the value state message that will be displayed as pop up under the component. - * - * **Note:** If not specified, a default text (in the respective language) will be displayed. - * - * **Note:** The `valueStateMessage` would be displayed, - * when the component is in `Information`, `Critical` or `Negative` value state. * @public */ @slot() valueStateMessage!: Slot; - _initialValueState?: `${ValueState}`; - - _formatter?: NumberFormat; - - _languageChangeHandler?: (lang: string) => Promise; - - _languageChanged?: boolean = false; - - _delimiter?: string; - - _groupSeparator?: string; - @i18n("@ui5/webcomponents") static i18nBundle: I18nBundle; + get _innerNumberInput(): NumberInput { + return this.shadowRoot!.querySelector("[ui5-number-input]")!; + } + async formElementAnchor() { return (await this.getFocusDomRefAsync() as UI5Element)?.getFocusDomRefAsync(); } @@ -323,21 +241,21 @@ class StepInput extends UI5Element implements IFormInputElement { const validity = this.formValidity; if (validity.patternMismatch) { - return StepInput.i18nBundle.getText(STEPINPUT_PATTER_MISSMATCH, this.valuePrecision); + return StepInput.i18nBundle.getText(NUMBERINPUT_PATTER_MISSMATCH, this.valuePrecision); } if (validity.rangeUnderflow) { - return StepInput.i18nBundle.getText(STEPINPUT_RANGEUNDERFLOW, this.min as number); + return StepInput.i18nBundle.getText(NUMBERINPUT_RANGEUNDERFLOW, this.min as number); } if (validity.rangeOverflow) { - return StepInput.i18nBundle.getText(STEPINPUT_RANGEOVERFLOW, this.max as number); + return StepInput.i18nBundle.getText(NUMBERINPUT_RANGEOVERFLOW, this.max as number); } - return ""; // No error + return ""; } get formValidity(): ValidityStateFlags { return { - patternMismatch: this.value !== 0 && !this._isValueWithCorrectPrecision, + patternMismatch: this.value !== 0 && (this._innerNumberInput?.formValidity.patternMismatch ?? false), rangeOverflow: this.max !== undefined && this.value >= this.max, rangeUnderflow: this.min !== undefined && this.value <= this.min, }; @@ -347,501 +265,58 @@ class StepInput extends UI5Element implements IFormInputElement { return this.value.toString(); } - get type() { - return InputType.Text; - } - - // icons-related - - get decIconTitle() { - return StepInput.i18nBundle.getText(STEPINPUT_DEC_ICON_TITLE); - } - - get incIconTitle() { - return StepInput.i18nBundle.getText(STEPINPUT_INC_ICON_TITLE); - } - - get _decIconClickable() { - return !this._decIconDisabled && !this.readonly && !this.disabled; - } - - get _incIconClickable() { - return !this._incIconDisabled && !this.readonly && !this.disabled; - } - - get _isFocused() { - return this.focused; - } - - get _displayValue() { - if (this._languageChanged) { - this._languageChanged = false; - this.valueState = ValueState.None; // to reset the value state visual - return this._formatNumber(this.value); - } - // 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; - } - - if (this.input && this.value === this._parseNumber(this.input.value)) { // For the cases where the number is fractional and is ending with 0s. - return this.input.value; - } - - return value; - } - - get accInfo(): InputAccInfo { - return { - "ariaRequired": this.required, - "ariaLabel": getEffectiveAriaLabelText(this) || getAssociatedLabelForTexts(this), - }; - } - - get inputAttributes() { - return { - min: this.min === undefined ? undefined : this.min, - max: this.max === undefined ? undefined : this.max, - step: this.step, - }; - } - - onBeforeRendering() { - this._setButtonState(); - } - onEnterDOM() { - this._setupLanguageChangeHandler(); - } - - onExitDOM() { - this._cleanupLanguageChangeHandler(); - } - - _setupLanguageChangeHandler() { - if (this._languageChangeHandler) { - return; - } - - this._languageChangeHandler = () => { - this._formatter = undefined; - this._languageChanged = true; - this._delimiter = undefined; - this._groupSeparator = undefined; - - return Promise.resolve(); - }; - attachLanguageChange(this._languageChangeHandler); - } - - _cleanupLanguageChangeHandler() { - if (this._languageChangeHandler) { - detachLanguageChange(this._languageChangeHandler); - this._languageChangeHandler = undefined; - } + getFocusDomRef(): HTMLElement | undefined { + return this._innerNumberInput?.getFocusDomRef(); } - get formatter(): NumberFormat { - if (!this._formatter) { - this._formatter = NumberFormat.getFloatInstance({ - decimals: this.valuePrecision, - }); - } - - return this._formatter; + get _associatedLabelText(): string | undefined { + return getAssociatedLabelForTexts(this) || undefined; } - get delimiter() { - if (!this._delimiter) { - const localeData = getCachedLocaleDataInstance(getLocale()); - this._delimiter = localeData.getNumberSymbol("decimal") || "."; - } - - return this._delimiter; + _onNiChange(e: Event) { + e.stopPropagation(); + this._syncFromInner(); + this.fireDecoratorEvent("change"); } - get groupSeparator() { - if (!this._groupSeparator) { - const localeData = getCachedLocaleDataInstance(getLocale()); - this._groupSeparator = localeData.getNumberSymbol("group") || ","; - } - - return this._groupSeparator; - } - - get input(): Input { - return this.shadowRoot!.querySelector("[ui5-input]")!; - } - - get innerInput(): HTMLInputElement { - return this.input.shadowRoot!.querySelector("input")!; - } - - get inputOuter() { - return this.shadowRoot!.querySelector(".ui5-step-input-input")!; - } - - _onButtonFocusOut() { - setTimeout(() => { - if (!this._inputFocused && !this.shadowRoot!.activeElement) { - this.inputOuter.removeAttribute("focused"); - } - }, 0); - } - - _onInput(e: CustomEvent) { + _onNiInput(e: CustomEvent) { + e.stopPropagation(); const prevented = !this.fireDecoratorEvent("input", { inputType: e.detail.inputType }); - if (prevented) { e.preventDefault(); } } - _onInputFocusIn() { - this._inputFocused = true; - } - - _onInputFocusOut() { - this._inputFocused = false; - this._onInputChange(); - } - - _onMouseWheel(e: WheelEvent) { - if (this.disabled || this.readonly || !this._isFocused) { - return; - } - - e.preventDefault(); - - 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; - } - - _validate() { - if (this._initialValueState === undefined) { - this._initialValueState = this.valueState; - } - - this._updateValueState(); - } - - _updateValueState() { - const isWithinRange = (this.min === undefined || this._parseNumber(this.input.value) >= this.min) - && (this.max === undefined || this._parseNumber(this.input.value) <= this.max); - const isValueWithCorrectPrecision = this._isValueWithCorrectPrecision; - const previousValueState = this.valueState; - const isValid = isWithinRange && isValueWithCorrectPrecision; - - this.valueState = isValid ? ValueState.None : ValueState.Negative; - - const eventPrevented = !this.fireDecoratorEvent("value-state-change", { - valueState: this.valueState, - valid: isValid, + _onNiValueStateChange(e: CustomEvent) { + e.stopPropagation(); + const prevented = !this.fireDecoratorEvent("value-state-change", { + valueState: e.detail.valueState, + valid: e.detail.valid, }); - - if (eventPrevented) { - this.valueState = previousValueState; - } - } - - _preciseValue(value: number) { - const pow = 10 ** this.valuePrecision; - return Math.round(value * pow) / pow; - } - - _fireChangeEvent() { - if (this._previousValue !== this.value) { - this._previousValue = this.value; - this.fireDecoratorEvent("change"); - } - } - - /** - * Value modifier - modifies the value of the component, validates the new value and enables/disables increment and - * decrement buttons according to the value and min/max values (if set). Fires `change` event when requested - * @private - * @param modifier modifies the value of the component with the given modifier (positive or negative) - * @param fireChangeEvent if `true`, fires `change` event when the value is changed - */ - _modifyValue(modifier: number, fireChangeEvent = false) { - let value; - value = this.value + modifier; - if (this.min !== undefined && value < this.min) { - value = this.min; - } - if (this.max !== undefined && value > this.max) { - value = this.max; - } - value = this._preciseValue(value); - if (value !== this.value) { - this.value = value; - this.input.value = this._formatNumber(value); - this._validate(); - this._setButtonState(); - this.focused = true; - this.inputOuter.setAttribute("focused", ""); - if (fireChangeEvent) { - this._fireChangeEvent(); - } else { - this.input.focus(); - } - } - } - - /** - * Formats a number with thousands separator based on current locale - * @private - */ - _formatNumber(value: number): string { - return this.formatter.format(value); - } - - /** - * Parses formatted number string back to numeric value - * @private - */ - _parseNumber(formattedValue: string): number { - return this.formatter.parse(formattedValue) as number; - } - - _incValue() { - if (this._incIconClickable && !this.disabled && !this.readonly) { - this._modifyValue(this.step, true); - this._previousValue = this.value; - } - } - - _decValue() { - if (this._decIconClickable && !this.disabled && !this.readonly) { - this._modifyValue(-this.step, true); - this._previousValue = this.value; - } - } - - get _isValueWithCorrectPrecision() { - const delimiter = this.delimiter; - // check if the value will be displayed with correct precision - // _displayValue has special formatting logic - if (this.valuePrecision === 0 && !this.input?.value.includes(delimiter) && ((this.value === 0) || (Number.isInteger(this.value)))) { - // integers and zero will be formatted with toFixed, so thex y're always valid - return true; - } - const numberParts = this.input?.value?.split(delimiter); - const decimalPartLength = numberParts?.length > 1 ? numberParts[1].length : 0; - - return decimalPartLength === this.valuePrecision; - } - - _onInputChange() { - this._setDefaultInputValueIfNeeded(); - const updatedValue = this._removeGroupSeparators(this.input.value); - const inputValue = this._parseNumber(updatedValue); - if (this._isValueChanged(inputValue)) { - this._updateValueAndValidate(Number.isNaN(inputValue) ? this.min || 0 : inputValue); - this.innerInput.value = this.input.value; + if (prevented) { + // Inner already applied the new valueState — revert it back to the outer's current value + this._innerNumberInput.valueState = this.valueState; + } else { + this.valueState = e.detail.valueState; } } - _setDefaultInputValueIfNeeded() { - if (this.input.value === "") { - 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 + _syncFromInner() { + const ni = this._innerNumberInput; + if (!ni) { + return; } + this.value = ni.value; + this.valueState = ni.valueState; } - _isValueChanged(inputValue: number) { - const isValueWithCorrectPrecision = this._isValueWithCorrectPrecision; - // Treat values as distinct when modified to match a specific precision (e.g., from 3.4000 to 3.40), - // even if JavaScript sees them as equal, to correctly update valueState based on expected valuePrecision. - const isPrecisionCorrectButValueStateError = isValueWithCorrectPrecision && this.valueState === ValueState.Negative; - - return this.value !== this._previousValue - || this.value !== inputValue - || inputValue === 0 - || !isValueWithCorrectPrecision - || isPrecisionCorrectButValueStateError - || Number.isNaN(inputValue); - } - - _updateValueAndValidate(inputValue: number) { - this.value = inputValue; - this._validate(); - this._setButtonState(); - this._fireChangeEvent(); - } - - _onfocusin() { - this.focused = true; - this._previousValue = this.value; - } - - _onfocusout() { - this.focused = false; - this._previousValue = undefined; - } - - _onInputRequestSubmit() { + _onRequestSubmit() { if (this._internals.form) { submitForm(this); } } - - _onkeydown(e: KeyboardEvent) { - let preventDefault = true; - if (this.disabled || this.readonly) { - return; - } - - if (isEnter(e)) { - this._onInputChange(); - return; - } - - if (isUp(e)) { - // step up - this._modifyValue(this.step); - } else if (isDown(e)) { - // step down - this._modifyValue(-this.step); - } else if (isEscape(e)) { - // return previous value - if (this._previousValue === undefined) { - this._previousValue = this.value; - } - this.value = this._previousValue; - this.input.value = this.value.toFixed(this.valuePrecision); - } else if (this.max !== undefined && (isPageUpShift(e) || isUpShiftCtrl(e))) { - // step to max - this._modifyValue(this.max - this.value); - } else if (this.min !== undefined && (isPageDownShift(e) || isDownShiftCtrl(e))) { - // step to min - this._modifyValue(this.min - this.value); - } else if (!isUpCtrl(e) && !isDownCtrl(e) && !isUpShift(e) && !isDownShift(e)) { - preventDefault = false; - } - - if (e.key && e.key.length !== 1) { - return; - } - - const caretPosition = this._getCaretPosition(); - const inputValue = this.innerInput.value; - const typedValue = this._getValueOnkeyDown(e, inputValue, caretPosition!); - const parsedValue = this._parseNumber(typedValue); - const isValidTypedValue = this._isInputValueValid(typedValue, parsedValue); - - if (preventDefault || !isValidTypedValue) { - e.preventDefault(); - } - - if (caretPosition === 0 && isMinus(e)) { - this._updateValueAndValidate(parsedValue); - } - } - - _getCaretPosition() { - return this.input.getDomRef()!.querySelector("input")!.selectionStart; - } - - _getValueOnkeyDown(e: KeyboardEvent, inputValue: string, cursorPosition?: number) { - const typedValue = `${inputValue.substring(0, cursorPosition)}${e.key}${inputValue.substring(cursorPosition!)}`; - const updatedValue = this._removeGroupSeparators(typedValue); - return updatedValue; - } - - _removeGroupSeparators(value: string) { - const groupSeparator = this.groupSeparator; - return value.replaceAll(groupSeparator, ""); - } - - _isInputValueValid(typedValue: string, parsedValue: number) { - return !Number.isNaN(parsedValue) && !/, {2,}/.test(typedValue); - } - - _decSpin(e: MouseEvent) { - if (this._isFocused || this._decIconDisabled) { - e.preventDefault(); - } - if (!this._decIconDisabled) { - this._spinValue(false, true); - } else { - this.input.focus(); - } - } - - _incSpin(e: MouseEvent) { - if (this._isFocused || this._incIconDisabled) { - e.preventDefault(); - } - if (!this._incIconDisabled) { - this._spinValue(true, true); - } else { - this.input.focus(); - } - } - - /** - * Calculates the time which should be waited until _spinValue function is called. - */ - _calcWaitTimeout() { - this._speed *= ACCELERATION; - this._waitTimeout = ((this._waitTimeout - this._speed) < MIN_WAIT_TIMEOUT ? MIN_WAIT_TIMEOUT : (this._waitTimeout - this._speed)); - return this._waitTimeout; - } - - /** - * Called when the increment or decrement button is pressed and held to set new value. - * @private - * @param increment - is this the increment button or not so the values should be spin accordingly up or down - * @param resetVariables - whether to reset the spin-related variables or not - */ - _spinValue(increment: boolean, resetVariables = false) { - if (resetVariables) { - this._waitTimeout = INITIAL_WAIT_TIMEOUT; - this._speed = INITIAL_SPEED; - this._btnDown = true; - } - this._spinTimeoutId = setTimeout(() => { - if (this._btnDown) { - this._spinStarted = true; - this._modifyValue(increment ? this.step : -this.step); - this._setButtonState(); - if ((!this._incIconDisabled && increment) || (!this._decIconDisabled && !increment)) { - this._spinValue(increment); - } else { - this._resetSpin(); - this._fireChangeEvent(); - } - } - }, this._calcWaitTimeout()); - } - - /** - * Resets spin process - */ - _resetSpin() { - clearTimeout(this._spinTimeoutId); - this._btnDown = false; - this._spinStarted = false; - } - - /** - * Resets spin process when mouse outs + or - buttons - */ - _resetSpinOut() { - if (this._btnDown) { - this._resetSpin(); - this._fireChangeEvent(); - } - } } + StepInput.define(); export default StepInput; diff --git a/packages/main/src/StepInputTemplate.tsx b/packages/main/src/StepInputTemplate.tsx index ade109972401e..fb362d27de4c9 100644 --- a/packages/main/src/StepInputTemplate.tsx +++ b/packages/main/src/StepInputTemplate.tsx @@ -1,94 +1,31 @@ import type StepInput from "./StepInput.js"; -import Icon from "./Icon.js"; -import Input from "./Input.js"; - -import less from "@ui5/webcomponents-icons/dist/less.js"; -import add from "@ui5/webcomponents-icons/dist/add.js"; +import NumberInput from "./NumberInput.js"; export default function StepInputTemplate(this: StepInput) { return ( -
- {/* Decrement Icon */} - {!this.readonly && -
- -
- } - - {/* INPUT */} - - {this.valueStateMessage.length > 0 && - - } - - - {/* Increment Icon */} - {!this.readonly && -
- -
+ {this.valueStateMessage.length > 0 && + } -
+
); } diff --git a/packages/main/src/bundle.esm.ts b/packages/main/src/bundle.esm.ts index d916b641dfaca..17b1c6a2a47c5 100644 --- a/packages/main/src/bundle.esm.ts +++ b/packages/main/src/bundle.esm.ts @@ -106,6 +106,7 @@ import SliderHandle from "./SliderHandle.js"; import SliderScale from "./SliderScale.js"; import SplitButton from "./SplitButton.js"; import StepInput from "./StepInput.js"; +import NumberInput from "./NumberInput.js"; import RangeSlider from "./RangeSlider.js"; import Switch from "./Switch.js"; import MessageStrip from "./MessageStrip.js"; diff --git a/packages/main/src/i18n/messagebundle.properties b/packages/main/src/i18n/messagebundle.properties index 98ce112d64416..f4c19376b3b04 100644 --- a/packages/main/src/i18n/messagebundle.properties +++ b/packages/main/src/i18n/messagebundle.properties @@ -787,18 +787,17 @@ SLIDER_TOOLTIP_INPUT_DESCRIPTION = Press F2 to enter a value #XACT: ARIA label for slider tooltip input SLIDER_TOOLTIP_INPUT_LABEL = Current Value -#XTOL: tooltip for decrease button of the StepInput -STEPINPUT_DEC_ICON_TITLE=Decrease +#XTOL: tooltip for decrease button of the NumberInput +NUMBERINPUT_DEC_ICON_TITLE=Decrease -#XTOL: tooltip for increase button of the StepInput -STEPINPUT_INC_ICON_TITLE=Increase +#XTOL: tooltip for increase button of the NumberInput +NUMBERINPUT_INC_ICON_TITLE=Increase -STEPINPUT_PATTER_MISSMATCH=This format is not supported. Fill in a number with {0} decimal places. +NUMBERINPUT_PATTER_MISSMATCH=This format is not supported. Fill in a number with {0} decimal places. -STEPINPUT_RANGEOVERFLOW=Fill in a number that is lower than the set max. value of {0}. - -STEPINPUT_RANGEUNDERFLOW=Fill in a number that is higher than the set min. value of {0}. +NUMBERINPUT_RANGEOVERFLOW=Fill in a number that is lower than the set max. value of {0}. +NUMBERINPUT_RANGEUNDERFLOW=Fill in a number that is higher than the set min. value of {0}. #XACT: Aria information for the Split Button SPLIT_BUTTON_DESCRIPTION=Split Button diff --git a/packages/main/src/themes/NumberInput.css b/packages/main/src/themes/NumberInput.css new file mode 100644 index 0000000000000..931d4c62158c9 --- /dev/null +++ b/packages/main/src/themes/NumberInput.css @@ -0,0 +1,321 @@ +@import "./FormComponents.css"; +@import "./InvisibleTextStyles.css"; +@import "./InputIcon.css"; +@import "./InputSharedStyles.css"; + +:host(:not([hidden])) { + display: inline-block; + line-height: normal; + letter-spacing: normal; + word-spacing: normal; +} + +:host([disabled]) { + opacity: var(--_ui5_input_disabled_opacity); + cursor: default; + pointer-events: none; + background-color: var(--_ui5-input_disabled_background); + border-color: var(--_ui5_input_disabled_border_color); +} + +:host { + color: var(--sapField_TextColor); + background-color: var(--sapField_Background); + border: var(--_ui5_number_input_border_style); + border-radius: var(--sapField_BorderCornerRadius); + box-sizing: border-box; + height: var(--_ui5_input_height); + position: relative; + isolation: isolate; + overflow: hidden; + margin: var(--_ui5_input_margin_top_bottom) 0; + min-width: var(--_ui5_number_input_min_width); + text-align: right; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; +} + +[ui5-input] { + --ui5_input_focus_pseudo_element_content: none; +} + +:host .ui5-number-input-input { + text-align: inherit; + height: inherit; +} + +:host(:not([value-state]):not([readonly]):not([disabled])){ + box-shadow: none; +} + +:host([value-state="Information"]:not([readonly]):not([disabled])) { + border-width: var(--_ui5_input_information_border_width); +} + +:host([readonly]:not([disabled])) { + border-width: var(--sapElement_BorderWidth); +} + +:host([value-state="Negative"]:not([readonly]):not([disabled])), +:host([value-state="Critical"]:not([readonly]):not([disabled])), +:host([value-state="Information"]:not([readonly]):not([disabled])) { + border-style: var(--_ui5_number_input_error_warning_information_border_style); +} + +:host(:not([value-state]):not([readonly]):not([disabled]):hover), +:host([value-state="None"]:not([readonly]):not([disabled]):hover) { + background-color: var(--_ui5_number_input_border_color_hover); + border: var(--_ui5_number_input_border_hover); +} +:host(:not([value-state]):not([readonly]):not([disabled]):not([focused]):hover), +:host([value-state="None"]:not([readonly]):not([disabled]):not([focused]):hover) { + background-color: var(--sapField_Hover_Background); + border: var(--_ui5_number_input_border_style_hover); + box-shadow: var(--sapField_Hover_Shadow); +} + +:host([value-state="Positive"]:not([readonly]):not([disabled]):not([focused]):hover) { + box-shadow: var(--sapField_Hover_SuccessShadow); +} + +:host([value-state="Information"]:not([readonly]):not([disabled]):not([focused]):hover) { + box-shadow: var(--sapField_Hover_InformationShadow); +} + +:host([value-state="Critical"]:not([readonly]):not([disabled]):not([focused]):hover) { + box-shadow: var(--sapField_Hover_WarningShadow); +} + +:host([value-state="Negative"]:not([readonly]):not([disabled]):not([focused]):hover) { + box-shadow: var(--sapField_Hover_InvalidShadow); +} + +:host([value-state="Positive"]:not([readonly]):not([disabled]):hover), +:host([value-state="Negative"]:not([readonly]):not([disabled]):hover), +:host([value-state="Information"]:not([readonly]):not([disabled]):hover), +:host([value-state="Critical"]:not([readonly]):not([disabled]):hover) { + background-color: var(--_ui5_number_input_button_state_hover_background_color); +} + +:host(:not([value-state]):not([readonly]):not([disabled])[focused]), +:host([value-state="None"]:not([readonly]):not([disabled])[focused]), +:host([value-state="Positive"]:not([readonly]):not([disabled])[focused]), +:host([value-state="Information"]:not([readonly]):not([disabled])[focused]), +:host([value-state="Critical"]:not([readonly]):not([disabled])[focused]){ + background-color: var(--sapField_Focus_Background); +} + +:host([value-state="Negative"]:not([readonly]):not([disabled])[focused]) { + background-color: var(--_ui5_number_input_error_focused_background); +} + +:host([value-state="Negative"]:not([readonly]):not([disabled]):hover), +:host([value-state="Negative"]:not([readonly]):not([disabled])[focused]:hover) { + background-color: var(--_ui5_number_input_error_hover_background); +} + +:host([value-state="Positive"]:not([readonly]):not([disabled]))::after, +:host([value-state="Negative"]:not([readonly]):not([disabled]))::after, +:host([value-state="None"]:not([readonly]):not([disabled]))::after, +:host([value-state="Information"]:not([readonly]):not([disabled]))::after, +:host([value-state="Critical"]:not([readonly]):not([disabled]))::after { + position: absolute; + content: ""; + top: -1px; + right: -1px; + bottom: -1px; + left: -1px; + outline: none; + pointer-events: none; + border-radius: var(--sapField_BorderCornerRadius); + border-style: var(--_ui5_number_input_after_border_style); + z-index: 0; + border-width: 0px; +} + +:host([value-state="Information"]:not([readonly]):not([disabled]))::after { + border-color: var(--sapField_InformationColor); + border-width: var(--_ui5_input_information_border_width); +} + +:host([value-state="Critical"]:not([readonly]):not([disabled]))::after { + border-color: var(--sapField_WarningColor); + border-width: 2px; +} + +:host([value-state="Positive"]:not([readonly]):not([disabled]))::after { + border-color: var(--sapField_SuccessColor); + border-width: 1px; +} + +:host([value-state="Negative"]:not([readonly]):not([disabled]))::after { + border-color: var(--sapField_InvalidColor); + border-width: var(--_ui5_input_information_border_width); +} + +:host([value-state])::after { + border-width: var(--_ui5_input_state_border_width); +} +:host([value-state="Negative"]:not([readonly]):not([disabled])) .ui5-number-input-input { + background-color: var(--_ui5_number_input_error_background_color); +} + +:host([value-state]:not([value-state="None"]) .ui5-number-input-input[focused]) { + outline: none; +} + +:host .ui5-number-input-input { + width: 100%; + color: inherit; + background-color: inherit; + border: var(--_ui5_number_input_input_border); + box-sizing: border-box; + vertical-align: top; + margin-top: var(--_ui5_number_input_input_margin_top); + min-width: var(--_ui5_number_input_min_width); + position: relative; + outline: none; + line-height: inherit; + letter-spacing: inherit; + word-spacing: inherit; +} + +:host .ui5-number-input-root.ui5-number-input-root--with-buttons .ui5-number-input-input { + padding-inline-start: var(--_ui5_number_input_padding_with_step_buttons); + padding-inline-end: var(--_ui5_number_input_padding_with_step_buttons); +} + +:host .ui5-number-input-input[readonly] { + padding: 0; +} + +:host .ui5-number-input-input:hover, +:host .ui5-number-input-input[focused]{ + box-shadow: none; +} + +:host .ui5-number-input-root { + line-height: inherit; + letter-spacing: inherit; + word-spacing: inherit; + height: inherit; +} + +:host .ui5-number-input-input::part(input) { + white-space: nowrap; +} + +:host .ui5-number-input-input[text-align=left]::part(input) { + text-align: left; +} + +:host .ui5-number-input-input[text-align=center]::part(input) { + text-align: center; +} + +:host .ui5-number-input-input[text-align=right]::part(input) { + text-align: right; +} + +::slotted([slot="valueStateMessage"]) { + text-align: left; +} + +:host .ui5-step-icon { + position: absolute; + display: var(--_ui5_number_input_button_display); + height: 2rem; + height: 100%; + background-color: var(--_ui5_number_input_button_background_color); + z-index: 0; +} + +:host .ui5-step-icon[focused] { + border: none; + outline: none; +} + +:host .ui5-step-icon.ui5-step-dec { + left: var(--_ui5_number_input_button_left); + z-index: 1; +} + +:host .ui5-step-icon.ui5-step-inc { + right: var(--_ui5_number_input_button_right); +} + +:host .ui5-step-icon *:not(.ui5-number-input-icon--clickable), +:host .ui5-step-icon *:not(.ui5-number-input-icon--clickable):active, +:host .ui5-step-icon *:not(.ui5-number-input-icon--clickable):hover { + opacity: 0.5; + background-color: transparent; + color: var(--sapContent_IconColor); +} + +:host .ui5-step-icon :not(.ui5-number-input-icon--clickable) *:hover, +:host .ui5-step-icon :not(.ui5-number-input-icon--clickable) *:active { + background-color: var(--sapField_Background); + color: var(--sapContent_IconColor); +} + +:host .ui5-number-input-input[focused]::after { + position: absolute; + content: ""; + border: var(--_ui5_number_input_input_border_focused_after); + top: var(--_ui5_number_input_input_border_top_bottom_focused_after); + right: 0px; + bottom: var(--_ui5_number_input_input_border_bottom_focused_after); + border-radius: var(--_ui5_number_input_input_border_radius_focused_after); + left: 0px; + outline: none; + pointer-events: none; + z-index: 1; +} + +:host([readonly]) .ui5-number-input-input[focused]::after{ + top: var(--_ui5_input_readonly_focus_offset); + bottom: var(--_ui5_input_readonly_focus_offset); + left: var(--_ui5_input_readonly_focus_offset); + right: var(--_ui5_input_readonly_focus_offset); + border-radius: var(--_ui5_input_readonly_focus_border_radius); + border-color: var(--sapContent_FocusColor); +} + +:host .ui5-number-input-input[focused] { + outline: none; +} + +:host([value-state="Information"]:not([readonly]):not([disabled])) .ui5-number-input-input[focused]::after { + border-color: var(--_ui5_number_input_input_information_border_color_focused_after); + top: var(--_ui5_number_input_input_border_top_information_focused_after); + bottom: var(--_ui5_number_input_input_border_bottom_information_focused_after); +} + +:host([value-state="Critical"]:not([readonly]):not([disabled])) .ui5-number-input-input[focused]::after { + border-color: var(--_ui5_number_input_input_warning_border_color_focused_after); +} + +:host([value-state="Positive"]:not([readonly]):not([disabled])) .ui5-number-input-input[focused]::after { + border-color: var(--_ui5_number_input_input_success_border_color_focused_after); +} + +:host([value-state="Negative"]:not([readonly]):not([disabled])) .ui5-number-input-input[focused]::after { + border-color: var(--_ui5_number_input_input_error_border_color_focused_after); +} + +/* Disable spin buttons in Chrome, Safari, Edge, Opera */ +:host .ui5-number-input-input::-webkit-outer-spin-button, +:host .ui5-number-input-input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +:host([disabled]) .ui5-step-icon { + background-color: var(--_ui5_number_input_disabled_button_background); +} + +:host([disabled]) .ui5-step-icon [ui5-icon] { + color: var(--sapField_ReadOnly_BorderColor); +} diff --git a/packages/main/src/themes/StepInput.css b/packages/main/src/themes/StepInput.css index 8fad78bff4bf8..deb7dfa70eb25 100644 --- a/packages/main/src/themes/StepInput.css +++ b/packages/main/src/themes/StepInput.css @@ -1,279 +1,3 @@ -@import "./FormComponents.css"; -@import "./InvisibleTextStyles.css"; -@import "./InputIcon.css"; -@import "./InputSharedStyles.css"; - :host(:not([hidden])) { display: inline-block; - line-height: normal; - letter-spacing: normal; - word-spacing: normal; -} - -:host { - color: var(--sapField_TextColor); - background-color: var(--sapField_Background); - border: var(--_ui5_step_input_border_style); - border-radius: var(--sapField_BorderCornerRadius); - box-sizing: border-box; - height: var(--_ui5_input_height); - position: relative; - isolation: isolate; - margin: var(--_ui5_input_margin_top_bottom) 0; - min-width: var(--_ui5_step_input_min_width); - text-align: right; - user-select: none; - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; -} - -[ui5-input] { - --ui5_input_focus_pseudo_element_content: none; -} - -:host .ui5-step-input-input { - text-align: inherit; - height: inherit; -} - -:host(:not([value-state]):not([readonly]):not([disabled])){ - box-shadow: none; -} - -:host(:not([value-state]):not([readonly]):not([disabled]):hover), -:host([value-state="None"]:not([readonly]):not([disabled]):hover) { - background-color: var(--_ui5_step_input_border_color_hover); - border: var(--_ui5_step_input_border_hover); -} -:host(:not([value-state]):not([readonly]):not([disabled]):not([focused]):hover), -:host([value-state="None"]:not([readonly]):not([disabled]):not([focused]):hover) { - background-color: var(--sapField_Hover_Background); - border: var(--_ui5_step_input_border_style_hover); - box-shadow: var(--sapField_Hover_Shadow); -} - -:host([value-state="Positive"]:not([readonly]):not([disabled]):not([focused]):hover) { - box-shadow: var(--sapField_Hover_SuccessShadow); -} - -:host([value-state="Information"]:not([readonly]):not([disabled]):not([focused]):hover) { - box-shadow: var(--sapField_Hover_InformationShadow); -} - -:host([value-state="Critical"]:not([readonly]):not([disabled]):not([focused]):hover) { - box-shadow: var(--sapField_Hover_WarningShadow); -} - -:host([value-state="Negative"]:not([readonly]):not([disabled]):not([focused]):hover) { - box-shadow: var(--sapField_Hover_InvalidShadow); -} - -:host([value-state="Positive"]:not([readonly]):not([disabled]):hover), -:host([value-state="Negative"]:not([readonly]):not([disabled]):hover), -:host([value-state="Information"]:not([readonly]):not([disabled]):hover), -:host([value-state="Critical"]:not([readonly]):not([disabled]):hover) { - background-color: var(--_ui5-step_input_button_state_hover_background_color); -} - -:host(:not([value-state]):not([readonly]):not([disabled])[focused]), -:host([value-state="None"]:not([readonly]):not([disabled])[focused]), -:host([value-state="Positive"]:not([readonly]):not([disabled])[focused]), -:host([value-state="Negative"]:not([readonly]):not([disabled])[focused]), -:host([value-state="Information"]:not([readonly]):not([disabled])[focused]), -:host([value-state="Critical"]:not([readonly]):not([disabled])[focused]){ - background-color: var(--sapField_Focus_Background); -} - -:host([value-state="Positive"]:not([readonly]):not([disabled]))::after, -:host([value-state="Negative"]:not([readonly]):not([disabled]))::after, -:host([value-state="None"]:not([readonly]):not([disabled]))::after, -:host([value-state="Information"]:not([readonly]):not([disabled]))::after, -:host([value-state="Critical"]:not([readonly]):not([disabled]))::after { - position: absolute; - content: ""; - top: -1px; - right: -1px; - bottom: -1px; - left: -1px; - outline: none; - pointer-events: none; - border-radius: var(--sapField_BorderCornerRadius); - border-style: var(--_ui5_input_error_warning_border_style); - z-index: 0; - border-width: 0px; -} - -:host([value-state="Information"]:not([readonly]):not([disabled]))::after { - border-color: var(--sapField_InformationColor); - border-width: var(--_ui5_input_information_border_width); -} - -:host([value-state="Critical"]:not([readonly]):not([disabled]))::after { - border-color: var(--sapField_WarningColor); - border-width: 2px; -} - -:host([value-state="Positive"]:not([readonly]):not([disabled]))::after { - border-color: var(--sapField_SuccessColor); - border-width: 1px; -} - -:host([value-state="Negative"]:not([readonly]):not([disabled]))::after { - border-color: var(--sapField_InvalidColor); - border-width: var(--_ui5_input_information_border_width); -} - -:host([value-state])::after { - border-width: var(--_ui5_input_state_border_width); -} -:host([value-state="Negative"]:not([readonly]):not([disabled])) .ui5-step-input-input { - background-color: var(--_ui5_input_input_background_color); -} - -:host([value-state="Negative"]:not([readonly]):not([disabled])) .ui5-step-input-input:hover { - background-color: var(--_ui5_step_input_input_error_background_color); -} -:host([value-state]:not([value-state="None"]) .ui5-step-input-input[focused]) { - outline: none; -} - -:host .ui5-step-input-input { - width: 100%; - color: inherit; - background-color: inherit; - border: var(--_ui5_step_input_input_border); - box-sizing: border-box; - vertical-align: top; - margin-top: var(--_ui5_step_input_input_margin_top); - min-width: var(--_ui5_step_input_min_width); - padding-inline-start: var(--_ui5_step_input_padding); - padding-inline-end: var(--_ui5_step_input_padding); - position: relative; - outline: none; - line-height: inherit; - letter-spacing: inherit; - word-spacing: inherit; -} - -:host .ui5-step-input-input[readonly] { - padding: 0; -} - -:host .ui5-step-input-input:hover, -:host .ui5-step-input-input[focused]{ - box-shadow: none; -} - -:host .ui5-step-input-root { - line-height: inherit; - letter-spacing: inherit; - word-spacing: inherit; - height: inherit; -} - -:host .ui5-step-input-input::part(input) { - white-space: nowrap; -} - -:host .ui5-step-input-input[text-align=left]::part(input) { - text-align: left; -} - -:host .ui5-step-input-input[text-align=center]::part(input) { - text-align: center; -} - -:host .ui5-step-input-input[text-align=right]::part(input) { - text-align: right; -} - -::slotted([slot="valueStateMessage"]) { - text-align: left; -} - -:host .ui5-step-icon { - position: absolute; - display: var(--_ui5_step_input_button_display); - height: 2rem; - height: 100%; - background-color: var(--_ui5_step_input_button_background_color); - z-index: 0; -} - -:host .ui5-step-icon[focused] { - border: none; - outline: none; -} - -:host .ui5-step-icon.ui5-step-dec { - left: var(--_ui5_step_input_button_left); - z-index: 1; -} - -:host .ui5-step-icon.ui5-step-inc { - right: var(--_ui5_step_input_button_right); -} - -:host .ui5-step-icon *:not(.ui5-step-input-icon--clickable), -:host .ui5-step-icon *:not(.ui5-step-input-icon--clickable):active, -:host .ui5-step-icon *:not(.ui5-step-input-icon--clickable):hover { - opacity: 0.5; - background-color: transparent; - color: var(--sapContent_IconColor); -} - -:host .ui5-step-icon :not(.ui5-step-input-icon--clickable) *:hover, -:host .ui5-step-icon :not(.ui5-step-input-icon--clickable) *:active { - background-color: var(--sapField_Background); - color: var(--sapContent_IconColor); -} - -:host .ui5-step-input-input[focused]::after { - position: absolute; - content: ""; - border: var(--_ui5_step_input_input_border_focused_after); - top: var(--_ui5_step_input_input_border_top_bottom_focused_after); - right: 0px; - bottom: var(--_ui5_step_input_input_border_top_bottom_focused_after); - border-radius: var(--_ui5_step_input_input_border_radius_focused_after); - left: 0px; - outline: none; - pointer-events: none; - z-index: 1; -} - -:host .ui5-step-input-input[focused] { - outline: none; -} - -:host([value-state="Information"]:not([readonly]):not([disabled])) .ui5-step-input-input[focused]::after { - border-color: var(--_ui5_step_input_input_information_border_color_focused_after); -} - -:host([value-state="Critical"]:not([readonly]):not([disabled])) .ui5-step-input-input[focused]::after { - border-color: var(--_ui5_step_input_input_warning_border_color_focused_after); -} - -:host([value-state="Positive"]:not([readonly]):not([disabled])) .ui5-step-input-input[focused]::after { - border-color: var(--_ui5_step_input_input_success_border_color_focused_after); -} - -:host([value-state="Negative"]:not([readonly]):not([disabled])) .ui5-step-input-input[focused]::after { - border-color: var(--_ui5_step_input_input_error_border_color_focused_after); -} - -/* Disable spin buttons in Chrome, Safari, Edge, Opera */ -:host .ui5-step-input-input::-webkit-outer-spin-button, -:host .ui5-step-input-input::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; -} - -:host([disabled]) .ui5-step-icon { - background-color: var(--_ui5_step_input_disabled_button_background); -} - -:host([disabled]) .ui5-step-icon [ui5-icon] { - color: var(--sapField_ReadOnly_BorderColor); } diff --git a/packages/main/src/themes/base/NumberInput-parameters.css b/packages/main/src/themes/base/NumberInput-parameters.css new file mode 100644 index 0000000000000..7a89c0f0db5b4 --- /dev/null +++ b/packages/main/src/themes/base/NumberInput-parameters.css @@ -0,0 +1,32 @@ +:host { + --_ui5_number_input_error_focused_background: var(--sapField_Focus_Background); + --_ui5_number_input_button_state_hover_background_color: var(--sapField_Background); + --_ui5_number_input_error_hover_background: var(--sapField_Background); + --_ui5_number_input_border_style: 1px solid var(--sapField_BorderColor);; + --_ui5_number_input_border_style_hover: 1px solid var(--sapField_Hover_BorderColor); + --_ui5_number_input_error_warning_information_border_style: none; + --_ui5_number_input_after_border_style: var(--_ui5_input_error_warning_border_style); + --_ui5_number_input_error_background_color: var(--_ui5_input_input_background_color); + --_ui5_number_input_error_background_color_hover: var(--_ui5_number_input_input_error_background_color); + --_ui5_number_input_button_background_color:var(--sapField_Background); + --_ui5_number_input_input_border: 1px solid transparent; + --_ui5_number_input_input_margin_top: -0.0625rem; + --_ui5_number_input_button_display: inline-block; + --_ui5_number_input_button_left: 0; + --_ui5_number_input_button_right: 0; + --_ui5_number_input_input_border_focused_after: var(--_ui5_input_focus_border_width) dotted var(--sapField_Active_BorderColor); + --_ui5_number_input_input_border_top_bottom_focused_after: 0.0625rem; + --_ui5_number_input_input_border_bottom_focused_after: 0.0625rem; + --_ui5_number_input_input_border_top_information_focused_after: 0.0625rem; + --_ui5_number_input_input_border_bottom_information_focused_after: 0.0625rem; + --_ui5_number_input_input_border_radius_focused_after: 0; + --_ui5_number_input_input_information_border_color_focused_after: var(--sapField_BorderColor); + --_ui5_number_input_input_warning_border_color_focused_after: var(--sapField_BorderColor); + --_ui5_number_input_input_success_border_color_focused_after: var(--sapField_BorderColor); + --_ui5_number_input_input_error_border_color_focused_after: var(--sapField_BorderColor); + --_ui5_number_input_disabled_button_background: var(--sapField_ReadOnly_Background); + --_ui5_number_input_border_color_hover: var(--sapField_Hover_Background); + --_ui5_number_input_border_hover: 1px solid var(--sapField_Hover_BorderColor); + --_ui5_input_input_background_color: var(--sapField_InvalidBackground); + --_ui5_number_input_padding_with_step_buttons: 2.5rem; +} diff --git a/packages/main/src/themes/base/StepInput-parameters.css b/packages/main/src/themes/base/StepInput-parameters.css deleted file mode 100644 index a203447b9be53..0000000000000 --- a/packages/main/src/themes/base/StepInput-parameters.css +++ /dev/null @@ -1,25 +0,0 @@ -:host { - --_ui5_step_input_input_error_background_color: var(--sapField_InvalidBackground); - --_ui5-step_input_button_state_hover_background_color: var(--sapField_Background); - --_ui5_step_input_border_style: 1px solid var(--sapField_BorderColor); - --_ui5_step_input_border_style_hover: 1px solid var(--sapField_Hover_BorderColor); - --_ui5_step_input_button_background_color:var(--sapField_Background); - --_ui5_step_input_input_border: 1px solid transparent; - --_ui5_step_input_input_margin_top: -0.0625rem; - --_ui5_step_input_button_display: inline-block; - --_ui5_step_input_button_left: 0; - --_ui5_step_input_button_right: 0; - --_ui5_step_input_input_border_focused_after: var(--_ui5_input_focus_border_width) dotted var(--sapContent_FocusColor); - --_ui5_step_input_input_border_top_bottom_focused_after: 0.0625rem; - --_ui5_step_input_input_border_radius_focused_after: 0; - --_ui5_step_input_input_information_border_color_focused_after: var(--sapField_BorderColor); - --_ui5_step_input_input_warning_border_color_focused_after: var(--sapField_BorderColor); - --_ui5_step_input_input_success_border_color_focused_after: var(--sapField_BorderColor); - --_ui5_step_input_input_error_border_color_focused_after: var(--sapField_BorderColor); - --_ui5_step_input_disabled_button_background: var(--sapField_ReadOnly_Background); - --_ui5_step_input_border_color_hover: var(--sapField_Hover_Background); - --_ui5_step_input_border_hover: 1px solid var(--sapField_Hover_BorderColor); - --_ui5_input_input_background_color: var(--sapField_InvalidBackground); - --_ui5_step_input_min_width: 7.25rem; - --_ui5_step_input_padding: 2.5rem; -} \ No newline at end of file diff --git a/packages/main/src/themes/base/sizes-parameters.css b/packages/main/src/themes/base/sizes-parameters.css index c17367ca2bedd..1d4f9f8c082f4 100644 --- a/packages/main/src/themes/base/sizes-parameters.css +++ b/packages/main/src/themes/base/sizes-parameters.css @@ -351,9 +351,9 @@ /* SplitButton */ --_ui5_split_button_middle_separator_height: 1.625rem; - /* StepInput */ - --_ui5_step_input_min_width: 6rem; - --_ui5_step_input_padding: 2rem; + /* NumberInput / StepInput */ + --_ui5_number_input_min_width: 6rem; + --_ui5_number_input_padding_with_step_buttons: 2rem; /* Tree */ --_ui5-tree-indent-step: 0.5rem; diff --git a/packages/main/src/themes/sap_fiori_3/NumverInput-parameters b/packages/main/src/themes/sap_fiori_3/NumverInput-parameters new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/main/src/themes/sap_fiori_3/parameters-bundle.css b/packages/main/src/themes/sap_fiori_3/parameters-bundle.css index e29d102d76409..dcb1e9caf17cb 100644 --- a/packages/main/src/themes/sap_fiori_3/parameters-bundle.css +++ b/packages/main/src/themes/sap_fiori_3/parameters-bundle.css @@ -56,6 +56,6 @@ @import "./SliderBase-parameters.css"; @import "./SliderHandle-parameters.css"; @import "./SliderScale-parameters.css"; -@import "../base/StepInput-parameters.css"; +@import "../base/NumberInput-parameters.css"; @import "../base/rtl-parameters.css"; @import "./sizes-parameters.css"; diff --git a/packages/main/src/themes/sap_fiori_3_dark/parameters-bundle.css b/packages/main/src/themes/sap_fiori_3_dark/parameters-bundle.css index 02e5ef577e543..45ff5ff91d69e 100644 --- a/packages/main/src/themes/sap_fiori_3_dark/parameters-bundle.css +++ b/packages/main/src/themes/sap_fiori_3_dark/parameters-bundle.css @@ -55,6 +55,6 @@ @import "./SliderBase-parameters.css"; @import "./SliderHandle-parameters.css"; @import "./SliderScale-parameters.css"; -@import "../base/StepInput-parameters.css"; +@import "../base/NumberInput-parameters.css"; @import "../base/rtl-parameters.css"; @import "./sizes-parameters.css"; diff --git a/packages/main/src/themes/sap_fiori_3_hcb/parameters-bundle.css b/packages/main/src/themes/sap_fiori_3_hcb/parameters-bundle.css index 2a7bfd3a340ee..776679fcd06da 100644 --- a/packages/main/src/themes/sap_fiori_3_hcb/parameters-bundle.css +++ b/packages/main/src/themes/sap_fiori_3_hcb/parameters-bundle.css @@ -57,6 +57,6 @@ @import "./SliderBase-parameters.css"; @import "./SliderHandle-parameters.css"; @import "./SliderScale-parameters.css"; -@import "../base/StepInput-parameters.css"; +@import "../base/NumberInput-parameters.css"; @import "./sizes-parameters.css"; @import "../base/rtl-parameters.css"; diff --git a/packages/main/src/themes/sap_fiori_3_hcw/parameters-bundle.css b/packages/main/src/themes/sap_fiori_3_hcw/parameters-bundle.css index 4c1b398cbd14c..9689bd2a4b577 100644 --- a/packages/main/src/themes/sap_fiori_3_hcw/parameters-bundle.css +++ b/packages/main/src/themes/sap_fiori_3_hcw/parameters-bundle.css @@ -57,6 +57,6 @@ @import "./SliderBase-parameters.css"; @import "./SliderHandle-parameters.css"; @import "./SliderScale-parameters.css"; -@import "../base/StepInput-parameters.css"; +@import "../base/NumberInput-parameters.css"; @import "./sizes-parameters.css"; @import "../base/rtl-parameters.css"; diff --git a/packages/main/src/themes/sap_horizon/NumberInput-parameters.css b/packages/main/src/themes/sap_horizon/NumberInput-parameters.css new file mode 100644 index 0000000000000..66a453ec13bd0 --- /dev/null +++ b/packages/main/src/themes/sap_horizon/NumberInput-parameters.css @@ -0,0 +1,26 @@ +@import "../base/NumberInput-parameters.css"; + +:host { + --_ui5_number_input_input_error_background_color: inherit; + --_ui5_number_input_button_state_hover_background_color: var(--sapField_Hover_Background); + --_ui5_number_input_border_style: none; + --_ui5_number_input_border_style_hover: none; + --_ui5_number_input_button_background_color: transparent; + --_ui5_number_input_input_border: none; + --_ui5_number_input_input_margin_top: 0; + --_ui5_number_input_button_display: inline-flex; + --_ui5_number_input_button_left: 0; + --_ui5_number_input_button_right: 0; + --_ui5_number_input_input_border_focused_after: 0.125rem solid var(--sapField_Active_BorderColor); + --_ui5_number_input_input_border_top_bottom_focused_after: 0; + --_ui5_number_input_input_border_bottom_focused_after: 0; + --_ui5_number_input_input_border_radius_focused_after: 0.25rem; + --_ui5_number_input_input_information_border_color_focused_after: var(--sapField_InformationColor); + --_ui5_number_input_input_warning_border_color_focused_after: var(--sapField_WarningColor); + --_ui5_number_input_input_success_border_color_focused_after: var(--sapField_SuccessColor); + --_ui5_number_input_input_error_border_color_focused_after: var(--sapField_InvalidColor); + --_ui5_number_input_disabled_button_background: none; + --_ui5_number_input_border_color_hover: none; + --_ui5_number_input_border_hover: none; + --_ui5_input_input_background_color: transparent; +} diff --git a/packages/main/src/themes/sap_horizon/StepInput-parameters.css b/packages/main/src/themes/sap_horizon/StepInput-parameters.css deleted file mode 100644 index 48da6c6c504c1..0000000000000 --- a/packages/main/src/themes/sap_horizon/StepInput-parameters.css +++ /dev/null @@ -1,25 +0,0 @@ -@import "../base/StepInput-parameters.css"; - -:host { - --_ui5_step_input_input_error_background_color: inherit; - --_ui5-step_input_button_state_hover_background_color: var(--sapField_Hover_Background); - --_ui5_step_input_border_style: none; - --_ui5_step_input_border_style_hover: none; - --_ui5_step_input_button_background_color: transparent; - --_ui5_step_input_input_border: none; - --_ui5_step_input_input_margin_top: 0; - --_ui5_step_input_button_display: inline-flex; - --_ui5_step_input_button_left: 0; - --_ui5_step_input_button_right: 0; - --_ui5_step_input_input_border_focused_after: 0.125rem solid #0070f2; - --_ui5_step_input_input_border_top_bottom_focused_after: 0; - --_ui5_step_input_input_border_radius_focused_after: 0.25rem; - --_ui5_step_input_input_information_border_color_focused_after: var(--sapField_InformationColor); - --_ui5_step_input_input_warning_border_color_focused_after: var(--sapField_WarningColor); - --_ui5_step_input_input_success_border_color_focused_after: var(--sapField_SuccessColor); - --_ui5_step_input_input_error_border_color_focused_after: var(--sapField_InvalidColor); - --_ui5_step_input_disabled_button_background: none; - --_ui5_step_input_border_color_hover: none; - --_ui5_step_input_border_hover: none; - --_ui5_input_input_background_color: transparent; -} \ No newline at end of file diff --git a/packages/main/src/themes/sap_horizon/parameters-bundle.css b/packages/main/src/themes/sap_horizon/parameters-bundle.css index 2a278265c0986..db50339dc815b 100644 --- a/packages/main/src/themes/sap_horizon/parameters-bundle.css +++ b/packages/main/src/themes/sap_horizon/parameters-bundle.css @@ -61,7 +61,7 @@ @import "./Slider-parameters.css"; @import "./ValueStateMessage-parameters.css"; @import "../base/Toolbar-parameters.css"; -@import "./StepInput-parameters.css"; +@import "./NumberInput-parameters.css"; @import "./GrowingButton-parameters.css"; @import "./sizes-parameters.css"; @import "./rtl-parameters.css"; \ No newline at end of file diff --git a/packages/main/src/themes/sap_horizon_dark/NumberInput-parameters.css b/packages/main/src/themes/sap_horizon_dark/NumberInput-parameters.css new file mode 100644 index 0000000000000..66a453ec13bd0 --- /dev/null +++ b/packages/main/src/themes/sap_horizon_dark/NumberInput-parameters.css @@ -0,0 +1,26 @@ +@import "../base/NumberInput-parameters.css"; + +:host { + --_ui5_number_input_input_error_background_color: inherit; + --_ui5_number_input_button_state_hover_background_color: var(--sapField_Hover_Background); + --_ui5_number_input_border_style: none; + --_ui5_number_input_border_style_hover: none; + --_ui5_number_input_button_background_color: transparent; + --_ui5_number_input_input_border: none; + --_ui5_number_input_input_margin_top: 0; + --_ui5_number_input_button_display: inline-flex; + --_ui5_number_input_button_left: 0; + --_ui5_number_input_button_right: 0; + --_ui5_number_input_input_border_focused_after: 0.125rem solid var(--sapField_Active_BorderColor); + --_ui5_number_input_input_border_top_bottom_focused_after: 0; + --_ui5_number_input_input_border_bottom_focused_after: 0; + --_ui5_number_input_input_border_radius_focused_after: 0.25rem; + --_ui5_number_input_input_information_border_color_focused_after: var(--sapField_InformationColor); + --_ui5_number_input_input_warning_border_color_focused_after: var(--sapField_WarningColor); + --_ui5_number_input_input_success_border_color_focused_after: var(--sapField_SuccessColor); + --_ui5_number_input_input_error_border_color_focused_after: var(--sapField_InvalidColor); + --_ui5_number_input_disabled_button_background: none; + --_ui5_number_input_border_color_hover: none; + --_ui5_number_input_border_hover: none; + --_ui5_input_input_background_color: transparent; +} diff --git a/packages/main/src/themes/sap_horizon_dark/StepInput-parameters.css b/packages/main/src/themes/sap_horizon_dark/StepInput-parameters.css deleted file mode 100644 index 48da6c6c504c1..0000000000000 --- a/packages/main/src/themes/sap_horizon_dark/StepInput-parameters.css +++ /dev/null @@ -1,25 +0,0 @@ -@import "../base/StepInput-parameters.css"; - -:host { - --_ui5_step_input_input_error_background_color: inherit; - --_ui5-step_input_button_state_hover_background_color: var(--sapField_Hover_Background); - --_ui5_step_input_border_style: none; - --_ui5_step_input_border_style_hover: none; - --_ui5_step_input_button_background_color: transparent; - --_ui5_step_input_input_border: none; - --_ui5_step_input_input_margin_top: 0; - --_ui5_step_input_button_display: inline-flex; - --_ui5_step_input_button_left: 0; - --_ui5_step_input_button_right: 0; - --_ui5_step_input_input_border_focused_after: 0.125rem solid #0070f2; - --_ui5_step_input_input_border_top_bottom_focused_after: 0; - --_ui5_step_input_input_border_radius_focused_after: 0.25rem; - --_ui5_step_input_input_information_border_color_focused_after: var(--sapField_InformationColor); - --_ui5_step_input_input_warning_border_color_focused_after: var(--sapField_WarningColor); - --_ui5_step_input_input_success_border_color_focused_after: var(--sapField_SuccessColor); - --_ui5_step_input_input_error_border_color_focused_after: var(--sapField_InvalidColor); - --_ui5_step_input_disabled_button_background: none; - --_ui5_step_input_border_color_hover: none; - --_ui5_step_input_border_hover: none; - --_ui5_input_input_background_color: transparent; -} \ No newline at end of file diff --git a/packages/main/src/themes/sap_horizon_dark/parameters-bundle.css b/packages/main/src/themes/sap_horizon_dark/parameters-bundle.css index f40f6d99ae2f2..f0414c31eede0 100644 --- a/packages/main/src/themes/sap_horizon_dark/parameters-bundle.css +++ b/packages/main/src/themes/sap_horizon_dark/parameters-bundle.css @@ -60,7 +60,7 @@ @import "./Tokenizer-parameters.css"; @import "./SliderBase-parameters.css"; @import "./ValueStateMessage-parameters.css"; -@import "./StepInput-parameters.css"; +@import "./NumberInput-parameters.css"; @import "./GrowingButton-parameters.css"; @import "./sizes-parameters.css"; @import "./rtl-parameters.css"; \ No newline at end of file diff --git a/packages/main/src/themes/sap_horizon_hcb/NumberInput-parameters.css b/packages/main/src/themes/sap_horizon_hcb/NumberInput-parameters.css new file mode 100644 index 0000000000000..339b92d1f55e4 --- /dev/null +++ b/packages/main/src/themes/sap_horizon_hcb/NumberInput-parameters.css @@ -0,0 +1,14 @@ +@import "../base/NumberInput-parameters.css"; + +:host { + --_ui5_number_input_border_style: 2px solid var(--sapField_BorderColor); + --_ui5_number_input_border_style_hover: 2px solid var(--sapField_Hover_BorderColor); + --_ui5_number_input_error_warning_information_border_style: dashed; + --_ui5_number_input_after_border_style: none; + --_ui5_number_input_error_background_color: transparent; + --_ui5_number_input_error_hover_background: var(--sapField_InvalidBackground); + --_ui5_number_input_error_focused_background: var(--sapField_InvalidBackground); + --_ui5_number_input_border_hover: 2px solid var(--sapField_Hover_BorderColor); + --_ui5_number_input_input_border_top_bottom_focused_after: 0.125rem; + --_ui5_number_input_input_border_bottom_focused_after: 0.1875rem; +} diff --git a/packages/main/src/themes/sap_horizon_hcb/parameters-bundle.css b/packages/main/src/themes/sap_horizon_hcb/parameters-bundle.css index d7380e0eaed8f..14f3b393ce5d5 100644 --- a/packages/main/src/themes/sap_horizon_hcb/parameters-bundle.css +++ b/packages/main/src/themes/sap_horizon_hcb/parameters-bundle.css @@ -57,6 +57,7 @@ @import "./SliderBase-parameters.css"; @import "./SliderHandle-parameters.css"; @import "./SliderScale-parameters.css"; -@import "../base/StepInput-parameters.css"; +@import "../base/NumberInput-parameters.css"; +@import "./NumberInput-parameters.css"; @import "./sizes-parameters.css"; @import "./rtl-parameters.css"; diff --git a/packages/main/src/themes/sap_horizon_hcw/NumberInput-parameters.css b/packages/main/src/themes/sap_horizon_hcw/NumberInput-parameters.css new file mode 100644 index 0000000000000..339b92d1f55e4 --- /dev/null +++ b/packages/main/src/themes/sap_horizon_hcw/NumberInput-parameters.css @@ -0,0 +1,14 @@ +@import "../base/NumberInput-parameters.css"; + +:host { + --_ui5_number_input_border_style: 2px solid var(--sapField_BorderColor); + --_ui5_number_input_border_style_hover: 2px solid var(--sapField_Hover_BorderColor); + --_ui5_number_input_error_warning_information_border_style: dashed; + --_ui5_number_input_after_border_style: none; + --_ui5_number_input_error_background_color: transparent; + --_ui5_number_input_error_hover_background: var(--sapField_InvalidBackground); + --_ui5_number_input_error_focused_background: var(--sapField_InvalidBackground); + --_ui5_number_input_border_hover: 2px solid var(--sapField_Hover_BorderColor); + --_ui5_number_input_input_border_top_bottom_focused_after: 0.125rem; + --_ui5_number_input_input_border_bottom_focused_after: 0.1875rem; +} diff --git a/packages/main/src/themes/sap_horizon_hcw/parameters-bundle.css b/packages/main/src/themes/sap_horizon_hcw/parameters-bundle.css index 0ba2a8c792640..9fdc7333cf1ed 100644 --- a/packages/main/src/themes/sap_horizon_hcw/parameters-bundle.css +++ b/packages/main/src/themes/sap_horizon_hcw/parameters-bundle.css @@ -57,6 +57,7 @@ @import "./SliderBase-parameters.css"; @import "./SliderHandle-parameters.css"; @import "./SliderScale-parameters.css"; -@import "../base/StepInput-parameters.css"; +@import "../base/NumberInput-parameters.css"; @import "./sizes-parameters.css"; @import "./rtl-parameters.css"; +@import "./NumberInput-parameters.css"; diff --git a/packages/main/test/pages/NumberInput.html b/packages/main/test/pages/NumberInput.html new file mode 100644 index 0000000000000..487a171114bcb --- /dev/null +++ b/packages/main/test/pages/NumberInput.html @@ -0,0 +1,266 @@ + + + + + + NumberInput test page + + + + + + + + +

NumberInput

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

NumberInput in Cozy

+ +
+ +
+

NumberInput in Compact

+ +
+ +
+

NumberInput with min=0, max=10 and step=1

+ +
Wrong Value
+
+
+ +
+

NumberInput with min=0, max=10, step=0.05 and valuePrecision=2

+ +
Wrong Value
+
+
+ +
+

Disabled NumberInput

+ +
+ +
+

Readonly NumberInput

+ +
+ +
+

NumberInput with valueStateChange Prevented

+ +
Value state change is prevented on this input
+
+
+ +
+

NumberInput with valueState=None

+ +
+ +
+

NumberInput with valueState=Positive

+ +
+ +
+

NumberInput with valueState=Information

+ +
+ +
+

NumberInput with valueState=Warning

+ +
+ +
+

NumberInput with valueState=Negative

+ +
+ +
+

NumberInput change event test (linked inputs)

+ + +
+ +

'input' event prevented

+ + +

'change' event count

+ + +
+

NumberInput with large value and precision (thousands separator)

+ + +
+ +
+

Form validation

+
+ + + + Check Validity +
+
+ +

Accessibility

+
+ Quantity: + +
+ + + + + diff --git a/packages/main/test/pages/styles/NumberInput.css b/packages/main/test/pages/styles/NumberInput.css new file mode 100644 index 0000000000000..b255e79a3af4b --- /dev/null +++ b/packages/main/test/pages/styles/NumberInput.css @@ -0,0 +1,15 @@ +h3 { + user-select: none; +} + +.numberinput-page { + background-color: var(--sapBackgroundColor); +} + +.numberinput-field { + width: 250px; +} + +form ui5-number-input:invalid { + outline: 2px solid var(--sapNegativeColor); +} diff --git a/packages/main/test/pages/styles/StepInput.css b/packages/main/test/pages/styles/StepInput.css index 7471626f7e721..9c667291b678d 100644 --- a/packages/main/test/pages/styles/StepInput.css +++ b/packages/main/test/pages/styles/StepInput.css @@ -35,6 +35,6 @@ h3 { width: 250px } -form ui5-step-input:invalid { +form ui5-step-input:invalid ui5-number-input { outline: 2px solid var(--sapNegativeColor); } diff --git a/packages/website/docs/_components_pages/main/NumberInput.mdx b/packages/website/docs/_components_pages/main/NumberInput.mdx new file mode 100644 index 0000000000000..ae5da9b3b9dce --- /dev/null +++ b/packages/website/docs/_components_pages/main/NumberInput.mdx @@ -0,0 +1,28 @@ +--- +slug: ../NumberInput +--- + +import Basic from "../../_samples/main/NumberInput/Basic/Basic.md"; +import ValuePrecision from "../../_samples/main/NumberInput/ValuePrecision/ValuePrecision.md"; +import MinMax from "../../_samples/main/NumberInput/MinMax/MinMax.md"; +import States from "../../_samples/main/NumberInput/States/States.md"; + +<%COMPONENT_OVERVIEW%> + +## Basic Sample + + +<%COMPONENT_METADATA%> + +## More Samples + +### Min Max + + +### Value Precision + + +### States +NumberInput supports several semantic value states, readonly, disabled, etc. + + diff --git a/packages/website/docs/_samples/main/NumberInput/Basic/Basic.md b/packages/website/docs/_samples/main/NumberInput/Basic/Basic.md new file mode 100644 index 0000000000000..0c062a836e844 --- /dev/null +++ b/packages/website/docs/_samples/main/NumberInput/Basic/Basic.md @@ -0,0 +1,5 @@ +import html from '!!raw-loader!./sample.html'; +import js from '!!raw-loader!./main.js'; +import react from '!!raw-loader!./sample.tsx'; + + diff --git a/packages/website/docs/_samples/main/NumberInput/Basic/main.js b/packages/website/docs/_samples/main/NumberInput/Basic/main.js new file mode 100644 index 0000000000000..0902003d9b467 --- /dev/null +++ b/packages/website/docs/_samples/main/NumberInput/Basic/main.js @@ -0,0 +1 @@ +import "@ui5/webcomponents/dist/NumberInput.js"; diff --git a/packages/website/docs/_samples/main/NumberInput/Basic/sample.html b/packages/website/docs/_samples/main/NumberInput/Basic/sample.html new file mode 100644 index 0000000000000..dc42d2d9c785f --- /dev/null +++ b/packages/website/docs/_samples/main/NumberInput/Basic/sample.html @@ -0,0 +1,19 @@ + + + + + + + Sample + + +
+ + + + +
+ + + + diff --git a/packages/website/docs/_samples/main/NumberInput/Basic/sample.tsx b/packages/website/docs/_samples/main/NumberInput/Basic/sample.tsx new file mode 100644 index 0000000000000..8e9c3a295dbe3 --- /dev/null +++ b/packages/website/docs/_samples/main/NumberInput/Basic/sample.tsx @@ -0,0 +1,10 @@ +import createReactComponent from "@ui5/webcomponents-base/dist/createReactComponent.js"; +import NumberInputClass from "@ui5/webcomponents/dist/NumberInput.js"; + +const NumberInput = createReactComponent(NumberInputClass); + +function App() { + return ; +} + +export default App; diff --git a/packages/website/docs/_samples/main/NumberInput/MinMax/MinMax.md b/packages/website/docs/_samples/main/NumberInput/MinMax/MinMax.md new file mode 100644 index 0000000000000..04db8e927c90a --- /dev/null +++ b/packages/website/docs/_samples/main/NumberInput/MinMax/MinMax.md @@ -0,0 +1,5 @@ +import html from '!!raw-loader!./sample.html'; +import js from '!!raw-loader!./main.js'; +import react from '!!raw-loader!./sample.tsx'; + + \ No newline at end of file diff --git a/packages/website/docs/_samples/main/NumberInput/MinMax/main.js b/packages/website/docs/_samples/main/NumberInput/MinMax/main.js new file mode 100644 index 0000000000000..1b847f3168257 --- /dev/null +++ b/packages/website/docs/_samples/main/NumberInput/MinMax/main.js @@ -0,0 +1 @@ +import "@ui5/webcomponents/dist/NumberInput.js"; \ No newline at end of file diff --git a/packages/website/docs/_samples/main/NumberInput/MinMax/sample.html b/packages/website/docs/_samples/main/NumberInput/MinMax/sample.html new file mode 100644 index 0000000000000..b1807cc8c35e4 --- /dev/null +++ b/packages/website/docs/_samples/main/NumberInput/MinMax/sample.html @@ -0,0 +1,19 @@ + + + + + + + Sample + + +
+ + + + +
+ + + + diff --git a/packages/website/docs/_samples/main/NumberInput/MinMax/sample.tsx b/packages/website/docs/_samples/main/NumberInput/MinMax/sample.tsx new file mode 100644 index 0000000000000..46970e22943d5 --- /dev/null +++ b/packages/website/docs/_samples/main/NumberInput/MinMax/sample.tsx @@ -0,0 +1,10 @@ +import createReactComponent from "@ui5/webcomponents-base/dist/createReactComponent.js"; +import NumberInputClass from "@ui5/webcomponents/dist/NumberInput.js"; + +const NumberInput = createReactComponent(NumberInputClass); + +function App() { + return ; +} + +export default App; diff --git a/packages/website/docs/_samples/main/NumberInput/States/States.md b/packages/website/docs/_samples/main/NumberInput/States/States.md new file mode 100644 index 0000000000000..04db8e927c90a --- /dev/null +++ b/packages/website/docs/_samples/main/NumberInput/States/States.md @@ -0,0 +1,5 @@ +import html from '!!raw-loader!./sample.html'; +import js from '!!raw-loader!./main.js'; +import react from '!!raw-loader!./sample.tsx'; + + \ No newline at end of file diff --git a/packages/website/docs/_samples/main/NumberInput/States/main.js b/packages/website/docs/_samples/main/NumberInput/States/main.js new file mode 100644 index 0000000000000..1b847f3168257 --- /dev/null +++ b/packages/website/docs/_samples/main/NumberInput/States/main.js @@ -0,0 +1 @@ +import "@ui5/webcomponents/dist/NumberInput.js"; \ No newline at end of file diff --git a/packages/website/docs/_samples/main/NumberInput/States/sample.html b/packages/website/docs/_samples/main/NumberInput/States/sample.html new file mode 100644 index 0000000000000..42208575f2f31 --- /dev/null +++ b/packages/website/docs/_samples/main/NumberInput/States/sample.html @@ -0,0 +1,27 @@ + + + + + + + Sample + + +
+ + +

+

+

+

+

+

+ +
Please provide valid value
+
+ +
+ + + + diff --git a/packages/website/docs/_samples/main/NumberInput/States/sample.tsx b/packages/website/docs/_samples/main/NumberInput/States/sample.tsx new file mode 100644 index 0000000000000..d4737f8f87d24 --- /dev/null +++ b/packages/website/docs/_samples/main/NumberInput/States/sample.tsx @@ -0,0 +1,38 @@ +import createReactComponent from "@ui5/webcomponents-base/dist/createReactComponent.js"; +import NumberInputClass from "@ui5/webcomponents/dist/NumberInput.js"; + +const NumberInput = createReactComponent(NumberInputClass); + +function App() { + return ( + <> + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
Please provide valid value
+
+ + ); +} + +export default App; diff --git a/packages/website/docs/_samples/main/NumberInput/ValuePrecision/ValuePrecision.md b/packages/website/docs/_samples/main/NumberInput/ValuePrecision/ValuePrecision.md new file mode 100644 index 0000000000000..04db8e927c90a --- /dev/null +++ b/packages/website/docs/_samples/main/NumberInput/ValuePrecision/ValuePrecision.md @@ -0,0 +1,5 @@ +import html from '!!raw-loader!./sample.html'; +import js from '!!raw-loader!./main.js'; +import react from '!!raw-loader!./sample.tsx'; + + \ No newline at end of file diff --git a/packages/website/docs/_samples/main/NumberInput/ValuePrecision/main.js b/packages/website/docs/_samples/main/NumberInput/ValuePrecision/main.js new file mode 100644 index 0000000000000..1b847f3168257 --- /dev/null +++ b/packages/website/docs/_samples/main/NumberInput/ValuePrecision/main.js @@ -0,0 +1 @@ +import "@ui5/webcomponents/dist/NumberInput.js"; \ No newline at end of file diff --git a/packages/website/docs/_samples/main/NumberInput/ValuePrecision/sample.html b/packages/website/docs/_samples/main/NumberInput/ValuePrecision/sample.html new file mode 100644 index 0000000000000..fa97bdb6b80d8 --- /dev/null +++ b/packages/website/docs/_samples/main/NumberInput/ValuePrecision/sample.html @@ -0,0 +1,19 @@ + + + + + + + Sample + + +
+ + + + +
+ + + + diff --git a/packages/website/docs/_samples/main/NumberInput/ValuePrecision/sample.tsx b/packages/website/docs/_samples/main/NumberInput/ValuePrecision/sample.tsx new file mode 100644 index 0000000000000..e5a4d20e8e299 --- /dev/null +++ b/packages/website/docs/_samples/main/NumberInput/ValuePrecision/sample.tsx @@ -0,0 +1,12 @@ +import createReactComponent from "@ui5/webcomponents-base/dist/createReactComponent.js"; +import NumberInputClass from "@ui5/webcomponents/dist/NumberInput.js"; + +const NumberInput = createReactComponent(NumberInputClass); + +function App() { + return ( + + ); +} + +export default App;