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..5f1b5e7eba8c 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() + .realType("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..e880b19bc5ad 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.21.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..f01f072955d9 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,19 @@ btnInLink.addEventListener("ui5-click", (e) => { e.preventDefault(); }); + + 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'; + }, 5000); + } + window.handleExternalFormSubmit = handleExternalFormSubmit;