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
-
+