diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md index e9e83c0b5..37ebe4c44 100644 --- a/docs/resources/changelog.md +++ b/docs/resources/changelog.md @@ -18,6 +18,7 @@ _During the beta period, these restrictions may be relaxed in the event of a mis - Fixed a bug that resulted in a console error being thrown on keydown in `` [#719](https://github.com/shoelace-style/shoelace/issues/719) - Fixed a bug that prevented `` from being closed when opened initially [#720](https://github.com/shoelace-style/shoelace/issues/720) - Fixed a bug that caused the test runner to fail when using a locale other than en-US [#726](https://github.com/shoelace-style/shoelace/issues/726) +- Improved form submit logic so most user-added event listeners will run after form data is attached and validation occurs [#718](https://github.com/shoelace-style/shoelace/issues/718) - Updated `` and `` to cycle through tabs and menu items instead of stopping at the first/last when using the keyboard - Removed path aliasing (again) because it doesn't work with Web Test Runner's esbuild plugin diff --git a/src/internal/form.ts b/src/internal/form.ts index 8596ee84a..50da506e1 100644 --- a/src/internal/form.ts +++ b/src/internal/form.ts @@ -20,7 +20,6 @@ export interface FormSubmitControllerOptions { export class FormSubmitController implements ReactiveController { host?: ReactiveControllerHost & Element; - form?: HTMLFormElement | null; options: FormSubmitControllerOptions; constructor(host: ReactiveControllerHost & Element, options?: Partial) { @@ -40,20 +39,15 @@ export class FormSubmitController implements ReactiveController { } hostConnected() { - this.form = this.options.form(this.host); - - if (this.form) { - this.form.addEventListener('formdata', this.handleFormData); - this.form.addEventListener('submit', this.handleFormSubmit); - } + // We use the capture phase and listen on the document to intercept the event as soon as possible. Otherwise, the + // user may attach listeners that run before we have a chance to do validation. + document.addEventListener('formdata', this.handleFormData, { capture: true }); + document.addEventListener('submit', this.handleFormSubmit, { capture: true }); } hostDisconnected() { - if (this.form) { - this.form.removeEventListener('formdata', this.handleFormData); - this.form.removeEventListener('submit', this.handleFormSubmit); - this.form = undefined; - } + document.removeEventListener('formdata', this.handleFormData, { capture: true }); + document.removeEventListener('submit', this.handleFormSubmit, { capture: true }); } handleFormData(event: FormDataEvent) { @@ -73,10 +67,11 @@ export class FormSubmitController implements ReactiveController { } handleFormSubmit(event: Event) { + const form = this.options.form(this.host); const disabled = this.options.disabled(this.host); const reportValidity = this.options.reportValidity; - if (this.form && !this.form.noValidate && !disabled && !reportValidity(this.host)) { + if (event.target === form && !disabled && !form?.noValidate && !reportValidity(this.host)) { event.preventDefault(); event.stopImmediatePropagation(); } @@ -84,15 +79,16 @@ export class FormSubmitController implements ReactiveController { /** Submits the form, triggering validation and form data injection. */ submit(submitter?: HTMLInputElement | SlButton) { - // Calling form.submit() bypasses the submit event and constraint validation. To prevent this, we can inject a - // native submit button into the form, "click" it, then remove it to simulate a standard form submission. - if (this.form) { + const form = this.options.form(this.host); + + if (form) { + // Calling form.submit() bypasses the submit event and constraint validation. To prevent this, we can inject a + // native submit button into the form, "click" it, then remove it to simulate a standard form submission. const button = document.createElement('button'); button.type = 'submit'; button.style.position = 'absolute'; button.style.width = '0'; button.style.height = '0'; - button.style.clip = 'rect(0 0 0 0)'; button.style.clipPath = 'inset(50%)'; button.style.overflow = 'hidden'; button.style.whiteSpace = 'nowrap'; @@ -106,7 +102,7 @@ export class FormSubmitController implements ReactiveController { }); } - this.form.append(button); + form.append(button); button.click(); button.remove(); }