Bug 945784, part 1 - Fire 'input' events for <input type=number> more frequently, per the new HTML5 rules. r=smaug
authorJonathan Watt <jwatt@jwatt.org>
Thu, 05 Dec 2013 16:20:33 +0000
changeset 174735 8b9c628c3c53f1959222a07f07196915a2e21c0a
parent 174734 22770b30545b26ee7280f94c6126e59c1f49d16c
child 174736 8a9e9debc9b19226ef62a5c434d538a634d6d1bc
push id445
push userffxbld
push dateMon, 10 Mar 2014 22:05:19 +0000
treeherdermozilla-release@dc38b741b04e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs945784
milestone28.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 945784, part 1 - Fire 'input' events for <input type=number> more frequently, per the new HTML5 rules. r=smaug
content/html/content/src/HTMLInputElement.cpp
content/html/content/src/HTMLInputElement.h
content/html/content/test/forms/test_input_event.html
content/html/content/test/forms/test_input_number_mouse_events.html
--- a/content/html/content/src/HTMLInputElement.cpp
+++ b/content/html/content/src/HTMLInputElement.cpp
@@ -2598,17 +2598,17 @@ HTMLInputElement::HandleNumberControlSpi
   nsNumberControlFrame* numberControlFrame =
     do_QueryFrame(input->GetPrimaryFrame());
   if (input->mType != NS_FORM_INPUT_NUMBER || !numberControlFrame) {
     // Type has changed (and possibly our frame type hasn't been updated yet)
     // or else we've lost our frame. Either way, stop the timer and don't do
     // anything else.
     input->StopNumberControlSpinnerSpin();
   } else {
-    input->ApplyStep(input->mNumberControlSpinnerSpinsUp ? 1 : -1);
+    input->StepNumberControlForUserEvent(input->mNumberControlSpinnerSpinsUp ? 1 : -1);
   }
 }
 
 void
 HTMLInputElement::MaybeDispatchProgressEvent(bool aFinalProgress)
 {
   nsRefPtr<HTMLInputElement> kungFuDeathGrip;
 
@@ -3493,17 +3493,29 @@ HTMLInputElement::StopNumberControlSpinn
   if (mNumberControlSpinnerIsSpinning) {
     if (nsIPresShell::GetCapturingContent() == this) {
       nsIPresShell::SetCapturingContent(nullptr, 0); // cancel capture
     }
 
     nsRepeatService::GetInstance()->Stop(HandleNumberControlSpin, this);
 
     mNumberControlSpinnerIsSpinning = false;
-  }
+
+    FireChangeEventIfNeeded();
+  }
+}
+
+void
+HTMLInputElement::StepNumberControlForUserEvent(int32_t aDirection)
+{
+  ApplyStep(aDirection);
+  nsContentUtils::DispatchTrustedEvent(OwnerDoc(),
+                                       static_cast<nsIDOMHTMLInputElement*>(this),
+                                       NS_LITERAL_STRING("input"), true,
+                                       false);
 }
 
 static bool
 SelectTextFieldOnFocus()
 {
   if (!gSelectTextFieldOnFocus) {
     int32_t selectTextfieldsOnKeyFocus = -1;
     nsresult rv =
@@ -3726,17 +3738,17 @@ HTMLInputElement::PostHandleEvent(nsEven
       // control to change the string in the text control will change, and
       // the cursor will be moved to the end of the text control, overwriting
       // the editor's handling of up/down keypress events. For that reason we
       // just ignore aVisitor.mEventStatus here and go ahead and handle the
       // event to increase/decrease the value of the number control.
       // XXX we still need to allow script to call preventDefault() on the
       // event, but right now we can't tell the difference between the editor
       // on script doing that (bug 930374).
-      ApplyStep(keyEvent->keyCode == NS_VK_UP ? 1 : -1);
+      StepNumberControlForUserEvent(keyEvent->keyCode == NS_VK_UP ? 1 : -1);
       aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
     } else if (nsEventStatus_eIgnore == aVisitor.mEventStatus) {
       switch (aVisitor.mEvent->message) {
 
         case NS_FOCUS_CONTENT:
         {
           // see if we should select the contents of the textbox. This happens
           // for text and password fields when the field was focused by the
@@ -3954,23 +3966,23 @@ HTMLInputElement::PostHandleEvent(nsEven
                   mouseEvent->IsOS())) {
               nsNumberControlFrame* numberControlFrame =
                 do_QueryFrame(GetPrimaryFrame());
               if (numberControlFrame) {
                 if (aVisitor.mEvent->message == NS_MOUSE_BUTTON_DOWN) {
                   switch (numberControlFrame->GetSpinButtonForPointerEvent(
                             aVisitor.mEvent->AsMouseEvent())) {
                   case nsNumberControlFrame::eSpinButtonUp:
-                    ApplyStep(1);
+                    StepNumberControlForUserEvent(1);
                     mNumberControlSpinnerSpinsUp = true;
                     StartNumberControlSpinnerSpin();
                     aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
                     break;
                   case nsNumberControlFrame::eSpinButtonDown:
-                    ApplyStep(-1);
+                    StepNumberControlForUserEvent(-1);
                     mNumberControlSpinnerSpinsUp = false;
                     StartNumberControlSpinnerSpin();
                     aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
                     break;
                   }
                 }
               }
             }
--- a/content/html/content/src/HTMLInputElement.h
+++ b/content/html/content/src/HTMLInputElement.h
@@ -675,16 +675,17 @@ public:
   void MozGetFileNameArray(nsTArray< nsString >& aFileNames);
 
   void MozSetFileNameArray(const Sequence< nsString >& aFileNames);
 
   HTMLInputElement* GetOwnerNumberControl();
 
   void StartNumberControlSpinnerSpin();
   void StopNumberControlSpinnerSpin();
+  void StepNumberControlForUserEvent(int32_t aDirection);
 
   /**
    * The callback function used by the nsRepeatService that we use to spin the
    * spinner for <input type=number>.
    */
   static void HandleNumberControlSpin(void* aData);
 
   bool MozIsTextField(bool aExcludePassword);
--- a/content/html/content/test/forms/test_input_event.html
+++ b/content/html/content/test/forms/test_input_event.html
@@ -25,34 +25,38 @@ https://bugzilla.mozilla.org/show_bug.cg
 <!-- "Non-text" inputs-->
 <input type="button" id="input_button" oninput="++NonTextInput[0];"></input>
 <input type="submit" id="input_submit" oninput="++NonTextInput[1];"></input>
 <input type="image" id="input_image" oninput="++NonTextInput[2];"></input>
 <input type="reset" id="input_reset" oninput="++NonTextInput[3];"></input>
 <input type="radio" id="input_radio" oninput="++NonTextInput[4];"></input>
 <input type="checkbox" id="input_checkbox" oninput="++NonTextInput[5];"></input>
 <input type="range" id="input_range" oninput="++rangeInput;"></input>
+<input type="number" id="input_number" oninput="++numberInput;"></input>
  
 </div>
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
   /** Test for input event. This is highly based on test_change_event.html **/
 
+  const isDesktop = !/Mobile|Tablet/.test(navigator.userAgent);
+
   var textareaInput = 0;
 
   // Those are types were the input event apply.
   var textTypes = ["text", "email", "search", "telephone", "url", "password"];
   var textInput = [0, 0, 0, 0, 0, 0];
 
   // Those are events were the input event does not apply.
   var NonTextTypes = ["button", "submit", "image", "reset", "radio", "checkbox"];
   var NonTextInput = [0, 0, 0, 0, 0, 0];
 
   var rangeInput = 0;
+  var numberInput = 0;
 
   SimpleTest.waitForExplicitFinish();
   var MockFilePicker = SpecialPowers.MockFilePicker;
   MockFilePicker.init(window);
 
   function testUserInput() {
     // Simulating an OK click and with a file name return.
     MockFilePicker.useAnyFile();
@@ -172,16 +176,40 @@ https://bugzilla.mozilla.org/show_bug.cg
     var centerOfRangeY = bcr.height / 2;
     synthesizeMouse(range, centerOfRangeX - 10, centerOfRangeY, { type: "mousedown" });
     is(rangeInput, 2, "Input event should be dispatched on mousedown if the value changes");
     synthesizeMouse(range, centerOfRangeX - 5, centerOfRangeY, { type: "mousemove" });
     is(rangeInput, 3, "Input event should be dispatched during a drag");
     synthesizeMouse(range, centerOfRangeX, centerOfRangeY, { type: "mouseup" });
     is(rangeInput, 4, "Input event should be dispatched at the end of a drag");
 
+    // Tests for type='number'.
+    // We only test key events here since input events for mouse event changes
+    // are tested in test_input_number_mouse_events.html
+    var number = document.getElementById("input_number");
+
+    if (isDesktop) { // up/down arrow keys not supported on android/b2g
+      number.value = "";
+      number.focus();
+      synthesizeKey("VK_UP", {});
+      is(numberInput, 1, "input event should be dispatched for up/down arrow key keypress");
+      is(number.value, 1, "sanity check value of number control after keypress");
+
+      synthesizeKey("VK_DOWN", { type: "keydown" });
+      synthesizeKey("VK_DOWN", { type: "keypress" });
+      synthesizeKey("VK_DOWN", { type: "keypress" });
+      synthesizeKey("VK_DOWN", { type: "keypress" });
+      synthesizeKey("VK_DOWN", { type: "keyup" });
+      is(numberInput, 4, "input event should be dispatched for each up/down arrow key keypress event, even when rapidly repeated");
+      is(number.value, -2, "sanity check value of number control after multiple keydown events");
+
+      number.blur();
+      is(numberInput, 4, "input event shouldn't be dispatched on blur");
+    }
+
     MockFilePicker.cleanup();
     SimpleTest.finish();
   }
 
   addLoadEvent(testUserInput);
 
 </script>
 </pre>
--- a/content/html/content/test/forms/test_input_number_mouse_events.html
+++ b/content/html/content/test/forms/test_input_number_mouse_events.html
@@ -30,29 +30,30 @@ https://bugzilla.mozilla.org/show_bug.cg
 /**
  * Test for Bug 935501
  * This test checks how the value of <input type=number> changes in response to
  * various mouse events.
  **/
 SimpleTest.waitForExplicitFinish();
 SimpleTest.waitForFocus(function() {
   test();
-  SimpleTest.finish();
 });
 
+var input = document.getElementById("input");
+var inputRect = input.getBoundingClientRect();
+
+// Points over the input's spin-up and spin-down buttons (as offsets from the
+// top-left of the input's bounding client rect):
+const SPIN_UP_X = inputRect.width - 3;
+const SPIN_UP_Y = 3;
+const SPIN_DOWN_X = inputRect.width - 3;
+const SPIN_DOWN_Y = inputRect.height - 3;
+
 function test() {
-  var input = document.getElementById("input");
-  var inputRect = input.getBoundingClientRect();
-
-  // Points over the input's spin-up and spin-down buttons (as offsets from the
-  // top-left of the input's bounding client rect):
-  const SPIN_UP_X = inputRect.width - 3;
-  const SPIN_UP_Y = 3;
-  const SPIN_DOWN_X = inputRect.width - 3;
-  const SPIN_DOWN_Y = inputRect.height - 3;
+  input.value = 0;
 
   // Test click on spin-up button:
   synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mousedown" });
   is(input.value, 1, "Test step-up on mousedown on spin-up button");
   synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mouseup" });
   is(input.value, 1, "Test mouseup on spin-up button");
 
   // Test click on spin-down button:
@@ -68,44 +69,117 @@ function test() {
   input.value = 1;
   input.addEventListener("mousedown", preventDefault, false);
   synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, {});
   is(input.value, 1, "Test that preventDefault() works for click on spin-up button");
   synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, {});
   is(input.value, 1, "Test that preventDefault() works for click on spin-down button");
   input.removeEventListener("mousedown", preventDefault, false);
 
-  // XXX TODO
-  // Test spining when the mouse button is kept depressed on the spin-up
-  // button:
+  // Run the spin tests:
+  runNextSpinTest();
+}
 
-  // XXX TODO
-  // Test spining when the mouse button is kept depressed on the spin-down
-  // button:
+function runNextSpinTest() {
+  try {
+    var [index, test] = spinTests.next();
+    test();
+  } catch(e) {
+    if (e == StopIteration) {
+      SimpleTest.finish();
+      return; // We're all done
+    }
+    throw e;
+  }
+}
+
+const SETTIMEOUT_DELAY = 500;
 
-  // XXX TODO
-  // Test spin direction reverses when the mouse button is depressod on the
-  // spin-up button, then moved over the spin-down button once the spin begins:
+var spinTests = Iterator([
+  // Test spining when the mouse button is kept depressed on the spin-up
+  // button, then moved over the spin-down button:
+  function() {
+    var inputEventCount = 0;
+    input.value = 0;
+    input.addEventListener("input", function(evt) {
+      ++inputEventCount;
+      if (inputEventCount == 3) {
+        ok(input.value, 3, "Testing spin-up button");
+        synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mousemove" });
+      } else if (inputEventCount == 6) {
+        ok(input.value, 0, "Testing spin direction is reversed after mouse moves from spin-up button to spin-down button");
+        input.removeEventListener("input", arguments.callee, false);
+        synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mouseup" });
+        runNextSpinTest();
+      }
+    }, false);
+    synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mousedown" });
+  },
 
-  // XXX TODO
-  // Test spin direction reverses when the mouse button is depressod on the
-  // spin-down button, then moved over the spin-up button once the spin begins:
+  // Test spining when the mouse button is kept depressed on the spin-down
+  // button, then moved over the spin-up button:
+  function() {
+    var inputEventCount = 0;
+    input.value = 0;
+    input.addEventListener("input", function(evt) {
+      ++inputEventCount;
+      if (inputEventCount == 3) {
+        ok(input.value, -3, "Testing spin-down button");
+        synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mousemove" });
+      } else if (inputEventCount == 6) {
+        ok(input.value, 0, "Testing spin direction is reversed after mouse moves from spin-down button to spin-up button");
+        input.removeEventListener("input", arguments.callee, false);
+        synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mouseup" });
+        runNextSpinTest();
+      }
+    }, false);
+    synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mousedown" });
+  },
 
-  // XXX TODO
-  // Test that the spin is stopped when the mouse button is depressod on the
-  // spin-down button, then moved outside both buttons once the spin starts:
-
-  // XXX TODO
   // Test that the spin is stopped when the mouse button is depressod on the
   // spin-up button, then moved outside both buttons once the spin starts:
-
-  // XXX TODO
-  // Test that changing the input type in the middle of a spin cancels the spin:
+  function() {
+    var inputEventCount = 0;
+    input.value = 0;
+    input.addEventListener("input", function(evt) {
+      ++inputEventCount;
+      if (inputEventCount == 3) {
+        synthesizeMouse(input, -1, -1, { type: "mousemove" });
+        var eventHandler = arguments.callee;
+        setTimeout(function() {
+          ok(input.value, 3, "Testing moving the mouse outside the spin buttons stops the spin");
+          ok(inputEventCount, 3, "Testing moving the mouse outside the spin buttons stops the spin input events");
+          input.removeEventListener("input", eventHandler, false);
+          synthesizeMouse(input, -1, -1, { type: "mouseup" });
+          runNextSpinTest();
+        }, SETTIMEOUT_DELAY);
+      }
+    }, false);
+    synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mousedown" });
+  },
 
-  // XXX TODO
-  // Check that we do not spin when a mousedown occurs outside the spin
-  // buttons and then the mouse is moved over the buttons:
-}
+  // Test that changing the input type in the middle of a spin cancels the spin:
+  function() {
+    var inputEventCount = 0;
+    input.value = 0;
+    input.addEventListener("input", function(evt) {
+      ++inputEventCount;
+      if (inputEventCount == 3) {
+        input.type = "text"
+        var eventHandler = arguments.callee;
+        setTimeout(function() {
+          ok(input.value, 3, "Testing changing input type during a spin stops the spin");
+          ok(inputEventCount, 3, "Testing changing input type during a spin stops the spin input events");
+          input.removeEventListener("input", eventHandler, false);
+          synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mouseup" });
+          input.type = "number"; // restore
+          runNextSpinTest();
+        }, SETTIMEOUT_DELAY);
+      }
+    }, false);
+    synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mousedown" });
+  }
+]);
 
 </script>
 </pre>
 </body>
 </html>