diff --git a/packages/main/cypress/specs/DatePicker.cy.tsx b/packages/main/cypress/specs/DatePicker.cy.tsx index 0f7fbb3866a3..63a2221d2fc6 100644 --- a/packages/main/cypress/specs/DatePicker.cy.tsx +++ b/packages/main/cypress/specs/DatePicker.cy.tsx @@ -1644,6 +1644,192 @@ describe("Date Picker Tests", () => { }); +describe("Validation inside a form ", () => { + it("has correct validity for valueMissing", () => { + 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("#submitBtn") + .realClick(); + + cy.get("@submit") + .should("have.not.been.called"); + + cy.get("[ui5-date-picker]") + .as("datePicker") + .ui5AssertValidityState({ + formValidity: { valueMissing: true }, + validity: { valueMissing: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#datePicker1:invalid") + .should("exist", "Required DatePicker without value should have :invalid CSS class"); + + cy.get("@datePicker") + .ui5DatePickerTypeDate("Apr 12, 2024"); + + cy.get("@datePicker") + .ui5AssertValidityState({ + formValidity: { valueMissing: false }, + validity: { valueMissing: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#datePicker1:invalid").should("not.exist", "Required DatePicker with value should not have :invalid CSS class"); + }); + + it("has correct validity for patternMismatch", () => { + 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("#datePicker2") + .as("datePicker") + .ui5DatePickerTypeDate("Test 33, 2024"); + + cy.get("#submitBtn") + .realClick(); + + cy.get("@submit") + .should("have.not.been.called"); + + cy.get("@datePicker") + .ui5AssertValidityState({ + formValidity: { patternMismatch: true }, + validity: { patternMismatch: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#datePicker2:invalid") + .should("exist", "DatePicker without correct formatted value should have :invalid CSS class"); + + cy.get("@datePicker") + .ui5DatePickerTypeDate("Apr 12, 2024"); + + cy.get("@datePicker") + .ui5AssertValidityState({ + formValidity: { patternMismatch: false }, + validity: { patternMismatch: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#datePicker2:invalid") + .should("not.exist", "DatePicker with correct 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("#datePicker3") + .as("datePicker") + .ui5DatePickerTypeDate("Apr 10, 2020"); + + cy.get("@datePicker") + .ui5AssertValidityState({ + formValidity: { rangeUnderflow: true }, + validity: { rangeUnderflow: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#datePicker3:invalid") + .should("exist", "DatePicker with value below minDate should have :invalid CSS class"); + + cy.get("@datePicker") + .ui5DatePickerTypeDate("Jan 20, 2024"); + + cy.get("@datePicker") + .ui5AssertValidityState({ + formValidity: { rangeUnderflow: false }, + validity: { rangeUnderflow: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#datePicker3:invalid") + .should("not.exist", "DatePicker with value above minDate 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("#datePicker3") + .ui5DatePickerTypeDate("Jan 14, 2024"); + + cy.get("@datePicker") + .ui5AssertValidityState({ + formValidity: { rangeOverflow: true }, + validity: { rangeOverflow: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#datePicker3:invalid") + .should("exist", "DatePicker with value above maxDate should have :invalid CSS class"); + + cy.get("@datePicker") + .ui5DatePickerTypeDate("Jan 5, 2024"); + + cy.get("@datePicker") + .ui5AssertValidityState({ + formValidity: { rangeOverflow: false }, + validity: { rangeOverflow: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#datePicker3:invalid") + .should("not.exist", "DatePicker with value below maxDate should not have :invalid CSS class"); + }); +}); + describe("Accessibility", () => { it("picker popover accessible name with external label", () => { const LABEL = "Deadline"; @@ -1723,85 +1909,4 @@ describe("Accessibility", () => { .find("span#descr") .should("have.text", DESCRIPTION); }); - - describe("Accessibility - ariaValueStateHiddenText", () => { - it("should correctly extract text from nested slot structure in value state messages", () => { - const ERROR_MESSAGE = "Invalid date format"; - - cy.mount( - -
{ERROR_MESSAGE}
-
- ); - - cy.get("[ui5-date-picker]") - .as("datePicker"); - - // Get the inner datetime input - cy.get("@datePicker") - .shadow() - .find("[ui5-datetime-input]") - .as("datetimeInput"); - - // Verify the input has proper value state - cy.get("@datetimeInput") - .should("have.attr", "value-state", "Negative"); - - // Test the ariaValueStateHiddenText getter directly - cy.get("@datetimeInput") - .then(($input) => { - const datetimeInput = $input[0] as any; - const ariaText = datetimeInput.ariaValueStateHiddenText; - - // Should contain both the value state type and the custom message - expect(ariaText).to.include("Error"); - expect(ariaText).to.include(ERROR_MESSAGE); - }); - - // Verify the aria-describedby points to an element with the correct text - cy.get("@datetimeInput") - .shadow() - .find("input") - .should("have.attr", "aria-describedby") - .then((describedBy) => { - cy.get("@datetimeInput") - .shadow() - .find(`#${describedBy}`) - .should("contain.text", "Error") - .and("contain.text", ERROR_MESSAGE); - }); - }); - - it("should handle complex nested slot structure from DatePicker forwarding", () => { - const CUSTOM_ERROR = "Please select a valid date"; - - cy.mount( - -
- {CUSTOM_ERROR} -
-
- ); - - cy.get("[ui5-date-picker]") - .as("datePicker"); - - cy.get("@datePicker") - .shadow() - .find("[ui5-datetime-input]") - .as("datetimeInput"); - - // Test nested slot content extraction - cy.get("@datetimeInput") - .then(($input) => { - const datetimeInput = $input[0] as any; - const ariaText = datetimeInput.ariaValueStateHiddenText; - - // Should extract text from nested structure - expect(ariaText).to.include("Warning"); - expect(ariaText).to.include(CUSTOM_ERROR); - expect(ariaText.trim()).to.not.be.empty; - }); - }); - }); -}); \ No newline at end of file +}); diff --git a/packages/main/cypress/specs/DateRangePicker.cy.tsx b/packages/main/cypress/specs/DateRangePicker.cy.tsx index 21190e781949..2269b1337c75 100644 --- a/packages/main/cypress/specs/DateRangePicker.cy.tsx +++ b/packages/main/cypress/specs/DateRangePicker.cy.tsx @@ -785,4 +785,187 @@ describe("Accessibility", () => { expect(endSelectionDay).to.have.attr("aria-selected", "true"); }); }); +}); + +describe("Validation inside a form", () => { + it("has correct validity for valueMissing", () => { + cy.mount( +
+ + +
+ ); + + cy.get("form").then($form => { + $form.get(0).addEventListener("submit", (e) => e.preventDefault()); + $form.get(0).addEventListener("submit", cy.stub().as("submit")); + }); + + cy.get("#submitBtn") + .realClick(); + + cy.get("@submit") + .should("have.not.been.called"); + + cy.get("#dateRangePicker") + .as("dateRangePicker") + .ui5AssertValidityState({ + formValidity: { valueMissing: true }, + validity: { valueMissing: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#dateRangePicker:invalid") + .should("exist"); + + cy.get("@dateRangePicker") + .ui5DatePickerTypeDate("09/09/2020 - 10/10/2020"); + + cy.get("@dateRangePicker") + .ui5AssertValidityState({ + formValidity: { valueMissing: false }, + validity: { valueMissing: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#dateRangePicker:invalid") + .should("not.exist"); + }); + + it("has correct validity for patternMismatch", () => { + cy.mount( +
+ + +
+ ); + + cy.get("form").then($form => { + $form.get(0).addEventListener("submit", (e) => e.preventDefault()); + $form.get(0).addEventListener("submit", cy.stub().as("submit")); + }); + + cy.get("#dateRangePicker") + .as("dateRangePicker") + .ui5DatePickerTypeDate("invalid input"); + + cy.get("#submitBtn") + .realClick(); + + cy.get("@submit") + .should("have.not.been.called"); + + cy.get("@dateRangePicker") + .ui5AssertValidityState({ + formValidity: { patternMismatch: true }, + validity: { patternMismatch: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#dateRangePicker:invalid") + .should("exist"); + + cy.get("@dateRangePicker") + .ui5DatePickerTypeDate("09/09/2020 - 10/10/2020"); + + cy.get("@dateRangePicker") + .ui5AssertValidityState({ + formValidity: { patternMismatch: false }, + validity: { patternMismatch: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#dateRangePicker:invalid") + .should("not.exist"); + }); + + it("has correct validity for rangeUnderflow", () => { + cy.mount( +
+ + +
+ ); + + cy.get("form").then($form => { + $form.get(0).addEventListener("submit", (e) => e.preventDefault()); + $form.get(0).addEventListener("submit", cy.stub().as("submit")); + }); + + cy.get("#dateRangePicker") + .as("dateRangePicker") + .ui5DatePickerTypeDate("01/10/2020 - 02/10/2020"); + + cy.get("@dateRangePicker") + .ui5AssertValidityState({ + formValidity: { rangeUnderflow: true }, + validity: { rangeUnderflow: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#dateRangePicker:invalid") + .should("exist"); + + cy.get("@dateRangePicker") + .ui5DatePickerTypeDate("11/10/2020 - 12/10/2020"); + + cy.get("@dateRangePicker") + .ui5AssertValidityState({ + formValidity: { rangeUnderflow: false }, + validity: { rangeUnderflow: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#dateRangePicker:invalid") + .should("not.exist"); + }); + + it("has correct validity for rangeOverflow", () => { + cy.mount( +
+ + +
+ ); + + cy.get("form").then($form => { + $form.get(0).addEventListener("submit", (e) => e.preventDefault()); + $form.get(0).addEventListener("submit", cy.stub().as("submit")); + }); + + cy.get("#dateRangePicker") + .as("dateRangePicker") + .ui5DatePickerTypeDate("11/10/2020 - 12/10/2020"); + + cy.get("@dateRangePicker") + .ui5AssertValidityState({ + formValidity: { rangeOverflow: true }, + validity: { rangeOverflow: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#dateRangePicker:invalid") + .should("exist"); + + cy.get("@dateRangePicker") + .ui5DatePickerTypeDate("07/09/2020 - 09/10/2020"); + + cy.get("@dateRangePicker") + .ui5AssertValidityState({ + formValidity: { rangeOverflow: false }, + validity: { rangeOverflow: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#dateRangePicker:invalid") + .should("not.exist"); + }); }); \ No newline at end of file diff --git a/packages/main/cypress/specs/DateTimePicker.cy.tsx b/packages/main/cypress/specs/DateTimePicker.cy.tsx index 5ae7ab7d861e..b77b4d5b1718 100644 --- a/packages/main/cypress/specs/DateTimePicker.cy.tsx +++ b/packages/main/cypress/specs/DateTimePicker.cy.tsx @@ -552,7 +552,7 @@ describe("DateTimePicker general interaction", () => { describe("Accessibility", () => { it("picker popover accessible name", () => { const LABEL = "Deadline"; - cy.mount(); + cy.mount(); cy.get("[ui5-datetime-picker]") .ui5DateTimePickerGetPopover() @@ -609,3 +609,191 @@ describe("Accessibility", () => { cy.get("#descr").should("have.text", DESCRIPTION); }); }); + + +describe("Validation inside a form", () => { + it("has correct validity for valueMissing", () => { + 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("#submitBtn") + .realClick(); + + cy.get("@submit") + .should("have.not.been.called"); + + cy.get("#dateTimePicker") + .as("dateTimePicker") + .ui5AssertValidityState({ + formValidity: { valueMissing: true }, + validity: { valueMissing: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#dateTimePicker:invalid") + .should("exist", "Required DatePicker without value should have :invalid CSS class"); + + cy.get("@dateTimePicker") + .ui5DatePickerTypeDate("now"); + + cy.get("@dateTimePicker") + .ui5AssertValidityState({ + formValidity: { valueMissing: false }, + validity: { valueMissing: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#dateTimePicker:invalid") + .should("not.exist", "Required DatePicker with value should not have :invalid CSS class"); + }); + + it("has correct validity for patternMismatch", () => { + 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("#dateTimePicker") + .as("dateTimePicker") + .ui5DatePickerTypeDate("Test 33, 2024 ss:tt:tt"); + + cy.get("#submitBtn") + .realClick(); + + cy.get("@submit") + .should("have.not.been.called"); + + cy.get("@dateTimePicker") + .ui5AssertValidityState({ + formValidity: { patternMismatch: true }, + validity: { patternMismatch: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#dateTimePicker:invalid") + .should("exist", "DateTimePicker without correct formatted value should have :invalid CSS class"); + + cy.get("@dateTimePicker") + .ui5DatePickerTypeDate("Apr 12, 2024 12:00:00"); + + cy.get("@dateTimePicker") + .ui5AssertValidityState({ + formValidity: { patternMismatch: false }, + validity: { patternMismatch: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#dateTimePicker:invalid") + .should("not.exist", "DateTimePicker with correct 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("#dateTimePicker") + .as("dateTimePicker") + .ui5DatePickerTypeDate("Jan 5, 2023 08:00:00"); + + cy.get("@dateTimePicker") + .ui5AssertValidityState({ + formValidity: { rangeUnderflow: true }, + validity: { rangeUnderflow: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#dateTimePicker:invalid") + .should("exist", "DateTimePicker with value below minDate should have :invalid CSS class"); + + cy.get("@dateTimePicker") + .ui5DatePickerTypeDate("Jan 20, 2024 08:00:00"); + + cy.get("@dateTimePicker") + .ui5AssertValidityState({ + formValidity: { rangeUnderflow: false }, + validity: { rangeUnderflow: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#dateTimePicker:invalid") + .should("not.exist", "DateTimePicker with value above minDate 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("#dateTimePicker") + .as("dateTimePicker") + .ui5DatePickerTypeDate("Jan 15, 2025 08:00:00"); + + cy.get("@dateTimePicker") + .ui5AssertValidityState({ + formValidity: { rangeOverflow: true }, + validity: { rangeOverflow: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#dateTimePicker:invalid") + .should("exist", "DateTimePicker with value above maxDate should have :invalid CSS class"); + + cy.get("@dateTimePicker") + .ui5DatePickerTypeDate("Jan 5, 2024 08:00:00"); + + cy.get("@dateTimePicker") + .ui5AssertValidityState({ + formValidity: { rangeOverflow: false }, + validity: { rangeOverflow: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#dateTimePicker:invalid") + .should("not.exist", "DateTimePicker with value below maxDate should not have :invalid CSS class"); + }); +}); \ No newline at end of file diff --git a/packages/main/cypress/specs/FileUploader.cy.tsx b/packages/main/cypress/specs/FileUploader.cy.tsx index d3b698ce3bdc..b2537c4decec 100644 --- a/packages/main/cypress/specs/FileUploader.cy.tsx +++ b/packages/main/cypress/specs/FileUploader.cy.tsx @@ -155,7 +155,7 @@ describe("Interaction", () => { ); - cy.get("[ui5-label]") + cy.get("[ui5-label]") .realClick(); cy.get("[ui5-file-uploader]") @@ -437,7 +437,7 @@ describe("Interaction", () => { }, { contents: Cypress.Buffer.from("file2 content"), - fileName: "file11.txt", + fileName: "file11.txt", mimeType: "text/plain" }, { @@ -548,4 +548,59 @@ describe("Accessibility", () => { .find("input[type='file']") .should("have.attr", "aria-description", DESCRIPTION) }); +}); + +describe("Validation inside form", () => { + it("has correct validity for valueMissing", () => { + cy.mount( +
+ + +
+ ); + + cy.get("form").then($form => { + $form.get(0).addEventListener("submit", cy.stub().as("submit")); + }); + + cy.get("#submitBtn") + .realClick(); + + cy.get("@submit") + .should("have.not.been.called"); + + cy.get("#uploader") + .as("uploader") + .ui5AssertValidityState({ + formValidity: { valueMissing: true }, + validity: { valueMissing: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#uploader:invalid") + .should("exist"); + + cy.get("@uploader") + .shadow() + .find("input[type='file']") + .selectFile([ + { + contents: new Uint8Array(1 * 1024 * 1024), // 2 MB buffer + fileName: "text.txt", + mimeType: "text/plain" + } + ], { force: true }); + + cy.get("@uploader") + .ui5AssertValidityState({ + formValidity: { valueMissing: false }, + validity: { valueMissing: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#uploader:invalid") + .should("not.exist"); + }); }); \ No newline at end of file diff --git a/packages/main/cypress/specs/FormSupport.cy.tsx b/packages/main/cypress/specs/FormSupport.cy.tsx index 49e7f49d10bb..40da79e8ed4b 100644 --- a/packages/main/cypress/specs/FormSupport.cy.tsx +++ b/packages/main/cypress/specs/FormSupport.cy.tsx @@ -1,3 +1,4 @@ +import "../../src/Assets.js"; import Button from "../../src/Button.js"; import CheckBox from "../../src/CheckBox.js"; import ColorPicker from "../../src/ColorPicker.js"; @@ -141,9 +142,9 @@ describe("Form support", () => { it("ui5-date-picker in form", () => { cy.mount(
- + - +
); @@ -164,7 +165,7 @@ describe("Form support", () => { .realClick(); cy.get("#date_picker5") - .realType("ok", { delay: 100 }); + .ui5DatePickerTypeDate("Jan 29, 2019", 100); cy.get("button") .realClick(); @@ -176,15 +177,15 @@ describe("Form support", () => { .then($el => { return getFormData($el.get(0)); }) - .should("be.equal", "date_picker3=&date_picker4=ok&date_picker5=ok"); + .should("be.equal", "date_picker3=&date_picker4=Jan 29, 2019&date_picker5=Jan 29, 2019"); }); it("ui5-daterange-picker in form", () => { cy.mount(
- + - +
); @@ -205,7 +206,7 @@ describe("Form support", () => { .realClick(); cy.get("#daterange_picker5") - .realType("ok", { delay: 100 }); + .ui5DatePickerTypeDate("Jul 16, 2020 - Jul 29, 2020", 100); cy.get("button") .realClick(); @@ -217,16 +218,16 @@ describe("Form support", () => { .then($el => { return getFormData($el.get(0)); }) - .should("be.equal", "daterange_picker3=&daterange_picker4=ok&daterange_picker5=ok"); + .should("be.equal", "daterange_picker3=&daterange_picker4=Jul 16, 2020 &daterange_picker4= Jul 29, 2020&daterange_picker5=Jul 16, 2020 &daterange_picker5= Jul 29, 2020"); }); it("ui5-datetime-picker in form", () => { cy.mount(
- + - - + +
); @@ -246,7 +247,7 @@ describe("Form support", () => { .realClick(); cy.get("#datetime_picker5") - .realType("ok", { delay: 100 }); + .ui5DatePickerTypeDate("Jan 20, 2024 08:00:00", 100); cy.get("button") .realClick(); @@ -258,7 +259,7 @@ describe("Form support", () => { .then($el => { return getFormData($el.get(0)); }) - .should("be.equal", "datetime_picker3=&datetime_picker4=ok&datetime_picker5=ok"); + .should("be.equal", "datetime_picker3=&datetime_picker4=Apr 12, 2024 08:00:00&datetime_picker5=Jan 20, 2024 08:00:00"); }); it("ui5-input in form", () => { @@ -917,7 +918,7 @@ describe("Form support", () => { .realClick(); cy.get("#time_picker3") - .realType("ok", { delay: 100 }); + .ui5DatePickerTypeDate("1:10:10 PM", 100); cy.get("button") .realClick(); @@ -929,7 +930,7 @@ describe("Form support", () => { .then($el => { return getFormData($el.get(0)); }) - .should("be.equal", "time_picker3=ok&time_picker4=1:10:10 PM"); + .should("be.equal", "time_picker3=1:10:10 PM&time_picker4=1:10:10 PM"); }); it("Button's click doesn't submit form on prevent default", () => { diff --git a/packages/main/cypress/specs/StepInput.cy.tsx b/packages/main/cypress/specs/StepInput.cy.tsx index 088ef615bc41..7339f9d975f0 100644 --- a/packages/main/cypress/specs/StepInput.cy.tsx +++ b/packages/main/cypress/specs/StepInput.cy.tsx @@ -72,7 +72,7 @@ describe("StepInput keyboard interaction tests", () => { cy.realPress(['Shift', 'PageDown']); cy.get("@stepInput") - .should("have.prop", "value", 0); + .should("have.prop", "value", 0); }); it("should set the value to the 'max' with 'Ctrl+Shift+ArrowUp'", () => { @@ -108,7 +108,7 @@ describe("StepInput keyboard interaction tests", () => { cy.realPress(['Control', 'Shift', 'ArrowDown']); cy.get("@stepInput") - .should("have.prop", "value", 0); + .should("have.prop", "value", 0); }); it("should restore the previous value with 'Escape'", () => { @@ -242,7 +242,7 @@ describe("StepInput misc interaction tests", () => { cy.realType("23.034"); cy.realPress("Enter"); - + cy.get("@stepInput") .should("have.prop", "valueState", "Negative"); }); @@ -378,7 +378,7 @@ describe("StepInput events", () => { cy.realPress("Escape"); cy.get("@stepInput") - .should("have.prop", "value", 0); + .should("have.prop", "value", 0); cy.get("@change") .should("not.have.been.called"); @@ -392,7 +392,7 @@ describe("StepInput events", () => { cy.get("[ui5-step-input]") .as("stepInput"); - cy.get("@stepInput") + cy.get("@stepInput") .ui5StepInputAttachHandler("ui5-change", "change"); cy.get("@stepInput") @@ -407,7 +407,7 @@ describe("StepInput events", () => { .should("have.been.calledOnce"); cy.get("@stepInput") - .should("have.prop", "value", 1); + .should("have.prop", "value", 1); }); it("should fire 'change' when using 'Increase' button'", () => { @@ -433,7 +433,7 @@ describe("StepInput events", () => { .should("have.been.calledOnce"); cy.get("@stepInput") - .should("have.prop", "value", 1); + .should("have.prop", "value", 1); }); it("should fire 'change' when using 'Decrease' button'", () => { @@ -630,7 +630,7 @@ describe("StepInput property propagation", () => { cy.get("[ui5-step-input]") .ui5StepInputCheckInnerInputProperty("placeholder", "Enter number"); - }); + }); it("should propagate 'min' property to inner input", () => { cy.mount( @@ -639,9 +639,9 @@ describe("StepInput property propagation", () => { cy.get("[ui5-step-input]") .ui5StepInputCheckInnerInputProperty("min", "0"); - }); + }); - it("should propagate 'max' property to inner input", () => { + it("should propagate 'max' property to inner input", () => { cy.mount( ); @@ -685,4 +685,161 @@ describe("StepInput property propagation", () => { cy.get("[ui5-step-input]") .ui5StepInputCheckInnerInputProperty("value", "5"); }); +}); + +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-step-input]") + .as("stepInput"); + + cy.get("@stepInput") + .ui5StepInputTypeNumber(2.34); + + cy.get("#submitBtn") + .realClick(); + + cy.get("@submit") + .should("have.not.been.called"); + + cy.get("@stepInput") + .ui5AssertValidityState({ + formValidity: { patternMismatch: true }, + validity: { patternMismatch: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#stepInput:invalid") + .should("exist", "StepInput without formatted value should have :invalid CSS class"); + + cy.get("@stepInput") + .ui5StepInputTypeNumber(2.345); + + cy.get("@stepInput") + .ui5AssertValidityState({ + formValidity: { patternMismatch: false }, + validity: { patternMismatch: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + cy.get("#stepInput:invalid") + .should("not.exist", "StepInput 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-step-input]") + .as("stepInput"); + + cy.get("@stepInput") + .ui5StepInputTypeNumber(2); + + cy.get("#submitBtn") + .realClick(); + + cy.get("@submit") + .should("have.not.been.called"); + + cy.get("@stepInput") + .ui5AssertValidityState({ + formValidity: { rangeUnderflow: true }, + validity: { rangeUnderflow: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#stepInput:invalid") + .should("exist", "StepInput with value lower than min should have :invalid CSS class"); + + cy.get("@stepInput") + .ui5StepInputTypeNumber(4); + + cy.get("@stepInput") + .ui5AssertValidityState({ + formValidity: { rangeUnderflow: false }, + validity: { rangeUnderflow: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#stepInput:invalid") + .should("not.exist", "StepInput 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-step-input]") + .as("stepInput"); + + cy.get("@stepInput") + .ui5StepInputTypeNumber(4); + + cy.get("#submitBtn") + .realClick(); + + cy.get("@submit") + .should("have.not.been.called"); + + cy.get("@stepInput") + .ui5AssertValidityState({ + formValidity: { rangeOverflow: true }, + validity: { rangeOverflow: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#stepInput:invalid") + .should("exist", "StepInput without value lower than min should have :invalid CSS class"); + + cy.get("@stepInput") + .ui5StepInputTypeNumber(2); + + cy.get("@stepInput") + .ui5AssertValidityState({ + formValidity: { rangeOverflow: false }, + validity: { rangeOverflow: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#stepInput:invalid") + .should("not.exist", "StepInput with value lower than max should not have :invalid CSS class"); + }); }); \ No newline at end of file diff --git a/packages/main/cypress/specs/Switch.cy.tsx b/packages/main/cypress/specs/Switch.cy.tsx index e4022f2fc31c..c443fb451ab1 100644 --- a/packages/main/cypress/specs/Switch.cy.tsx +++ b/packages/main/cypress/specs/Switch.cy.tsx @@ -120,4 +120,49 @@ describe("General interactions in form", () => { expect($form[0].checkValidity()).to.be.true; }); }); + + it("Should fire 'invalid' event on form submit when 'required' switch is not checked", () => { + cy.mount( +
+ + + +
+ ); + + cy.get("form") + .then($item => { + $item.get(0).addEventListener("submit", cy.stub().as("submit")); + }); + + cy.get("#switchSubmit") + .realClick(); + + cy.get("@submit") + .should("have.not.been.called"); + + cy.get("#requiredTestSwitch") + .ui5AssertValidityState({ + formValidity: { valueMissing: true }, + validity: { valueMissing: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#requiredTestSwitch:invalid") + .should("exist", "Unchecked required Switch should have :invalid CSS class"); + + cy.get("#requiredTestSwitch") + .realClick(); + + cy.get("#requiredTestSwitch") + .ui5AssertValidityState({ + formValidity: { valueMissing: false }, + validity: { valueMissing: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#requiredTestSwitch:invalid").should("not.exist", "Checked required Switch should not have :invalid CSS class"); + }); }); \ No newline at end of file diff --git a/packages/main/cypress/specs/TimePicker.cy.tsx b/packages/main/cypress/specs/TimePicker.cy.tsx index 632e91f0fafd..b2d96e1f399e 100644 --- a/packages/main/cypress/specs/TimePicker.cy.tsx +++ b/packages/main/cypress/specs/TimePicker.cy.tsx @@ -450,4 +450,97 @@ describe("Accessibility", () => { .ui5TimePickerGetInnerInput() .should("have.attr", "aria-label", "Pick a time"); }); +}); + +describe("Validation inside a form", () => { + it("has correct validity for valueMissing", () => { + cy.mount(
+ + +
); + + cy.get("form") + .then($item => { + $item.get(0).addEventListener("submit", cy.stub().as("submit")); + }); + + cy.get("#submitBtn") + .realClick(); + + cy.get("@submit") + .should("have.not.been.called"); + + cy.get("#timePicker") + .ui5AssertValidityState({ + formValidity: { valueMissing: true }, + validity: { valueMissing: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#timePicker:invalid") + .should("exist", "Required timepicker without value should have :invalid CSS class"); + + cy.get("[ui5-time-picker]") + .ui5TimePickerTypeTime("now") + + cy.get("@timePicker") + .ui5AssertValidityState({ + formValidity: { valueMissing: false }, + validity: { valueMissing: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#timePicker:invalid").should("not.exist", "Required TimePicker with value should not have :invalid CSS class"); + }); + + it("has correct validity for patternMismatch", () => { + cy.mount( +
+ + +
+ ); + + cy.get("#timePicker").as("timePicker"); + + cy.get("form") + .then($item => { + $item.get(0).addEventListener("submit", cy.stub().as("submit")); + }); + + cy.get("@timePicker") + .ui5TimePickerTypeTime("invalid"); + + cy.get("#submitBtn").click(); + + cy.get("@submit") + .should("have.not.been.called"); + + cy.get("@timePicker") + .ui5AssertValidityState({ + formValidity: { patternMismatch: true }, + validity: { patternMismatch: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#timePicker:invalid") + .should("exist", "Timepicker without correct formatted value should have :invalid CSS class"); + + cy.get("@timePicker") + .ui5TimePickerTypeTime("14:00:00"); + + cy.get("@timePicker") + .ui5AssertValidityState({ + formValidity: { patternMismatch: false }, + validity: { patternMismatch: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#timePicker:invalid") + .should("not.exist", "Timepicker with correct formatted value should not have :invalid CSS class"); + }); }); \ No newline at end of file diff --git a/packages/main/cypress/support/commands.ts b/packages/main/cypress/support/commands.ts index 54ff9f71f1a6..541f9abe882d 100644 --- a/packages/main/cypress/support/commands.ts +++ b/packages/main/cypress/support/commands.ts @@ -70,6 +70,15 @@ declare global { interface Chainable { ui5SimulateDevice(device?: SimulationDevices): Chainable ui5DOMRef(): Chainable + ui5AssertValidityState( + expected: { + formValidity?: Partial; + validity?: Partial; + valid?: boolean; + checkValidity?: boolean; + reportValidity?: boolean; + } + ): Chainable; ui5CalendarGetDay(calendarSelector: string, timestamp: string): Chainable> ui5CalendarGetMonth(calendarSelector: string, timestamp: string): Chainable> ui5CalendarShowYearRangePicker(): Chainable @@ -127,3 +136,40 @@ Cypress.Commands.add("ui5SimulateDevice", (device: SimulationDevices = "phone") .invoke("isPhone") .should("be.true"); }); + +Cypress.Commands.add( + "ui5AssertValidityState", + { prevSubject: true }, + ( + subject, + expected: { + formValidity?: Partial; + validity?: Partial; + valid?: boolean; + checkValidity?: boolean; + reportValidity?: boolean; + } + ) => { + const el = subject[0]; + + if (expected.formValidity) { + Object.entries(expected.formValidity).forEach(([key, value]) => { + expect(el.formValidity[key], `formValidity.${key}`).to.equal(value); + }); + } + if (expected.validity) { + Object.entries(expected.validity).forEach(([key, value]) => { + expect(el.validity[key], `validity.${key}`).to.equal(value); + }); + } + if (expected.valid !== undefined) { + expect(el.validity.valid, "validity.valid").to.equal(expected.valid); + } + if (expected.checkValidity !== undefined) { + expect(el.checkValidity(), "checkValidity()").to.equal(expected.checkValidity); + } + if (expected.reportValidity !== undefined) { + expect(el.reportValidity(), "reportValidity()").to.equal(expected.reportValidity); + } + } +); diff --git a/packages/main/cypress/support/commands/StepInput.commands.ts b/packages/main/cypress/support/commands/StepInput.commands.ts index 90e54e3fc27e..264e5a773893 100644 --- a/packages/main/cypress/support/commands/StepInput.commands.ts +++ b/packages/main/cypress/support/commands/StepInput.commands.ts @@ -60,6 +60,21 @@ Cypress.Commands.add("ui5StepInputCheckInnerInputProperty", { prevSubject: true .should("have.prop", 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"); +}); + declare global { namespace Cypress { interface Chainable { @@ -67,6 +82,7 @@ declare global { ui5StepInputChangeValueWithButtons(expectedValue: number, decreaseValue?: boolean): Chainable ui5StepInputAttachHandler(eventName: string, stubName: string): Chainable ui5StepInputCheckInnerInputProperty(propName: string, expectedValue: any): Chainable + ui5StepInputTypeNumber(value: number): Chainable } } } \ No newline at end of file diff --git a/packages/main/cypress/support/commands/TimePicker.commands.ts b/packages/main/cypress/support/commands/TimePicker.commands.ts index 9d2ea1b24edc..f1df68dd165d 100644 --- a/packages/main/cypress/support/commands/TimePicker.commands.ts +++ b/packages/main/cypress/support/commands/TimePicker.commands.ts @@ -69,6 +69,19 @@ Cypress.Commands.add("ui5TimePickerGetSubmitButton", { prevSubject: true }, subj .find("#submit"); }); +Cypress.Commands.add("ui5TimePickerTypeTime", { prevSubject: true }, (subject: string, time) => { + cy.wrap(subject) + .as("timePicker"); + + cy.get("@timePicker") + .ui5TimePickerGetInnerInput() + .realClick() + .should("be.focused") + + cy.realType(time); + cy.realPress("Enter"); +}); + declare global { namespace Cypress { interface Chainable { @@ -88,6 +101,10 @@ declare global { ui5TimePickerGetSubmitButton( this: Chainable> ): Chainable>; + ui5TimePickerTypeTime( + this: Chainable>, + time: string + ): Chainable; } } } \ No newline at end of file diff --git a/packages/main/src/DatePicker.ts b/packages/main/src/DatePicker.ts index 342a0914cd95..80dbaa486ffc 100644 --- a/packages/main/src/DatePicker.ts +++ b/packages/main/src/DatePicker.ts @@ -47,12 +47,15 @@ import { DATEPICKER_DATE_DESCRIPTION, DATETIME_COMPONENTS_PLACEHOLDER_PREFIX, INPUT_SUGGESTIONS_TITLE, - FORM_TEXTFIELD_REQUIRED, DATEPICKER_POPOVER_ACCESSIBLE_NAME, VALUE_STATE_ERROR, VALUE_STATE_INFORMATION, VALUE_STATE_SUCCESS, VALUE_STATE_WARNING, + DATEPICKER_VALUE_MISSING, + DATEPICKER_PATTERN_MISSMATCH, + DATEPICKER_RANGE_UNDERFLOW, + DATEPICKER_RANGE_OVERFLOW, } from "./generated/i18n/i18n-defaults.js"; import DateComponentBase from "./DateComponentBase.js"; import type ResponsivePopover from "./ResponsivePopover.js"; @@ -396,11 +399,33 @@ class DatePicker extends DateComponentBase implements IFormInputElement { static i18nBundle: I18nBundle; get formValidityMessage() { - return DatePicker.i18nBundle.getText(FORM_TEXTFIELD_REQUIRED); + const validity = this.formValidity; + + if (validity.valueMissing) { + // @ts-ignore oFormatOptions is a private API of DateFormat + return DatePicker.i18nBundle.getText(DATEPICKER_VALUE_MISSING, this.getFormat().oFormatOptions.pattern as string); + } + if (validity.patternMismatch) { + // @ts-ignore oFormatOptions is a private API of DateFormat + return DatePicker.i18nBundle.getText(DATEPICKER_PATTERN_MISSMATCH, this.getFormat().oFormatOptions.pattern as string); + } + if (validity.rangeUnderflow) { + return DatePicker.i18nBundle.getText(DATEPICKER_RANGE_UNDERFLOW, this.minDate); + } + if (validity.rangeOverflow) { + return DatePicker.i18nBundle.getText(DATEPICKER_RANGE_OVERFLOW, this.maxDate); + } + + return ""; } get formValidity(): ValidityStateFlags { - return { valueMissing: this.required && !this.value }; + return { + valueMissing: this.required && !this.value, + patternMismatch: !this.isValidValue(this.value), + rangeUnderflow: !this.isValidMin(this.value), + rangeOverflow: !this.isValidMax(this.value), + }; } async formElementAnchor() { @@ -546,24 +571,22 @@ class DatePicker extends DateComponentBase implements IFormInputElement { this._updateValueAndFireEvents(newValue, true, ["change", "value-changed"]); } - _updateValueAndFireEvents(value: string, normalizeValue: boolean, events: Array<"change" | "value-changed" | "input">, updateValue = true) { + _updateValueAndFireEvents(value: string, normalizeValue: boolean, events: Array<"change" | "value-changed" | "input">, updateValue: boolean = true) { const valid = this._checkValueValidity(value); this.isLiveUpdate = !updateValue; - - if ((valid && normalizeValue) || !this.isLiveUpdate) { + if ((valid && normalizeValue) || !this.isLiveUpdate) { // in case that value is not valid we format it in change event value = this.getDisplayValueFromValue(value); value = this.normalizeDisplayValue(value); // transform valid values (in any format) to the correct format } let executeEvent = true; this.liveValue = value; - const previousValue = this.value; if (updateValue) { this._dateTimeInput.value = value; this.value = this.getValueFromDisplayValue(value); - this._updateValueState(); // Change the value state to Error/None, but only if needed + this._updateValueState(); } events.forEach(e => { @@ -725,6 +748,34 @@ class DatePicker extends DateComponentBase implements IFormInputElement { return calendarDate.valueOf() >= this._minDate.valueOf() && calendarDate.valueOf() <= this._maxDate.valueOf(); } + isValidMin(value: string): boolean { + if (value === "" || value === undefined) { + return true; + } + + const calendarDate = this._getCalendarDateFromString(value); + + if (!calendarDate || !this._minDate) { + return false; + } + + return calendarDate.valueOf() >= this._minDate.valueOf(); + } + + isValidMax(value: string): boolean { + if (value === "" || value === undefined) { + return true; + } + + const calendarDate = this._getCalendarDateFromString(value); + + if (!calendarDate || !this._maxDate) { + return false; + } + + return calendarDate.valueOf() <= this._maxDate.valueOf(); + } + isInValidRangeDisplayValue(value: string): boolean { if (value === "" || value === undefined) { return true; @@ -809,7 +860,7 @@ class DatePicker extends DateComponentBase implements IFormInputElement { return isPhone(); } - get displayValue() : string { + get displayValue(): string { if (!this.getValueFormat().parse(this.value, true)) { return this.value; } @@ -921,7 +972,7 @@ class DatePicker extends DateComponentBase implements IFormInputElement { } get _calendarPickersMode() { - const format = this.getFormat() as DateFormat & { aFormatArray: Array<{type: string}> }; + const format = this.getFormat() as DateFormat & { aFormatArray: Array<{ type: string }> }; const patternSymbolTypes = format.aFormatArray.map(patternSymbolSettings => { return patternSymbolSettings.type.toLowerCase(); }); diff --git a/packages/main/src/DateRangePicker.ts b/packages/main/src/DateRangePicker.ts index e13634f312f2..2772dfcff4f2 100644 --- a/packages/main/src/DateRangePicker.ts +++ b/packages/main/src/DateRangePicker.ts @@ -10,6 +10,10 @@ import { DATERANGE_DESCRIPTION, DATERANGEPICKER_POPOVER_ACCESSIBLE_NAME, DATETIME_COMPONENTS_PLACEHOLDER_PREFIX, + DATERANGE_VALUE_MISSING, + DATERANGE_PATTERN_MISMATCH, + DATERANGE_UNDERFLOW, + DATERANGE_OVERFLOW, } from "./generated/i18n/i18n-defaults.js"; import DateRangePickerTemplate from "./DateRangePickerTemplate.js"; @@ -82,6 +86,35 @@ class DateRangePicker extends DatePicker implements IFormInputElement { private _prevDelimiter: string | null; + get formValidityMessage() { + const validity = this.formValidity; + + if (validity.valueMissing) { + // @ts-ignore oFormatOptions is a private API of DateFormat + return DateRangePicker.i18nBundle.getText(DATERANGE_VALUE_MISSING, this.getFormat().oFormatOptions.pattern as string); + } + if (validity.patternMismatch) { + // @ts-ignore oFormatOptions is a private API of DateFormat + return DateRangePicker.i18nBundle.getText(DATERANGE_PATTERN_MISMATCH, this.getFormat().oFormatOptions.pattern as string); + } + if (validity.rangeUnderflow) { + return DateRangePicker.i18nBundle.getText(DATERANGE_UNDERFLOW, this.minDate); + } + if (validity.rangeOverflow) { + return DateRangePicker.i18nBundle.getText(DATERANGE_OVERFLOW, this.maxDate); + } + return ""; + } + + get formValidity(): ValidityStateFlags { + return { + valueMissing: this.required && !this.value, + patternMismatch: !!this.value && !this.isValidValue(this.value), + rangeUnderflow: !!this.value && !this.isValidMin(this.value), + rangeOverflow: !!this.value && !this.isValidMax(this.value), + }; + } + get formFormattedValue() { const values = this._splitValueByDelimiter(this.value || "").filter(Boolean); @@ -246,8 +279,7 @@ class DateRangePicker extends DatePicker implements IFormInputElement { * @param value A value to be tested against the current date format */ isValid(value: string): boolean { - let parts = this._splitValueByDelimiter(value).filter(str => str !== ""); - parts = parts.filter(str => str !== " "); // remove empty strings + const parts = this._splitValueByDelimiter(value).filter(str => str.trim() !== ""); return parts.length <= 2 && parts.every(dateString => super.isValid(dateString)); // must be at most 2 dates and each must be valid } @@ -258,8 +290,7 @@ class DateRangePicker extends DatePicker implements IFormInputElement { * @param value A value to be tested against the current date format */ isValidValue(value: string): boolean { - let parts = this._splitValueByDelimiter(value).filter(str => str !== ""); - parts = parts.filter(str => str !== " "); // remove empty strings + const parts = this._splitValueByDelimiter(value).filter(str => str.trim() !== ""); return parts.length <= 2 && parts.every(dateString => super.isValidValue(dateString)); // must be at most 2 dates and each must be valid } @@ -270,8 +301,7 @@ class DateRangePicker extends DatePicker implements IFormInputElement { * @param value A value to be tested against the current date format */ isValidDisplayValue(value: string): boolean { - let parts = this._splitValueByDelimiter(value).filter(str => str !== ""); - parts = parts.filter(str => str !== " "); // remove empty strings + const parts = this._splitValueByDelimiter(value).filter(str => str.trim() !== ""); return parts.length <= 2 && parts.every(dateString => super.isValidDisplayValue(dateString)); // must be at most 2 dates and each must be valid } @@ -282,12 +312,23 @@ class DateRangePicker extends DatePicker implements IFormInputElement { * @param value A value to be checked */ isInValidRange(value: string): boolean { - let parts = this._splitValueByDelimiter(value).filter(str => str !== ""); - parts = parts.filter(str => str !== " "); // remove empty strings + const parts = this._splitValueByDelimiter(value).filter(str => str.trim() !== ""); return parts.length <= 2 && parts.every(dateString => super.isInValidRange(dateString)); } + isValidMin(value: string): boolean { + const parts = this._splitValueByDelimiter(value).filter(str => str.trim() !== ""); + + return parts.length <= 2 && parts.every(dateString => super.isValidMin(dateString)); + } + + isValidMax(value: string): boolean { + const parts = this._splitValueByDelimiter(value).filter(str => str.trim() !== ""); + + return parts.length <= 2 && parts.every(dateString => super.isValidMax(dateString)); + } + /** * Extract both dates as timestamps, flip if necessary, and build (which will use the desired format so we enforce the format too) * @override @@ -532,7 +573,7 @@ class DateRangePicker extends DatePicker implements IFormInputElement { firstDateString = this._getDisplayStringFromTimestamp((this._extractFirstTimestamp(value) as number) * 1000); lastDateString = this._getDisplayStringFromTimestamp((this._extractLastTimestamp(value) as number) * 1000); - if (!firstDateString && !lastDateString) { + if (!firstDateString || !lastDateString) { return value; } diff --git a/packages/main/src/DateTimePicker.ts b/packages/main/src/DateTimePicker.ts index 6e0c5f2f68b9..94a23272c8ed 100644 --- a/packages/main/src/DateTimePicker.ts +++ b/packages/main/src/DateTimePicker.ts @@ -28,6 +28,10 @@ import { DATETIME_PICKER_DATE_BUTTON, DATETIME_PICKER_TIME_BUTTON, DATETIMEPICKER_POPOVER_ACCESSIBLE_NAME, + DATETIME_VALUE_MISSING, + DATETIME_PATTERN_MISMATCH, + DATETIME_RANGEUNDERFLOW, + DATETIME_RANGEOVERFLOW, } from "./generated/i18n/i18n-defaults.js"; // Template @@ -210,6 +214,36 @@ class DateTimePicker extends DatePicker implements IFormInputElement { } } + get formValidityMessage() { + const validity = this.formValidity; + + if (validity.valueMissing) { + // @ts-ignore oFormatOptions is a private API of DateFormat + return DateTimePicker.i18nBundle.getText(DATETIME_VALUE_MISSING, this.getFormat().oFormatOptions.pattern as string); + } + if (validity.patternMismatch) { + // @ts-ignore oFormatOptions is a private API of DateFormat + return DateTimePicker.i18nBundle.getText(DATETIME_PATTERN_MISMATCH, this.getFormat().oFormatOptions.pattern as string); + } + if (validity.rangeUnderflow) { + return DateTimePicker.i18nBundle.getText(DATETIME_RANGEUNDERFLOW, this.minDate); + } + if (validity.rangeOverflow) { + return DateTimePicker.i18nBundle.getText(DATETIME_RANGEOVERFLOW, this.maxDate); + } + + return ""; + } + + get formValidity(): ValidityStateFlags { + return { + valueMissing: this.required && !this.value, + patternMismatch: !this.isValidValue(this.value), + rangeUnderflow: !this.isValidMin(this.value), + rangeOverflow: !this.isValidMax(this.value), + }; + } + get _formatPattern() { const hasHours = !!(this.formatPattern || "").match(/H/i); const fallback = !this.formatPattern || !hasHours; diff --git a/packages/main/src/FileUploader.ts b/packages/main/src/FileUploader.ts index 5867845f2fb6..5f1216b2639c 100644 --- a/packages/main/src/FileUploader.ts +++ b/packages/main/src/FileUploader.ts @@ -37,6 +37,7 @@ import { FILEUPLOADER_DEFAULT_PLACEHOLDER, FILEUPLOADER_DEFAULT_MULTIPLE_PLACEHOLDER, FILEUPLOADER_ROLE_DESCRIPTION, + FILEUPLOAER_VALUE_MISSING, } from "./generated/i18n/i18n-defaults.js"; import type { InputAccInfo } from "./Input.js"; @@ -308,6 +309,22 @@ class FileUploader extends UI5Element implements IFormInputElement { @i18n("@ui5/webcomponents") static i18nBundle: I18nBundle; + get formValidityMessage() { + const validity = this.formValidity; + + if (validity.valueMissing) { + return FileUploader.i18nBundle.getText(FILEUPLOAER_VALUE_MISSING); + } + + return ""; + } + + get formValidity(): ValidityStateFlags { + return { + valueMissing: this.required && (!this.files || this.files.length === 0), + }; + } + async formElementAnchor() { return this.getFocusDomRefAsync(); } diff --git a/packages/main/src/StepInput.ts b/packages/main/src/StepInput.ts index e93eaaacee3a..87b527641399 100644 --- a/packages/main/src/StepInput.ts +++ b/packages/main/src/StepInput.ts @@ -25,7 +25,13 @@ import { getEffectiveAriaLabelText } from "@ui5/webcomponents-base/dist/util/Acc import type { Timeout } from "@ui5/webcomponents-base/dist/types.js"; import type { IFormInputElement } from "@ui5/webcomponents-base/dist/features/InputElementsFormSupport.js"; import StepInputTemplate from "./StepInputTemplate.js"; -import { STEPINPUT_DEC_ICON_TITLE, STEPINPUT_INC_ICON_TITLE } from "./generated/i18n/i18n-defaults.js"; +import { + STEPINPUT_DEC_ICON_TITLE, + STEPINPUT_INC_ICON_TITLE, + STEPINPUT_PATTER_MISSMATCH, + STEPINPUT_RANGEOVERFLOW, + STEPINPUT_RANGEUNDERFLOW, +} from "./generated/i18n/i18n-defaults.js"; import "@ui5/webcomponents-icons/dist/less.js"; import "@ui5/webcomponents-icons/dist/add.js"; @@ -294,6 +300,30 @@ class StepInput extends UI5Element implements IFormInputElement { return (await this.getFocusDomRefAsync() as UI5Element)?.getFocusDomRefAsync(); } + get formValidityMessage() { + const validity = this.formValidity; + + if (validity.patternMismatch) { + return StepInput.i18nBundle.getText(STEPINPUT_PATTER_MISSMATCH, this.valuePrecision); + } + if (validity.rangeUnderflow) { + return StepInput.i18nBundle.getText(STEPINPUT_RANGEUNDERFLOW, this.min as number); + } + if (validity.rangeOverflow) { + return StepInput.i18nBundle.getText(STEPINPUT_RANGEOVERFLOW, this.max as number); + } + + return ""; // No error + } + + 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(); } @@ -484,9 +514,9 @@ class StepInput extends UI5Element implements IFormInputElement { get _isValueWithCorrectPrecision() { // gets either "." or "," as delimiter which is based on locale, and splits the number by it - const delimiter = this.input.value.includes(".") ? "." : ","; - const numberParts = this.input.value.split(delimiter); - const decimalPartLength = numberParts.length > 1 ? numberParts[1].length : 0; + const delimiter = this.input?.value?.includes(".") ? "." : ","; + const numberParts = this.input?.value?.split(delimiter); + const decimalPartLength = numberParts?.length > 1 ? numberParts[1].length : 0; return decimalPartLength === this.valuePrecision; } diff --git a/packages/main/src/TimePicker.ts b/packages/main/src/TimePicker.ts index cbf948aab36d..723b42ea7299 100644 --- a/packages/main/src/TimePicker.ts +++ b/packages/main/src/TimePicker.ts @@ -53,11 +53,12 @@ import { TIMEPICKER_INPUT_DESCRIPTION, TIMEPICKER_POPOVER_ACCESSIBLE_NAME, DATETIME_COMPONENTS_PLACEHOLDER_PREFIX, - FORM_TEXTFIELD_REQUIRED, VALUE_STATE_ERROR, VALUE_STATE_INFORMATION, VALUE_STATE_SUCCESS, VALUE_STATE_WARNING, + TIMEPICKER_VALUE_MISSING, + TIMEPICKER_PATTERN_MISSMATCH, } from "./generated/i18n/i18n-defaults.js"; // Styles @@ -351,11 +352,25 @@ class TimePicker extends UI5Element implements IFormInputElement { static i18nBundle: I18nBundle; get formValidityMessage() { - return TimePicker.i18nBundle.getText(FORM_TEXTFIELD_REQUIRED); + const validity = this.formValidity; + + if (validity.valueMissing) { + // @ts-ignore oFormatOptions is a private API of DateFormat + return TimePicker.i18nBundle.getText(TIMEPICKER_VALUE_MISSING, this.getFormat().oFormatOptions.pattern as string); + } + if (validity.patternMismatch) { + // @ts-ignore oFormatOptions is a private API of DateFormat + return TimePicker.i18nBundle.getText(TIMEPICKER_PATTERN_MISSMATCH, this.getFormat().oFormatOptions.pattern as string); + } + + return ""; } get formValidity(): ValidityStateFlags { - return { valueMissing: this.required && !this.value }; + return { + valueMissing: this.required && !this.value, + patternMismatch: !this.isValid(this.value), + }; } async formElementAnchor() { diff --git a/packages/main/src/i18n/messagebundle.properties b/packages/main/src/i18n/messagebundle.properties index c1a7eaf42035..91e1e9198f9b 100644 --- a/packages/main/src/i18n/messagebundle.properties +++ b/packages/main/src/i18n/messagebundle.properties @@ -178,12 +178,36 @@ DATEPICKER_OPEN_ICON_TITLE=Open Picker #XACT: Aria information for the Date Picker DATEPICKER_DATE_DESCRIPTION=Date Input +DATEPICKER_VALUE_MISSING=Fill in the date value in the format: {0}. + +DATEPICKER_PATTERN_MISSMATCH=This format is not supported. Fill in the date and time range values in the format: {0}. + +DATEPICKER_RANGE_OVERFLOW=Fill in a date value that is lower than the set max. value of {0}. + +DATEPICKER_RANGE_UNDERFLOW=Fill in a date value that is higher than the set min. value of {0}. + #XACT: Aria information for the Date Time Picker DATETIME_DESCRIPTION=Date Time Input +DATETIME_VALUE_MISSING=Fill in the date and time values in the format: {0}. + +DATETIME_PATTERN_MISMATCH=This format is not supported. Fill in the date and time values in the format: {0}. + +DATETIME_RANGEOVERFLOW=Fill in a value that is lower than the set max. value of {0}. + +DATETIME_RANGEUNDERFLOW=Fill in a value that is higher than the set min. value of {0}. + #XACT: Aria information for the Date Range Picker DATERANGE_DESCRIPTION=Date Range Input +DATERANGE_VALUE_MISSING=Fill in the date range in the format: {0} - {0}. + +DATERANGE_PATTERN_MISMATCH=This format is not supported. Fill in the date values in the format: {0} - {0}. + +DATERANGE_OVERFLOW=Fill in a value that is lower than the set max. value of {0}. + +DATERANGE_UNDERFLOW=Fill in a value that is higher than the set min. value of {0}. + #XACT: Aria information for the Date Picker popover DATEPICKER_POPOVER_ACCESSIBLE_NAME=Choose Date for {0} @@ -236,6 +260,8 @@ FILEUPLOADER_VALUE_HELP_TOOLTIP=Browse and replace all files #XTOL: Default tooltip text for the ui5-file-uploader's clear icon FILEUPLOADER_CLEAR_ICON_TOOLTIP=Remove all files +FILEUPLOAER_VALUE_MISSING=Select or drag and drop a file to upload. + GROUP_HEADER_TEXT=Group Header #XACT: ARIA announcement for the Select`s roledescription attribute @@ -511,6 +537,10 @@ TIMEPICKER_INPUTS_ENTER_MINUTES=Please enter minutes #XACT: Time Picker Inputs tooltip/aria-label for Seconds input TIMEPICKER_INPUTS_ENTER_SECONDS=Please enter seconds +TIMEPICKER_VALUE_MISSING=Fill in the time value in the format: {0}. + +TIMEPICKER_PATTERN_MISSMATCH=This format is not supported. Fill in the time value in the format: {0}. + #XACT: Aria information for the Duration Picker DURATION_INPUT_DESCRIPTION=Duration Input @@ -658,6 +688,12 @@ STEPINPUT_DEC_ICON_TITLE=Decrease #XTOL: tooltip for increase button of the StepInput STEPINPUT_INC_ICON_TITLE=Increase +STEPINPUT_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}. + #XACT: Aria information for the Split Button SPLIT_BUTTON_DESCRIPTION=Split Button diff --git a/packages/main/test/pages/DatePicker.html b/packages/main/test/pages/DatePicker.html index 954be849a237..17d3689a3969 100644 --- a/packages/main/test/pages/DatePicker.html +++ b/packages/main/test/pages/DatePicker.html @@ -175,12 +175,45 @@

DatePicker with format `yyyy` should open picker on years

- +
+ Form validation +
+ + +

+ Check Validity +
+
diff --git a/packages/main/test/pages/DateRangePicker.html b/packages/main/test/pages/DateRangePicker.html index c44746ca6f97..39475cd3a0c4 100644 --- a/packages/main/test/pages/DateRangePicker.html +++ b/packages/main/test/pages/DateRangePicker.html @@ -80,8 +80,44 @@

daterange-picker with value state

Information message. This is a Link. Extra long text used as an information message. Extra long text used as an information message - 2. Extra long text used as an information message - 3.
+ +
+ Form validation +
+ + +

+ Check Validity +
+
diff --git a/packages/main/test/pages/StepInput.html b/packages/main/test/pages/StepInput.html index aa7624125cd2..21a5ed3d911a 100644 --- a/packages/main/test/pages/StepInput.html +++ b/packages/main/test/pages/StepInput.html @@ -169,6 +169,22 @@

'input' event prevented

'change' event result

+
+

Form validation

+
+ + + Check Validity +
+
+ diff --git a/packages/main/test/pages/Switch.html b/packages/main/test/pages/Switch.html index 37285aac21b8..1b3785991e87 100644 --- a/packages/main/test/pages/Switch.html +++ b/packages/main/test/pages/Switch.html @@ -77,6 +77,14 @@

Switch in form test

Submit +

Form validation

+
+ + +

+ Check Validity +
+

Custom Switch

@@ -103,7 +111,10 @@

sap_horizon

diff --git a/packages/main/test/pages/TimePicker.html b/packages/main/test/pages/TimePicker.html index c7c8930ee39e..d060b16fff6f 100644 --- a/packages/main/test/pages/TimePicker.html +++ b/packages/main/test/pages/TimePicker.html @@ -96,6 +96,15 @@

TimePicker in Compact

+
+

Form validation

+
+ + + Check Validity +
+
+ diff --git a/packages/main/test/pages/styles/DatePicker.css b/packages/main/test/pages/styles/DatePicker.css index ef2436fa33e8..5abb4db30720 100644 --- a/packages/main/test/pages/styles/DatePicker.css +++ b/packages/main/test/pages/styles/DatePicker.css @@ -5,3 +5,8 @@ .datepicker1auto { background-color: var(--sapBackgroundColor); } + + +form ui5-date-picker:invalid { + outline: 2px solid var(--sapNegativeColor); +} \ No newline at end of file diff --git a/packages/main/test/pages/styles/DateRangePicker.css b/packages/main/test/pages/styles/DateRangePicker.css index 94d4300f0914..7cf005729231 100644 --- a/packages/main/test/pages/styles/DateRangePicker.css +++ b/packages/main/test/pages/styles/DateRangePicker.css @@ -5,3 +5,7 @@ .daterangepicker1auto { background-color: var(--sapBackgroundColor); } + +form ui5-daterange-picker:invalid { + outline: 2px solid var(--sapNegativeColor); +} diff --git a/packages/main/test/pages/styles/DateTimePicker.css b/packages/main/test/pages/styles/DateTimePicker.css index 0775b7c194cc..2efcf9870d90 100644 --- a/packages/main/test/pages/styles/DateTimePicker.css +++ b/packages/main/test/pages/styles/DateTimePicker.css @@ -13,3 +13,8 @@ ui5-datetime-picker { .datetimepicker2auto { width: 300px } + +form ui5-datetime-picker:invalid { + outline: 2px solid var(--sapNegativeColor); +} + diff --git a/packages/main/test/pages/styles/FileUploader.css b/packages/main/test/pages/styles/FileUploader.css index 877df901e06d..1db9ea6b6c0d 100644 --- a/packages/main/test/pages/styles/FileUploader.css +++ b/packages/main/test/pages/styles/FileUploader.css @@ -4,4 +4,8 @@ body > div { .fileuploader1auto { background-color: var(--sapBackgroundColor); +} + +form ui5-file-uploader:invalid { + outline: 2px solid var(--sapNegativeColor); } \ No newline at end of file diff --git a/packages/main/test/pages/styles/StepInput.css b/packages/main/test/pages/styles/StepInput.css index 0824f95c17e6..7471626f7e72 100644 --- a/packages/main/test/pages/styles/StepInput.css +++ b/packages/main/test/pages/styles/StepInput.css @@ -34,3 +34,7 @@ h3 { text-align: center; width: 250px } + +form ui5-step-input:invalid { + outline: 2px solid var(--sapNegativeColor); +} diff --git a/packages/main/test/pages/styles/Switch.css b/packages/main/test/pages/styles/Switch.css index 8e3de3b92f99..84dd41f6534b 100644 --- a/packages/main/test/pages/styles/Switch.css +++ b/packages/main/test/pages/styles/Switch.css @@ -81,3 +81,7 @@ ui5-switch { border-radius: var(--sapElement_BorderCornerRadius); padding: 1rem; } + +#formValidation ui5-switch:invalid{ + outline: 2px solid var(--sapNegativeColor); +} diff --git a/packages/main/test/pages/styles/TimePicker.css b/packages/main/test/pages/styles/TimePicker.css index 6c952e7c4e67..fbddb5f3af96 100644 --- a/packages/main/test/pages/styles/TimePicker.css +++ b/packages/main/test/pages/styles/TimePicker.css @@ -22,3 +22,7 @@ html,body { .timepicker2auto { width: 100% } + +form ui5-time-picker:invalid { + outline: 2px solid var(--sapNegativeColor); +}