Bug 1496242 - Part II, Convert datetimebox to UA Widget r=dholbert,jaws,smaug
authorTimothy Guan-tin Chien <timdream@gmail.com>
Sat, 03 Nov 2018 05:31:05 +0000
changeset 444213 66df568f60f633bb8e361be0a256cb4490330bf8
parent 444212 1387262d2f93391e11c1298f5148fc62ffcc4557
child 444214 4ec5f59ab7236bce89f734ccdea1ec4c7f5fd95d
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)
reviewersdholbert, jaws, smaug
bugs1496242, 1483972
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 II, Convert datetimebox to UA Widget r=dholbert,jaws,smaug This patch converts datetimebox.xml to datetimebox.js and loads it as a UA Widget, while touches things here and there to make it work. In HTMLInputElement manages the lifecycle of the datetimebox UA Widget. It is loaded when in <input> has type date or time, or have its type switch to date or time. nsDateTimeControlFrame is changed so that when UA Widget is enabled, it would not generate <xul:datetimebox>. Like bug 1483972, a check is added in nsCSSFrameConstructor::CreateGeneratedContentItem() to make sure we don't generate pseudo content inside <input>. Assertions in IntlUtils is changed to allow UAWidget to call the methods. Depends on D9056 Differential Revision: https://phabricator.services.mozilla.com/D9057
dom/base/IntlUtils.cpp
dom/base/nsContentUtils.cpp
dom/base/nsContentUtils.h
dom/html/HTMLInputElement.cpp
dom/webidl/HTMLInputElement.webidl
dom/webidl/Window.webidl
layout/base/nsCSSFrameConstructor.cpp
layout/forms/nsDateTimeControlFrame.cpp
layout/forms/nsDateTimeControlFrame.h
layout/generic/nsFrameIdList.h
toolkit/actors/DateTimePickerChild.jsm
toolkit/actors/UAWidgetsChild.jsm
toolkit/content/jar.mn
toolkit/content/widgets/datetimebox.css
toolkit/content/widgets/datetimebox.js
toolkit/content/widgets/docs/ua_widget.rst
--- a/dom/base/IntlUtils.cpp
+++ b/dom/base/IntlUtils.cpp
@@ -37,17 +37,18 @@ IntlUtils::WrapObject(JSContext* aCx, JS
 }
 
 void
 IntlUtils::GetDisplayNames(const Sequence<nsString>& aLocales,
                            const DisplayNameOptions& aOptions,
                            DisplayNameResult& aResult, ErrorResult& aError)
 {
   MOZ_ASSERT(nsContentUtils::IsCallerChrome() ||
-             nsContentUtils::IsCallerContentXBL());
+             nsContentUtils::IsCallerContentXBL() ||
+             nsContentUtils::IsCallerUAWidget());
 
   nsCOMPtr<mozIMozIntl> mozIntl = do_GetService("@mozilla.org/mozintl;1");
   if (!mozIntl) {
     aError.Throw(NS_ERROR_UNEXPECTED);
     return;
   }
 
   aError.MightThrowJSException();
@@ -93,17 +94,18 @@ IntlUtils::GetDisplayNames(const Sequenc
   }
 }
 
 void
 IntlUtils::GetLocaleInfo(const Sequence<nsString>& aLocales,
                          LocaleInfo& aResult, ErrorResult& aError)
 {
   MOZ_ASSERT(nsContentUtils::IsCallerChrome() ||
-             nsContentUtils::IsCallerContentXBL());
+             nsContentUtils::IsCallerContentXBL() ||
+             nsContentUtils::IsCallerUAWidget());
 
   nsCOMPtr<mozIMozIntl> mozIntl = do_GetService("@mozilla.org/mozintl;1");
   if (!mozIntl) {
     aError.Throw(NS_ERROR_UNEXPECTED);
     return;
   }
 
   AutoJSAPI jsapi;
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -2340,16 +2340,32 @@ nsContentUtils::IsCallerContentXBL()
     MOZ_ASSERT(nsContentUtils::AllowXULXBLForPrincipal(xpc::GetRealmPrincipal(realm)));
     return true;
   }
 
   return xpc::IsContentXBLScope(realm);
 }
 
 bool
+nsContentUtils::IsCallerUAWidget()
+{
+  JSContext *cx = GetCurrentJSContext();
+  if (!cx) {
+    return false;
+  }
+
+  JS::Realm* realm = JS::GetCurrentRealmOrNull(cx);
+  if (!realm) {
+    return false;
+  }
+
+  return xpc::IsUAWidgetScope(realm);
+}
+
+bool
 nsContentUtils::IsSystemCaller(JSContext* aCx)
 {
   // Note that SubjectPrincipal() assumes we are in a compartment here.
   return SubjectPrincipal(aCx) == sSystemPrincipal;
 }
 
 bool
 nsContentUtils::ThreadsafeIsSystemCaller(JSContext* aCx)
--- a/dom/base/nsContentUtils.h
+++ b/dom/base/nsContentUtils.h
@@ -214,16 +214,17 @@ public:
   static nsresult Init();
 
   // Strip off "wyciwyg://n/" part of a URL. aURI must have "wyciwyg" scheme.
   static nsresult RemoveWyciwygScheme(nsIURI* aURI, nsIURI** aReturn);
 
   static bool IsCallerChrome();
   static bool ThreadsafeIsCallerChrome();
   static bool IsCallerContentXBL();
+  static bool IsCallerUAWidget();
   static bool IsFuzzingEnabled()
 #ifndef FUZZING
   {
     return false;
   }
 #else
   ;
 #endif
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -2218,17 +2218,17 @@ void HTMLInputElement::GetDateTimeInputB
   }
 
   aValue = *mDateTimeInputBoxValue;
 }
 
 Element* HTMLInputElement::GetDateTimeBoxElement()
 {
   nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame());
-  if (frame) {
+  if (frame && frame->GetInputAreaContent()) {
     return frame->GetInputAreaContent()->AsElement();
   }
   return nullptr;
 }
 
 void
 HTMLInputElement::OpenDateTimePicker(const DateTimeValue& aInitialValue)
 {
@@ -4638,16 +4638,29 @@ HTMLInputElement::BindToTree(nsIDocument
   // If there is a disabled fieldset in the parent chain, the element is now
   // barred from constraint validation and can't suffer from value missing
   // (call done before).
   UpdateBarredFromConstraintValidation();
 
   // And now make sure our state is up to date
   UpdateState(false);
 
+  if ((mType == NS_FORM_INPUT_TIME || mType == NS_FORM_INPUT_DATE) &&
+      nsContentUtils::IsUAWidgetEnabled() &&
+      IsInComposedDoc()) {
+    // Construct Shadow Root so web content can be hidden in the DOM.
+    AttachAndSetUAShadowRoot();
+    AsyncEventDispatcher* dispatcher =
+      new AsyncEventDispatcher(this,
+                               NS_LITERAL_STRING("UAWidgetBindToTree"),
+                               CanBubble::eYes,
+                               ChromeOnlyDispatch::eYes);
+    dispatcher->RunDOMEventWhenSafe();
+  }
+
   if (mType == NS_FORM_INPUT_PASSWORD) {
     if (IsInComposedDoc()) {
       AsyncEventDispatcher* dispatcher =
         new AsyncEventDispatcher(this,
                                  NS_LITERAL_STRING("DOMInputPasswordAdded"),
                                  CanBubble::eYes,
                                  ChromeOnlyDispatch::eYes);
       dispatcher->PostDOMEvent();
@@ -4679,16 +4692,30 @@ HTMLInputElement::UnbindFromTree(bool aD
   // GetCurrentDoc is returning nullptr so we can update the value
   // missing validity state to reflect we are no longer into a doc.
   UpdateValueMissingValidityState();
   // We might be no longer disabled because of parent chain changed.
   UpdateBarredFromConstraintValidation();
 
   // And now make sure our state is up to date
   UpdateState(false);
+
+  if (GetShadowRoot() && IsInComposedDoc()) {
+    RefPtr<Element> self = this;
+    nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
+      "HTMLInputElement::UnbindFromTree::UAWidgetUnbindFromTree",
+      [self]() {
+        nsContentUtils::DispatchChromeEvent(
+          self->OwnerDoc(), self,
+          NS_LITERAL_STRING("UAWidgetUnbindFromTree"),
+          CanBubble::eYes, Cancelable::eNo);
+        self->UnattachShadow();
+      })
+    );
+  }
 }
 
 void
 HTMLInputElement::HandleTypeChange(uint8_t aNewType, bool aNotify)
 {
   uint8_t oldType = mType;
   MOZ_ASSERT(oldType != aNewType);
 
@@ -4844,16 +4871,52 @@ HTMLInputElement::HandleTypeChange(uint8
   if (mType == NS_FORM_INPUT_PASSWORD && IsInComposedDoc()) {
     AsyncEventDispatcher* dispatcher =
       new AsyncEventDispatcher(this,
                                NS_LITERAL_STRING("DOMInputPasswordAdded"),
                                CanBubble::eYes,
                                ChromeOnlyDispatch::eYes);
     dispatcher->PostDOMEvent();
   }
+
+  if (nsContentUtils::IsUAWidgetEnabled() && IsInComposedDoc()) {
+    if (oldType == NS_FORM_INPUT_TIME || oldType == NS_FORM_INPUT_DATE) {
+      if (mType != NS_FORM_INPUT_TIME && mType != NS_FORM_INPUT_DATE) {
+        // Switch away from date/time type.
+        RefPtr<Element> self = this;
+        nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
+          "HTMLInputElement::UnbindFromTree::UAWidgetUnbindFromTree",
+          [self]() {
+            nsContentUtils::DispatchChromeEvent(
+              self->OwnerDoc(), self,
+              NS_LITERAL_STRING("UAWidgetUnbindFromTree"),
+              CanBubble::eYes, Cancelable::eNo);
+            self->UnattachShadow();
+          })
+        );
+      } else {
+        // Switch between date and time.
+        AsyncEventDispatcher* dispatcher =
+          new AsyncEventDispatcher(this,
+                                   NS_LITERAL_STRING("UAWidgetAttributeChanged"),
+                                   CanBubble::eYes,
+                                   ChromeOnlyDispatch::eYes);
+        dispatcher->RunDOMEventWhenSafe();
+      }
+    } else if (mType == NS_FORM_INPUT_TIME || mType == NS_FORM_INPUT_DATE) {
+      // Switch to date/time type.
+      AttachAndSetUAShadowRoot();
+      AsyncEventDispatcher* dispatcher =
+        new AsyncEventDispatcher(this,
+                                 NS_LITERAL_STRING("UAWidgetBindToTree"),
+                                 CanBubble::eYes,
+                                 ChromeOnlyDispatch::eYes);
+      dispatcher->RunDOMEventWhenSafe();
+    }
+  }
 }
 
 void
 HTMLInputElement::SanitizeValue(nsAString& aValue)
 {
   NS_ASSERTION(mDoneCreating, "The element creation should be finished!");
 
   switch (mType) {
--- a/dom/webidl/HTMLInputElement.webidl
+++ b/dom/webidl/HTMLInputElement.webidl
@@ -190,17 +190,17 @@ partial interface HTMLInputElement {
 [NoInterfaceObject]
 interface MozEditableElement {
   [Pure, ChromeOnly]
   readonly attribute nsIEditor? editor;
 
   // This is similar to set .value on nsIDOMInput/TextAreaElements, but handling
   // of the value change is closer to the normal user input, so 'change' event
   // for example will be dispatched when focusing out the element.
-  [Func="IsChromeOrXBL", NeedsSubjectPrincipal]
+  [Func="IsChromeOrXBLOrUAWidget", NeedsSubjectPrincipal]
   void setUserInput(DOMString input);
 };
 
 HTMLInputElement implements MozEditableElement;
 
 partial interface HTMLInputElement {
   [Pref="dom.input.dirpicker", SetterThrows]
   attribute boolean allowdirs;
@@ -247,36 +247,36 @@ partial interface HTMLInputElement {
   [Pref="dom.forms.datetime", ChromeOnly,
    BinaryName="getMinimumAsDouble"]
   double getMinimum();
 
   [Pref="dom.forms.datetime", ChromeOnly,
    BinaryName="getMaximumAsDouble"]
   double getMaximum();
 
-  [Pref="dom.forms.datetime", Func="IsChromeOrXBL"]
+  [Pref="dom.forms.datetime", Func="IsChromeOrXBLOrUAWidget"]
   void openDateTimePicker(optional DateTimeValue initialValue);
 
-  [Pref="dom.forms.datetime", Func="IsChromeOrXBL"]
+  [Pref="dom.forms.datetime", Func="IsChromeOrXBLOrUAWidget"]
   void updateDateTimePicker(optional DateTimeValue value);
 
-  [Pref="dom.forms.datetime", Func="IsChromeOrXBL"]
+  [Pref="dom.forms.datetime", Func="IsChromeOrXBLOrUAWidget"]
   void closeDateTimePicker();
 
-  [Pref="dom.forms.datetime", Func="IsChromeOrXBL"]
+  [Pref="dom.forms.datetime", Func="IsChromeOrXBLOrUAWidget"]
   void setFocusState(boolean aIsFocused);
 
-  [Pref="dom.forms.datetime", Func="IsChromeOrXBL"]
+  [Pref="dom.forms.datetime", Func="IsChromeOrXBLOrUAWidget"]
   void updateValidityState();
 
-  [Pref="dom.forms.datetime", Func="IsChromeOrXBL",
+  [Pref="dom.forms.datetime", Func="IsChromeOrXBLOrUAWidget",
    BinaryName="getStepAsDouble"]
   double getStep();
 
-  [Pref="dom.forms.datetime", Func="IsChromeOrXBL",
+  [Pref="dom.forms.datetime", Func="IsChromeOrXBLOrUAWidget",
    BinaryName="getStepBaseAsDouble"]
   double getStepBase();
 };
 
 partial interface HTMLInputElement {
   [ChromeOnly]
   attribute DOMString previewValue;
 };
--- a/dom/webidl/Window.webidl
+++ b/dom/webidl/Window.webidl
@@ -555,24 +555,24 @@ partial interface Window {
    *
    * The result is a sorted list of valid locale IDs and it should be
    * used for all APIs that accept list of locales, like ECMA402 and L10n APIs.
    *
    * This API always returns at least one locale.
    *
    * Example: ["en-US", "de", "pl", "sr-Cyrl", "zh-Hans-HK"]
    */
-  [Func="IsChromeOrXBL"]
+  [Func="IsChromeOrXBLOrUAWidget"]
   sequence<DOMString> getRegionalPrefsLocales();
 
   /**
    * Getter funcion for IntlUtils, which provides helper functions for
    * localization.
    */
-  [Throws, Func="IsChromeOrXBL"]
+  [Throws, Func="IsChromeOrXBLOrUAWidget"]
   readonly attribute IntlUtils intlUtils;
 };
 
 Window implements WebGPUProvider;
 
 partial interface Window {
   [SameObject, Pref="dom.visualviewport.enabled", Replaceable]
   readonly attribute VisualViewport visualViewport;
--- a/layout/base/nsCSSFrameConstructor.cpp
+++ b/layout/base/nsCSSFrameConstructor.cpp
@@ -1790,18 +1790,20 @@ nsCSSFrameConstructor::CreateGeneratedCo
                                                   ComputedStyle& aStyle,
                                                   CSSPseudoElementType aPseudoElement,
                                                   FrameConstructionItemList& aItems)
 {
   MOZ_ASSERT(aPseudoElement == CSSPseudoElementType::before ||
              aPseudoElement == CSSPseudoElementType::after,
              "unexpected aPseudoElement");
 
-  if (aParentFrame && aParentFrame->IsHTMLVideoFrame()) {
-    // Video frames may not be leafs when backed by an UA widget, but we still don't want to expose generated content.
+  if (aParentFrame &&
+      (aParentFrame->IsHTMLVideoFrame() || aParentFrame->IsDateTimeControlFrame())) {
+    // Video frames and date time control frames may not be leafs when backed by an UA widget,
+    // but we still don't want to expose generated content.
     MOZ_ASSERT(aOriginatingElement.GetShadowRoot()->IsUAWidget());
     return;
   }
 
   ServoStyleSet* styleSet = mPresShell->StyleSet();
 
   // Probe for the existence of the pseudo-element
   RefPtr<ComputedStyle> pseudoStyle =
--- a/layout/forms/nsDateTimeControlFrame.cpp
+++ b/layout/forms/nsDateTimeControlFrame.cpp
@@ -11,16 +11,17 @@
 
 #include "nsDateTimeControlFrame.h"
 
 #include "nsContentUtils.h"
 #include "nsCheckboxRadioFrame.h"
 #include "nsGkAtoms.h"
 #include "nsContentUtils.h"
 #include "nsContentCreatorFunctions.h"
+#include "mozilla/AsyncEventDispatcher.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"
@@ -52,61 +53,129 @@ nsDateTimeControlFrame::DestroyFrom(nsIF
 {
   aPostDestroyData.AddAnonymousContent(mInputAreaContent.forget());
   nsContainerFrame::DestroyFrom(aDestructRoot, aPostDestroyData);
 }
 
 void
 nsDateTimeControlFrame::OnValueChanged()
 {
-  nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
-    do_QueryInterface(mInputAreaContent);
-  if (inputAreaContent) {
-    inputAreaContent->NotifyInputElementValueChanged();
+  if (mInputAreaContent) {
+    nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
+      do_QueryInterface(mInputAreaContent);
+    if (inputAreaContent) {
+      inputAreaContent->NotifyInputElementValueChanged();
+    }
+  } else {
+    Element* inputAreaContent = GetInputAreaContentAsElement();
+    if (!inputAreaContent) {
+      return;
+    }
+
+    AsyncEventDispatcher* dispatcher =
+      new AsyncEventDispatcher(inputAreaContent,
+                               NS_LITERAL_STRING("MozDateTimeValueChanged"),
+                               CanBubble::eNo,
+                               ChromeOnlyDispatch::eNo);
+    dispatcher->RunDOMEventWhenSafe();
   }
 }
 
 void
 nsDateTimeControlFrame::OnMinMaxStepAttrChanged()
 {
-  nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
-    do_QueryInterface(mInputAreaContent);
-  if (inputAreaContent) {
-    inputAreaContent->NotifyMinMaxStepAttrChanged();
+  if (mInputAreaContent) {
+    nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
+      do_QueryInterface(mInputAreaContent);
+    if (inputAreaContent) {
+      inputAreaContent->NotifyMinMaxStepAttrChanged();
+    }
+  } else {
+    Element* inputAreaContent = GetInputAreaContentAsElement();
+    if (!inputAreaContent) {
+      return;
+    }
+
+    AsyncEventDispatcher* dispatcher =
+      new AsyncEventDispatcher(inputAreaContent,
+                               NS_LITERAL_STRING("MozNotifyMinMaxStepAttrChanged"),
+                               CanBubble::eNo,
+                               ChromeOnlyDispatch::eNo);
+    dispatcher->RunDOMEventWhenSafe();
   }
 }
 
 void
 nsDateTimeControlFrame::HandleFocusEvent()
 {
-  nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
-    do_QueryInterface(mInputAreaContent);
-  if (inputAreaContent) {
-    inputAreaContent->FocusInnerTextBox();
+  if (mInputAreaContent) {
+    nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
+      do_QueryInterface(mInputAreaContent);
+    if (inputAreaContent) {
+      inputAreaContent->FocusInnerTextBox();
+    }
+  } else {
+    Element* inputAreaContent = GetInputAreaContentAsElement();
+    if (!inputAreaContent) {
+      return;
+    }
+
+    AsyncEventDispatcher* dispatcher =
+      new AsyncEventDispatcher(inputAreaContent,
+                               NS_LITERAL_STRING("MozFocusInnerTextBox"),
+                               CanBubble::eNo,
+                               ChromeOnlyDispatch::eNo);
+    dispatcher->RunDOMEventWhenSafe();
   }
 }
 
 void
 nsDateTimeControlFrame::HandleBlurEvent()
 {
-  nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
-    do_QueryInterface(mInputAreaContent);
-  if (inputAreaContent) {
-    inputAreaContent->BlurInnerTextBox();
+  if (mInputAreaContent) {
+    nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
+      do_QueryInterface(mInputAreaContent);
+    if (inputAreaContent) {
+      inputAreaContent->BlurInnerTextBox();
+    }
+  } else {
+    Element* inputAreaContent = GetInputAreaContentAsElement();
+    if (!inputAreaContent) {
+      return;
+    }
+
+    AsyncEventDispatcher* dispatcher =
+      new AsyncEventDispatcher(inputAreaContent,
+                               NS_LITERAL_STRING("MozBlurInnerTextBox"),
+                               CanBubble::eNo,
+                               ChromeOnlyDispatch::eNo);
+    dispatcher->RunDOMEventWhenSafe();
   }
 }
 
 bool
 nsDateTimeControlFrame::HasBadInput()
 {
+  Element* editWrapperElement = nullptr;
+  if (mInputAreaContent) {
+    // edit-wrapper is inside an XBL binding
+    editWrapperElement = mInputAreaContent->GetComposedDoc()->
+      GetAnonymousElementByAttribute(mInputAreaContent,
+        nsGkAtoms::anonid, NS_LITERAL_STRING("edit-wrapper"));
+  } else if (mContent->GetShadowRoot()) {
+    // edit-wrapper is inside an UA Widget Shadow DOM
+    editWrapperElement = mContent->GetShadowRoot()->
+      GetElementById(NS_LITERAL_STRING("edit-wrapper"));
+  }
+
+  if (!editWrapperElement) {
+    return false;
+  }
+
   // Incomplete field does not imply bad input.
-  Element* editWrapperElement = mInputAreaContent->GetComposedDoc()->
-    GetAnonymousElementByAttribute(mInputAreaContent,
-      nsGkAtoms::anonid, NS_LITERAL_STRING("edit-wrapper"));
-
   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;
       }
     }
@@ -166,17 +235,17 @@ nsDateTimeControlFrame::Reflow(nsPresCon
   DO_GLOBAL_REFLOW_COUNT("nsDateTimeControlFrame");
   DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus);
   MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
   NS_FRAME_TRACE(NS_FRAME_TRACE_CALLS,
                  ("enter nsDateTimeControlFrame::Reflow: availSize=%d,%d",
                   aReflowInput.AvailableWidth(),
                   aReflowInput.AvailableHeight()));
 
-  NS_ASSERTION(mInputAreaContent, "The input area content must exist!");
+  NS_ASSERTION(mFrames.GetLength() <= 1, "There should be no more than 1 frames");
 
   const WritingMode myWM = aReflowInput.GetWritingMode();
 
   // The ISize of our content box, which is the available ISize
   // for our anonymous content:
   const nscoord contentBoxISize = aReflowInput.ComputedISize();
   nscoord contentBoxBSize = aReflowInput.ComputedBSize();
 
@@ -194,19 +263,16 @@ nsDateTimeControlFrame::Reflow(nsPresCon
   nsIFrame* inputAreaFrame = mFrames.FirstChild();
   if (!inputAreaFrame) { // display:none?
     if (contentBoxBSize == NS_INTRINSICSIZE) {
       contentBoxBSize = 0;
       borderBoxBSize =
         aReflowInput.ComputedLogicalBorderPadding().BStartEnd(myWM);
     }
   } else {
-    NS_ASSERTION(inputAreaFrame->GetContent() == mInputAreaContent,
-                 "What is this child doing here?");
-
     ReflowOutput childDesiredSize(aReflowInput);
 
     WritingMode wm = inputAreaFrame->GetWritingMode();
     LogicalSize availSize = aReflowInput.ComputedSize(wm);
     availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE;
 
     ReflowInput childReflowOuput(aPresContext, aReflowInput,
                                  inputAreaFrame, availSize);
@@ -289,19 +355,40 @@ nsDateTimeControlFrame::Reflow(nsPresCon
   FinishAndStoreOverflow(&aDesiredSize);
 
   NS_FRAME_TRACE(NS_FRAME_TRACE_CALLS,
                  ("exit nsDateTimeControlFrame::Reflow: size=%d,%d",
                   aDesiredSize.Width(), aDesiredSize.Height()));
   NS_FRAME_SET_TRUNCATION(aStatus, aReflowInput, aDesiredSize);
 }
 
+/**
+ * nsDateTimeControlFrame should be a non-leaf frame when UA Widget is enabled,
+ * so the datetimebox container element inserted under the Shadow Root can be
+ * picked up. No frames will be generated from elements from the web content,
+ * given that they have been replaced by the Shadow Root without an <slots>
+ * element in the DOM tree.
+ *
+ * When the UA Widget is disabled, i.e. the datetimebox is bound as anonymous
+ * content with XBL, nsDateTimeControlFrame has to be a leaf so no frames from
+ * web content element will be generated.
+ */
+bool
+nsDateTimeControlFrame::IsLeafDynamic() const
+{
+  return !nsContentUtils::IsUAWidgetEnabled();
+}
+
 nsresult
 nsDateTimeControlFrame::CreateAnonymousContent(nsTArray<ContentInfo>& aElements)
 {
+  if (nsContentUtils::IsUAWidgetEnabled()) {
+    return NS_OK;
+  }
+
   // Set up "datetimebox" XUL element which will be XBL-bound to the
   // actual controls.
   nsNodeInfoManager* nodeInfoManager =
     mContent->GetComposedDoc()->NodeInfoManager();
   RefPtr<NodeInfo> nodeInfo =
     nodeInfoManager->GetNodeInfo(nsGkAtoms::datetimebox, nullptr,
                                  kNameSpaceID_XUL, nsINode::ELEMENT_NODE);
   NS_ENSURE_TRUE(nodeInfo, NS_ERROR_OUT_OF_MEMORY);
@@ -319,64 +406,125 @@ nsDateTimeControlFrame::AppendAnonymousC
   if (mInputAreaContent) {
     aElements.AppendElement(mInputAreaContent);
   }
 }
 
 void
 nsDateTimeControlFrame::SyncDisabledState()
 {
-  NS_ASSERTION(mInputAreaContent, "The input area content must exist!");
-  nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
-    do_QueryInterface(mInputAreaContent);
-  if (!inputAreaContent) {
-    return;
+  if (mInputAreaContent) {
+    nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
+      do_QueryInterface(mInputAreaContent);
+    if (!inputAreaContent) {
+      return;
+    }
+    inputAreaContent->UpdateEditAttributes();
+  } else {
+    Element* inputAreaContent = GetInputAreaContentAsElement();
+    if (!inputAreaContent) {
+      return;
+    }
+
+    AsyncEventDispatcher* dispatcher =
+      new AsyncEventDispatcher(inputAreaContent,
+                               NS_LITERAL_STRING("MozDateTimeAttributeChanged"),
+                               CanBubble::eNo,
+                               ChromeOnlyDispatch::eNo);
+    dispatcher->RunDOMEventWhenSafe();
   }
-  inputAreaContent->UpdateEditAttributes();
 }
 
 nsresult
 nsDateTimeControlFrame::AttributeChanged(int32_t aNameSpaceID,
                                          nsAtom* aAttribute,
                                          int32_t aModType)
 {
-  NS_ASSERTION(mInputAreaContent, "The input area content must exist!");
 
   // nsGkAtoms::disabled is handled by SyncDisabledState
   if (aNameSpaceID == kNameSpaceID_None) {
     if (aAttribute == nsGkAtoms::value ||
         aAttribute == nsGkAtoms::readonly ||
         aAttribute == nsGkAtoms::tabindex) {
       MOZ_ASSERT(mContent->IsHTMLElement(nsGkAtoms::input), "bad cast");
       auto contentAsInputElem = static_cast<dom::HTMLInputElement*>(GetContent());
       // If script changed the <input>'s type before setting these attributes
       // then we don't need to do anything since we are going to be reframed.
       if (contentAsInputElem->ControlType() == NS_FORM_INPUT_TIME ||
           contentAsInputElem->ControlType() == NS_FORM_INPUT_DATE) {
-        nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
-          do_QueryInterface(mInputAreaContent);
-        if (aAttribute == nsGkAtoms::value) {
-          if (inputAreaContent) {
-            nsContentUtils::AddScriptRunner(NewRunnableMethod(
-              "nsIDateTimeInputArea::NotifyInputElementValueChanged",
-              inputAreaContent,
-              &nsIDateTimeInputArea::NotifyInputElementValueChanged));
+        if (mInputAreaContent) {
+          nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
+            do_QueryInterface(mInputAreaContent);
+          if (aAttribute == nsGkAtoms::value) {
+            if (inputAreaContent) {
+              nsContentUtils::AddScriptRunner(NewRunnableMethod(
+                "nsIDateTimeInputArea::NotifyInputElementValueChanged",
+                inputAreaContent,
+                &nsIDateTimeInputArea::NotifyInputElementValueChanged));
+            }
+          } else {
+            if (inputAreaContent) {
+              inputAreaContent->UpdateEditAttributes();
+            }
           }
         } else {
-          if (inputAreaContent) {
-            inputAreaContent->UpdateEditAttributes();
+          Element* inputAreaContent = GetInputAreaContentAsElement();
+          if (aAttribute == nsGkAtoms::value) {
+            if (inputAreaContent) {
+              AsyncEventDispatcher* dispatcher =
+                new AsyncEventDispatcher(inputAreaContent,
+                                         NS_LITERAL_STRING("NotifyInputElementValueChanged"),
+                                         CanBubble::eNo,
+                                         ChromeOnlyDispatch::eNo);
+              dispatcher->RunDOMEventWhenSafe();
+            }
+          } else {
+            if (inputAreaContent) {
+              AsyncEventDispatcher* dispatcher =
+                new AsyncEventDispatcher(inputAreaContent,
+                                         NS_LITERAL_STRING("MozDateTimeValueChanged"),
+                                         CanBubble::eNo,
+                                         ChromeOnlyDispatch::eNo);
+              dispatcher->RunDOMEventWhenSafe();
+            }
           }
         }
       }
     }
   }
 
   return nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute,
                                             aModType);
 }
 
+nsIContent*
+nsDateTimeControlFrame::GetInputAreaContent()
+{
+  if (mInputAreaContent) {
+    return mInputAreaContent;
+  }
+  if (mContent->GetShadowRoot()) {
+    // The datetimebox <div> is the only child of the UA Widget Shadow Root
+    // if it is present.
+    MOZ_ASSERT(mContent->GetShadowRoot()->IsUAWidget());
+    MOZ_ASSERT(1 >= mContent->GetShadowRoot()->GetChildCount());
+    return mContent->GetShadowRoot()->GetFirstChild();
+  }
+  return nullptr;
+}
+
+Element*
+nsDateTimeControlFrame::GetInputAreaContentAsElement()
+{
+  nsIContent* inputAreaContent = GetInputAreaContent();
+  if (inputAreaContent) {
+    return inputAreaContent->AsElement();
+  }
+  return nullptr;
+}
+
 void
 nsDateTimeControlFrame::ContentStatesChanged(EventStates aStates)
 {
   if (aStates.HasState(NS_EVENT_STATE_DISABLED)) {
     nsContentUtils::AddScriptRunner(new SyncDisabledStateEvent(this));
   }
 }
--- a/layout/forms/nsDateTimeControlFrame.h
+++ b/layout/forms/nsDateTimeControlFrame.h
@@ -61,25 +61,27 @@ public:
 
   nscoord GetPrefISize(gfxContext* aRenderingContext) override;
 
   void Reflow(nsPresContext* aPresContext,
               ReflowOutput& aDesiredSize,
               const ReflowInput& aReflowInput,
               nsReflowStatus& aStatus) override;
 
+  bool IsLeafDynamic() const override;
+
   // 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; }
+  nsIContent* GetInputAreaContent();
 
   void OnValueChanged();
   void OnMinMaxStepAttrChanged();
   void HandleFocusEvent();
   void HandleBlurEvent();
   bool HasBadInput();
 
 private:
@@ -107,14 +109,16 @@ private:
     WeakFrame mFrame;
   };
 
   /**
    * Sync the disabled state of the anonymous children up with our content's.
    */
   void SyncDisabledState();
 
+  mozilla::dom::Element* GetInputAreaContentAsElement();
+
   // Anonymous child which is bound via XBL to an element that wraps the input
   // area and reset button.
   RefPtr<mozilla::dom::Element> mInputAreaContent;
 };
 
 #endif // nsDateTimeControlFrame_h__
--- a/layout/generic/nsFrameIdList.h
+++ b/layout/generic/nsFrameIdList.h
@@ -16,17 +16,17 @@ FRAME_ID(nsButtonBoxFrame, Box, NotLeaf)
 FRAME_ID(nsCanvasFrame, Canvas, NotLeaf)
 FRAME_ID(nsCheckboxRadioFrame, CheckboxRadio, Leaf)
 FRAME_ID(nsColorControlFrame, ColorControl, Leaf)
 FRAME_ID(nsColumnSetFrame, ColumnSet, NotLeaf)
 FRAME_ID(ColumnSetWrapperFrame, ColumnSetWrapper, NotLeaf)
 FRAME_ID(nsComboboxControlFrame, ComboboxControl, NotLeaf)
 FRAME_ID(nsComboboxDisplayFrame, ComboboxDisplay, NotLeaf)
 FRAME_ID(nsContinuingTextFrame, Text, Leaf)
-FRAME_ID(nsDateTimeControlFrame, DateTimeControl, Leaf)
+FRAME_ID(nsDateTimeControlFrame, DateTimeControl, DynamicLeaf)
 FRAME_ID(nsDeckFrame, Deck, NotLeaf)
 FRAME_ID(nsDocElementBoxFrame, Box, NotLeaf)
 FRAME_ID(nsFieldSetFrame, FieldSet, NotLeaf)
 FRAME_ID(nsFileControlFrame, Block, Leaf)
 FRAME_ID(nsFirstLetterFrame, Letter, NotLeaf)
 FRAME_ID(nsFirstLineFrame, Line, NotLeaf)
 FRAME_ID(nsFlexContainerFrame, FlexContainer, NotLeaf)
 FRAME_ID(nsFrame, None, NotLeaf)
--- a/toolkit/actors/DateTimePickerChild.jsm
+++ b/toolkit/actors/DateTimePickerChild.jsm
@@ -25,19 +25,32 @@ class DateTimePickerChild extends ActorC
     this._inputElement = null;
   }
 
   /**
    * Cleanup function called when picker is closed.
    */
   close() {
     this.removeListeners();
+    if (!this._inputElement.dateTimeBoxElement) {
+      this._inputElement = null;
+      return;
+    }
+
     if (this._inputElement.dateTimeBoxElement instanceof Ci.nsIDateTimeInputArea) {
       this._inputElement.dateTimeBoxElement.setPickerState(false);
+    } else if (this._inputElement.openOrClosedShadowRoot) {
+      // dateTimeBoxElement is within UA Widget Shadow DOM.
+      // An event dispatch to it can't be accessed by document.
+      let win = this._inputElement.ownerGlobal;
+      this._inputElement.dateTimeBoxElement.dispatchEvent(
+        new win.CustomEvent("MozSetDateTimePickerState",
+          { detail: false }, win));
     }
+
     this._inputElement = null;
   }
 
   /**
    * Called after picker is opened to start listening for input box update
    * events.
    */
   addListeners() {
@@ -86,18 +99,29 @@ class DateTimePickerChild extends ActorC
    */
   receiveMessage(aMessage) {
     switch (aMessage.name) {
       case "FormDateTime:PickerClosed": {
         this.close();
         break;
       }
       case "FormDateTime:PickerValueChanged": {
+        if (!this._inputElement.dateTimeBoxElement) {
+          return;
+        }
+
         if (this._inputElement.dateTimeBoxElement instanceof Ci.nsIDateTimeInputArea) {
           this._inputElement.dateTimeBoxElement.setValueFromPicker(aMessage.data);
+        } else if (this._inputElement.openOrClosedShadowRoot) {
+          // dateTimeBoxElement is within UA Widget Shadow DOM.
+          // An event dispatch to it can't be accessed by document.
+          let win = this._inputElement.ownerGlobal;
+          this._inputElement.dateTimeBoxElement.dispatchEvent(
+            new win.CustomEvent("MozPickerValueChanged",
+              { detail: Cu.cloneInto(aMessage.data, win) }, win));
         }
         break;
       }
       default:
         break;
     }
   }
 
@@ -118,18 +142,30 @@ class DateTimePickerChild extends ActorC
           // 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;
 
+        if (!this._inputElement.dateTimeBoxElement) {
+          throw new Error("How do we get this event without a UA Widget or XBL binding?");
+        }
+
         if (this._inputElement.dateTimeBoxElement instanceof Ci.nsIDateTimeInputArea) {
           this._inputElement.dateTimeBoxElement.setPickerState(true);
+        } else if (this._inputElement.openOrClosedShadowRoot) {
+          // dateTimeBoxElement is within UA Widget Shadow DOM.
+          // An event dispatch to it can't be accessed by document, because
+          // the event is not composed.
+          let win = this._inputElement.ownerGlobal;
+          this._inputElement.dateTimeBoxElement.dispatchEvent(
+            new win.CustomEvent("MozSetDateTimePickerState",
+              { detail: true }, win));
         }
 
         this.addListeners();
 
         let value = this._inputElement.getDateTimeInputBoxValue();
         this.mm.sendAsyncMessage("FormDateTime:OpenPicker", {
           rect: this.getBoundingContentRect(this._inputElement),
           dir: this.getComputedDirection(this._inputElement),
--- a/toolkit/actors/UAWidgetsChild.jsm
+++ b/toolkit/actors/UAWidgetsChild.jsm
@@ -48,17 +48,18 @@ class UAWidgetsChild extends ActorChild 
     let widgetName;
     switch (aElement.localName) {
       case "video":
       case "audio":
         uri = "chrome://global/content/elements/videocontrols.js";
         widgetName = "VideoControlsPageWidget";
         break;
       case "input":
-        // TODO (datetimebox)
+        uri = "chrome://global/content/elements/datetimebox.js";
+        widgetName = "DateTimeBoxWidget";
         break;
       case "applet":
       case "embed":
       case "object":
         // TODO (pluginProblems)
         break;
     }
 
--- a/toolkit/content/jar.mn
+++ b/toolkit/content/jar.mn
@@ -88,16 +88,17 @@ toolkit.jar:
 *  content/global/bindings/textbox.xml         (widgets/textbox.xml)
    content/global/bindings/timekeeper.js       (widgets/timekeeper.js)
    content/global/bindings/timepicker.js       (widgets/timepicker.js)
    content/global/bindings/toolbar.xml         (widgets/toolbar.xml)
    content/global/bindings/toolbarbutton.xml   (widgets/toolbarbutton.xml)
    content/global/bindings/tree.xml            (widgets/tree.xml)
    content/global/bindings/videocontrols.xml   (widgets/videocontrols.xml)
 *  content/global/bindings/wizard.xml          (widgets/wizard.xml)
+   content/global/elements/datetimebox.js      (widgets/datetimebox.js)
    content/global/elements/findbar.js          (widgets/findbar.js)
    content/global/elements/editor.js          (widgets/editor.js)
    content/global/elements/general.js          (widgets/general.js)
    content/global/elements/progressmeter.js    (widgets/progressmeter.js)
    content/global/elements/radio.js            (widgets/radio.js)
    content/global/elements/stringbundle.js     (widgets/stringbundle.js)
    content/global/elements/tabbox.js           (widgets/tabbox.js)
    content/global/elements/textbox.js          (widgets/textbox.js)
--- a/toolkit/content/widgets/datetimebox.css
+++ b/toolkit/content/widgets/datetimebox.css
@@ -1,14 +1,18 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 @namespace url("http://www.w3.org/1999/xhtml");
 
+.datetimebox {
+  display: flex;
+}
+
 .datetime-input-box-wrapper {
   display: inline-flex;
   flex: 1;
   background-color: inherit;
   min-width: 0;
   justify-content: space-between;
 }
 
copy from toolkit/content/widgets/datetimebox.xml
copy to toolkit/content/widgets/datetimebox.js
--- a/toolkit/content/widgets/datetimebox.xml
+++ b/toolkit/content/widgets/datetimebox.js
@@ -1,1790 +1,1608 @@
-<?xml version="1.0"?>
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// This is a UA widget. It runs in per-origin UA widget scope,
+// to be loaded by UAWidgetsChild.jsm.
+
+/*
+ * This is the class of entry. It will construct the actual implementation
+ * according to the value of the "type" property.
+ */
+this.DateTimeBoxWidget = class {
+  constructor(shadowRoot) {
+    this.shadowRoot = shadowRoot;
+    this.element = shadowRoot.host;
+    this.document = this.element.ownerDocument;
+    this.window = this.document.defaultView;
+
+    this.switchImpl();
+  }
+
+  /*
+   * Callback called by UAWidgets when the "type" property changes.
+   */
+  onattributechange() {
+    this.switchImpl();
+  }
+
+  /*
+   * Actually switch the implementation.
+   * - With type equal to "date", DateInputImplWidget should load.
+   * - With type equal to "time", TimeInputImplWidget should load.
+   * - Otherwise, nothing should load and loaded impl should be unloaded.
+   */
+  switchImpl() {
+    let newImpl;
+    if (this.element.type == "date") {
+      newImpl = DateInputImplWidget;
+    } else if (this.element.type == "time") {
+      newImpl = TimeInputImplWidget;
+    }
+    // Skip if we are asked to load the same implementation.
+    // This can happen if the property is set again w/o value change.
+    if (this.impl && this.impl.constructor == newImpl) {
+      return;
+    }
+    if (this.impl) {
+      this.impl.destructor();
+      this.shadowRoot.firstChild.remove();
+    }
+    if (newImpl) {
+      this.impl = new newImpl(this.shadowRoot);
+    } else {
+      this.impl = undefined;
+    }
+  }
+
+  destructor() {
+    if (!this.impl) {
+      return;
+    }
+    this.impl.destructor();
+    this.shadowRoot.firstChild.remove();
+    delete this.impl;
+  }
+};
+
+this.DateTimeInputBaseImplWidget = class {
+  constructor(shadowRoot) {
+    this.shadowRoot = shadowRoot;
+    this.element = shadowRoot.host;
+    this.document = this.element.ownerDocument;
+    this.window = this.document.defaultView;
+
+    this.generateContent();
+
 
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+    this.DEBUG = false;
+    this.mDateTimeBoxElement = shadowRoot.firstChild;
+    this.mInputElement = this.element;
+    this.mLocales = this.window.getRegionalPrefsLocales();
+
+    this.mIsRTL = false;
+    let intlUtils = this.window.intlUtils;
+    if (intlUtils) {
+      this.mIsRTL =
+        intlUtils.getLocaleInfo(this.mLocales).direction === "rtl";
+    }
+
+    if (this.mIsRTL) {
+      let inputBoxWrapper =
+        this.shadowRoot.getElementById("input-box-wrapper");
+      inputBoxWrapper.dir = "rtl";
+    }
+
+    this.mMin = this.mInputElement.min;
+    this.mMax = this.mInputElement.max;
+    this.mStep = this.mInputElement.step;
+    this.mIsPickerOpen = false;
+
+    this.mResetButton =
+      this.shadowRoot.getElementById("reset-button");
+    this.mResetButton.style.visibility = "hidden";
+    this.mResetButton.addEventListener("mousedown", this, {
+      mozSystemGroup: true,
+    });
+
+    this.mInputElement.addEventListener("keypress", this, {
+      capture: true,
+      mozSystemGroup: true,
+    }, false);
+    // This is to open the picker when input element is clicked (this
+    // includes padding area).
+    this.mInputElement.addEventListener("click", this,
+                                        { mozSystemGroup: true },
+                                        false);
 
-<!DOCTYPE bindings [
-<!ENTITY % datetimeboxDTD SYSTEM "chrome://global/locale/datetimebox.dtd">
-%datetimeboxDTD;
-]>
+    // Those events are dispatched to <div class="datetimebox"> with bubble set
+    // to false. They are trapped inside UA Widget Shadow DOM and are not
+    // dispatched to the document.
+    this.CONTROL_EVENTS.forEach((eventName) => {
+      this.mDateTimeBoxElement.addEventListener(eventName, this, {}, false);
+    });
+  }
+
+  generateContent() {
+    /*
+     * Pass the markup through XML parser purely for the reason of loading the localization DTD.
+     * Remove it when migrate to Fluent (bug 1504363).
+     */
+    const parser = new this.window.DOMParser();
+    let parserDoc = parser.parseFromString(`<!DOCTYPE bindings [
+      <!ENTITY % datetimeboxDTD SYSTEM "chrome://global/locale/datetimebox.dtd">
+      %datetimeboxDTD;
+      ]>
+      <div class="datetimebox" xmlns="http://www.w3.org/1999/xhtml" role="none">
+        <link rel="stylesheet" type="text/css" href="chrome://global/content/bindings/datetimebox.css" />
+        <div class="datetime-input-box-wrapper" id="input-box-wrapper" role="presentation">
+          <span class="datetime-input-edit-wrapper"
+                     id="edit-wrapper">
+            <!-- Each of the date/time input types will append their input child
+               - elements here -->
+          </span>
+
+          <button class="datetime-reset-button" id="reset-button" tabindex="-1" aria-label="&datetime.reset.label;">
+            <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12">
+              <path d="M 3.9,3 3,3.9 5.1,6 3,8.1 3.9,9 6,6.9 8.1,9 9,8.1 6.9,6 9,3.9 8.1,3 6,5.1 Z M 12,6 A 6,6 0 0 1 6,12 6,6 0 0 1 0,6 6,6 0 0 1 6,0 6,6 0 0 1 12,6 Z"/>
+            </svg>
+          </button>
+        </div>
+        <div id="strings"
+          data-m-year-place-holder="&date.year.placeholder;"
+          data-m-year-label="&date.year.label;"
+          data-m-month-place-holder="&date.month.placeholder;"
+          data-m-month-label="&date.month.label;"
+          data-m-day-place-holder="&date.day.placeholder;"
+          data-m-day-label="&date.day.label;"
 
-<bindings id="datetimeboxBindings"
-   xmlns="http://www.mozilla.org/xbl"
-   xmlns:html="http://www.w3.org/1999/xhtml"
-   xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-   xmlns:xbl="http://www.mozilla.org/xbl">
+          data-m-hour-place-holder="&time.hour.placeholder;"
+          data-m-hour-label="&time.hour.label;"
+          data-m-minute-place-holder="&time.minute.placeholder;"
+          data-m-minute-label="&time.minute.label;"
+          data-m-second-place-holder="&time.second.placeholder;"
+          data-m-second-label="&time.second.label;"
+          data-m-millisecond-place-holder="&time.millisecond.placeholder;"
+          data-m-millisecond-label="&time.millisecond.label;"
+          data-m-day-period-place-holder="&time.dayperiod.placeholder;"
+          data-m-day-period-label="&time.dayperiod.label;"
+        ></div>
+      </div>`, "application/xml");
+
+    /*
+     * The <div id="strings"> is also parsed in the document so that there is no
+     * need to create another XML document just to get the strings.
+     */
+    let stringsElement = parserDoc.getElementById("strings");
+    stringsElement.remove();
+    for (let key in stringsElement.dataset) {
+      // key will be camelCase version of the attribute key above,
+      // like mYearPlaceHolder.
+      this[key] = stringsElement.dataset[key];
+    }
+
+    this.shadowRoot.importNodeAndAppendChildAt(this.shadowRoot, parserDoc.documentElement, true);
+  }
+
+  destructor() {
+    this.mResetButton.addEventListener("mousedown", this, {
+      mozSystemGroup: true,
+    });
+
+    this.mInputElement.removeEventListener("keypress", this, {
+      capture: true,
+      mozSystemGroup: true,
+    });
+    this.mInputElement.removeEventListener("click", this,
+                                           { mozSystemGroup: true });
+
+    this.CONTROL_EVENTS.forEach((eventName) => {
+      this.mDateTimeBoxElement.removeEventListener(eventName, this);
+    });
+    this.mInputElement = null;
+  }
+
+  get FIELD_EVENTS() {
+    return ["focus", "blur", "copy", "cut", "paste"];
+  }
+
+  get CONTROL_EVENTS() {
+    return ["MozDateTimeValueChanged", "MozNotifyMinMaxStepAttrChanged",
+            "MozFocusInnerTextBox", "MozBlurInnerTextBox",
+            "MozDateTimeAttributeChanged", "MozPickerValueChanged",
+            "MozSetDateTimePickerState"];
+  }
+
+  addEventListenersToField(aElement) {
+    // These events don't bubble out of the Shadow DOM, so we'll have to add
+    // event listeners specifically on each of the fields, not just
+    // on the <input>
+    this.FIELD_EVENTS.forEach((eventName) => {
+      aElement.addEventListener(eventName, this, { mozSystemGroup: true }, false);
+    });
+  }
+
+  removeEventListenersToField(aElement) {
+    if (!aElement) {
+      return;
+    }
+
+    this.FIELD_EVENTS.forEach((eventName) => {
+      aElement.removeEventListener(eventName, this, { mozSystemGroup: true });
+    });
+  }
+
+  log(aMsg) {
+    if (this.DEBUG) {
+      this.window.dump("[DateTimeBox] " + aMsg + "\n");
+    }
+  }
 
-  <binding id="date-input"
-           simpleScopeChain="true"
-           extends="chrome://global/content/bindings/datetimebox.xml#datetime-input-base">
-    <resources>
-      <stylesheet src="chrome://global/content/bindings/datetimebox.css"/>
-    </resources>
+  createEditFieldAndAppend(aPlaceHolder, aLabel, aIsNumeric,
+                           aMinDigits, aMaxLength,
+                           aMinValue, aMaxValue, aPageUpDownInterval) {
+    let root = this.shadowRoot.getElementById("edit-wrapper");
+    let field = this.shadowRoot.createElementAndAppendChildAt(root, "span");
+    field.classList.add("datetime-edit-field");
+    field.textContent = aPlaceHolder;
+    field.placeholder = aPlaceHolder;
+    field.tabIndex = this.mInputElement.tabIndex;
+
+    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", "");
+      // 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");
 
-    <implementation>
-      <constructor>
-      <![CDATA[
-        /* eslint-disable no-multi-spaces */
-        this.mYearPlaceHolder = ]]>"&date.year.placeholder;"<![CDATA[;
-        this.mMonthPlaceHolder = ]]>"&date.month.placeholder;"<![CDATA[;
-        this.mDayPlaceHolder = ]]>"&date.day.placeholder;"<![CDATA[;
+      if (this.mIsRTL) {
+        // Force the direction to be "ltr", so that the field stays in the
+        // same order even when it's empty (with placeholder). By using
+        // "embed", the text inside the element is still displayed based
+        // on its directionality.
+        field.style.unicodeBidi = "embed";
+        field.style.direction = "ltr";
+      }
+    } else {
+      // Set generic textbox ARIA role
+      field.setAttribute("role", "textbox");
+    }
+
+    return field;
+  }
+
+  updateResetButtonVisibility() {
+    if (this.isAnyFieldAvailable(false)) {
+      this.mResetButton.style.visibility = "visible";
+    } else {
+      this.mResetButton.style.visibility = "hidden";
+    }
+  }
+
+  focusInnerTextBox() {
+    this.log("Focus inner editable field.");
+
+    let editRoot = this.shadowRoot.getElementById("edit-wrapper");
+    for (let child of editRoot.querySelectorAll(":scope > span.datetime-edit-field")) {
+      this.mLastFocusedField = child;
+      child.focus();
+      this.log("focused");
+      break;
+    }
+  }
+
+  blurInnerTextBox() {
+    this.log("Blur inner editable field.");
+
+    if (this.mLastFocusedField) {
+      this.mLastFocusedField.blur();
+    } else {
+      // If .mLastFocusedField hasn't been set, blur all editable fields,
+      // so that the bound element will actually be blurred. Note that
+      // blurring on a element that has no focus won't have any effect.
+      let editRoot = this.shadowRoot.getElementById("edit-wrapper");
+      for (let child of editRoot.querySelectorAll(":scope > span.datetime-edit-field")) {
+        child.blur();
+      }
+    }
+  }
 
-        this.mYearLabel = ]]>"&date.year.label;"<![CDATA[;
-        this.mMonthLabel = ]]>"&date.month.label;"<![CDATA[;
-        this.mDayLabel = ]]>"&date.day.label;"<![CDATA[;
-        /* eslint-enable no-multi-spaces */
+  notifyInputElementValueChanged() {
+    this.log("inputElementValueChanged");
+    this.setFieldsFromInputValue();
+  }
+
+  notifyMinMaxStepAttrChanged() {
+    // No operation by default
+  }
+
+  setValueFromPicker(aValue) {
+    this.setFieldsFromPicker(aValue);
+  }
+
+  advanceToNextField(aReverse) {
+    this.log("advanceToNextField");
+
+    let focusedInput = this.mLastFocusedField;
+    let next = aReverse ? focusedInput.previousElementSibling
+                        : focusedInput.nextElementSibling;
+    if (!next && !aReverse) {
+      this.setInputValueFromFields();
+      return;
+    }
+
+    while (next) {
+      if (next.matches("span.datetime-edit-field")) {
+        next.focus();
+        break;
+      }
+      next = aReverse ? next.previousElementSibling
+                      : next.nextElementSibling;
+    }
+  }
+
+  setPickerState(aIsOpen) {
+    this.log("picker is now " + (aIsOpen ? "opened" : "closed"));
+    this.mIsPickerOpen = aIsOpen;
+  }
+
+  updateEditAttributes() {
+    this.log("updateEditAttributes");
+
+    let editRoot =
+      this.shadowRoot.getElementById("edit-wrapper");
+
+    for (let child of editRoot.querySelectorAll(":scope > span.datetime-edit-field")) {
+      // "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);
+
+      // Set property as well for convenience.
+      child.disabled = this.mInputElement.disabled;
+      child.readOnly = this.mInputElement.readOnly;
+
+      // tabIndex works on all elements
+      child.tabIndex = this.mInputElement.tabIndex;
+    }
+
+    this.mResetButton.disabled = this.mInputElement.disabled;
+  }
+
+  isEmpty(aValue) {
+    return (aValue == undefined || 0 === aValue.length);
+  }
 
-        this.mMinMonth = 1;
-        this.mMaxMonth = 12;
-        this.mMinDay = 1;
-        this.mMaxDay = 31;
-        this.mMinYear = 1;
-        // Maximum year limited by ECMAScript date object range, year <= 275760.
-        this.mMaxYear = 275760;
-        this.mMonthDayLength = 2;
-        this.mYearLength = 4;
-        this.mMonthPageUpDownInterval = 3;
-        this.mDayPageUpDownInterval = 7;
-        this.mYearPageUpDownInterval = 10;
+  getFieldValue(aField) {
+    if (!aField || !aField.classList.contains("numeric")) {
+      return undefined;
+    }
+
+    let value = aField.getAttribute("value");
+    // Avoid returning 0 when field is empty.
+    return (this.isEmpty(value) ? undefined : Number(value));
+  }
+
+  clearFieldValue(aField) {
+    aField.textContent = aField.placeholder;
+    aField.setAttribute("value", "");
+    if (aField.classList.contains("numeric")) {
+      aField.setAttribute("typeBuffer", "");
+    }
+    this.updateResetButtonVisibility();
+  }
+
+  setFieldValue() {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  }
+
+  clearInputFields() {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  }
+
+  setFieldsFromInputValue() {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  }
+
+  setInputValueFromFields() {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  }
+
+  setFieldsFromPicker() {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  }
+
+  handleKeypress() {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  }
+
+  handleKeyboardNav() {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  }
+
+  getCurrentValue() {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  }
+
+  isAnyFieldAvailable() {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  }
+
+  notifyPicker() {
+    if (this.mIsPickerOpen && this.isAnyFieldAvailable(true)) {
+      this.mInputElement.updateDateTimePicker(this.getCurrentValue());
+    }
+  }
+
+  isDisabled() {
+    return this.mInputElement.hasAttribute("disabled");
+  }
+
+  isReadonly() {
+    return this.mInputElement.hasAttribute("readonly");
+  }
+
+  handleEvent(aEvent) {
+    this.log("handleEvent: " + aEvent.type);
+
+    if (!aEvent.isTrusted) {
+      return;
+    }
 
-        this.buildEditFields();
+    switch (aEvent.type) {
+      case "MozDateTimeValueChanged": {
+        this.notifyInputElementValueChanged();
+        break;
+      }
+      case "MozNotifyMinMaxStepAttrChanged": {
+        this.notifyMinMaxStepAttrChanged();
+        break;
+      }
+      case "MozFocusInnerTextBox": {
+        this.focusInnerTextBox();
+        break;
+      }
+      case "MozBlurInnerTextBox": {
+        this.blurInnerTextBox();
+        break;
+      }
+      case "MozDateTimeAttributeChanged": {
         this.updateEditAttributes();
+        break;
+      }
+      case "MozPickerValueChanged": {
+        this.setValueFromPicker(aEvent.detail);
+        break;
+      }
+      case "MozSetDateTimePickerState": {
+        this.setPickerState(aEvent.detail);
+        break;
+      }
+      case "keypress": {
+        this.onKeyPress(aEvent);
+        break;
+      }
+      case "click": {
+        this.onClick(aEvent);
+        break;
+      }
+      case "focus": {
+        this.onFocus(aEvent);
+        break;
+      }
+      case "blur": {
+        this.onBlur(aEvent);
+        break;
+      }
+      case "mousedown":
+      case "copy":
+      case "cut":
+      case "paste": {
+        aEvent.preventDefault();
+        break;
+      }
+      default:
+        break;
+    }
+  }
 
-        if (this.mInputElement.value) {
-          this.setFieldsFromInputValue();
-        }
-      ]]>
-      </constructor>
+  onFocus(aEvent) {
+    this.log("onFocus originalTarget: " + aEvent.originalTarget);
+    if (this.document.activeElement != this.mInputElement) {
+      return;
+    }
+
+    let target = aEvent.originalTarget;
+    if (target.matches("span.datetime-edit-field")) {
+      if (target.disabled) {
+        return;
+      }
+      this.mLastFocusedField = target;
+      this.mInputElement.setFocusState(true);
+    }
+  }
+
+  onBlur(aEvent) {
+    this.log("onBlur originalTarget: " + aEvent.originalTarget +
+      " target: " + aEvent.target);
+
+    let target = aEvent.originalTarget;
+    target.setAttribute("typeBuffer", "");
+    this.setInputValueFromFields();
+    this.mInputElement.setFocusState(false);
+  }
+
+  onKeyPress(aEvent) {
+    this.log("onKeyPress key: " + aEvent.key);
 
-      <method name="buildEditFields">
-        <body>
-        <![CDATA[
-          const HTML_NS = "http://www.w3.org/1999/xhtml";
-          let root =
-            document.getAnonymousElementByAttribute(this, "anonid", "edit-wrapper");
+    switch (aEvent.key) {
+      // Close picker on Enter, Escape or Space key.
+      case "Enter":
+      case "Escape":
+      case " ": {
+        if (this.mIsPickerOpen) {
+          this.mInputElement.closeDateTimePicker();
+          aEvent.preventDefault();
+        }
+        break;
+      }
+      case "Backspace": {
+        let targetField = aEvent.originalTarget;
+        this.clearFieldValue(targetField);
+        this.setInputValueFromFields();
+        aEvent.preventDefault();
+        break;
+      }
+      case "ArrowRight":
+      case "ArrowLeft": {
+        this.advanceToNextField(!(aEvent.key == "ArrowRight"));
+        aEvent.preventDefault();
+        break;
+      }
+      case "ArrowUp":
+      case "ArrowDown":
+      case "PageUp":
+      case "PageDown":
+      case "Home":
+      case "End": {
+        this.handleKeyboardNav(aEvent);
+        aEvent.preventDefault();
+        break;
+      }
+      default: {
+        // printable characters
+        if (aEvent.keyCode == 0 &&
+            !(aEvent.ctrlKey || aEvent.altKey || aEvent.metaKey)) {
+          this.handleKeypress(aEvent);
+          aEvent.preventDefault();
+        }
+        break;
+      }
+    }
+  }
+
+  onClick(aEvent) {
+    this.log("onClick originalTarget: " + aEvent.originalTarget +
+      " target: " + aEvent.target);
+
+    if (aEvent.defaultPrevented || this.isDisabled() || this.isReadonly()) {
+      return;
+    }
 
-          let yearMaxLength = this.mMaxYear.toString().length;
-          this.mYearField = this.createEditField(this.mYearPlaceHolder,
+    if (aEvent.originalTarget == this.mResetButton) {
+      this.clearInputFields(false);
+    } else if (!this.mIsPickerOpen) {
+      this.mInputElement.openDateTimePicker(this.getCurrentValue());
+    }
+  }
+};
+
+this.DateInputImplWidget = class extends DateTimeInputBaseImplWidget {
+  constructor(shadowRoot) {
+    super(shadowRoot);
+
+    this.mMinMonth = 1;
+    this.mMaxMonth = 12;
+    this.mMinDay = 1;
+    this.mMaxDay = 31;
+    this.mMinYear = 1;
+    // Maximum year limited by ECMAScript date object range, year <= 275760.
+    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();
+    }
+  }
+
+  destructor() {
+    this.removeEventListenersToField(this.mYearField);
+    this.removeEventListenersToField(this.mMonthField);
+    this.removeEventListenersToField(this.mDayField);
+    super.destructor();
+  }
+
+  buildEditFields() {
+    let root = this.shadowRoot.getElementById("edit-wrapper");
+
+    let yearMaxLength = this.mMaxYear.toString().length;
+
+    let formatter = Intl.DateTimeFormat(this.mLocales, {
+      year: "numeric",
+      month: "numeric",
+      day: "numeric",
+    });
+    formatter.formatToParts(Date.now()).map(part => {
+      switch (part.type) {
+        case "year":
+          this.mYearField = this.createEditFieldAndAppend(this.mYearPlaceHolder,
             this.mYearLabel, true, this.mYearLength, yearMaxLength,
             this.mMinYear, this.mMaxYear, this.mYearPageUpDownInterval);
-          this.mMonthField = this.createEditField(this.mMonthPlaceHolder,
+          this.addEventListenersToField(this.mYearField);
+          break;
+        case "month":
+          this.mMonthField = this.createEditFieldAndAppend(this.mMonthPlaceHolder,
             this.mMonthLabel, true, this.mMonthDayLength, this.mMonthDayLength,
             this.mMinMonth, this.mMaxMonth, this.mMonthPageUpDownInterval);
-          this.mDayField = this.createEditField(this.mDayPlaceHolder,
+          this.addEventListenersToField(this.mMonthField);
+          break;
+        case "day":
+          this.mDayField = this.createEditFieldAndAppend(this.mDayPlaceHolder,
             this.mDayLabel, true, this.mMonthDayLength, this.mMonthDayLength,
             this.mMinDay, this.mMaxDay, this.mDayPageUpDownInterval);
+          this.addEventListenersToField(this.mDayField);
+          break;
+        default:
+          let span = this.shadowRoot.createElementAndAppendChildAt(root, "span");
+          span.textContent = part.value;
+          break;
+      }
+    });
+  }
 
-          let fragment = document.createDocumentFragment();
-          let formatter = Intl.DateTimeFormat(this.mLocales, {
-            year: "numeric",
-            month: "numeric",
-            day: "numeric",
-          });
-          formatter.formatToParts(Date.now()).map(part => {
-            switch (part.type) {
-              case "year":
-                fragment.appendChild(this.mYearField);
-                break;
-              case "month":
-                fragment.appendChild(this.mMonthField);
-                break;
-              case "day":
-                fragment.appendChild(this.mDayField);
-                break;
-              default:
-                let span = document.createElementNS(HTML_NS, "span");
-                span.textContent = part.value;
-                fragment.appendChild(span);
-                break;
-            }
-          });
+  clearInputFields(aFromInputElement) {
+    this.log("clearInputFields");
+
+    if (this.isDisabled() || this.isReadonly()) {
+      return;
+    }
 
-          root.appendChild(fragment);
-        ]]>
-        </body>
-      </method>
+    if (this.mMonthField && !this.mMonthField.disabled &&
+        !this.mMonthField.readOnly) {
+      this.clearFieldValue(this.mMonthField);
+    }
+
+    if (this.mDayField && !this.mDayField.disabled &&
+        !this.mDayField.readOnly) {
+      this.clearFieldValue(this.mDayField);
+    }
 
-      <method name="clearInputFields">
-        <parameter name="aFromInputElement"/>
-        <body>
-        <![CDATA[
-          this.log("clearInputFields");
+    if (this.mYearField && !this.mYearField.disabled &&
+        !this.mYearField.readOnly) {
+      this.clearFieldValue(this.mYearField);
+    }
 
-          if (this.isDisabled() || this.isReadonly()) {
-            return;
-          }
-
-          if (this.mMonthField && !this.mMonthField.disabled &&
-              !this.mMonthField.readOnly) {
-            this.clearFieldValue(this.mMonthField);
-          }
+    if (!aFromInputElement) {
+      if (this.mInputElement.value) {
+        this.mInputElement.setUserInput("");
+      } else {
+        this.mInputElement.updateValidityState();
+      }
+    }
+  }
 
-          if (this.mDayField && !this.mDayField.disabled &&
-              !this.mDayField.readOnly) {
-            this.clearFieldValue(this.mDayField);
-          }
-
-          if (this.mYearField && !this.mYearField.disabled &&
-              !this.mYearField.readOnly) {
-            this.clearFieldValue(this.mYearField);
-          }
+  setFieldsFromInputValue() {
+    let value = this.mInputElement.value;
+    if (!value) {
+      this.clearInputFields(true);
+      return;
+    }
 
-          if (!aFromInputElement) {
-            if (this.mInputElement.value) {
-              this.mInputElement.setUserInput("");
-            } else {
-              this.mInputElement.updateValidityState();
-            }
-          }
-        ]]>
-        </body>
-      </method>
+    this.log("setFieldsFromInputValue: " + value);
+    let [year, month, day] = value.split("-");
+
+    this.setFieldValue(this.mYearField, year);
+    this.setFieldValue(this.mMonthField, month);
+    this.setFieldValue(this.mDayField, day);
 
-      <method name="setFieldsFromInputValue">
-        <body>
-        <![CDATA[
-          let value = this.mInputElement.value;
-          if (!value) {
-            this.clearInputFields(true);
-            return;
-          }
+    this.notifyPicker();
+  }
 
-          this.log("setFieldsFromInputValue: " + value);
-          let [year, month, day] = value.split("-");
+  setInputValueFromFields() {
+    if (this.isAnyFieldEmpty()) {
+      // Clear input element's value if any of the field has been cleared,
+      // otherwise update the validity state, since it may become "not"
+      // invalid if fields are not complete.
+      if (this.mInputElement.value) {
+        this.mInputElement.setUserInput("");
+      } else {
+        this.mInputElement.updateValidityState();
+      }
+      // We still need to notify picker in case any of the field has
+      // changed.
+      this.notifyPicker();
+      return;
+    }
 
-          this.setFieldValue(this.mYearField, year);
-          this.setFieldValue(this.mMonthField, month);
-          this.setFieldValue(this.mDayField, day);
+    let { year, month, day } = this.getCurrentValue();
 
-          this.notifyPicker();
-        ]]>
-        </body>
-      </method>
+    // Convert to a valid date string according to:
+    // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-date-string
+    year = year.toString().padStart(this.mYearLength, "0");
+    month = (month < 10) ? ("0" + month) : month;
+    day = (day < 10) ? ("0" + day) : day;
 
-      <method name="setInputValueFromFields">
-        <body>
-        <![CDATA[
-          if (this.isAnyFieldEmpty()) {
-            // Clear input element's value if any of the field has been cleared,
-            // otherwise update the validity state, since it may become "not"
-            // invalid if fields are not complete.
-            if (this.mInputElement.value) {
-              this.mInputElement.setUserInput("");
-            } else {
-              this.mInputElement.updateValidityState();
-            }
-            // We still need to notify picker in case any of the field has
-            // changed.
-            this.notifyPicker();
-            return;
-          }
+    let date = [year, month, day].join("-");
 
-          let { year, month, day } = this.getCurrentValue();
+    if (date == this.mInputElement.value) {
+      return;
+    }
+
+    this.log("setInputValueFromFields: " + date);
+    this.notifyPicker();
+    this.mInputElement.setUserInput(date);
+  }
 
-          // Convert to a valid date string according to:
-          // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-date-string
-          year = year.toString().padStart(this.mYearLength, "0");
-          month = (month < 10) ? ("0" + month) : month;
-          day = (day < 10) ? ("0" + day) : day;
-
-          let date = [year, month, day].join("-");
-
-          if (date == this.mInputElement.value) {
-            return;
-          }
+  setFieldsFromPicker(aValue) {
+    let year = aValue.year;
+    let month = aValue.month;
+    let day = aValue.day;
 
-          this.log("setInputValueFromFields: " + date);
-          this.notifyPicker();
-          this.mInputElement.setUserInput(date);
-        ]]>
-        </body>
-      </method>
+    if (!this.isEmpty(year)) {
+      this.setFieldValue(this.mYearField, year);
+    }
 
-      <method name="setFieldsFromPicker">
-        <parameter name="aValue"/>
-        <body>
-        <![CDATA[
-          let year = aValue.year;
-          let month = aValue.month;
-          let day = aValue.day;
+    if (!this.isEmpty(month)) {
+      this.setFieldValue(this.mMonthField, month);
+    }
+
+    if (!this.isEmpty(day)) {
+      this.setFieldValue(this.mDayField, day);
+    }
 
-          if (!this.isEmpty(year)) {
-            this.setFieldValue(this.mYearField, year);
-          }
+    // Update input element's .value if needed.
+    this.setInputValueFromFields();
+  }
 
-          if (!this.isEmpty(month)) {
-            this.setFieldValue(this.mMonthField, month);
-          }
-
-          if (!this.isEmpty(day)) {
-            this.setFieldValue(this.mDayField, day);
-          }
+  handleKeypress(aEvent) {
+    if (this.isDisabled() || this.isReadonly()) {
+      return;
+    }
 
-          // Update input element's .value if needed.
-          this.setInputValueFromFields();
-        ]]>
-        </body>
-      </method>
+    let targetField = aEvent.originalTarget;
+    let key = aEvent.key;
 
-      <method name="handleKeypress">
-        <parameter name="aEvent"/>
-        <body>
-        <![CDATA[
-          if (this.isDisabled() || this.isReadonly()) {
-            return;
-          }
+    if (targetField.classList.contains("numeric") && key.match(/[0-9]/)) {
+      let buffer = targetField.getAttribute("typeBuffer") || "";
 
-          let targetField = aEvent.originalTarget;
-          let key = aEvent.key;
+      buffer = buffer.concat(key);
+      this.setFieldValue(targetField, buffer);
 
-          if (targetField.classList.contains("numeric") && key.match(/[0-9]/)) {
-            let buffer = targetField.getAttribute("typeBuffer") || "";
+      let n = Number(buffer);
+      let max = targetField.getAttribute("max");
+      let maxLength = targetField.getAttribute("maxlength");
+      if (buffer.length >= maxLength || n * 10 > max) {
+        buffer = "";
+        this.advanceToNextField();
+      }
+      targetField.setAttribute("typeBuffer", buffer);
+      this.setInputValueFromFields();
+    }
+  }
 
-            buffer = buffer.concat(key);
-            this.setFieldValue(targetField, buffer);
+  incrementFieldValue(aTargetField, aTimes) {
+    let value = this.getFieldValue(aTargetField);
 
-            let n = Number(buffer);
-            let max = targetField.getAttribute("max");
-            let maxLength = targetField.getAttribute("maxlength");
-            if (buffer.length >= maxLength || n * 10 > max) {
-              buffer = "";
-              this.advanceToNextField();
-            }
-            targetField.setAttribute("typeBuffer", buffer);
-            this.setInputValueFromFields();
-          }
-        ]]>
-        </body>
-      </method>
+    // Use current date if field is empty.
+    if (this.isEmpty(value)) {
+      let now = new Date();
 
-      <method name="incrementFieldValue">
-        <parameter name="aTargetField"/>
-        <parameter name="aTimes"/>
-        <body>
-        <![CDATA[
-          let value = this.getFieldValue(aTargetField);
+      if (aTargetField == this.mYearField) {
+        value = now.getFullYear();
+      } else if (aTargetField == this.mMonthField) {
+        value = now.getMonth() + 1;
+      } else if (aTargetField == this.mDayField) {
+        value = now.getDate();
+      } else {
+        this.log("Field not supported in incrementFieldValue.");
+        return;
+      }
+    }
 
-          // Use current date if field is empty.
-          if (this.isEmpty(value)) {
-            let now = new Date();
+    let min = Number(aTargetField.getAttribute("min"));
+    let max = Number(aTargetField.getAttribute("max"));
 
-            if (aTargetField == this.mYearField) {
-              value = now.getFullYear();
-            } else if (aTargetField == this.mMonthField) {
-              value = now.getMonth() + 1;
-            } else if (aTargetField == this.mDayField) {
-              value = now.getDate();
-            } else {
-              this.log("Field not supported in incrementFieldValue.");
-              return;
-            }
-          }
+    value += Number(aTimes);
+    if (value > max) {
+      value -= (max - min + 1);
+    } else if (value < min) {
+      value += (max - min + 1);
+    }
 
-          let min = Number(aTargetField.getAttribute("min"));
-          let max = Number(aTargetField.getAttribute("max"));
+    this.setFieldValue(aTargetField, value);
+  }
 
-          value += Number(aTimes);
-          if (value > max) {
-            value -= (max - min + 1);
-          } else if (value < min) {
-            value += (max - min + 1);
-          }
+  handleKeyboardNav(aEvent) {
+    if (this.isDisabled() || this.isReadonly()) {
+      return;
+    }
 
-          this.setFieldValue(aTargetField, value);
-        ]]>
-        </body>
-      </method>
+    let targetField = aEvent.originalTarget;
+    let key = aEvent.key;
 
-      <method name="handleKeyboardNav">
-        <parameter name="aEvent"/>
-        <body>
-        <![CDATA[
-          if (this.isDisabled() || this.isReadonly()) {
-            return;
-          }
+    // Home/End key does nothing on year field.
+    if (targetField == this.mYearField && (key == "Home" ||
+                                           key == "End")) {
+      return;
+    }
 
-          let targetField = aEvent.originalTarget;
-          let key = aEvent.key;
-
-          // Home/End key does nothing on year field.
-          if (targetField == this.mYearField && (key == "Home" ||
-                                                 key == "End")) {
-            return;
-          }
+    switch (key) {
+      case "ArrowUp":
+        this.incrementFieldValue(targetField, 1);
+        break;
+      case "ArrowDown":
+        this.incrementFieldValue(targetField, -1);
+        break;
+      case "PageUp": {
+        let interval = targetField.getAttribute("pginterval");
+        this.incrementFieldValue(targetField, interval);
+        break;
+      }
+      case "PageDown": {
+        let interval = targetField.getAttribute("pginterval");
+        this.incrementFieldValue(targetField, 0 - interval);
+        break;
+      }
+      case "Home":
+        let min = targetField.getAttribute("min");
+        this.setFieldValue(targetField, min);
+        break;
+      case "End":
+        let max = targetField.getAttribute("max");
+        this.setFieldValue(targetField, max);
+        break;
+    }
+    this.setInputValueFromFields();
+  }
 
-          switch (key) {
-            case "ArrowUp":
-              this.incrementFieldValue(targetField, 1);
-              break;
-            case "ArrowDown":
-              this.incrementFieldValue(targetField, -1);
-              break;
-            case "PageUp": {
-              let interval = targetField.getAttribute("pginterval");
-              this.incrementFieldValue(targetField, interval);
-              break;
-            }
-            case "PageDown": {
-              let interval = targetField.getAttribute("pginterval");
-              this.incrementFieldValue(targetField, 0 - interval);
-              break;
-            }
-            case "Home":
-              let min = targetField.getAttribute("min");
-              this.setFieldValue(targetField, min);
-              break;
-            case "End":
-              let max = targetField.getAttribute("max");
-              this.setFieldValue(targetField, max);
-              break;
-          }
-          this.setInputValueFromFields();
-        ]]>
-        </body>
-      </method>
+  getCurrentValue() {
+    let year = this.getFieldValue(this.mYearField);
+    let month = this.getFieldValue(this.mMonthField);
+    let day = this.getFieldValue(this.mDayField);
+
+    let date = { year, month, day };
 
-      <method name="getCurrentValue">
-        <body>
-        <![CDATA[
-          let year = this.getFieldValue(this.mYearField);
-          let month = this.getFieldValue(this.mMonthField);
-          let day = this.getFieldValue(this.mDayField);
+    this.log("getCurrentValue: " + JSON.stringify(date));
+    return date;
+  }
 
-          let date = { year, month, day };
-
-          this.log("getCurrentValue: " + JSON.stringify(date));
-          return date;
-        ]]>
-        </body>
-      </method>
+  setFieldValue(aField, aValue) {
+    if (!aField || !aField.classList.contains("numeric")) {
+      return;
+    }
 
-      <method name="setFieldValue">
-       <parameter name="aField"/>
-       <parameter name="aValue"/>
-        <body>
-        <![CDATA[
-          if (!aField || !aField.classList.contains("numeric")) {
-            return;
-          }
+    let value = Number(aValue);
+    if (isNaN(value)) {
+      this.log("NaN on setFieldValue!");
+      return;
+    }
 
-          let value = Number(aValue);
-          if (isNaN(value)) {
-            this.log("NaN on setFieldValue!");
-            return;
-          }
-
-          let maxLength = aField.getAttribute("maxlength");
-          if (aValue.length == maxLength) {
-            let min = Number(aField.getAttribute("min"));
-            let max = Number(aField.getAttribute("max"));
+    let maxLength = aField.getAttribute("maxlength");
+    if (aValue.length == maxLength) {
+      let min = Number(aField.getAttribute("min"));
+      let max = Number(aField.getAttribute("max"));
 
-            if (value < min) {
-              value = min;
-            } else if (value > max) {
-              value = max;
-            }
-          }
+      if (value < min) {
+        value = min;
+      } else if (value > max) {
+        value = max;
+      }
+    }
 
-          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,
-          });
+    aField.setAttribute("value", value);
 
-          aField.textContent = formatted;
-          aField.setAttribute("aria-valuetext", formatted);
-          this.updateResetButtonVisibility();
-        ]]>
-        </body>
-      </method>
+    // Display formatted value based on locale.
+    let minDigits = aField.getAttribute("mindigits");
+    let formatted = value.toLocaleString(this.mLocales, {
+      minimumIntegerDigits: minDigits,
+      useGrouping: false,
+    });
 
-      <method name="isAnyFieldAvailable">
-        <parameter name="aForPicker"/>
-        <body>
-        <![CDATA[
-          let { year, month, day } = this.getCurrentValue();
+    aField.textContent = formatted;
+    aField.setAttribute("aria-valuetext", formatted);
+    this.updateResetButtonVisibility();
+  }
 
-          return !this.isEmpty(year) || !this.isEmpty(month) ||
-                 !this.isEmpty(day);
-        ]]>
-        </body>
-      </method>
+  isAnyFieldAvailable(aForPicker) {
+    let { year, month, day } = this.getCurrentValue();
+
+    return !this.isEmpty(year) || !this.isEmpty(month) ||
+           !this.isEmpty(day);
+  }
+
+  isAnyFieldEmpty() {
+    let { year, month, day } = this.getCurrentValue();
 
-      <method name="isAnyFieldEmpty">
-        <body>
-        <![CDATA[
-          let { year, month, day } = this.getCurrentValue();
+    return (this.isEmpty(year) || this.isEmpty(month) ||
+            this.isEmpty(day));
+  }
+};
 
-          return (this.isEmpty(year) || this.isEmpty(month) ||
-                  this.isEmpty(day));
-        ]]>
-        </body>
-      </method>
+this.TimeInputImplWidget = class extends DateTimeInputBaseImplWidget {
+  constructor(shadowRoot) {
+    super(shadowRoot);
 
-    </implementation>
-  </binding>
+    const kDefaultAMString = "AM";
+    const kDefaultPMString = "PM";
 
-  <binding id="time-input"
-           simpleScopeChain="true"
-           extends="chrome://global/content/bindings/datetimebox.xml#datetime-input-base">
-    <resources>
-      <stylesheet src="chrome://global/content/bindings/datetimebox.css"/>
-    </resources>
+    let { amString, pmString } =
+      this.getStringsForLocale(this.mLocales);
+
+    this.mAMIndicator = amString || kDefaultAMString;
+    this.mPMIndicator = pmString || kDefaultPMString;
 
-    <implementation>
-      <property name="kMsPerSecond" readonly="true" onget="return 1000;" />
-      <property name="kMsPerMinute" readonly="true" onget="return (60 * 1000);" />
-
-      <constructor>
-      <![CDATA[
-        const kDefaultAMString = "AM";
-        const kDefaultPMString = "PM";
-
-        let { amString, pmString } =
-          this.getStringsForLocale(this.mLocales);
+    this.mHour12 = this.is12HourTime(this.mLocales);
+    this.mMillisecSeparatorText = ".";
+    this.mMaxLength = 2;
+    this.mMillisecMaxLength = 3;
+    this.mDefaultStep = 60 * 1000; // in milliseconds
 
-        this.mAMIndicator = amString || kDefaultAMString;
-        this.mPMIndicator = pmString || kDefaultPMString;
+    this.mMinHour = this.mHour12 ? 1 : 0;
+    this.mMaxHour = this.mHour12 ? 12 : 23;
+    this.mMinMinute = 0;
+    this.mMaxMinute = 59;
+    this.mMinSecond = 0;
+    this.mMaxSecond = 59;
+    this.mMinMillisecond = 0;
+    this.mMaxMillisecond = 999;
 
-        /* eslint-disable no-multi-spaces */
-        this.mHourPlaceHolder = ]]>"&time.hour.placeholder;"<![CDATA[;
-        this.mMinutePlaceHolder = ]]>"&time.minute.placeholder;"<![CDATA[;
-        this.mSecondPlaceHolder = ]]>"&time.second.placeholder;"<![CDATA[;
-        this.mMillisecPlaceHolder = ]]>"&time.millisecond.placeholder;"<![CDATA[;
-        this.mDayPeriodPlaceHolder = ]]>"&time.dayperiod.placeholder;"<![CDATA[;
+    this.mHourPageUpDownInterval = 3;
+    this.mMinSecPageUpDownInterval = 10;
 
-        this.mHourLabel = ]]>"&time.hour.label;"<![CDATA[;
-        this.mMinuteLabel = ]]>"&time.minute.label;"<![CDATA[;
-        this.mSecondLabel = ]]>"&time.second.label;"<![CDATA[;
-        this.mMillisecLabel = ]]>"&time.millisecond.label;"<![CDATA[;
-        this.mDayPeriodLabel = ]]>"&time.dayperiod.label;"<![CDATA[;
-        /* eslint-enable no-multi-spaces */
+    this.buildEditFields();
 
-        this.mHour12 = this.is12HourTime(this.mLocales);
-        this.mMillisecSeparatorText = ".";
-        this.mMaxLength = 2;
-        this.mMillisecMaxLength = 3;
-        this.mDefaultStep = 60 * 1000; // in milliseconds
+    if (this.mInputElement.value) {
+      this.setFieldsFromInputValue();
+    }
+  }
 
-        this.mMinHour = this.mHour12 ? 1 : 0;
-        this.mMaxHour = this.mHour12 ? 12 : 23;
-        this.mMinMinute = 0;
-        this.mMaxMinute = 59;
-        this.mMinSecond = 0;
-        this.mMaxSecond = 59;
-        this.mMinMillisecond = 0;
-        this.mMaxMillisecond = 999;
+  destructor() {
+    this.removeEventListenersToField(this.mHourField);
+    this.removeEventListenersToField(this.mMinuteField);
+    this.removeEventListenersToField(this.mSecondField);
+    this.removeEventListenersToField(this.mMillisecField);
+    this.removeEventListenersToField(this.mDayPeriodField);
+    super.destructor();
+  }
 
-        this.mHourPageUpDownInterval = 3;
-        this.mMinSecPageUpDownInterval = 10;
+  get kMsPerSecond() {
+    return 1000;
+  }
 
-        this.buildEditFields();
+  get kMsPerMinute() {
+    return (60 * 1000);
+  }
 
-        if (this.mInputElement.value) {
-          this.setFieldsFromInputValue();
-        }
-        ]]>
-      </constructor>
+  getInputElementValues() {
+    let value = this.mInputElement.value;
+    if (value.length === 0) {
+      return {};
+    }
 
-      <method name="getInputElementValues">
-        <body>
-        <![CDATA[
-          let value = this.mInputElement.value;
-          if (value.length === 0) {
-            return {};
-          }
+    let hour, minute, second, millisecond;
+    [hour, minute, second] = value.split(":");
+    if (second) {
+      [second, millisecond] = second.split(".");
 
-          let hour, minute, second, millisecond;
-          [hour, minute, second] = value.split(":");
-          if (second) {
-            [second, millisecond] = second.split(".");
+      // Convert fraction of second to milliseconds.
+      if (millisecond && millisecond.length === 1) {
+        millisecond *= 100;
+      } else if (millisecond && millisecond.length === 2) {
+        millisecond *= 10;
+      }
+    }
 
-            // Convert fraction of second to milliseconds.
-            if (millisecond && millisecond.length === 1) {
-              millisecond *= 100;
-            } else if (millisecond && millisecond.length === 2) {
-              millisecond *= 10;
-            }
-          }
+    return { hour, minute, second, millisecond };
+  }
 
-          return { hour, minute, second, millisecond };
-        ]]>
-        </body>
-      </method>
+  hasSecondField() {
+    return !!this.mSecondField;
+  }
 
-      <method name="hasSecondField">
-        <body>
-        <![CDATA[
-          return !!this.mSecondField;
-        ]]>
-        </body>
-      </method>
+  hasMillisecField() {
+    return !!this.mMillisecField;
+  }
 
-      <method name="hasMillisecField">
-        <body>
-        <![CDATA[
-          return !!this.mMillisecField;
-        ]]>
-        </body>
-      </method>
+  hasDayPeriodField() {
+    return !!this.mDayPeriodField;
+  }
+
+  shouldShowSecondField() {
+    let { second } = this.getInputElementValues();
+    if (second != undefined) {
+      return true;
+    }
 
-      <method name="hasDayPeriodField">
-        <body>
-        <![CDATA[
-          return !!this.mDayPeriodField;
-        ]]>
-        </body>
-      </method>
+    let stepBase = this.mInputElement.getStepBase();
+    if ((stepBase % this.kMsPerMinute) != 0) {
+      return true;
+    }
 
-      <method name="shouldShowSecondField">
-        <body>
-        <![CDATA[
-          let { second } = this.getInputElementValues();
-          if (second != undefined) {
-            return true;
-          }
+    let step = this.mInputElement.getStep();
+    if ((step % this.kMsPerMinute) != 0) {
+      return true;
+    }
 
-          let stepBase = this.mInputElement.getStepBase();
-          if ((stepBase % this.kMsPerMinute) != 0) {
-            return true;
-          }
+    return false;
+  }
+
+  shouldShowMillisecField() {
+    let { millisecond } = this.getInputElementValues();
+    if (millisecond != undefined) {
+      return true;
+    }
 
-          let step = this.mInputElement.getStep();
-          if ((step % this.kMsPerMinute) != 0) {
-            return true;
-          }
+    let stepBase = this.mInputElement.getStepBase();
+    if ((stepBase % this.kMsPerSecond) != 0) {
+      return true;
+    }
 
-          return false;
-        ]]>
-        </body>
-      </method>
+    let step = this.mInputElement.getStep();
+    if ((step % this.kMsPerSecond) != 0) {
+      return true;
+    }
 
-      <method name="shouldShowMillisecField">
-        <body>
-        <![CDATA[
-          let { millisecond } = this.getInputElementValues();
-          if (millisecond != undefined) {
-            return true;
-          }
+    return false;
+  }
 
-          let stepBase = this.mInputElement.getStepBase();
-          if ((stepBase % this.kMsPerSecond) != 0) {
-            return true;
-          }
+  rebuildEditFieldsIfNeeded() {
+    if ((this.shouldShowSecondField() == this.hasSecondField()) &&
+        (this.shouldShowMillisecField() == this.hasMillisecField())) {
+      return;
+    }
 
-          let step = this.mInputElement.getStep();
-          if ((step % this.kMsPerSecond) != 0) {
-            return true;
-          }
-
-          return false;
-        ]]>
-        </body>
-      </method>
+    let root =
+      this.shadowRoot.getElementById("edit-wrapper");
+    while (root.firstChild) {
+      root.firstChild.remove();
+    }
 
-      <method name="rebuildEditFieldsIfNeeded">
-        <body>
-        <![CDATA[
-          if ((this.shouldShowSecondField() == this.hasSecondField()) &&
-              (this.shouldShowMillisecField() == this.hasMillisecField())) {
-            return;
-          }
+    this.removeEventListenersToField(this.mHourField);
+    this.removeEventListenersToField(this.mMinuteField);
+    this.removeEventListenersToField(this.mSecondField);
+    this.removeEventListenersToField(this.mMillisecField);
+    this.removeEventListenersToField(this.mDayPeriodField);
 
-          let root =
-            document.getAnonymousElementByAttribute(this, "anonid", "edit-wrapper");
-          while (root.firstChild) {
-            root.firstChild.remove();
-          }
+    this.mHourField = null;
+    this.mMinuteField = null;
+    this.mSecondField = null;
+    this.mMillisecField = null;
+    this.mDayPeriodField = null;
 
-          this.mHourField = null;
-          this.mMinuteField = null;
-          this.mSecondField = null;
-          this.mMillisecField = null;
+    this.buildEditFields();
+  }
 
-          this.buildEditFields();
-        ]]>
-        </body>
-      </method>
+  buildEditFields() {
+    let root = this.shadowRoot.getElementById("edit-wrapper");
+
+    let options = {
+      hour: "numeric",
+      minute: "numeric",
+      hour12: this.mHour12,
+    };
 
-      <method name="buildEditFields">
-        <body>
-        <![CDATA[
-          const HTML_NS = "http://www.w3.org/1999/xhtml";
-          let root =
-            document.getAnonymousElementByAttribute(this, "anonid", "edit-wrapper");
+    if (this.shouldShowSecondField()) {
+      options.second = "numeric";
+    }
 
-          let options = {
-            hour: "numeric",
-            minute: "numeric",
-            hour12: this.mHour12,
-          };
-
-          this.mHourField = this.createEditField(this.mHourPlaceHolder,
+    let formatter = Intl.DateTimeFormat(this.mLocales, options);
+    formatter.formatToParts(Date.now()).map(part => {
+      switch (part.type) {
+        case "hour":
+          this.mHourField = this.createEditFieldAndAppend(this.mHourPlaceHolder,
             this.mHourLabel, true, this.mMaxLength, this.mMaxLength,
             this.mMinHour, this.mMaxHour, this.mHourPageUpDownInterval);
-          this.mMinuteField = this.createEditField(this.mMinutePlaceHolder,
+          this.addEventListenersToField(this.mHourField);
+          break;
+        case "minute":
+          this.mMinuteField = this.createEditFieldAndAppend(this.mMinutePlaceHolder,
             this.mMinuteLabel, true, this.mMaxLength, this.mMaxLength,
             this.mMinMinute, this.mMaxMinute, this.mMinSecPageUpDownInterval);
-
-          if (this.mHour12) {
-            this.mDayPeriodField = this.createEditField(
+          this.addEventListenersToField(this.mMinuteField);
+          break;
+        case "second":
+          this.mSecondField = this.createEditFieldAndAppend(this.mSecondPlaceHolder,
+            this.mSecondLabel, true, this.mMaxLength, this.mMaxLength,
+            this.mMinSecond, this.mMaxSecond, this.mMinSecPageUpDownInterval);
+          this.addEventListenersToField(this.mSecondField);
+          if (this.shouldShowMillisecField()) {
+            // Intl.DateTimeFormat does not support millisecond, so we
+            // need to handle this on our own.
+            let span = this.shadowRoot.createElementAndAppendChildAt(root, "span");
+            span.textContent = this.mMillisecSeparatorText;
+            this.mMillisecField = this.createEditFieldAndAppend(
+              this.mMillisecPlaceHolder, this.mMillisecLabel, true,
+              this.mMillisecMaxLength, this.mMillisecMaxLength,
+              this.mMinMillisecond, this.mMaxMillisecond,
+              this.mMinSecPageUpDownInterval);
+            this.addEventListenersToField(this.mMillisecField);
+          }
+          break;
+        case "dayPeriod":
+            this.mDayPeriodField = this.createEditFieldAndAppend(
               this.mDayPeriodPlaceHolder, this.mDayPeriodLabel, false);
+            this.addEventListenersToField(this.mDayPeriodField);
 
             // Give aria autocomplete hint for am/pm
             this.mDayPeriodField.setAttribute("aria-autocomplete", "inline");
-          }
-
-          if (this.shouldShowSecondField()) {
-            options.second = "numeric";
-            this.mSecondField = this.createEditField(this.mSecondPlaceHolder,
-              this.mSecondLabel, true, this.mMaxLength, this.mMaxLength,
-              this.mMinSecond, this.mMaxSecond, this.mMinSecPageUpDownInterval);
-
-            if (this.shouldShowMillisecField()) {
-              this.mMillisecField = this.createEditField(
-                this.mMillisecPlaceHolder, this.mMillisecLabel, true,
-                this.mMillisecMaxLength, this.mMillisecMaxLength,
-                this.mMinMillisecond, this.mMaxMillisecond,
-                this.mMinSecPageUpDownInterval);
-            }
-          }
+          break;
+        default:
+          let span =
+            this.shadowRoot.createElementAndAppendChildAt(root, "span");
+          span.textContent = part.value;
+          break;
+      }
+    });
+  }
 
-          let fragment = document.createDocumentFragment();
-          let formatter = Intl.DateTimeFormat(this.mLocales, options);
-          formatter.formatToParts(Date.now()).map(part => {
-            switch (part.type) {
-              case "hour":
-                fragment.appendChild(this.mHourField);
-                break;
-              case "minute":
-                fragment.appendChild(this.mMinuteField);
-                break;
-              case "second":
-                fragment.appendChild(this.mSecondField);
-                if (this.shouldShowMillisecField()) {
-                  // Intl.DateTimeFormat does not support millisecond, so we
-                  // need to handle this on our own.
-                  let span = document.createElementNS(HTML_NS, "span");
-                  span.textContent = this.mMillisecSeparatorText;
-                  fragment.appendChild(span);
-                  fragment.appendChild(this.mMillisecField);
-                }
-                break;
-              case "dayPeriod":
-                fragment.appendChild(this.mDayPeriodField);
-                break;
-              default:
-                let span = document.createElementNS(HTML_NS, "span");
-                span.textContent = part.value;
-                fragment.appendChild(span);
-                break;
-            }
-          });
+  getStringsForLocale(aLocales) {
+    this.log("getStringsForLocale: " + aLocales);
 
-          root.appendChild(fragment);
-        ]]>
-        </body>
-      </method>
+    let intlUtils = this.window.intlUtils;
+    if (!intlUtils) {
+      return {};
+    }
 
-      <method name="getStringsForLocale">
-        <parameter name="aLocales"/>
-        <body>
-        <![CDATA[
-          this.log("getStringsForLocale: " + aLocales);
-
-          let intlUtils = window.intlUtils;
-          if (!intlUtils) {
-            return {};
-          }
+    let amString, pmString;
+    let keys = [ "dates/gregorian/dayperiods/am",
+                 "dates/gregorian/dayperiods/pm" ];
 
-          let amString, pmString;
-          let keys = [ "dates/gregorian/dayperiods/am",
-                       "dates/gregorian/dayperiods/pm" ];
-
-          let result = intlUtils.getDisplayNames(this.mLocales, {
-            style: "short",
-            keys,
-          });
-
-          [ amString, pmString ] = keys.map(key => result.values[key]);
+    let result = intlUtils.getDisplayNames(this.mLocales, {
+      style: "short",
+      keys,
+    });
 
-          return { amString, pmString };
-        ]]>
-        </body>
-      </method>
+    [ amString, pmString ] = keys.map(key => result.values[key]);
 
-      <method name="is12HourTime">
-        <parameter name="aLocales"/>
-          <body>
-          <![CDATA[
-            let options = (new Intl.DateTimeFormat(aLocales, {
-              hour: "numeric",
-            })).resolvedOptions();
-
-            return options.hour12;
-          ]]>
-          </body>
-      </method>
+    return { amString, pmString };
+  }
 
-      <method name="setFieldsFromInputValue">
-        <body>
-        <![CDATA[
-          let { hour, minute, second, millisecond } =
-            this.getInputElementValues();
-
-          if (this.isEmpty(hour) && this.isEmpty(minute)) {
-            this.clearInputFields(true);
-            return;
-          }
-
-          // Second and millisecond part are optional, rebuild edit fields if
-          // needed.
-          this.rebuildEditFieldsIfNeeded();
+  is12HourTime(aLocales) {
+    let options = (new Intl.DateTimeFormat(aLocales, {
+      hour: "numeric",
+    })).resolvedOptions();
 
-          this.setFieldValue(this.mHourField, hour);
-          this.setFieldValue(this.mMinuteField, minute);
-          if (this.mHour12) {
-            this.setDayPeriodValue(hour >= this.mMaxHour ? this.mPMIndicator
-                                                         : this.mAMIndicator);
-          }
+    return options.hour12;
+  }
 
-          if (this.hasSecondField()) {
-            this.setFieldValue(this.mSecondField,
-              (second != undefined) ? second : 0);
-          }
-
-          if (this.hasMillisecField()) {
-            this.setFieldValue(this.mMillisecField,
-              (millisecond != undefined) ? millisecond : 0);
-          }
-
-          this.notifyPicker();
-        ]]>
-        </body>
-      </method>
+  setFieldsFromInputValue() {
+    let { hour, minute, second, millisecond } =
+      this.getInputElementValues();
 
-      <method name="setInputValueFromFields">
-        <body>
-        <![CDATA[
-          if (this.isAnyFieldEmpty()) {
-            // Clear input element's value if any of the field has been cleared,
-            // otherwise update the validity state, since it may become "not"
-            // invalid if fields are not complete.
-            if (this.mInputElement.value) {
-              this.mInputElement.setUserInput("");
-            } else {
-              this.mInputElement.updateValidityState();
-            }
-            // We still need to notify picker in case any of the field has
-            // changed.
-            this.notifyPicker();
-            return;
-          }
+    if (this.isEmpty(hour) && this.isEmpty(minute)) {
+      this.clearInputFields(true);
+      return;
+    }
 
-          let { hour, minute, second, millisecond } = this.getCurrentValue();
-          let dayPeriod = this.getDayPeriodValue();
-
-          // Convert to a valid time string according to:
-          // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-time-string
-          if (this.mHour12) {
-            if (dayPeriod == this.mPMIndicator && hour < this.mMaxHour) {
-              hour += this.mMaxHour;
-            } else if (dayPeriod == this.mAMIndicator &&
-                       hour == this.mMaxHour) {
-              hour = 0;
-            }
-          }
-
-          hour = (hour < 10) ? ("0" + hour) : hour;
-          minute = (minute < 10) ? ("0" + minute) : minute;
+    // Second and millisecond part are optional, rebuild edit fields if
+    // needed.
+    this.rebuildEditFieldsIfNeeded();
 
-          let time = hour + ":" + minute;
-          if (second != undefined) {
-            second = (second < 10) ? ("0" + second) : second;
-            time += ":" + second;
-          }
-
-          if (millisecond != undefined) {
-            // Convert milliseconds to fraction of second.
-            millisecond = millisecond.toString().padStart(
-              this.mMillisecMaxLength, "0");
-            time += "." + millisecond;
-          }
-
-          if (time == this.mInputElement.value) {
-            return;
-          }
+    this.setFieldValue(this.mHourField, hour);
+    this.setFieldValue(this.mMinuteField, minute);
+    if (this.mHour12) {
+      this.setDayPeriodValue(hour >= this.mMaxHour ? this.mPMIndicator
+                                                   : this.mAMIndicator);
+    }
 
-          this.log("setInputValueFromFields: " + time);
-          this.notifyPicker();
-          this.mInputElement.setUserInput(time);
-        ]]>
-        </body>
-      </method>
-
-      <method name="setFieldsFromPicker">
-        <parameter name="aValue"/>
-        <body>
-        <![CDATA[
-          let hour = aValue.hour;
-          let minute = aValue.minute;
-          this.log("setFieldsFromPicker: " + hour + ":" + minute);
-
-          if (!this.isEmpty(hour)) {
-            this.setFieldValue(this.mHourField, hour);
-            if (this.mHour12) {
-              this.setDayPeriodValue(hour >= this.mMaxHour ? this.mPMIndicator
-                                                           : this.mAMIndicator);
-            }
-          }
+    if (this.hasSecondField()) {
+      this.setFieldValue(this.mSecondField,
+        (second != undefined) ? second : 0);
+    }
 
-          if (!this.isEmpty(minute)) {
-            this.setFieldValue(this.mMinuteField, minute);
-          }
-
-          // Update input element's .value if needed.
-          this.setInputValueFromFields();
-        ]]>
-        </body>
-       </method>
-
-      <method name="clearInputFields">
-        <parameter name="aFromInputElement"/>
-        <body>
-        <![CDATA[
-          this.log("clearInputFields");
+    if (this.hasMillisecField()) {
+      this.setFieldValue(this.mMillisecField,
+        (millisecond != undefined) ? millisecond : 0);
+    }
 
-          if (this.isDisabled() || this.isReadonly()) {
-            return;
-          }
-
-          if (this.mHourField && !this.mHourField.disabled &&
-              !this.mHourField.readOnly) {
-            this.clearFieldValue(this.mHourField);
-          }
-
-          if (this.mMinuteField && !this.mMinuteField.disabled &&
-              !this.mMinuteField.readOnly) {
-            this.clearFieldValue(this.mMinuteField);
-          }
-
-          if (this.hasSecondField() && !this.mSecondField.disabled &&
-              !this.mSecondField.readOnly) {
-            this.clearFieldValue(this.mSecondField);
-          }
+    this.notifyPicker();
+  }
 
-          if (this.hasMillisecField() && !this.mMillisecField.disabled &&
-              !this.mMillisecField.readOnly) {
-            this.clearFieldValue(this.mMillisecField);
-          }
-
-          if (this.hasDayPeriodField() && !this.mDayPeriodField.disabled &&
-              !this.mDayPeriodField.readOnly) {
-            this.clearFieldValue(this.mDayPeriodField);
-          }
-
-          if (!aFromInputElement) {
-            if (this.mInputElement.value) {
-              this.mInputElement.setUserInput("");
-            } else {
-              this.mInputElement.updateValidityState();
-            }
-          }
-        ]]>
-        </body>
-      </method>
+  setInputValueFromFields() {
+    if (this.isAnyFieldEmpty()) {
+      // Clear input element's value if any of the field has been cleared,
+      // otherwise update the validity state, since it may become "not"
+      // invalid if fields are not complete.
+      if (this.mInputElement.value) {
+        this.mInputElement.setUserInput("");
+      } else {
+        this.mInputElement.updateValidityState();
+      }
+      // We still need to notify picker in case any of the field has
+      // changed.
+      this.notifyPicker();
+      return;
+    }
 
-      <method name="notifyMinMaxStepAttrChanged">
-        <body>
-        <![CDATA[
-          // Second and millisecond part are optional, rebuild edit fields if
-          // needed.
-          this.rebuildEditFieldsIfNeeded();
-          // Fill in values again.
-          this.setFieldsFromInputValue();
-        ]]>
-        </body>
-      </method>
-
-      <method name="incrementFieldValue">
-        <parameter name="aTargetField"/>
-        <parameter name="aTimes"/>
-        <body>
-        <![CDATA[
-          let value = this.getFieldValue(aTargetField);
-
-          // Use current time if field is empty.
-          if (this.isEmpty(value)) {
-            let now = new Date();
+    let { hour, minute, second, millisecond } = this.getCurrentValue();
+    let dayPeriod = this.getDayPeriodValue();
 
-            if (aTargetField == this.mHourField) {
-              value = now.getHours();
-              if (this.mHour12) {
-                value = (value % this.mMaxHour) || this.mMaxHour;
-              }
-            } else if (aTargetField == this.mMinuteField) {
-              value = now.getMinutes();
-            } else if (aTargetField == this.mSecondField) {
-              value = now.getSeconds();
-            } else if (aTargetField == this.mMillisecField) {
-              value = now.getMilliseconds();
-            } else {
-              this.log("Field not supported in incrementFieldValue.");
-              return;
-            }
-          }
-
-          let min = aTargetField.getAttribute("min");
-          let max = aTargetField.getAttribute("max");
-
-          value += Number(aTimes);
-          if (value > max) {
-            value -= (max - min + 1);
-          } else if (value < min) {
-            value += (max - min + 1);
-          }
+    // Convert to a valid time string according to:
+    // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-time-string
+    if (this.mHour12) {
+      if (dayPeriod == this.mPMIndicator && hour < this.mMaxHour) {
+        hour += this.mMaxHour;
+      } else if (dayPeriod == this.mAMIndicator &&
+                 hour == this.mMaxHour) {
+        hour = 0;
+      }
+    }
 
-          this.setFieldValue(aTargetField, value);
-        ]]>
-        </body>
-      </method>
-
-      <method name="handleKeyboardNav">
-        <parameter name="aEvent"/>
-        <body>
-        <![CDATA[
-          if (this.isDisabled() || this.isReadonly()) {
-            return;
-          }
+    hour = (hour < 10) ? ("0" + hour) : hour;
+    minute = (minute < 10) ? ("0" + minute) : minute;
 
-          let targetField = aEvent.originalTarget;
-          let key = aEvent.key;
-
-          if (this.hasDayPeriodField() &&
-              targetField == this.mDayPeriodField) {
-            // Home/End key does nothing on AM/PM field.
-            if (key == "Home" || key == "End") {
-              return;
-            }
-
-            this.setDayPeriodValue(
-              this.getDayPeriodValue() == this.mAMIndicator ? this.mPMIndicator
-                                                            : this.mAMIndicator);
-            this.setInputValueFromFields();
-            return;
-          }
+    let time = hour + ":" + minute;
+    if (second != undefined) {
+      second = (second < 10) ? ("0" + second) : second;
+      time += ":" + second;
+    }
 
-          switch (key) {
-            case "ArrowUp":
-              this.incrementFieldValue(targetField, 1);
-              break;
-            case "ArrowDown":
-              this.incrementFieldValue(targetField, -1);
-              break;
-            case "PageUp": {
-              let interval = targetField.getAttribute("pginterval");
-              this.incrementFieldValue(targetField, interval);
-              break;
-            }
-            case "PageDown": {
-              let interval = targetField.getAttribute("pginterval");
-              this.incrementFieldValue(targetField, 0 - interval);
-              break;
-            }
-            case "Home":
-              let min = targetField.getAttribute("min");
-              this.setFieldValue(targetField, min);
-              break;
-            case "End":
-              let max = targetField.getAttribute("max");
-              this.setFieldValue(targetField, max);
-              break;
-          }
-          this.setInputValueFromFields();
-        ]]>
-        </body>
-      </method>
+    if (millisecond != undefined) {
+      // Convert milliseconds to fraction of second.
+      millisecond = millisecond.toString().padStart(
+        this.mMillisecMaxLength, "0");
+      time += "." + millisecond;
+    }
 
-      <method name="handleKeypress">
-        <parameter name="aEvent"/>
-        <body>
-        <![CDATA[
-          if (this.isDisabled() || this.isReadonly()) {
-            return;
-          }
+    if (time == this.mInputElement.value) {
+      return;
+    }
+
+    this.log("setInputValueFromFields: " + time);
+    this.notifyPicker();
+    this.mInputElement.setUserInput(time);
+  }
 
-          let targetField = aEvent.originalTarget;
-          let key = aEvent.key;
-
-          if (this.hasDayPeriodField() &&
-              targetField == this.mDayPeriodField) {
-            if (key == "a" || key == "A") {
-              this.setDayPeriodValue(this.mAMIndicator);
-            } else if (key == "p" || key == "P") {
-              this.setDayPeriodValue(this.mPMIndicator);
-            }
-            this.setInputValueFromFields();
-            return;
-          }
-
-          if (targetField.classList.contains("numeric") && key.match(/[0-9]/)) {
-            let buffer = targetField.getAttribute("typeBuffer") || "";
-
-            buffer = buffer.concat(key);
-            this.setFieldValue(targetField, buffer);
+  setFieldsFromPicker(aValue) {
+    let hour = aValue.hour;
+    let minute = aValue.minute;
+    this.log("setFieldsFromPicker: " + hour + ":" + minute);
 
-            let n = Number(buffer);
-            let max = targetField.getAttribute("max");
-            let maxLength = targetField.getAttribute("maxLength");
-            if (buffer.length >= maxLength || n * 10 > max) {
-              buffer = "";
-              this.advanceToNextField();
-            }
-            targetField.setAttribute("typeBuffer", buffer);
-            this.setInputValueFromFields();
-          }
-        ]]>
-        </body>
-      </method>
+    if (!this.isEmpty(hour)) {
+      this.setFieldValue(this.mHourField, hour);
+      if (this.mHour12) {
+        this.setDayPeriodValue(hour >= this.mMaxHour ? this.mPMIndicator
+                                                     : this.mAMIndicator);
+      }
+    }
 
-      <method name="setFieldValue">
-       <parameter name="aField"/>
-       <parameter name="aValue"/>
-        <body>
-        <![CDATA[
-          if (!aField || !aField.classList.contains("numeric")) {
-            return;
-          }
+    if (!this.isEmpty(minute)) {
+      this.setFieldValue(this.mMinuteField, minute);
+    }
 
-          let value = Number(aValue);
-          if (isNaN(value)) {
-            this.log("NaN on setFieldValue!");
-            return;
-          }
+    // Update input element's .value if needed.
+    this.setInputValueFromFields();
+  }
 
-          if (aField == this.mHourField) {
-            if (this.mHour12) {
-              // Try to change to 12hr format if user input is 0 or greater
-              // than 12.
-              let maxLength = aField.getAttribute("maxlength");
-              if (value == 0 && aValue.length == maxLength) {
-                value = this.mMaxHour;
-              } else {
-                value = (value > this.mMaxHour) ? value % this.mMaxHour : value;
-              }
-            } else if (value > this.mMaxHour) {
-              value = this.mMaxHour;
-            }
-          }
+  clearInputFields(aFromInputElement) {
+    this.log("clearInputFields");
 
-          aField.setAttribute("value", value);
-
-          let minDigits = aField.getAttribute("mindigits");
-          let formatted = value.toLocaleString(this.mLocales, {
-            minimumIntegerDigits: minDigits,
-            useGrouping: false,
-          });
-
-          aField.textContent = formatted;
-          aField.setAttribute("aria-valuetext", formatted);
-          this.updateResetButtonVisibility();
-        ]]>
-        </body>
-      </method>
+    if (this.isDisabled() || this.isReadonly()) {
+      return;
+    }
 
-      <method name="getDayPeriodValue">
-        <parameter name="aValue"/>
-        <body>
-        <![CDATA[
-          if (!this.hasDayPeriodField()) {
-            return "";
-          }
-
-          let placeholder = this.mDayPeriodField.placeholder;
-          let value = this.mDayPeriodField.textContent;
-
-          return (value == placeholder ? "" : value);
-        ]]>
-        </body>
-      </method>
+    if (this.mHourField && !this.mHourField.disabled &&
+        !this.mHourField.readOnly) {
+      this.clearFieldValue(this.mHourField);
+    }
 
-      <method name="setDayPeriodValue">
-        <parameter name="aValue"/>
-        <body>
-        <![CDATA[
-          if (!this.hasDayPeriodField()) {
-            return;
-          }
+    if (this.mMinuteField && !this.mMinuteField.disabled &&
+        !this.mMinuteField.readOnly) {
+      this.clearFieldValue(this.mMinuteField);
+    }
 
-          this.mDayPeriodField.textContent = aValue;
-          this.mDayPeriodField.setAttribute("value", aValue);
-          this.updateResetButtonVisibility();
-        ]]>
-        </body>
-      </method>
-
-      <method name="isAnyFieldAvailable">
-        <parameter name="aForPicker"/>
-        <body>
-        <![CDATA[
-          let { hour, minute, second, millisecond } = this.getCurrentValue();
-          let dayPeriod = this.getDayPeriodValue();
+    if (this.hasSecondField() && !this.mSecondField.disabled &&
+        !this.mSecondField.readOnly) {
+      this.clearFieldValue(this.mSecondField);
+    }
 
-          let available = !this.isEmpty(hour) || !this.isEmpty(minute);
-          if (available) {
-            return true;
-          }
-
-          // Picker only cares about hour:minute.
-          if (aForPicker) {
-            return false;
-          }
+    if (this.hasMillisecField() && !this.mMillisecField.disabled &&
+        !this.mMillisecField.readOnly) {
+      this.clearFieldValue(this.mMillisecField);
+    }
 
-          return (this.hasDayPeriodField() && !this.isEmpty(dayPeriod)) ||
-                 (this.hasSecondField() && !this.isEmpty(second)) ||
-                 (this.hasMillisecField() && !this.isEmpty(millisecond));
-        ]]>
-        </body>
-      </method>
-
-      <method name="isAnyFieldEmpty">
-        <body>
-        <![CDATA[
-          let { hour, minute, second, millisecond } = this.getCurrentValue();
-          let dayPeriod = this.getDayPeriodValue();
-
-          return (this.isEmpty(hour) || this.isEmpty(minute) ||
-                  (this.hasDayPeriodField() && this.isEmpty(dayPeriod)) ||
-                  (this.hasSecondField() && this.isEmpty(second)) ||
-                  (this.hasMillisecField() && this.isEmpty(millisecond)));
-        ]]>
-        </body>
-      </method>
+    if (this.hasDayPeriodField() && !this.mDayPeriodField.disabled &&
+        !this.mDayPeriodField.readOnly) {
+      this.clearFieldValue(this.mDayPeriodField);
+    }
 
-      <method name="getCurrentValue">
-        <body>
-        <![CDATA[
-          let hour = this.getFieldValue(this.mHourField);
-          if (!this.isEmpty(hour)) {
-            if (this.mHour12) {
-              let dayPeriod = this.getDayPeriodValue();
-              if (dayPeriod == this.mPMIndicator && hour < this.mMaxHour) {
-                hour += this.mMaxHour;
-              } else if (dayPeriod == this.mAMIndicator &&
-                         hour == this.mMaxHour) {
-                hour = 0;
-              }
-            }
-          }
-
-          let minute = this.getFieldValue(this.mMinuteField);
-          let second = this.getFieldValue(this.mSecondField);
-          let millisecond = this.getFieldValue(this.mMillisecField);
-
-          let time = { hour, minute, second, millisecond };
+    if (!aFromInputElement) {
+      if (this.mInputElement.value) {
+        this.mInputElement.setUserInput("");
+      } else {
+        this.mInputElement.updateValidityState();
+      }
+    }
+  }
 
-          this.log("getCurrentValue: " + JSON.stringify(time));
-          return time;
-        ]]>
-        </body>
-      </method>
-    </implementation>
-  </binding>
+  notifyMinMaxStepAttrChanged() {
+    // Second and millisecond part are optional, rebuild edit fields if
+    // needed.
+    this.rebuildEditFieldsIfNeeded();
+    // Fill in values again.
+    this.setFieldsFromInputValue();
+  }
 
-  <binding id="datetime-input-base"
-           simpleScopeChain="true">
-    <resources>
-      <stylesheet src="chrome://global/content/bindings/datetimebox.css"/>
-    </resources>
+  incrementFieldValue(aTargetField, aTimes) {
+    let value = this.getFieldValue(aTargetField);
 
-    <content>
-      <html:div class="datetime-input-box-wrapper" anonid="input-box-wrapper"
-                xbl:inherits="context,disabled,readonly" role="presentation">
-        <html:span class="datetime-input-edit-wrapper"
-                   anonid="edit-wrapper">
-          <!-- Each of the date/time input types will append their input child
-             - elements here -->
-        </html:span>
+    // Use current time if field is empty.
+    if (this.isEmpty(value)) {
+      let now = new Date();
 
-        <html:button class="datetime-reset-button" anonid="reset-button"
-                     tabindex="-1" xbl:inherits="disabled" aria-label="&datetime.reset.label;">
-          <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12">
-            <path d="M 3.9,3 3,3.9 5.1,6 3,8.1 3.9,9 6,6.9 8.1,9 9,8.1 6.9,6 9,3.9 8.1,3 6,5.1 Z M 12,6 A 6,6 0 0 1 6,12 6,6 0 0 1 0,6 6,6 0 0 1 6,0 6,6 0 0 1 12,6 Z"/>
-          </svg>
-        </html:button>
-      </html:div>
-    </content>
-
-    <implementation implements="nsIDateTimeInputArea">
-      <constructor>
-      <![CDATA[
-        this.DEBUG = false;
-        this.mInputElement = this.parentNode;
-        this.mLocales = window.getRegionalPrefsLocales();
-
-        this.mIsRTL = false;
-        let intlUtils = window.intlUtils;
-        if (intlUtils) {
-          this.mIsRTL =
-            intlUtils.getLocaleInfo(this.mLocales).direction === "rtl";
+      if (aTargetField == this.mHourField) {
+        value = now.getHours();
+        if (this.mHour12) {
+          value = (value % this.mMaxHour) || this.mMaxHour;
         }
-
-        if (this.mIsRTL) {
-          let inputBoxWrapper =
-            document.getAnonymousElementByAttribute(this, "anonid",
-                                                    "input-box-wrapper");
-          inputBoxWrapper.dir = "rtl";
-        }
-
-        this.mMin = this.mInputElement.min;
-        this.mMax = this.mInputElement.max;
-        this.mStep = this.mInputElement.step;
-        this.mIsPickerOpen = false;
+      } else if (aTargetField == this.mMinuteField) {
+        value = now.getMinutes();
+      } else if (aTargetField == this.mSecondField) {
+        value = now.getSeconds();
+      } else if (aTargetField == this.mMillisecField) {
+        value = now.getMilliseconds();
+      } else {
+        this.log("Field not supported in incrementFieldValue.");
+        return;
+      }
+    }
 
-        this.mResetButton =
-          document.getAnonymousElementByAttribute(this, "anonid", "reset-button");
-        this.mResetButton.style.visibility = "hidden";
-
-        this.EVENTS.forEach((eventName) => {
-          this.addEventListener(eventName, this, { mozSystemGroup: true }, false);
-        });
-        // Handle keypress separately since we need to catch it on capturing.
-        this.addEventListener("keypress", this, {
-          capture: true,
-          mozSystemGroup: true,
-        }, false);
-        // This is to open the picker when input element is clicked (this
-        // includes padding area).
-        this.mInputElement.addEventListener("click", this,
-                                            { mozSystemGroup: true },
-                                            false);
-      ]]>
-      </constructor>
-
-      <destructor>
-      <![CDATA[
-        this.EVENTS.forEach((eventName) => {
-          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>
+    let min = aTargetField.getAttribute("min");
+    let max = aTargetField.getAttribute("max");
 
-      <property name="EVENTS" readonly="true">
-        <getter>
-        <![CDATA[
-          return ["focus", "blur", "copy", "cut", "paste", "mousedown"];
-        ]]>
-        </getter>
-      </property>
+    value += Number(aTimes);
+    if (value > max) {
+      value -= (max - min + 1);
+    } else if (value < min) {
+      value += (max - min + 1);
+    }
 
-      <method name="log">
-        <parameter name="aMsg"/>
-        <body>
-        <![CDATA[
-          if (this.DEBUG) {
-            dump("[DateTimeBox] " + aMsg + "\n");
-          }
-        ]]>
-        </body>
-      </method>
+    this.setFieldValue(aTargetField, value);
+  }
 
-      <method name="createEditField">
-        <parameter name="aPlaceHolder"/>
-        <parameter name="aLabel"/>
-        <parameter name="aIsNumeric"/>
-        <parameter name="aMinDigits"/>
-        <parameter name="aMaxLength"/>
-        <parameter name="aMinValue"/>
-        <parameter name="aMaxValue"/>
-        <parameter name="aPageUpDownInterval"/>
-        <body>
-        <![CDATA[
-          const HTML_NS = "http://www.w3.org/1999/xhtml";
+  handleKeyboardNav(aEvent) {
+    if (this.isDisabled() || this.isReadonly()) {
+      return;
+    }
 
-          let field = document.createElementNS(HTML_NS, "span");
-          field.classList.add("datetime-edit-field");
-          field.textContent = aPlaceHolder;
-          field.placeholder = aPlaceHolder;
-          field.tabIndex = this.mInputElement.tabIndex;
-
-          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", "");
+    let targetField = aEvent.originalTarget;
+    let key = aEvent.key;
 
-          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", "");
-            // 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");
+    if (this.hasDayPeriodField() &&
+        targetField == this.mDayPeriodField) {
+      // Home/End key does nothing on AM/PM field.
+      if (key == "Home" || key == "End") {
+        return;
+      }
+
+      this.setDayPeriodValue(
+        this.getDayPeriodValue() == this.mAMIndicator ? this.mPMIndicator
+                                                      : this.mAMIndicator);
+      this.setInputValueFromFields();
+      return;
+    }
 
-            if (this.mIsRTL) {
-              // Force the direction to be "ltr", so that the field stays in the
-              // same order even when it's empty (with placeholder). By using
-              // "embed", the text inside the element is still displayed based
-              // on its directionality.
-              field.style.unicodeBidi = "embed";
-              field.style.direction = "ltr";
-            }
-          } else {
-            // Set generic textbox ARIA role
-            field.setAttribute("role", "textbox");
-          }
-
-          return field;
-        ]]>
-        </body>
-      </method>
-
-      <method name="updateResetButtonVisibility">
-        <body>
-          <![CDATA[
-            if (this.isAnyFieldAvailable(false)) {
-              this.mResetButton.style.visibility = "visible";
-            } else {
-              this.mResetButton.style.visibility = "hidden";
-            }
-          ]]>
-        </body>
-      </method>
-
-      <method name="focusInnerTextBox">
-        <body>
-        <![CDATA[
-          this.log("Focus inner editable field.");
-
-          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">
-        <body>
-        <![CDATA[
-          this.log("Blur inner editable field.");
+    switch (key) {
+      case "ArrowUp":
+        this.incrementFieldValue(targetField, 1);
+        break;
+      case "ArrowDown":
+        this.incrementFieldValue(targetField, -1);
+        break;
+      case "PageUp": {
+        let interval = targetField.getAttribute("pginterval");
+        this.incrementFieldValue(targetField, interval);
+        break;
+      }
+      case "PageDown": {
+        let interval = targetField.getAttribute("pginterval");
+        this.incrementFieldValue(targetField, 0 - interval);
+        break;
+      }
+      case "Home":
+        let min = targetField.getAttribute("min");
+        this.setFieldValue(targetField, min);
+        break;
+      case "End":
+        let max = targetField.getAttribute("max");
+        this.setFieldValue(targetField, max);
+        break;
+    }
+    this.setInputValueFromFields();
+  }
 
-          if (this.mLastFocusedField) {
-            this.mLastFocusedField.blur();
-          } else {
-            // If .mLastFocusedField hasn't been set, blur all editable fields,
-            // so that the bound element will actually be blurred. Note that
-            // blurring on a element that has no focus won't have any effect.
-            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.blur();
-              }
-            }
-          }
-        ]]>
-        </body>
-      </method>
+  handleKeypress(aEvent) {
+    if (this.isDisabled() || this.isReadonly()) {
+      return;
+    }
 
-      <method name="notifyInputElementValueChanged">
-        <body>
-        <![CDATA[
-          this.log("inputElementValueChanged");
-          this.setFieldsFromInputValue();
-        ]]>
-        </body>
-      </method>
-
-      <method name="notifyMinMaxStepAttrChanged">
-        <body>
-        <!-- No operation by default -->
-        </body>
-      </method>
-
-      <method name="setValueFromPicker">
-        <parameter name="aValue"/>
-        <body>
-        <![CDATA[
-          this.setFieldsFromPicker(aValue);
-        ]]>
-        </body>
-      </method>
+    let targetField = aEvent.originalTarget;
+    let key = aEvent.key;
 
-      <method name="advanceToNextField">
-        <parameter name="aReverse"/>
-        <body>
-        <![CDATA[
-          this.log("advanceToNextField");
-
-          let focusedInput = this.mLastFocusedField;
-          let next = aReverse ? focusedInput.previousElementSibling
-                              : focusedInput.nextElementSibling;
-          if (!next && !aReverse) {
-            this.setInputValueFromFields();
-            return;
-          }
+    if (this.hasDayPeriodField() &&
+        targetField == this.mDayPeriodField) {
+      if (key == "a" || key == "A") {
+        this.setDayPeriodValue(this.mAMIndicator);
+      } else if (key == "p" || key == "P") {
+        this.setDayPeriodValue(this.mPMIndicator);
+      }
+      this.setInputValueFromFields();
+      return;
+    }
 
-          while (next) {
-            if ((next instanceof HTMLSpanElement) &&
-                next.classList.contains("datetime-edit-field")) {
-              next.focus();
-              break;
-            }
-            next = aReverse ? next.previousElementSibling
-                            : next.nextElementSibling;
-          }
-        ]]>
-        </body>
-      </method>
+    if (targetField.classList.contains("numeric") && key.match(/[0-9]/)) {
+      let buffer = targetField.getAttribute("typeBuffer") || "";
 
-      <method name="setPickerState">
-        <parameter name="aIsOpen"/>
-        <body>
-        <![CDATA[
-          this.log("picker is now " + (aIsOpen ? "opened" : "closed"));
-          this.mIsPickerOpen = aIsOpen;
-        ]]>
-        </body>
-      </method>
-
-      <method name="updateEditAttributes">
-        <body>
-        <![CDATA[
-          this.log("updateEditAttributes");
-
-          let editRoot =
-            document.getAnonymousElementByAttribute(this, "anonid", "edit-wrapper");
+      buffer = buffer.concat(key);
+      this.setFieldValue(targetField, buffer);
 
-          for (let child = editRoot.firstChild; child; child = child.nextSibling) {
-            if ((child instanceof HTMLSpanElement) &&
-                child.classList.contains("datetime-edit-field")) {
-              // "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);
-
-              // Set property as well for convenience.
-              child.disabled = this.mInputElement.disabled;
-              child.readOnly = this.mInputElement.readOnly;
-
-              // tabIndex works on all elements
-              child.tabIndex = this.mInputElement.tabIndex;
-            }
-          }
-        ]]>
-        </body>
-      </method>
-
-      <method name="isEmpty">
-        <parameter name="aValue"/>
-        <body>
-          return (aValue == undefined || 0 === aValue.length);
-        </body>
-      </method>
+      let n = Number(buffer);
+      let max = targetField.getAttribute("max");
+      let maxLength = targetField.getAttribute("maxLength");
+      if (buffer.length >= maxLength || n * 10 > max) {
+        buffer = "";
+        this.advanceToNextField();
+      }
+      targetField.setAttribute("typeBuffer", buffer);
+      this.setInputValueFromFields();
+    }
+  }
 
-      <method name="getFieldValue">
-        <parameter name="aField"/>
-        <body>
-        <![CDATA[
-          if (!aField || !aField.classList.contains("numeric")) {
-            return undefined;
-          }
-
-          let value = aField.getAttribute("value");
-          // Avoid returning 0 when field is empty.
-          return (this.isEmpty(value) ? undefined : Number(value));
-        ]]>
-        </body>
-      </method>
+  setFieldValue(aField, aValue) {
+    if (!aField || !aField.classList.contains("numeric")) {
+      return;
+    }
 
-      <method name="clearFieldValue">
-        <parameter name="aField"/>
-        <body>
-        <![CDATA[
-          aField.textContent = aField.placeholder;
-          aField.setAttribute("value", "");
-          if (aField.classList.contains("numeric")) {
-            aField.setAttribute("typeBuffer", "");
-          }
-          this.updateResetButtonVisibility();
-        ]]>
-        </body>
-      </method>
-
-      <method name="setFieldValue">
-        <body>
-          throw Cr.NS_ERROR_NOT_IMPLEMENTED;
-        </body>
-      </method>
-
-      <method name="clearInputFields">
-        <body>
-          throw Cr.NS_ERROR_NOT_IMPLEMENTED;
-        </body>
-      </method>
+    let value = Number(aValue);
+    if (isNaN(value)) {
+      this.log("NaN on setFieldValue!");
+      return;
+    }
 
-      <method name="setFieldsFromInputValue">
-        <body>
-          throw Cr.NS_ERROR_NOT_IMPLEMENTED;
-        </body>
-      </method>
-
-      <method name="setInputValueFromFields">
-        <body>
-          throw Cr.NS_ERROR_NOT_IMPLEMENTED;
-        </body>
-      </method>
-
-      <method name="setFieldsFromPicker">
-        <body>
-          throw Cr.NS_ERROR_NOT_IMPLEMENTED;
-        </body>
-      </method>
+    if (aField == this.mHourField) {
+      if (this.mHour12) {
+        // Try to change to 12hr format if user input is 0 or greater
+        // than 12.
+        let maxLength = aField.getAttribute("maxlength");
+        if (value == 0 && aValue.length == maxLength) {
+          value = this.mMaxHour;
+        } else {
+          value = (value > this.mMaxHour) ? value % this.mMaxHour : value;
+        }
+      } else if (value > this.mMaxHour) {
+        value = this.mMaxHour;
+      }
+    }
 
-      <method name="handleKeypress">
-        <body>
-          throw Cr.NS_ERROR_NOT_IMPLEMENTED;
-        </body>
-      </method>
-
-      <method name="handleKeyboardNav">
-        <body>
-          throw Cr.NS_ERROR_NOT_IMPLEMENTED;
-        </body>
-      </method>
-
-      <method name="getCurrentValue">
-        <body>
-          throw Cr.NS_ERROR_NOT_IMPLEMENTED;
-        </body>
-      </method>
+    aField.setAttribute("value", value);
 
-      <method name="isAnyFieldAvailable">
-        <body>
-          throw Cr.NS_ERROR_NOT_IMPLEMENTED;
-        </body>
-      </method>
+    let minDigits = aField.getAttribute("mindigits");
+    let formatted = value.toLocaleString(this.mLocales, {
+      minimumIntegerDigits: minDigits,
+      useGrouping: false,
+    });
 
-      <method name="notifyPicker">
-        <body>
-        <![CDATA[
-          if (this.mIsPickerOpen && this.isAnyFieldAvailable(true)) {
-            this.mInputElement.updateDateTimePicker(this.getCurrentValue());
-          }
-        ]]>
-        </body>
-      </method>
+    aField.textContent = formatted;
+    aField.setAttribute("aria-valuetext", formatted);
+    this.updateResetButtonVisibility();
+  }
 
-      <method name="isDisabled">
-        <body>
-        <![CDATA[
-          return this.mInputElement.hasAttribute("disabled");
-        ]]>
-        </body>
-      </method>
+  getDayPeriodValue(aValue) {
+    if (!this.hasDayPeriodField()) {
+      return "";
+    }
 
-      <method name="isReadonly">
-        <body>
-        <![CDATA[
-          return this.mInputElement.hasAttribute("readonly");
-        ]]>
-        </body>
-      </method>
+    let placeholder = this.mDayPeriodField.placeholder;
+    let value = this.mDayPeriodField.textContent;
 
-      <method name="handleEvent">
-        <parameter name="aEvent"/>
-        <body>
-        <![CDATA[
-          this.log("handleEvent: " + aEvent.type);
+    return (value == placeholder ? "" : value);
+  }
 
-          switch (aEvent.type) {
-            case "keypress": {
-              this.onKeyPress(aEvent);
-              break;
-            }
-            case "click": {
-              this.onClick(aEvent);
-              break;
-            }
-            case "focus": {
-              this.onFocus(aEvent);
-              break;
-            }
-            case "blur": {
-              this.onBlur(aEvent);
-              break;
-            }
-            case "mousedown": {
-              if (aEvent.originalTarget == this.mResetButton) {
-                aEvent.preventDefault();
-              }
-              break;
-            }
-            case "copy":
-            case "cut":
-            case "paste": {
-              aEvent.preventDefault();
-              break;
-            }
-            default:
-              break;
-          }
-        ]]>
-        </body>
-      </method>
+  setDayPeriodValue(aValue) {
+    if (!this.hasDayPeriodField()) {
+      return;
+    }
+
+    this.mDayPeriodField.textContent = aValue;
+    this.mDayPeriodField.setAttribute("value", aValue);
+    this.updateResetButtonVisibility();
+  }
+
+  isAnyFieldAvailable(aForPicker) {
+    let { hour, minute, second, millisecond } = this.getCurrentValue();
+    let dayPeriod = this.getDayPeriodValue();
 
-      <method name="onFocus">
-        <parameter name="aEvent"/>
-        <body>
-        <![CDATA[
-          this.log("onFocus originalTarget: " + aEvent.originalTarget);
-
-          if (document.activeElement != this.mInputElement) {
-            return;
-          }
+    let available = !this.isEmpty(hour) || !this.isEmpty(minute);
+    if (available) {
+      return true;
+    }
 
-          let target = aEvent.originalTarget;
-          if ((target instanceof HTMLSpanElement) &&
-              target.classList.contains("datetime-edit-field")) {
-            if (target.disabled) {
-              return;
-            }
-            this.mLastFocusedField = target;
-            this.mInputElement.setFocusState(true);
-          }
-        ]]>
-        </body>
-      </method>
+    // Picker only cares about hour:minute.
+    if (aForPicker) {
+      return false;
+    }
 
-      <method name="onBlur">
-        <parameter name="aEvent"/>
-        <body>
-        <![CDATA[
-          this.log("onBlur originalTarget: " + aEvent.originalTarget +
-            " target: " + aEvent.target);
-
-          let target = aEvent.originalTarget;
-          target.setAttribute("typeBuffer", "");
-          this.setInputValueFromFields();
-          this.mInputElement.setFocusState(false);
-        ]]>
-        </body>
-      </method>
+    return (this.hasDayPeriodField() && !this.isEmpty(dayPeriod)) ||
+           (this.hasSecondField() && !this.isEmpty(second)) ||
+           (this.hasMillisecField() && !this.isEmpty(millisecond));
+  }
 
-      <method name="onKeyPress">
-        <parameter name="aEvent"/>
-        <body>
-        <![CDATA[
-          this.log("onKeyPress key: " + aEvent.key);
+  isAnyFieldEmpty() {
+    let { hour, minute, second, millisecond } = this.getCurrentValue();
+    let dayPeriod = this.getDayPeriodValue();
+
+    return (this.isEmpty(hour) || this.isEmpty(minute) ||
+            (this.hasDayPeriodField() && this.isEmpty(dayPeriod)) ||
+            (this.hasSecondField() && this.isEmpty(second)) ||
+            (this.hasMillisecField() && this.isEmpty(millisecond)));
+  }
 
-          switch (aEvent.key) {
-            // Close picker on Enter, Escape or Space key.
-            case "Enter":
-            case "Escape":
-            case " ": {
-              if (this.mIsPickerOpen) {
-                this.mInputElement.closeDateTimePicker();
-                aEvent.preventDefault();
-              }
-              break;
-            }
-            case "Backspace": {
-              let targetField = aEvent.originalTarget;
-              this.clearFieldValue(targetField);
-              this.setInputValueFromFields();
-              aEvent.preventDefault();
-              break;
-            }
-            case "ArrowRight":
-            case "ArrowLeft": {
-              this.advanceToNextField(!(aEvent.key == "ArrowRight"));
-              aEvent.preventDefault();
-              break;
-            }
-            case "ArrowUp":
-            case "ArrowDown":
-            case "PageUp":
-            case "PageDown":
-            case "Home":
-            case "End": {
-              this.handleKeyboardNav(aEvent);
-              aEvent.preventDefault();
-              break;
-            }
-            default: {
-              // printable characters
-              if (aEvent.keyCode == 0 &&
-                  !(aEvent.ctrlKey || aEvent.altKey || aEvent.metaKey)) {
-                this.handleKeypress(aEvent);
-                aEvent.preventDefault();
-              }
-              break;
-            }
-          }
-        ]]>
-        </body>
-      </method>
+  getCurrentValue() {
+    let hour = this.getFieldValue(this.mHourField);
+    if (!this.isEmpty(hour)) {
+      if (this.mHour12) {
+        let dayPeriod = this.getDayPeriodValue();
+        if (dayPeriod == this.mPMIndicator && hour < this.mMaxHour) {
+          hour += this.mMaxHour;
+        } else if (dayPeriod == this.mAMIndicator &&
+                   hour == this.mMaxHour) {
+          hour = 0;
+        }
+      }
+    }
 
-      <method name="onClick">
-        <parameter name="aEvent"/>
-        <body>
-        <![CDATA[
-          this.log("onClick originalTarget: " + aEvent.originalTarget +
-                   " target: " + aEvent.target);
-
-          if (aEvent.defaultPrevented || this.isDisabled() || this.isReadonly()) {
-            return;
-          }
+    let minute = this.getFieldValue(this.mMinuteField);
+    let second = this.getFieldValue(this.mSecondField);
+    let millisecond = this.getFieldValue(this.mMillisecField);
 
-          if (aEvent.originalTarget == this.mResetButton) {
-            this.clearInputFields(false);
-          } else if (!this.mIsPickerOpen) {
-            this.mInputElement.openDateTimePicker(this.getCurrentValue());
-          }
-        ]]>
-        </body>
-      </method>
-    </implementation>
-  </binding>
+    let time = { hour, minute, second, millisecond };
 
-</bindings>
+    this.log("getCurrentValue: " + JSON.stringify(time));
+    return time;
+  }
+};
--- a/toolkit/content/widgets/docs/ua_widget.rst
+++ b/toolkit/content/widgets/docs/ua_widget.rst
@@ -16,16 +16,18 @@ When the element is appended to the tree
 UAWidgetsChild then grabs the sandbox for that origin (lazily creating it as needed), loads the script as needed, and initializes an instance by calling the JS constructor with a reference to the UA Widget Shadow Root created by the DOM. We will discuss the sandbox in the latter section.
 
 When the element is removed from the tree, ``UAWidgetUnbindFromTree`` is dispatched so UAWidgetsChild can destroy the widget, if it exists. If so, the UAWidgetsChild calls the ``destructor()`` method on the widget, causing the widget to destruct itself.
 
 When a UA Widget initializes, it should create its own DOM inside the passed UA Widget Shadow Root, including the ``<link>`` element necessary to load the stylesheet, add event listeners, etc. When destroyed (i.e. the destructor method is called), it should do the opposite.
 
 **Specialization**: for video controls, we do not want to do the work if the control is not needed (i.e. when the ``<video>`` or ``<audio>`` element has no "controls" attribute set), so we forgo dispatching the event from HTMLMediaElement in the BindToTree method. Instead, a ``UAWidgetAttributeChanged`` event will cause the sandbox and the widget instance to construct when the attribute is set to true. The same event is also responsible for triggering the ``onattributechange()`` method on UA Widgets if the widget is already initialized.
 
+Likewise, the datetime box widget is only loaded when the ``type`` attribute of an ``<input>`` is either `date` or `time`.
+
 The specialization does not apply to the lifecycle of the UA Widget Shadow Root. It is always constructed in order to suppress children of the DOM element from the web content from receiving a layout frame.
 
 UA Widget Shadow Root
 ---------------------
 
 The UA Widget Shadow Root is a closed shadow root, with the UA Widget flag turned on. As a closed shadow root, it may not be accessed by other scripts. It is attached on host element which the spec disallow a shadow root to be attached.
 
 The UA Widget flag enables the security feature covered in the next section.