diff --git a/packages/main/cypress/specs/ComboBox.cy.tsx b/packages/main/cypress/specs/ComboBox.cy.tsx index b1a2fa6b5bff..cbea94de23ec 100644 --- a/packages/main/cypress/specs/ComboBox.cy.tsx +++ b/packages/main/cypress/specs/ComboBox.cy.tsx @@ -91,7 +91,7 @@ describe("General Interaction", () => { cy.get("@combobox").should("have.prop", "value", "One"); cy.get("[ui5-cb-item]").first().should("have.prop", "selected", true); - + cy.window().then(window => { return window.getSelection()?.toString(); }).should("contains", "ne"); @@ -921,7 +921,7 @@ describe("Accessibility", () => { // open the popover cy.get("@combo").shadow().find("input").realPress("F4"); cy.get("@combo").shadow().find("input").realPress("ArrowDown"); - + cy.get("@invisibleMessageSpan").should("have.text", "List item 2 of 6"); }); @@ -1257,14 +1257,14 @@ describe("Additional Navigation", () => { const scrollableRect = picker[0].shadowRoot.querySelector(".ui5-popup-content").getBoundingClientRect(); const lastItem = document.querySelector("#combo-grouping ui5-cb-item-group:last-child ui5-cb-item:last-child"); const elementRect = lastItem!.getBoundingClientRect(); - + const isInVisibleArea = !( elementRect.bottom < scrollableRect.top || elementRect.top > scrollableRect.bottom || elementRect.right < scrollableRect.left || elementRect.left > scrollableRect.right ); - + expect(isInVisibleArea).to.be.true; }); @@ -1279,14 +1279,14 @@ describe("Additional Navigation", () => { const scrollableRect = picker[0].shadowRoot.querySelector(".ui5-popup-content").getBoundingClientRect(); const firstItem = document.querySelector("#combo-grouping ui5-cb-item-group:first-child ui5-cb-item:first-child"); const elementRect = firstItem!.getBoundingClientRect(); - + const isInVisibleArea = !( elementRect.bottom < scrollableRect.top || elementRect.top > scrollableRect.bottom || elementRect.right < scrollableRect.left || elementRect.left > scrollableRect.right ); - + expect(isInVisibleArea).to.be.true; }); }); @@ -1398,7 +1398,7 @@ describe("Keyboard interaction", () => { cy.get("#combo-grouping").shadow().find("input").realClick(); cy.get("#combo-grouping").should("be.focused"); - + cy.get("#combo-grouping").shadow().find("input").realClick(); cy.get("#combo-grouping").should("be.focused"); @@ -1922,7 +1922,7 @@ describe("Event firing", () => { // Focus out to trigger change event cy.get("body").realClick(); - + cy.get("@changeSpy").should('have.been.calledOnce'); cy.get("@changeSpy").should('have.been.calledWithMatch', Cypress.sinon.match(event => { return event.target.value === "Algeria"; @@ -1997,7 +1997,7 @@ describe("Event firing", () => { // Verify change event was fired cy.get("@changeSpy").should('have.callCount', 1); - + // Verify the event contains correct data cy.get("@changeSpy").should('have.been.calledWithMatch', Cypress.sinon.match(event => { return event.target.value === "Bahrain"; @@ -2106,11 +2106,11 @@ describe("Event firing", () => { cy.get("@combo").shadow().find("[ui5-icon]").realClick(); cy.get("@combo").shadow().find("[inner-input]").realPress("ArrowDown"); - + cy.get("@changeSpy").should('have.callCount', 0); - + cy.get("@combo").find("[ui5-cb-item]").first().realClick(); - + cy.get("@changeSpy").should('have.been.calledOnce'); cy.get("@changeSpy").should('have.been.calledWithMatch', Cypress.sinon.match(event => { return event.target.value === "Algeria"; @@ -2131,7 +2131,7 @@ describe("Event firing", () => { cy.get("#change-cb").shadow().find("[inner-input]").realClick(); cy.get("#change-cb").shadow().find("[inner-input]").realPress("ArrowDown"); - + cy.get("@changeSpy").should('have.callCount', 0); cy.get("#change-cb").shadow().find("[inner-input]").realPress("ArrowDown"); @@ -2185,7 +2185,7 @@ describe("Event firing", () => { cy.get("#input-cb").shadow().find("[inner-input]").realClick(); cy.get("#input-cb").shadow().find("[inner-input]").realPress("ArrowDown"); - + cy.get("@inputSpy").should('have.been.calledOnce'); cy.get("@inputSpy").should('have.been.calledWithMatch', Cypress.sinon.match(event => { return event.target.value === "Argentina"; @@ -2597,7 +2597,7 @@ describe("Event firing", () => { cy.get("@combo").should("have.prop", "_effectiveShowClearIcon", true); cy.get("@combo").shadow().find(".ui5-input-clear-icon-wrapper").realClick(); - cy.get("@inputSpy").should('have.been.calledTwice'); + cy.get("@inputSpy").should('have.been.calledTwice'); }); it("should show all items if value does not match any item and arrow is pressed", () => { @@ -2843,7 +2843,7 @@ describe("ComboBox Composition", () => { cy.get("@combobox").should("have.prop", "_isComposing", true); cy.get("@nativeInput").trigger("compositionend", { data: "사랑" }); - + cy.get("@nativeInput") .invoke("val", "사랑") .trigger("input", { inputType: "insertCompositionText" }); @@ -2897,7 +2897,7 @@ describe("ComboBox Composition", () => { cy.get("@combobox").should("have.prop", "_isComposing", true); cy.get("@nativeInput").trigger("compositionend", { data: "ありがとう" }); - + cy.get("@nativeInput") .invoke("val", "ありがとう") .trigger("input", { inputType: "insertCompositionText" }); @@ -2951,7 +2951,7 @@ describe("ComboBox Composition", () => { cy.get("@combobox").should("have.prop", "_isComposing", true); cy.get("@nativeInput").trigger("compositionend", { data: "谢谢" }); - + cy.get("@nativeInput") .invoke("val", "谢谢") .trigger("input", { inputType: "insertCompositionText" }); @@ -2973,3 +2973,57 @@ describe("ComboBox Composition", () => { .should("have.attr", "value", "谢谢"); }); }); + +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("[ui5-combobox]") + .as("combo") + .ui5AssertValidityState({ + formValidity: { valueMissing: true }, + validity: { valueMissing: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#cmbForm:invalid") + .should("exist"); + + cy.get("@combo") + .realType("Albania"); + + cy.get("@combo") + .ui5AssertValidityState({ + formValidity: { valueMissing: false }, + validity: { valueMissing: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#cmbForm:invalid") + .should("not.exist"); + + cy.get("#submitBtn") + .realClick(); + + cy.get("@submit") + .should("have.been.calledOnce"); + }); +}); diff --git a/packages/main/cypress/specs/Input.cy.tsx b/packages/main/cypress/specs/Input.cy.tsx index d35260bbdf6e..6f5a161aef82 100644 --- a/packages/main/cypress/specs/Input.cy.tsx +++ b/packages/main/cypress/specs/Input.cy.tsx @@ -2913,3 +2913,184 @@ describe("Input Composition", () => { .should("have.attr", "value", "谢谢"); }); }); + +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("[ui5-input]") + .as("input") + .ui5AssertValidityState({ + formValidity: { valueMissing: true }, + validity: { valueMissing: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#inpForm:invalid") + .should("exist"); + + cy.get("@input") + .realType("Albania"); + + cy.get("@input") + .ui5AssertValidityState({ + formValidity: { valueMissing: false }, + validity: { valueMissing: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#inpForm:invalid") + .should("not.exist"); + + cy.get("#submitBtn") + .realClick(); + + cy.get("@submit") + .should("have.been.calledOnce"); + }); + + it("has correct validity for typeMismatch- Email", () => { + 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("[ui5-input]") + .as("input") + .realClick() + .realType("email"); + + cy.get("@input") + .should("have.value", "email"); + + cy.get("#submitBtn") + .realClick(); + + cy.get("@submit") + .should("have.not.been.called"); + + cy.get("@input") + .ui5AssertValidityState({ + formValidity: { typeMismatch: true }, + validity: { typeMismatch: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#inpForm:invalid") + .should("exist"); + + cy.get("@input") + .shadow() + .find("input") + .clear(); + + cy.get("@input") + .realType("email@gmail.com"); + + cy.get("@input") + .ui5AssertValidityState({ + formValidity: { patternMismatch: false }, + validity: { patternMismatch: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#inpForm:invalid") + .should("not.exist"); + + cy.get("#submitBtn") + .realClick(); + + cy.get("@submit") + .should("have.been.calledOnce"); + }); + it("has correct validity for typeMismatch- URL", () => { + 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("[ui5-input]") + .as("input") + .realClick() + .realType("google"); + + cy.get("@input") + .should("have.value", "google"); + + cy.get("#submitBtn") + .realClick(); + + cy.get("@submit") + .should("have.not.been.called"); + + cy.get("@input") + .ui5AssertValidityState({ + formValidity: { typeMismatch: true }, + validity: { typeMismatch: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#inpForm:invalid") + .should("exist"); + + cy.get("@input") + .shadow() + .find("input") + .clear(); + + cy.get("@input") + .realType("https://www.google.com"); + + cy.get("@input") + .ui5AssertValidityState({ + formValidity: { typeMismatch: false }, + validity: { typeMismatch: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#inpForm:invalid") + .should("not.exist"); + + cy.get("#submitBtn") + .realClick(); + + cy.get("@submit") + .should("have.been.calledOnce"); + }); +}); diff --git a/packages/main/cypress/specs/MultiComboBox.cy.tsx b/packages/main/cypress/specs/MultiComboBox.cy.tsx index 4e51fb2029e5..7a712c662fc8 100644 --- a/packages/main/cypress/specs/MultiComboBox.cy.tsx +++ b/packages/main/cypress/specs/MultiComboBox.cy.tsx @@ -4392,3 +4392,61 @@ describe("MultiComboBox Composition", () => { .should("have.length", 0); }); }); + +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("[ui5-multi-combobox]") + .as("mcb") + .ui5AssertValidityState({ + formValidity: { valueMissing: true }, + validity: { valueMissing: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#mcbInAForm:invalid") + .should("exist"); + + cy.get("@mcb") + .realType("Albania"); + + cy.get("@mcb") + .ui5AssertValidityState({ + formValidity: { valueMissing: false }, + validity: { valueMissing: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#mcbInAForm:invalid") + .should("not.exist"); + + cy.get("#submitBtn") + .realClick(); + + cy.get("@submit") + .should("have.been.calledOnce"); + }); +}); diff --git a/packages/main/cypress/specs/TextArea.cy.tsx b/packages/main/cypress/specs/TextArea.cy.tsx index 6db77dbdf060..0211883fa1a2 100644 --- a/packages/main/cypress/specs/TextArea.cy.tsx +++ b/packages/main/cypress/specs/TextArea.cy.tsx @@ -847,3 +847,121 @@ describe("TextArea general interaction", () => { }); }); }); + +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("[ui5-textarea]") + .as("textarea") + .ui5AssertValidityState({ + formValidity: { valueMissing: true }, + validity: { valueMissing: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#textareaForm:invalid") + .should("exist"); + + cy.get("@textarea") + .realType("Albania"); + + cy.get("@textarea") + .ui5AssertValidityState({ + formValidity: { valueMissing: false }, + validity: { valueMissing: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#textareaForm:invalid") + .should("not.exist"); + + cy.get("#submitBtn") + .realClick(); + + cy.get("@submit") + .should("have.been.calledOnce"); + }); + + it("has correct validity for tooLong", () => { + 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("[ui5-textarea]") + .as("textarea") + .realClick() + .realType("Some long text"); + + cy.get("@textarea") + .should("have.value", "Some long text"); + + cy.get("#submitBtn") + .realClick(); + + cy.get("@submit") + .should("have.not.been.called"); + + cy.get("@textarea") + .ui5AssertValidityState({ + formValidity: { tooLong: true }, + validity: { tooLong: true, valid: false }, + checkValidity: false, + reportValidity: false + }); + + cy.get("#textareaForm:invalid") + .should("exist"); + + cy.get("@textarea") + .shadow() + .find("textarea") + .clear(); + + cy.get("@textarea") + .realType("Short text"); + + cy.get("@textarea") + .ui5AssertValidityState({ + formValidity: { tooLong: false }, + validity: { tooLong: false, valid: true }, + checkValidity: true, + reportValidity: true + }); + + cy.get("#textareaForm:invalid") + .should("not.exist"); + + cy.get("#submitBtn") + .realClick(); + + cy.get("@submit") + .should("have.been.calledOnce"); + }); +}); diff --git a/packages/main/src/Input.ts b/packages/main/src/Input.ts index 74a4c979eafe..bfc26327cf29 100644 --- a/packages/main/src/Input.ts +++ b/packages/main/src/Input.ts @@ -89,7 +89,6 @@ import { INPUT_AVALIABLE_VALUES, INPUT_SUGGESTIONS_OK_BUTTON, INPUT_SUGGESTIONS_CANCEL_BUTTON, - FORM_TEXTFIELD_REQUIRED, } from "./generated/i18n/i18n-defaults.js"; // Styles @@ -652,7 +651,7 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement _handleLinkNavigation: boolean = false; get formValidityMessage() { - return Input.i18nBundle.getText(FORM_TEXTFIELD_REQUIRED); + return this.nativeInput?.validationMessage; } get _effectiveShowSuggestions() { @@ -660,7 +659,11 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement } get formValidity(): ValidityStateFlags { - return { valueMissing: this.required && !this.value }; + return { + valueMissing: this.nativeInput?.validity.valueMissing, + typeMismatch: this.required && this.nativeInput?.validity.typeMismatch, + patternMismatch: this.nativeInput?.validity.patternMismatch, + }; } async formElementAnchor() { diff --git a/packages/main/src/InputTemplate.tsx b/packages/main/src/InputTemplate.tsx index 3eae53793cee..c9cf951217de 100644 --- a/packages/main/src/InputTemplate.tsx +++ b/packages/main/src/InputTemplate.tsx @@ -33,6 +33,7 @@ export default function InputTemplate(this: Input, hooks?: { preContent: Templat inner-input-with-icon={!!this.icon.length} disabled={this.disabled} readonly={this._readonly} + required={this.required} value={this._innerValue} placeholder={this._placeholder} maxlength={this.maxlength} diff --git a/packages/main/src/MultiInput.ts b/packages/main/src/MultiInput.ts index 1703900e3720..668f6cd6ab9e 100644 --- a/packages/main/src/MultiInput.ts +++ b/packages/main/src/MultiInput.ts @@ -23,6 +23,7 @@ import { MULTIINPUT_ROLEDESCRIPTION_TEXT, MULTIINPUT_VALUE_HELP_LABEL, MULTIINPUT_VALUE_HELP, + FORM_MIXED_TEXTFIELD_REQUIRED, MULTIINPUT_FILTER_BUTTON_LABEL, } from "./generated/i18n/i18n-defaults.js"; import Input from "./Input.js"; @@ -154,6 +155,10 @@ class MultiInput extends Input implements IFormInputElement { _skipOpenSuggestions: boolean; _valueHelpIconPressed: boolean; + get formValidityMessage() { + return MultiInput.i18nBundle.getText(FORM_MIXED_TEXTFIELD_REQUIRED); + } + get formValidity(): ValidityStateFlags { const tokens = (this.tokens || []); diff --git a/packages/main/src/TextArea.ts b/packages/main/src/TextArea.ts index 775e33a77098..71419cf8a8ac 100644 --- a/packages/main/src/TextArea.ts +++ b/packages/main/src/TextArea.ts @@ -32,6 +32,7 @@ import { TEXTAREA_CHARACTERS_LEFT, TEXTAREA_CHARACTERS_EXCEEDED, FORM_TEXTFIELD_REQUIRED, + TEXTAREA_EXCEEDS_MAXLENGTH, } from "./generated/i18n/i18n-defaults.js"; // Styles @@ -350,11 +351,20 @@ class TextArea extends UI5Element implements IFormInputElement { static i18nBundle: I18nBundle; get formValidityMessage() { - return TextArea.i18nBundle.getText(FORM_TEXTFIELD_REQUIRED); + if (this.formValidity.valueMissing) { + return TextArea.i18nBundle.getText(FORM_TEXTFIELD_REQUIRED); + } + + if (this.formValidity.tooLong) { + return TextArea.i18nBundle.getText(TEXTAREA_EXCEEDS_MAXLENGTH, this.value.length - (this.maxlength ?? 0)); + } } get formValidity(): ValidityStateFlags { - return { valueMissing: this.required && !this.value }; + return { + valueMissing: this.required && !this.value, + tooLong: this.showExceededText && (this.value.length > (this.maxlength ?? 0)), + }; } async formElementAnchor() { diff --git a/packages/main/src/i18n/messagebundle.properties b/packages/main/src/i18n/messagebundle.properties index 4787fa4fa447..499b49b72af2 100644 --- a/packages/main/src/i18n/messagebundle.properties +++ b/packages/main/src/i18n/messagebundle.properties @@ -525,6 +525,9 @@ TEXTAREA_CHARACTERS_LEFT={0} characters remaining #XTXT: Text for characters over TEXTAREA_CHARACTERS_EXCEEDED={0} characters over limit +#XTXT: Text for characters over in form +TEXTAREA_EXCEEDS_MAXLENGTH =Value too long by {0} characters. + #XFLD: Timepicker slider header TIMEPICKER_HOURS_LABEL=Hours diff --git a/packages/main/test/pages/ComboBox.html b/packages/main/test/pages/ComboBox.html index 29bf7c4db79c..abf48ebbf21c 100644 --- a/packages/main/test/pages/ComboBox.html +++ b/packages/main/test/pages/ComboBox.html @@ -413,6 +413,48 @@