Bug 1524212 - Make nsContentUtils::DispatchInputEvent() update HTMLInputElement.validationMessage before dispatching "input" event r=smaug a=lizzard
authorMasayuki Nakano <masayuki@d-toybox.com>
Fri, 08 Feb 2019 22:17:10 +0000
changeset 513033 753ea9ad88e0476cd2304cc8330fd8af01c1472a
parent 513032 dab6c98e862612971e4196a379e375dc3fe143f2
child 513034 7e0d528168b367712128dadbd59f947b498c6143
push id10690
push userapavel@mozilla.com
push dateWed, 13 Feb 2019 04:19:22 +0000
treeherdermozilla-beta@05a7b1efca0d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug, lizzard
bugs1524212
milestone66.0
Bug 1524212 - Make nsContentUtils::DispatchInputEvent() update HTMLInputElement.validationMessage before dispatching "input" event r=smaug a=lizzard "input" event listener may want to check HTMLInputElement.validationMessage. However, due to moving "input" event dispatcher from HTMLInputElement::SetUserInput() to editor, HTMLInputElement::SetValueInternal() updates it **after** dispatching "input" event. This patch makes nsContentUtils::DispatchInputEvent() guarantees to update validationMessage value before dispatching every event. On the other hand, SetValueInternal() may be called without "input" event dispatchers. Therefore, it needs to keep updating validationMessage value in such cases. Differential Revision: https://phabricator.services.mozilla.com/D19126
dom/base/nsContentUtils.cpp
dom/html/HTMLInputElement.cpp
dom/html/HTMLInputElement.h
dom/html/test/forms/test_MozEditableElement_setUserInput.html
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -4157,16 +4157,25 @@ nsresult nsContentUtils::DispatchInputEv
   else {
     nsCOMPtr<nsITextControlElement> textControlElement =
         do_QueryInterface(aEventTargetElement);
     MOZ_ASSERT(!textControlElement,
                "The event target may have editor, but we've not known it yet.");
   }
 #endif  // #ifdef DEBUG
 
+  // If the event target is an <input> element, we need to update
+  // validationMessage value before dispatching "input" event because
+  // "input" event listener may need to check it.
+  HTMLInputElement* inputElement =
+      HTMLInputElement::FromNode(aEventTargetElement);
+  if (inputElement) {
+    inputElement->MaybeUpdateAllValidityStates();
+  }
+
   if (!useInputEvent) {
     MOZ_ASSERT(aEditorInputType == EditorInputType::eUnknown);
     // Dispatch "input" event with Event instance.
     WidgetEvent widgetEvent(true, eUnidentifiedEvent);
     widgetEvent.mSpecifiedEventType = nsGkAtoms::oninput;
     widgetEvent.mFlags.mCancelable = false;
     // Using same time as nsContentUtils::DispatchEvent() for backward
     // compatibility.
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -2603,18 +2603,22 @@ nsresult HTMLInputElement::SetValueInter
         // Note that if aFlags includes
         // nsTextEditorState::eSetValue_BySetUserInput, "input" event is
         // automatically dispatched by nsTextEditorState::SetValue().
         // If you'd change condition of calling this method, you need to
         // maintain SetUserInput() too.
         if (!mInputData.mState->SetValue(value, aOldValue, aFlags)) {
           return NS_ERROR_OUT_OF_MEMORY;
         }
-        if (mType == NS_FORM_INPUT_EMAIL) {
-          UpdateAllValidityStates(!mDoneCreating);
+        // If the caller won't dispatch "input" event via
+        // nsContentUtils::DispatchInputEvent(), we need to modify
+        // validationMessage value here.
+        if (aFlags & (nsTextEditorState::eSetValue_Internal |
+                      nsTextEditorState::eSetValue_ByContent)) {
+          MaybeUpdateAllValidityStates();
         }
       } else {
         free(mInputData.mValue);
         mInputData.mValue = ToNewUnicode(value);
         if (setValueChanged) {
           SetValueChanged(true);
         }
         if (mType == NS_FORM_INPUT_NUMBER) {
--- a/dom/html/HTMLInputElement.h
+++ b/dom/html/HTMLInputElement.h
@@ -341,16 +341,24 @@ class HTMLInputElement final : public ns
   void UpdateRangeOverflowValidityState();
   void UpdateRangeUnderflowValidityState();
   void UpdateStepMismatchValidityState();
   void UpdateBadInputValidityState();
   // Update all our validity states and then update our element state
   // as needed.  aNotify controls whether the element state update
   // needs to notify.
   void UpdateAllValidityStates(bool aNotify);
+  void MaybeUpdateAllValidityStates() {
+    // If you need to add new type which supports validationMessage, you should
+    // add test cases into test_MozEditableElement_setUserInput.html.
+    if (mType == NS_FORM_INPUT_EMAIL) {
+      UpdateAllValidityStates(!mDoneCreating);
+    }
+  }
+
   // Update all our validity states without updating element state.
   // This should be called instead of UpdateAllValidityStates any time
   // we're guaranteed that element state will be updated anyway.
   void UpdateAllValidityStatesButNotElementState();
   void UpdateBarredFromConstraintValidation();
   nsresult GetValidationMessage(nsAString& aValidationMessage,
                                 ValidityStateType aType) override;
 
--- a/dom/html/test/forms/test_MozEditableElement_setUserInput.html
+++ b/dom/html/test/forms/test_MozEditableElement_setUserInput.html
@@ -231,13 +231,82 @@ SimpleTest.waitForFocus(() => {
          `"input" event should be never cancelable (${tag}, after getting focus)`);
       is(inputEvents[0].bubbles, true,
          `"input" event should always bubble (${tag}, after getting focus)`);
     }
 
     target.removeEventListener("input", onInput);
   }
 
+  function testValidationMessage(aType, aInvalidValue, aValidValue) {
+    let tag = `<input type="${aType}">`
+    content.innerHTML = tag;
+    content.scrollTop; // Flush pending layout.
+    let target = content.firstChild;
+
+    let inputEvents = [];
+    let validationMessage = "";
+
+    function reset() {
+      inputEvents = [];
+      validationMessage = "";
+    }
+
+    function onInput(aEvent) {
+      inputEvents.push(aEvent);
+      validationMessage = aEvent.target.validationMessage;
+    }
+    target.addEventListener("input", onInput);
+
+    reset();
+    SpecialPowers.wrap(target).setUserInput(aInvalidValue);
+    is(inputEvents.length, 1,
+       `Only one "input" event should be dispatched when setUserInput("${aInvalidValue}") is called before ${tag} gets focus`);
+    isnot(validationMessage, "",
+          `${tag}.validationMessage should not be empty when setUserInput("${aInvalidValue}") is called before ${tag} gets focus`);
+
+    reset();
+    SpecialPowers.wrap(target).setUserInput(aValidValue);
+    is(inputEvents.length, 1,
+       `Only one "input" event should be dispatched when setUserInput("${aValidValue}") is called before ${tag} gets focus`);
+    is(validationMessage, "",
+       `${tag}.validationMessage should be empty when setUserInput("${aValidValue}") is called before ${tag} gets focus`);
+
+    reset();
+    SpecialPowers.wrap(target).setUserInput(aInvalidValue);
+    is(inputEvents.length, 1,
+       `Only one "input" event should be dispatched again when setUserInput("${aInvalidValue}") is called before ${tag} gets focus`);
+    isnot(validationMessage, "",
+          `${tag}.validationMessage should not be empty again when setUserInput("${aInvalidValue}") is called before ${tag} gets focus`);
+
+    target.value = "";
+    target.focus();
+
+    reset();
+    SpecialPowers.wrap(target).setUserInput(aInvalidValue);
+    is(inputEvents.length, 1,
+       `Only one "input" event should be dispatched when setUserInput("${aInvalidValue}") is called after ${tag} gets focus`);
+    isnot(validationMessage, "",
+          `${tag}.validationMessage should not be empty when setUserInput("${aInvalidValue}") is called after ${tag} gets focus`);
+
+    reset();
+    SpecialPowers.wrap(target).setUserInput(aValidValue);
+    is(inputEvents.length, 1,
+       `Only one "input" event should be dispatched when setUserInput("${aValidValue}") is called after ${tag} gets focus`);
+    is(validationMessage, "",
+       `${tag}.validationMessage should be empty when setUserInput("${aValidValue}") is called after ${tag} gets focus`);
+
+    reset();
+    SpecialPowers.wrap(target).setUserInput(aInvalidValue);
+    is(inputEvents.length, 1,
+       `Only one "input" event should be dispatched again when setUserInput("${aInvalidValue}") is called after ${tag} gets focus`);
+    isnot(validationMessage, "",
+          `${tag}.validationMessage should not be empty again when setUserInput("${aInvalidValue}") is called after ${tag} gets focus`);
+
+    target.removeEventListener("input", onInput);
+  }
+  testValidationMessage("email", "f", "foo@example.com");
+
   SimpleTest.finish();
 });
 </script>
 </body>
 </html>