From 3151fd8d904f0ac529f3a4f872a9dac537a65dc0 Mon Sep 17 00:00:00 2001 From: Elizabeth Mitchell Date: Thu, 11 Jan 2024 15:05:48 -0800 Subject: [PATCH] fix(textfield): error styles not removing when an unrelated control is invalid The bug: given a form with two required text fields, 1. Try to submit the form, both fields show error. 2. Add a value to the first field. 3. Try to submit the form, the first field does not remove its error. This is fixed by listening to form submits and clearing the error state if the control is valid. I refactored `injectFormReportValidityHooks()` into `addFormReportValidListener()` to keep the `OnReportValidity` class cleaner and better identify the problem we're trying to solve. PiperOrigin-RevId: 597664564 --- labs/behaviors/on-report-validity.ts | 214 ++++++++++++++-------- labs/behaviors/on-report-validity_test.ts | 54 ++++++ textfield/demo/stories.ts | 1 + 3 files changed, 193 insertions(+), 76 deletions(-) diff --git a/labs/behaviors/on-report-validity.ts b/labs/behaviors/on-report-validity.ts index 92584bbda..137cc3cf9 100644 --- a/labs/behaviors/on-report-validity.ts +++ b/labs/behaviors/on-report-validity.ts @@ -168,101 +168,163 @@ export function mixinOnReportValidity< super.formAssociatedCallback(form); } - // Clean up previous submit listener + // Clean up previous form listeners. this[privateCleanupFormListeners].abort(); if (!form) { return; } this[privateCleanupFormListeners] = new AbortController(); - // If the element's form submits, then all controls are valid. This lets - // the element remove its error styles that may have been set when - // `reportValidity()` was called. - form.addEventListener( - 'submit', + + // Add a listener that fires when the form runs constraint validation and + // the control is valid, so that it may remove its error styles. + // + // This happens on `form.reportValidity()` and `form.requestSubmit()` + // (both when the submit fails and passes). + addFormReportValidListener( + this, + form, () => { this[onReportValidity](null); }, - { - signal: this[privateCleanupFormListeners].signal, - }, + this[privateCleanupFormListeners].signal, ); - - // Inject a callback when `form.reportValidity()` is called and the form - // is valid. There isn't an event that is dispatched to alert us (unlike - // the 'invalid' event), and we need to remove error styles when - // `form.reportValidity()` is called and returns true. - let reportedInvalidEventFromForm = false; - let formReportValidityCleanup = new AbortController(); - injectFormReportValidityHooks({ - form, - cleanup: this[privateCleanupFormListeners].signal, - beforeReportValidity: () => { - reportedInvalidEventFromForm = false; - this.addEventListener( - 'invalid', - () => { - reportedInvalidEventFromForm = true; - // Constructor's invalid listener will handle reporting invalid - // events. - }, - {signal: formReportValidityCleanup.signal}, - ); - }, - afterReportValidity: () => { - formReportValidityCleanup.abort(); - formReportValidityCleanup = new AbortController(); - if (reportedInvalidEventFromForm) { - reportedInvalidEventFromForm = false; - return; - } - - // Report successful form validation if an invalid event wasn't - // fired. - this[onReportValidity](null); - }, - }); } } return OnReportValidityElement; } -const FORM_REPORT_VALIDITY_HOOKS = new WeakMap(); +/** + * Add a listener that fires when a form runs constraint validation on a control + * and it is valid. This is needed to clear previously invalid styles. + * + * @param control The control of the form to listen for valid events. + * @param form The control's form that can run constraint validation. + * @param onControlValid A listener that is called when the form runs constraint + * validation and the control is valid. + * @param cleanup A cleanup signal to remove the listener. + */ +function addFormReportValidListener( + control: Element, + form: HTMLFormElement, + onControlValid: () => void, + cleanup: AbortSignal, +) { + const validateHooks = getFormValidateHooks(form); -function injectFormReportValidityHooks({ - form, - beforeReportValidity, - afterReportValidity, - cleanup, -}: { - form: HTMLFormElement; - beforeReportValidity: () => void; - afterReportValidity: () => void; - cleanup: AbortSignal; -}) { - if (!FORM_REPORT_VALIDITY_HOOKS.has(form)) { - // Patch form.reportValidity() to add an event target that can be used to - // react when the method is called. - // We should only patch this method once, since multiple controls and other - // forces may want to patch this method. We cannot reliably clean it up by - // resetting the method to "superReportValidity", which may be a patched - // function. - // Instead, we never clean up the patch but add and clean up event listener - // hooks once it's patched. + // When a form validates its controls, check if an invalid event is dispatched + // on the control. If it is not, then inform the control to report its valid + // state. + let controlFiredInvalid = false; + let cleanupInvalidListener: AbortController | undefined; + let isNextSubmitFromHook = false; + validateHooks.addEventListener( + 'before', + () => { + isNextSubmitFromHook = true; + cleanupInvalidListener = new AbortController(); + controlFiredInvalid = false; + control.addEventListener( + 'invalid', + () => { + controlFiredInvalid = true; + }, + { + signal: cleanupInvalidListener.signal, + }, + ); + }, + {signal: cleanup}, + ); + + validateHooks.addEventListener( + 'after', + () => { + isNextSubmitFromHook = false; + cleanupInvalidListener?.abort(); + if (controlFiredInvalid) { + return; + } + + onControlValid(); + }, + {signal: cleanup}, + ); + + // The above hooks handle imperatively submitting the form, but not + // declaratively submitting the form. This happens when: + // 1. A non-custom element `