Bug 1496242 - Part I, Simplify nsIDateTimeInputArea interface r=mconley,smaug
authorTimothy Guan-tin Chien <timdream@gmail.com>
Fri, 02 Nov 2018 23:29:42 +0000
changeset 444212 1387262d2f93391e11c1298f5148fc62ffcc4557
parent 444211 e9ac4160d051f7ec520c7a0534b75c4b10386873
child 444213 66df568f60f633bb8e361be0a256cb4490330bf8
push id34985
push usershindli@mozilla.com
push dateSat, 03 Nov 2018 09:40:09 +0000
treeherdermozilla-central@6655fa7cff48 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley, smaug
bugs1496242, 1456833
milestone65.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 1496242 - Part I, Simplify nsIDateTimeInputArea interface r=mconley,smaug This patch simplifies the nsIDateTimeInputArea interface, implemented by the datetimebox bindings, to a point that is easier to convert it to dispatch events, by doing the following: - hasBadInput() is re-implemented in C++ in nsIDateTimeControlFrame since C++ needs the return value synchronously. - SetValueFromPicker() and SetPickerState() are avoided completed since they are simply called by HTMLInputElement methods exposed to the frame script. They are avoided by having the frame script access the NAC and call the nsIDateTimeInputArea methods directly. - Merge setEditAttribute() and removeEditAttribute() to updateEditAttributes() which takes no arguments, and have the method access the attribute values by reading the values from <input>. This patch is a scaled-down version of the patch proposed in bug 1456833. The event approach is only usable in UA Widget version of datetimebox because there is no way to avoid leaking events to the document without Shadow DOM. Differential Revision: https://phabricator.services.mozilla.com/D9056
dom/html/HTMLInputElement.cpp
dom/html/HTMLInputElement.h
dom/html/nsIDateTimeInputArea.idl
dom/webidl/HTMLInputElement.webidl
layout/forms/nsDateTimeControlFrame.cpp
layout/forms/nsDateTimeControlFrame.h
toolkit/actors/DateTimePickerChild.jsm
toolkit/content/widgets/datetimebox.xml
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -2215,40 +2215,23 @@ void HTMLInputElement::GetDateTimeInputB
 {
   if (NS_WARN_IF(!IsDateTimeInputType(mType)) || !mDateTimeInputBoxValue) {
     return;
   }
 
   aValue = *mDateTimeInputBoxValue;
 }
 
-void
-HTMLInputElement::UpdateDateTimeInputBox(const DateTimeValue& aValue)
-{
-  if (NS_WARN_IF(!IsDateTimeInputType(mType))) {
-    return;
-  }
-
+Element* HTMLInputElement::GetDateTimeBoxElement()
+{
   nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame());
   if (frame) {
-    frame->SetValueFromPicker(aValue);
-  }
-}
-
-void
-HTMLInputElement::SetDateTimePickerState(bool aOpen)
-{
-  if (NS_WARN_IF(!IsDateTimeInputType(mType))) {
-    return;
-  }
-
-  nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame());
-  if (frame) {
-    frame->SetPickerState(aOpen);
-  }
+    return frame->GetInputAreaContent()->AsElement();
+  }
+  return nullptr;
 }
 
 void
 HTMLInputElement::OpenDateTimePicker(const DateTimeValue& aInitialValue)
 {
   if (NS_WARN_IF(!IsDateTimeInputType(mType))) {
     return;
   }
--- a/dom/html/HTMLInputElement.h
+++ b/dom/html/HTMLInputElement.h
@@ -841,18 +841,22 @@ public:
   void MozSetFileArray(const Sequence<OwningNonNull<File>>& aFiles);
   void MozSetDirectory(const nsAString& aDirectoryPath, ErrorResult& aRv);
 
   /*
    * The following functions are called from datetime picker to let input box
    * know the current state of the picker or to update the input box on changes.
    */
   void GetDateTimeInputBoxValue(DateTimeValue& aValue);
-  void UpdateDateTimeInputBox(const DateTimeValue& aValue);
-  void SetDateTimePickerState(bool aOpen);
+
+  /*
+   * This allows chrome JavaScript to dispatch event to the inner datetimebox
+   * anonymous element or access nsIDateTimeInputArea implmentation.
+   */
+  Element* GetDateTimeBoxElement();
 
   /*
    * The following functions are called from datetime input box XBL to control
    * and update the picker.
    */
   void OpenDateTimePicker(const DateTimeValue& aInitialValue);
   void UpdateDateTimePicker(const DateTimeValue& aValue);
   void CloseDateTimePicker();
--- a/dom/html/nsIDateTimeInputArea.idl
+++ b/dom/html/nsIDateTimeInputArea.idl
@@ -31,29 +31,19 @@ interface nsIDateTimeInputArea : nsISupp
   void focusInnerTextBox();
 
   /**
    * Called from DOM/Layout to blur inner text box.
    */
   void blurInnerTextBox();
 
   /**
-   * Called from DOM/Layout to know whether the current entered value is valid.
-   */
-  boolean hasBadInput();
-
-  /**
    * Set the current state of the picker, true if it's opened, false otherwise.
    */
   void setPickerState(in boolean isOpen);
 
   /**
-   * Set the attribute of the inner text boxes. Only "tabindex", "readonly",
-   * and "disabled" are allowed.
+   * Update the attribute of the inner text boxes by copying the attribute value
+   * from the input. Only values set to "tabindex", "readonly",
+   * and "disabled" attributes are copied.
    */
-  void setEditAttribute(in AString name, in AString value);
-
-  /**
-   * Remove the attribute of the inner text boxes. Only "tabindex", "readonly",
-   * and "disabled" are allowed.
-   */
-  void removeEditAttribute(in AString name);
+  void updateEditAttributes();
 };
--- a/dom/webidl/HTMLInputElement.webidl
+++ b/dom/webidl/HTMLInputElement.webidl
@@ -237,20 +237,17 @@ dictionary DateTimeValue {
   long day;
 };
 
 partial interface HTMLInputElement {
   [Pref="dom.forms.datetime", ChromeOnly]
   DateTimeValue getDateTimeInputBoxValue();
 
   [Pref="dom.forms.datetime", ChromeOnly]
-  void updateDateTimeInputBox(optional DateTimeValue value);
-
-  [Pref="dom.forms.datetime", ChromeOnly]
-  void setDateTimePickerState(boolean open);
+  readonly attribute Element? dateTimeBoxElement;
 
   [Pref="dom.forms.datetime", ChromeOnly,
    BinaryName="getMinimumAsDouble"]
   double getMinimum();
 
   [Pref="dom.forms.datetime", ChromeOnly,
    BinaryName="getMaximumAsDouble"]
   double getMaximum();
--- a/layout/forms/nsDateTimeControlFrame.cpp
+++ b/layout/forms/nsDateTimeControlFrame.cpp
@@ -13,16 +13,17 @@
 
 #include "nsContentUtils.h"
 #include "nsCheckboxRadioFrame.h"
 #include "nsGkAtoms.h"
 #include "nsContentUtils.h"
 #include "nsContentCreatorFunctions.h"
 #include "mozilla/dom/HTMLInputElement.h"
 #include "mozilla/dom/MutationEventBinding.h"
+#include "nsDOMTokenList.h"
 #include "nsNodeInfoManager.h"
 #include "nsIDateTimeInputArea.h"
 #include "nsIObserverService.h"
 #include "jsapi.h"
 #include "nsJSUtils.h"
 #include "nsThreadUtils.h"
 
 using namespace mozilla;
@@ -69,57 +70,16 @@ nsDateTimeControlFrame::OnMinMaxStepAttr
   nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
     do_QueryInterface(mInputAreaContent);
   if (inputAreaContent) {
     inputAreaContent->NotifyMinMaxStepAttrChanged();
   }
 }
 
 void
-nsDateTimeControlFrame::SetValueFromPicker(const DateTimeValue& aValue)
-{
-  nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
-    do_QueryInterface(mInputAreaContent);
-  if (inputAreaContent) {
-    AutoJSAPI api;
-    if (!api.Init(mContent->OwnerDoc()->GetScopeObject())) {
-      return;
-    }
-
-    JSObject* wrapper = mContent->GetWrapper();
-    if (!wrapper) {
-      return;
-    }
-
-    JSObject* scope = xpc::GetXBLScope(api.cx(), wrapper);
-    AutoJSAPI jsapi;
-    if (!scope || !jsapi.Init(scope)) {
-      return;
-    }
-
-    JS::Rooted<JS::Value> jsValue(jsapi.cx());
-    if (!ToJSValue(jsapi.cx(), aValue, &jsValue)) {
-      return;
-    }
-
-    inputAreaContent->SetValueFromPicker(jsValue);
-  }
-}
-
-void
-nsDateTimeControlFrame::SetPickerState(bool aOpen)
-{
-  nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
-    do_QueryInterface(mInputAreaContent);
-  if (inputAreaContent) {
-    inputAreaContent->SetPickerState(aOpen);
-  }
-}
-
-void
 nsDateTimeControlFrame::HandleFocusEvent()
 {
   nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
     do_QueryInterface(mInputAreaContent);
   if (inputAreaContent) {
     inputAreaContent->FocusInnerTextBox();
   }
 }
@@ -132,25 +92,36 @@ nsDateTimeControlFrame::HandleBlurEvent(
   if (inputAreaContent) {
     inputAreaContent->BlurInnerTextBox();
   }
 }
 
 bool
 nsDateTimeControlFrame::HasBadInput()
 {
-  nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
-    do_QueryInterface(mInputAreaContent);
+  // Incomplete field does not imply bad input.
+  Element* editWrapperElement = mInputAreaContent->GetComposedDoc()->
+    GetAnonymousElementByAttribute(mInputAreaContent,
+      nsGkAtoms::anonid, NS_LITERAL_STRING("edit-wrapper"));
 
-  bool result = false;
-  if (inputAreaContent) {
-    inputAreaContent->HasBadInput(&result);
+  for (Element* child = editWrapperElement->GetFirstElementChild(); child; child = child->GetNextElementSibling()) {
+    if (child->ClassList()->Contains(NS_LITERAL_STRING("datetime-edit-field"))) {
+      nsAutoString value;
+      child->GetAttr(kNameSpaceID_None, nsGkAtoms::value, value);
+      if (value.IsEmpty()) {
+        return false;
+      }
+    }
   }
 
-  return result;
+  // All fields are available but input element's value is empty implies
+  // it has been sanitized.
+  nsAutoString value;
+  HTMLInputElement::FromNode(mContent)->GetValue(value, CallerType::System);
+  return value.IsEmpty();
 }
 
 nscoord
 nsDateTimeControlFrame::GetMinISize(gfxContext* aRenderingContext)
 {
   nscoord result;
   DISPLAY_MIN_INLINE_SIZE(this, result);
 
@@ -333,40 +304,16 @@ nsDateTimeControlFrame::CreateAnonymousC
   RefPtr<NodeInfo> nodeInfo =
     nodeInfoManager->GetNodeInfo(nsGkAtoms::datetimebox, nullptr,
                                  kNameSpaceID_XUL, nsINode::ELEMENT_NODE);
   NS_ENSURE_TRUE(nodeInfo, NS_ERROR_OUT_OF_MEMORY);
 
   NS_TrustedNewXULElement(getter_AddRefs(mInputAreaContent), nodeInfo.forget());
   aElements.AppendElement(mInputAreaContent);
 
-  nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
-    do_QueryInterface(mInputAreaContent);
-  if (inputAreaContent) {
-    // Propogate our tabindex.
-    nsAutoString tabIndexStr;
-    if (mContent->AsElement()->GetAttr(kNameSpaceID_None,
-                                       nsGkAtoms::tabindex,
-                                       tabIndexStr)) {
-      inputAreaContent->SetEditAttribute(NS_LITERAL_STRING("tabindex"),
-                                         tabIndexStr);
-    }
-
-    // Propagate our readonly state.
-    nsAutoString readonly;
-    if (mContent->AsElement()->GetAttr(kNameSpaceID_None,
-                                       nsGkAtoms::readonly,
-                                       readonly)) {
-      inputAreaContent->SetEditAttribute(NS_LITERAL_STRING("readonly"),
-                                         readonly);
-    }
-
-    SyncDisabledState();
-  }
-
   return NS_OK;
 }
 
 void
 nsDateTimeControlFrame::AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements,
                                                  uint32_t aFilter)
 {
   if (mInputAreaContent) {
@@ -378,24 +325,17 @@ void
 nsDateTimeControlFrame::SyncDisabledState()
 {
   NS_ASSERTION(mInputAreaContent, "The input area content must exist!");
   nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
     do_QueryInterface(mInputAreaContent);
   if (!inputAreaContent) {
     return;
   }
-
-  EventStates eventStates = mContent->AsElement()->State();
-  if (eventStates.HasState(NS_EVENT_STATE_DISABLED)) {
-    inputAreaContent->SetEditAttribute(NS_LITERAL_STRING("disabled"),
-                                       EmptyString());
-  } else {
-    inputAreaContent->RemoveEditAttribute(NS_LITERAL_STRING("disabled"));
-  }
+  inputAreaContent->UpdateEditAttributes();
 }
 
 nsresult
 nsDateTimeControlFrame::AttributeChanged(int32_t aNameSpaceID,
                                          nsAtom* aAttribute,
                                          int32_t aModType)
 {
   NS_ASSERTION(mInputAreaContent, "The input area content must exist!");
@@ -416,30 +356,18 @@ nsDateTimeControlFrame::AttributeChanged
         if (aAttribute == nsGkAtoms::value) {
           if (inputAreaContent) {
             nsContentUtils::AddScriptRunner(NewRunnableMethod(
               "nsIDateTimeInputArea::NotifyInputElementValueChanged",
               inputAreaContent,
               &nsIDateTimeInputArea::NotifyInputElementValueChanged));
           }
         } else {
-          if (aModType == MutationEvent_Binding::REMOVAL) {
-            if (inputAreaContent) {
-              nsAtomString name(aAttribute);
-              inputAreaContent->RemoveEditAttribute(name);
-            }
-          } else {
-            MOZ_ASSERT(aModType == MutationEvent_Binding::ADDITION ||
-                       aModType == MutationEvent_Binding::MODIFICATION);
-            if (inputAreaContent) {
-              nsAtomString name(aAttribute);
-              nsAutoString value;
-              contentAsInputElem->GetAttr(aNameSpaceID, aAttribute, value);
-              inputAreaContent->SetEditAttribute(name, value);
-            }
+          if (inputAreaContent) {
+            inputAreaContent->UpdateEditAttributes();
           }
         }
       }
     }
   }
 
   return nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute,
                                             aModType);
--- a/layout/forms/nsDateTimeControlFrame.h
+++ b/layout/forms/nsDateTimeControlFrame.h
@@ -69,22 +69,22 @@ public:
   // nsIAnonymousContentCreator
   nsresult CreateAnonymousContent(nsTArray<ContentInfo>& aElements) override;
   void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements,
                                 uint32_t aFilter) override;
 
   nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
                             int32_t aModType) override;
 
+  nsIContent* GetInputAreaContent() const { return mInputAreaContent; }
+
   void OnValueChanged();
   void OnMinMaxStepAttrChanged();
-  void SetValueFromPicker(const DateTimeValue& aValue);
   void HandleFocusEvent();
   void HandleBlurEvent();
-  void SetPickerState(bool aOpen);
   bool HasBadInput();
 
 private:
   class SyncDisabledStateEvent;
   friend class SyncDisabledStateEvent;
   class SyncDisabledStateEvent : public mozilla::Runnable
   {
   public:
--- a/toolkit/actors/DateTimePickerChild.jsm
+++ b/toolkit/actors/DateTimePickerChild.jsm
@@ -25,17 +25,19 @@ class DateTimePickerChild extends ActorC
     this._inputElement = null;
   }
 
   /**
    * Cleanup function called when picker is closed.
    */
   close() {
     this.removeListeners();
-    this._inputElement.setDateTimePickerState(false);
+    if (this._inputElement.dateTimeBoxElement instanceof Ci.nsIDateTimeInputArea) {
+      this._inputElement.dateTimeBoxElement.setPickerState(false);
+    }
     this._inputElement = null;
   }
 
   /**
    * Called after picker is opened to start listening for input box update
    * events.
    */
   addListeners() {
@@ -84,17 +86,19 @@ class DateTimePickerChild extends ActorC
    */
   receiveMessage(aMessage) {
     switch (aMessage.name) {
       case "FormDateTime:PickerClosed": {
         this.close();
         break;
       }
       case "FormDateTime:PickerValueChanged": {
-        this._inputElement.updateDateTimeInputBox(aMessage.data);
+        if (this._inputElement.dateTimeBoxElement instanceof Ci.nsIDateTimeInputArea) {
+          this._inputElement.dateTimeBoxElement.setValueFromPicker(aMessage.data);
+        }
         break;
       }
       default:
         break;
     }
   }
 
   /**
@@ -113,17 +117,21 @@ class DateTimePickerChild extends ActorC
         if (this._inputElement) {
           // This happens when we're trying to open a picker when another picker
           // is still open. We ignore this request to let the first picker
           // close gracefully.
           return;
         }
 
         this._inputElement = aEvent.originalTarget;
-        this._inputElement.setDateTimePickerState(true);
+
+        if (this._inputElement.dateTimeBoxElement instanceof Ci.nsIDateTimeInputArea) {
+          this._inputElement.dateTimeBoxElement.setPickerState(true);
+        }
+
         this.addListeners();
 
         let value = this._inputElement.getDateTimeInputBoxValue();
         this.mm.sendAsyncMessage("FormDateTime:OpenPicker", {
           rect: this.getBoundingContentRect(this._inputElement),
           dir: this.getComputedDirection(this._inputElement),
           type: this._inputElement.type,
           detail: {
--- a/toolkit/content/widgets/datetimebox.xml
+++ b/toolkit/content/widgets/datetimebox.xml
@@ -44,16 +44,17 @@
         this.mMaxYear = 275760;
         this.mMonthDayLength = 2;
         this.mYearLength = 4;
         this.mMonthPageUpDownInterval = 3;
         this.mDayPageUpDownInterval = 7;
         this.mYearPageUpDownInterval = 10;
 
         this.buildEditFields();
+        this.updateEditAttributes();
 
         if (this.mInputElement.value) {
           this.setFieldsFromInputValue();
         }
       ]]>
       </constructor>
 
       <method name="buildEditFields">
@@ -380,17 +381,17 @@
 
             if (value < min) {
               value = min;
             } else if (value > max) {
               value = max;
             }
           }
 
-          aField.setAttribute("rawValue", value);
+          aField.setAttribute("value", value);
 
           // Display formatted value based on locale.
           let minDigits = aField.getAttribute("mindigits");
           let formatted = value.toLocaleString(this.mLocales, {
             minimumIntegerDigits: minDigits,
             useGrouping: false,
           });
 
@@ -1075,17 +1076,17 @@
               } else {
                 value = (value > this.mMaxHour) ? value % this.mMaxHour : value;
               }
             } else if (value > this.mMaxHour) {
               value = this.mMaxHour;
             }
           }
 
-          aField.setAttribute("rawValue", value);
+          aField.setAttribute("value", value);
 
           let minDigits = aField.getAttribute("mindigits");
           let formatted = value.toLocaleString(this.mLocales, {
             minimumIntegerDigits: minDigits,
             useGrouping: false,
           });
 
           aField.textContent = formatted;
@@ -1115,16 +1116,17 @@
         <parameter name="aValue"/>
         <body>
         <![CDATA[
           if (!this.hasDayPeriodField()) {
             return;
           }
 
           this.mDayPeriodField.textContent = aValue;
+          this.mDayPeriodField.setAttribute("value", aValue);
           this.updateResetButtonVisibility();
         ]]>
         </body>
       </method>
 
       <method name="isAnyFieldAvailable">
         <parameter name="aForPicker"/>
         <body>
@@ -1269,17 +1271,16 @@
           this.removeEventListener(eventName, this, { mozSystemGroup: true });
         });
         this.removeEventListener("keypress", this, {
           capture: true,
           mozSystemGroup: true,
         });
         this.mInputElement.removeEventListener("click", this,
                                                { mozSystemGroup: true });
-
         this.mInputElement = null;
       ]]>
       </destructor>
 
       <property name="EVENTS" readonly="true">
         <getter>
         <![CDATA[
           return ["focus", "blur", "copy", "cut", "paste", "mousedown"];
@@ -1319,30 +1320,33 @@
 
           field.setAttribute("readonly", this.mInputElement.readOnly);
           field.setAttribute("disabled", this.mInputElement.disabled);
           // Set property as well for convenience.
           field.disabled = this.mInputElement.disabled;
           field.readOnly = this.mInputElement.readOnly;
           field.setAttribute("aria-label", aLabel);
 
+          // Used to store the non-formatted value, cleared when value is
+          // cleared.
+          // nsDateTimeControlFrame::HasBadInput() will read this to decide
+          // if the input has value.
+          field.setAttribute("value", "");
+
           if (aIsNumeric) {
             field.classList.add("numeric");
             // Maximum value allowed.
             field.setAttribute("min", aMinValue);
             // Minumim value allowed.
             field.setAttribute("max", aMaxValue);
             // Interval when pressing pageUp/pageDown key.
             field.setAttribute("pginterval", aPageUpDownInterval);
             // Used to store what the user has already typed in the field,
             // cleared when value is cleared and when field is blurred.
             field.setAttribute("typeBuffer", "");
-            // Used to store the non-formatted number, clered when value is
-            // cleared.
-            field.setAttribute("rawValue", "");
             // Minimum digits to display, padded with leading 0s.
             field.setAttribute("mindigits", aMinDigits);
             // Maximum length for the field, will be advance to the next field
             // automatically if exceeded.
             field.setAttribute("maxlength", aMaxLength);
             // Set spinbutton ARIA role
             field.setAttribute("role", "spinbutton");
 
@@ -1384,16 +1388,17 @@
           let editRoot =
             document.getAnonymousElementByAttribute(this, "anonid", "edit-wrapper");
 
           for (let child = editRoot.firstChild; child; child = child.nextSibling) {
             if ((child instanceof HTMLSpanElement) &&
                 child.classList.contains("datetime-edit-field")) {
               this.mLastFocusedField = child;
               child.focus();
+              this.log("focused");
               break;
             }
           }
         ]]>
         </body>
       </method>
 
       <method name="blurInnerTextBox">
@@ -1439,35 +1444,16 @@
         <parameter name="aValue"/>
         <body>
         <![CDATA[
           this.setFieldsFromPicker(aValue);
         ]]>
         </body>
       </method>
 
-      <method name="hasBadInput">
-        <body>
-        <![CDATA[
-          // Incomplete field does not imply bad input.
-          if (this.isAnyFieldEmpty()) {
-            return false;
-          }
-
-          // All fields are available but input element's value is empty implies
-          // it has been sanitized.
-          if (!this.mInputElement.value) {
-            return true;
-          }
-
-          return false;
-        ]]>
-        </body>
-      </method>
-
       <method name="advanceToNextField">
         <parameter name="aReverse"/>
         <body>
         <![CDATA[
           this.log("advanceToNextField");
 
           let focusedInput = this.mLastFocusedField;
           let next = aReverse ? focusedInput.previousElementSibling
@@ -1495,82 +1481,39 @@
         <body>
         <![CDATA[
           this.log("picker is now " + (aIsOpen ? "opened" : "closed"));
           this.mIsPickerOpen = aIsOpen;
         ]]>
         </body>
       </method>
 
-      <method name="setEditAttribute">
-        <parameter name="aName"/>
-        <parameter name="aValue"/>
+      <method name="updateEditAttributes">
         <body>
         <![CDATA[
-          this.log("setAttribute: " + aName + "=" + aValue);
-
-          if (aName != "tabindex" && aName != "disabled" &&
-              aName != "readonly") {
-            return;
-          }
+          this.log("updateEditAttributes");
 
           let editRoot =
             document.getAnonymousElementByAttribute(this, "anonid", "edit-wrapper");
 
           for (let child = editRoot.firstChild; child; child = child.nextSibling) {
             if ((child instanceof HTMLSpanElement) &&
                 child.classList.contains("datetime-edit-field")) {
-
-              switch (aName) {
-                case "tabindex":
-                  child.setAttribute(aName, aValue);
-                  break;
-                case "disabled": {
-                  let value = this.mInputElement.disabled;
-                  child.setAttribute("disabled", value);
-                  child.disabled = value;
-                  break;
-                }
-                case "readonly": {
-                  let value = this.mInputElement.readOnly;
-                  child.setAttribute("readonly", value);
-                  child.readOnly = value;
-                  break;
-                }
-              }
-            }
-          }
-        ]]>
-        </body>
-      </method>
+              // "disabled" and "readonly" must be set as attributes because they
+              // are not defined properties of HTMLSpanElement, and the stylesheet
+              // checks the literal string attribute values.
+              child.setAttribute("disabled", this.mInputElement.disabled);
+              child.setAttribute("readonly", this.mInputElement.readOnly);
 
-      <method name="removeEditAttribute">
-        <parameter name="aName"/>
-        <body>
-        <![CDATA[
-          this.log("removeAttribute: " + aName);
-
-          if (aName != "tabindex" && aName != "disabled" &&
-              aName != "readonly") {
-            return;
-          }
+              // Set property as well for convenience.
+              child.disabled = this.mInputElement.disabled;
+              child.readOnly = this.mInputElement.readOnly;
 
-          let editRoot =
-            document.getAnonymousElementByAttribute(this, "anonid", "edit-wrapper");
-
-          for (let child = editRoot.firstChild; child; child = child.nextSibling) {
-            if ((child instanceof HTMLSpanElement) &&
-                child.classList.contains("datetime-edit-field")) {
-              child.removeAttribute(aName);
-              // Update property as well.
-              if (aName == "readonly") {
-                child.readOnly = false;
-              } else if (aName == "disabled") {
-                child.disabled = false;
-              }
+              // tabIndex works on all elements
+              child.tabIndex = this.mInputElement.tabIndex;
             }
           }
         ]]>
         </body>
       </method>
 
       <method name="isEmpty">
         <parameter name="aValue"/>
@@ -1582,31 +1525,31 @@
       <method name="getFieldValue">
         <parameter name="aField"/>
         <body>
         <![CDATA[
           if (!aField || !aField.classList.contains("numeric")) {
             return undefined;
           }
 
-          let value = aField.getAttribute("rawValue");
+          let value = aField.getAttribute("value");
           // Avoid returning 0 when field is empty.
           return (this.isEmpty(value) ? undefined : Number(value));
         ]]>
         </body>
       </method>
 
       <method name="clearFieldValue">
         <parameter name="aField"/>
         <body>
         <![CDATA[
           aField.textContent = aField.placeholder;
+          aField.setAttribute("value", "");
           if (aField.classList.contains("numeric")) {
             aField.setAttribute("typeBuffer", "");
-            aField.setAttribute("rawValue", "");
           }
           this.updateResetButtonVisibility();
         ]]>
         </body>
       </method>
 
       <method name="setFieldValue">
         <body>