Bug 1446722: set default value for 'mFocusedValue' for the html input elements 'date' and 'time'. r=smaug
authorMirko Brodesser <mbrodesser@mozilla.com>
Fri, 22 Mar 2019 10:52:06 +0000
changeset 465702 98343ff4132291a8ba9b5a23d3e8ec6e94a8c454
parent 465701 681a61e11e60347704b1d2e1301ff66e3e7be0ce
child 465703 01cdf8342a49c2e06728f15835217f9d23b25cb9
push id35746
push usershindli@mozilla.com
push dateSat, 23 Mar 2019 09:46:24 +0000
treeherdermozilla-central@02b7484f316b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1446722
milestone68.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 1446722: set default value for 'mFocusedValue' for the html input elements 'date' and 'time'. r=smaug In order to trigger the 'onchange' event when resetting the 'date' or 'time' html input elements. Differential Revision: https://phabricator.services.mozilla.com/D24206
dom/html/HTMLInputElement.cpp
dom/html/HTMLInputElement.h
dom/html/test/forms/mochitest.ini
dom/html/test/forms/test_input_datetime_reset_default_value_input_change_event.html
dom/html/test/forms/utils.js
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -5657,37 +5657,53 @@ nsresult HTMLInputElement::SetDefaultVal
                "GetValueMode() should return VALUE_MODE_VALUE!");
 
   // The element has a content attribute value different from it's value when
   // it's in the value mode value.
   nsAutoString resetVal;
   GetDefaultValue(resetVal);
 
   // SetValueInternal is going to sanitize the value.
+  // TODO(mbrodesser): sanitizing will only happen if `mDoneCreating` is true.
   return SetValueInternal(resetVal, nsTextEditorState::eSetValue_Internal);
 }
 
 void HTMLInputElement::SetDirectionFromValue(bool aNotify) {
   if (IsSingleLineTextControl(true)) {
     nsAutoString value;
     GetValue(value, CallerType::System);
     SetDirectionalityFromValue(this, value, aNotify);
   }
 }
 
+namespace {
+
+bool IsDateOrTime(uint8_t aType) {
+  return (aType == NS_FORM_INPUT_DATE) || (aType == NS_FORM_INPUT_TIME);
+}
+
+}  // namespace
+
 NS_IMETHODIMP
 HTMLInputElement::Reset() {
   // We should be able to reset all dirty flags regardless of the type.
   SetCheckedChanged(false);
   SetValueChanged(false);
   mLastValueChangeWasInteractive = false;
 
   switch (GetValueMode()) {
-    case VALUE_MODE_VALUE:
-      return SetDefaultValueAsValue();
+    case VALUE_MODE_VALUE: {
+      nsresult result = SetDefaultValueAsValue();
+      if (IsDateOrTime(mType)) {
+        // mFocusedValue has to be set here, so that `FireChangeEventIfNeeded`
+        // can fire a change event if necessary.
+        GetValue(mFocusedValue, CallerType::System);
+      }
+      return result;
+    }
     case VALUE_MODE_DEFAULT_ON:
       DoSetChecked(DefaultChecked(), true, false);
       return NS_OK;
     case VALUE_MODE_FILENAME:
       ClearFiles(false);
       return NS_OK;
     case VALUE_MODE_DEFAULT:
     default:
@@ -5912,24 +5928,30 @@ void HTMLInputElement::DoneCreatingEleme
   //
   // If restore does not occur, we initialize .checked using the CHECKED
   // property.
   //
   if (!restoredCheckedState && mShouldInitChecked) {
     DoSetChecked(DefaultChecked(), false, false);
   }
 
-  // Sanitize the value.
+  // Sanitize the value and potentially set mFocusedValue.
   if (GetValueMode() == VALUE_MODE_VALUE) {
     nsAutoString aValue;
     GetValue(aValue, CallerType::System);
     // TODO: What should we do if SetValueInternal fails?  (The allocation
     // may potentially be big, but most likely we've failed to allocate
     // before the type change.)
     SetValueInternal(aValue, nsTextEditorState::eSetValue_Internal);
+
+    if (IsDateOrTime(mType)) {
+      // mFocusedValue has to be set here, so that `FireChangeEventIfNeeded` can
+      // fire a change event if necessary.
+      mFocusedValue = aValue;
+    }
   }
 
   mShouldInitChecked = false;
 }
 
 EventStates HTMLInputElement::IntrinsicState() const {
   // If you add states here, and they're type-dependent, you need to add them
   // to the type case in AfterSetAttr.
--- a/dom/html/HTMLInputElement.h
+++ b/dom/html/HTMLInputElement.h
@@ -407,17 +407,19 @@ class HTMLInputElement final : public ns
    *
    * @param aIsFocused Whether the element is currently focused.
    *
    * @note The caller is responsible to call ContentStatesChanged.
    */
   void UpdateValidityUIBits(bool aIsFocused);
 
   /**
-   * Fires change event if mFocusedValue and current value held are unequal.
+   * Fires change event if mFocusedValue and current value held are unequal and
+   * if a change event may be fired on bluring.
+   * Sets mFocusedValue to value, if a change event is fired.
    */
   void FireChangeEventIfNeeded();
 
   /**
    * Returns the input element's value as a Decimal.
    * Returns NaN if the current element's value is not a floating point number.
    *
    * @return the input element's value as a Decimal.
@@ -1460,17 +1462,18 @@ class HTMLInputElement final : public ns
   struct FileData;
   UniquePtr<FileData> mFileData;
 
   /**
    * The value of the input element when first initialized and it is updated
    * when the element is either changed through a script, focused or dispatches
    * a change event. This is to ensure correct future change event firing.
    * NB: This is ONLY applicable where the element is a text control. ie,
-   * where type= "text", "email", "search", "tel", "url" or "password".
+   * where type= "date", "time", "text", "email", "search", "tel", "url" or
+   * "password".
    */
   nsString mFocusedValue;
 
   /**
    * If mIsDraggingRange is true, this is the value that the input had before
    * the drag started. Used to reset the input to its old value if the drag is
    * canceled.
    */
--- a/dom/html/test/forms/mochitest.ini
+++ b/dom/html/test/forms/mochitest.ini
@@ -1,12 +1,13 @@
 [DEFAULT]
 support-files =
   save_restore_radio_groups.sjs
   test_input_number_data.js
+  utils.js
   !/dom/html/test/reflect.js
   FAIL.html
   PASS.html
 
 [test_autocomplete.html]
 [test_bug1039548.html]
 [test_bug1283915.html]
 [test_bug1286509.html]
@@ -34,16 +35,17 @@ skip-if = android_version == '18' # Andr
 skip-if = android_version == '18' # Android, bug 1147974
 [test_input_date_bad_input.html]
 [test_input_date_key_events.html]
 [test_input_datetime_input_change_events.html]
 [test_input_datetime_focus_blur.html]
 [test_input_datetime_focus_blur_events.html]
 [test_input_datetime_focus_state.html]
 [test_input_datetime_hidden.html]
+[test_input_datetime_reset_default_value_input_change_event.html]
 [test_input_datetime_tabindex.html]
 [test_input_defaultValue.html]
 [test_input_email.html]
 [test_input_event.html]
 skip-if = android_version == '18' # bug 1147974
 [test_input_file_picker.html]
 [test_input_hasBeenTypePassword.html]
 [test_input_hasBeenTypePassword_navigation.html]
new file mode 100644
--- /dev/null
+++ b/dom/html/test/forms/test_input_datetime_reset_default_value_input_change_event.html
@@ -0,0 +1,153 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+  https://bugzilla.mozilla.org/show_bug.cgi?id=1446722
+-->
+<head>
+<title>Test for bug 1446722</title>
+<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+<script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+<script type="application/javascript" src="utils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1446722">Mozilla bug 1446722</a>
+<p id="display"></p>
+<div id="content">
+<form>
+<input type="time" id="input_time" value="10:30" onchange="++numberChangeEvents"
+                                                 oninput="++numberInputEvents">
+<input type="date" id="input_date" value="2012-05-06" onchange="++numberChangeEvents"
+                                                      oninput="++numberInputEvents">
+<input type="time" id="input_time2" value="11:30" onchange="++numberChangeEvents"
+                                                  oninput="++numberInputEvents">
+<input type="date" id="input_date2" value="2014-07-08"
+                                    onchange="++numberChangeEvents"
+                                    oninput="++numberInputEvents">
+<input type="time" id="input_time3" value="12:30" onchange="++numberChangeEvents"
+                                                  oninput="++numberInputEvents">
+<input type="date" id="input_date3" value="2014-08-09"
+                                    onchange="++numberChangeEvents"
+                                    oninput="++numberInputEvents">
+<input type="reset" id="input_reset">
+</form>
+</div>
+<pre id="test">
+<script class="testbody" type="application/javascript">
+
+/**
+ * Test for bug 1446722.
+ *
+ * Test change and input events are fired for date and time inputs when the
+ * default value is reset from the date UI and the time UI.
+ * Test they are not fired when the value is changed via a script.
+ * Test clicking the reset button of a form does not fire these events.
+ **/
+
+const INPUT_FIELD_ID_PREFIX = "input_";
+
+var numberChangeEvents = 0;
+var numberInputEvents = 0;
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+  if (isDesktopUserAgent(navigator)) {
+    test_reset_in_ui_triggers_change_and_input_event(
+      "time", numberChangeEvents, numberInputEvents);
+    test_reset_in_ui_triggers_change_and_input_event(
+      "date", numberChangeEvents, numberInputEvents);
+  }
+
+  test_reset_in_script_does_not_trigger_change_and_input_event(
+    "time2", numberChangeEvents, numberInputEvents);
+  test_reset_in_script_does_not_trigger_change_and_input_event(
+    "date2", numberChangeEvents, numberInputEvents);
+
+  test_reset_form_does_not_trigger_change_and_input_events("time3", "14:00",
+    numberChangeEvents, numberInputEvents);
+  test_reset_form_does_not_trigger_change_and_input_events("date3", "2016-01-01",
+    numberChangeEvents, numberInputEvents);
+
+  SimpleTest.finish();
+});
+
+function test_reset_in_ui_triggers_change_and_input_event(
+  inputFieldIdSuffix, oldNumberChangeEvents, oldNumberInputEvents) {
+  const inputFieldName = INPUT_FIELD_ID_PREFIX + inputFieldIdSuffix;
+  var input = document.getElementById(inputFieldName);
+
+  is(input.value, input.defaultValue,
+    "Check " + inputFieldName + "'s default value is initialized correctly.");
+  is(numberChangeEvents, oldNumberChangeEvents,
+    "Check numberChangeEvents is initialized correctly for " + inputFieldName +
+  ".");
+  is(numberInputEvents, oldNumberInputEvents,
+    "Check numberInputEvents is initialized correctly for " + inputFieldName +
+    ".");
+
+  simulateUserClicksResetButton(input);
+
+  is(input.value, "",
+    "Check " + inputFieldName + "'s value was set correctly.");
+  is(numberChangeEvents, oldNumberChangeEvents + 1,
+    "Change event should be dispatched for " + inputFieldName + ".");
+  is(numberInputEvents, oldNumberInputEvents + 1,
+    "Input event should be dispatched for " + inputFieldName + ".");
+}
+
+function test_reset_in_script_does_not_trigger_change_and_input_event(
+  inputFieldIdSuffix, oldNumberChangeEvents, oldNumberInputEvents) {
+  const inputFieldName = INPUT_FIELD_ID_PREFIX + inputFieldIdSuffix;
+  var input = document.getElementById(inputFieldName);
+
+  is(input.value, input.defaultValue,
+    "Check " + inputFieldName + "'s default value is initialized correctly.");
+  is(numberChangeEvents, oldNumberChangeEvents,
+    "Check numberChangeEvents is initialized correctly for " + inputFieldName +
+  ".");
+  is(numberInputEvents, oldNumberInputEvents,
+    "Check numberInputEvents is initialized correctly for " + inputFieldName +
+    ".");
+
+  input.value = "";
+
+  is(numberChangeEvents, oldNumberChangeEvents,
+    "Change event should not be dispatched for " + inputFieldName + ".");
+  is(numberInputEvents, oldNumberInputEvents,
+    "Input event should not be dispatched for " + inputFieldName + ".");
+}
+
+function test_reset_form_does_not_trigger_change_and_input_events(
+  inputFieldIdSuffix, newValue, oldNumberChangeEvents, oldNumberInputEvents) {
+  const inputFieldName = INPUT_FIELD_ID_PREFIX + inputFieldIdSuffix;
+  const inputFieldResetButtonName = "input_reset";
+  var input = document.getElementById(inputFieldName);
+
+  is(input.value, input.defaultValue,
+    "Check " + inputFieldName + "'s default value is initialized correctly.");
+  isnot(input.defaultValue, newValue, "Check default value differs from newValue for " +
+    inputFieldName + ".");
+  is(numberChangeEvents, oldNumberChangeEvents,
+    "Check numberChangeEvents is initialized correctly for " + inputFieldName +
+  ".");
+  is(numberInputEvents, oldNumberInputEvents,
+    "Check numberInputEvents is initialized correctly for " + inputFieldName +
+    ".");
+
+  input.value = newValue;
+
+  var resetButton = document.getElementById(inputFieldResetButtonName);
+  synthesizeMouseAtCenter(resetButton, {});
+
+  is(input.value, input.defaultValue, "Check value is reset to default for " +
+    inputFieldName + ".");
+  is(numberChangeEvents, oldNumberChangeEvents,
+    "Change event should not be dispatched for " + inputFieldResetButtonName + ".");
+  is(numberInputEvents, oldNumberInputEvents,
+    "Input event should not be dispatched for " + inputFieldResetButtonName + ".");
+}
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/html/test/forms/utils.js
@@ -0,0 +1,21 @@
+/**
+ * Simulate the user clicks the reset button of the given date or time element.
+ *
+ * @param inputElement A date or time input element of default size.
+ */
+function simulateUserClicksResetButton(inputElement) {
+  var inputRectangle = inputElement.getBoundingClientRect();
+  const offsetX = inputRectangle.width - 15;
+  const offsetY = inputRectangle.height / 2;
+
+  synthesizeMouse(inputElement, offsetX, offsetY, {});
+}
+
+/**
+ * @param navigator https://www.w3schools.com/jsref/obj_navigator.asp.
+ * @return true, iff it's a desktop user agent.
+ */
+function isDesktopUserAgent(navigator) {
+  return !/Mobile|Tablet/.test(navigator.userAgent);
+}
+