From 27003f70faf2e7bc96855addeabf69c823f398b6 Mon Sep 17 00:00:00 2001 From: Diana Petcheva Date: Mon, 23 Mar 2026 22:25:28 +0200 Subject: [PATCH 1/4] feat(ui5-button): introduce support for form attribute --- .../src/features/InputElementsFormSupport.ts | 29 +++- packages/main/cypress/specs/Button.cy.tsx | 124 ++++++++++++++++++ packages/main/src/Button.ts | 13 ++ packages/main/test/pages/Button.html | 26 ++++ 4 files changed, 189 insertions(+), 3 deletions(-) diff --git a/packages/base/src/features/InputElementsFormSupport.ts b/packages/base/src/features/InputElementsFormSupport.ts index 50078f7401a2..7212b7cc0afe 100644 --- a/packages/base/src/features/InputElementsFormSupport.ts +++ b/packages/base/src/features/InputElementsFormSupport.ts @@ -8,6 +8,22 @@ interface IFormInputElement extends UI5Element { formElementAnchor?: () => HTMLElement | undefined | Promise; } +/** + * Gets the associated form for an element. + * If the element has a `form` attribute, it looks up the form by ID. + * Otherwise, it falls back to the form associated via ElementInternals. + */ +const getAssociatedForm = (element: UI5Element): HTMLFormElement | null => { + const formAttribute = element.getAttribute("form"); + + if (formAttribute) { + const form = document.getElementById(formAttribute); + return form instanceof HTMLFormElement ? form : null; + } + + return element._internals?.form ?? null; +}; + const updateFormValue = (element: IFormInputElement | UI5Element) => { if (isInputElement(element)) { setFormValue(element); @@ -46,15 +62,22 @@ const setFormValidity = async (element: IFormInputElement) => { }; const submitForm = async (element: UI5Element) => { - const elements = [...(element._internals?.form?.elements ?? [])] as Array; + const form = getAssociatedForm(element); + + if (!form) { + return; + } + + const elements = [...form.elements] as Array; await Promise.all(elements.map(el => { return isInputElement(el) ? setFormValidity(el) : Promise.resolve(); })); - element._internals?.form?.requestSubmit(); + form.requestSubmit(); }; const resetForm = (element: UI5Element) => { - element._internals?.form?.reset(); + const form = getAssociatedForm(element); + form?.reset(); }; const isInputElement = (element: IFormInputElement | UI5Element): element is IFormInputElement => { diff --git a/packages/main/cypress/specs/Button.cy.tsx b/packages/main/cypress/specs/Button.cy.tsx index 3e4be6b64865..687b89e04818 100644 --- a/packages/main/cypress/specs/Button.cy.tsx +++ b/packages/main/cypress/specs/Button.cy.tsx @@ -638,4 +638,128 @@ describe("Accessibility", () => { expect(info.description).to.include("Negative Action"); }); }); +}); + +describe("Button form attribute", () => { + it("should submit an external form using form attribute", () => { + const submitSpy = cy.spy().as("submitSpy"); + + cy.mount( +
+
{ + e.preventDefault(); + submitSpy(); + }}> + +
+ +
+ ); + + cy.get("[ui5-button]") + .realClick(); + + cy.get("@submitSpy") + .should("have.been.calledOnce"); + }); + + it("should reset an external form using form attribute", () => { + cy.mount( +
+
+ +
+ +
+ ); + + // Change the input value + cy.get("#testInput") + .clear() + .type("changed"); + + cy.get("#testInput") + .should("have.value", "changed"); + + // Click the reset button + cy.get("[ui5-button]") + .realClick(); + + // Verify the form was reset + cy.get("#testInput") + .should("have.value", "initial"); + }); + + it("should not submit when form attribute references non-existent form", () => { + const submitSpy = cy.spy().as("submitSpy"); + + cy.mount( +
+
{ + e.preventDefault(); + submitSpy(); + }}> + +
+ +
+ ); + + cy.get("[ui5-button]") + .realClick(); + + cy.get("@submitSpy") + .should("not.have.been.called"); + }); + + it("should prioritize form attribute over parent form", () => { + const parentFormSubmitSpy = cy.spy().as("parentFormSubmit"); + const externalFormSubmitSpy = cy.spy().as("externalFormSubmit"); + + cy.mount( +
+
{ + e.preventDefault(); + externalFormSubmitSpy(); + }}> + +
+
{ + e.preventDefault(); + parentFormSubmitSpy(); + }}> + + +
+
+ ); + + cy.get("[ui5-button]") + .realClick(); + + cy.get("@externalFormSubmit") + .should("have.been.calledOnce"); + cy.get("@parentFormSubmit") + .should("not.have.been.called"); + }); + + it("should fall back to parent form when form attribute is not set", () => { + const submitSpy = cy.spy().as("submitSpy"); + + cy.mount( +
{ + e.preventDefault(); + submitSpy(); + }}> + + +
+ ); + + cy.get("[ui5-button]") + .realClick(); + + cy.get("@submitSpy") + .should("have.been.calledOnce"); + }); }); \ No newline at end of file diff --git a/packages/main/src/Button.ts b/packages/main/src/Button.ts index 4e4ad60366c1..cac934119239 100644 --- a/packages/main/src/Button.ts +++ b/packages/main/src/Button.ts @@ -204,6 +204,19 @@ class Button extends UI5Element implements IButton { @property({ type: Boolean }) submits = false; + /** + * Associates the button with a form element by the form's `id` attribute. + * When set, the button can submit or reset the specified form even if the button + * is not a descendant of that form. + * + * **Note:** This property takes effect only when the button's "type" property is set to "Submit" or "Reset". + * @default undefined + * @public + * @since 2.1x.0 + */ + @property() + form?: string; + /** * Defines the tooltip of the component. * diff --git a/packages/main/test/pages/Button.html b/packages/main/test/pages/Button.html index 26ccaa3c83f2..8d7b3243fc21 100644 --- a/packages/main/test/pages/Button.html +++ b/packages/main/test/pages/Button.html @@ -309,6 +309,22 @@ Reset +
+
+ Buttons with form attribute (outside form) +
+
+ External Form: + +
+
+

These buttons are outside the form but control it via the form attribute:

+ Submit External Form + Reset External Form + +
+
+ Show Registration Dialog @@ -413,6 +429,16 @@ btnInLink.addEventListener("ui5-click", (e) => { e.preventDefault(); }); + + function handleExternalFormSubmit(event) { + event.preventDefault(); + const message = document.getElementById('externalFormMessage'); + message.style.display = 'inline'; + setTimeout(() => { + message.style.display = 'none'; + }, 3000); + } + window.handleExternalFormSubmit = handleExternalFormSubmit; From 5ad9397ea795691077fe6e6a2e79f98fee70a288 Mon Sep 17 00:00:00 2001 From: Diana Petcheva Date: Tue, 24 Mar 2026 10:17:33 +0200 Subject: [PATCH 2/4] chore: change dev sample --- packages/main/test/pages/Button.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/main/test/pages/Button.html b/packages/main/test/pages/Button.html index 8d7b3243fc21..f01f072955d9 100644 --- a/packages/main/test/pages/Button.html +++ b/packages/main/test/pages/Button.html @@ -432,11 +432,14 @@ function handleExternalFormSubmit(event) { event.preventDefault(); + const formData = new FormData(event.target); + const values = Object.fromEntries(formData.entries()); const message = document.getElementById('externalFormMessage'); + message.textContent = 'Submitted: ' + JSON.stringify(values); message.style.display = 'inline'; setTimeout(() => { message.style.display = 'none'; - }, 3000); + }, 5000); } window.handleExternalFormSubmit = handleExternalFormSubmit; From 548ab0f1c82d1d58d4773ee1dac139e06a4da708 Mon Sep 17 00:00:00 2001 From: Diana Petcheva Date: Tue, 24 Mar 2026 10:33:50 +0200 Subject: [PATCH 3/4] chore: use RealEvents --- packages/main/cypress/specs/Button.cy.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/main/cypress/specs/Button.cy.tsx b/packages/main/cypress/specs/Button.cy.tsx index 687b89e04818..5f1b5e7eba8c 100644 --- a/packages/main/cypress/specs/Button.cy.tsx +++ b/packages/main/cypress/specs/Button.cy.tsx @@ -676,7 +676,7 @@ describe("Button form attribute", () => { // Change the input value cy.get("#testInput") .clear() - .type("changed"); + .realType("changed"); cy.get("#testInput") .should("have.value", "changed"); From 20a442623c16a580723b25d42698de248eacb5f8 Mon Sep 17 00:00:00 2001 From: Diana Petcheva Date: Thu, 26 Mar 2026 13:48:04 +0200 Subject: [PATCH 4/4] chore: add correct version number --- packages/main/src/Button.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/main/src/Button.ts b/packages/main/src/Button.ts index cac934119239..e880b19bc5ad 100644 --- a/packages/main/src/Button.ts +++ b/packages/main/src/Button.ts @@ -212,7 +212,7 @@ class Button extends UI5Element implements IButton { * **Note:** This property takes effect only when the button's "type" property is set to "Submit" or "Reset". * @default undefined * @public - * @since 2.1x.0 + * @since 2.21.0 */ @property() form?: string;