Bug 1288591 - Implement the layout for <input type=time>. r=mconley, r=dholbert, r=smaug
authorJessica Jong <jjong@mozilla.com>
Thu, 06 Oct 2016 00:17:00 -0400
changeset 316808 ccff388a7ef4eafbd2ea4ae969ed67415f9160f4
parent 316807 9a154874f694c3e5b54401d70034591b5861e71a
child 316809 d0be8b2713f47e02a06f5549915bafbd35d12acf
push id82553
push userryanvm@gmail.com
push dateFri, 07 Oct 2016 00:30:31 +0000
treeherdermozilla-inbound@d0be8b2713f4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley, dholbert, smaug
bugs1288591
milestone52.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 1288591 - Implement the layout for <input type=time>. r=mconley, r=dholbert, r=smaug
browser/base/content/browser.xul
browser/base/content/tabbrowser.xml
browser/components/nsBrowserGlue.js
dom/base/nsGkAtomList.h
dom/html/HTMLInputElement.cpp
dom/html/HTMLInputElement.h
dom/html/moz.build
dom/html/nsIDateTimeInputArea.idl
dom/html/nsIFormControl.h
dom/html/reftests/autofocus/input-time-ref.html
dom/html/reftests/autofocus/input-time.html
dom/html/reftests/autofocus/reftest.list
dom/html/test/forms/mochitest.ini
dom/html/test/forms/test_input_datetime_focus_blur.html
dom/html/test/forms/test_input_datetime_tabindex.html
dom/html/test/forms/test_input_time_key_events.html
dom/html/test/forms/test_input_typing_sanitization.html
dom/webidl/HTMLInputElement.webidl
dom/xul/nsXULElement.cpp
layout/base/nsCSSFrameConstructor.cpp
layout/forms/moz.build
layout/forms/nsDateTimeControlFrame.cpp
layout/forms/nsDateTimeControlFrame.h
layout/generic/nsFrameIdList.h
layout/generic/nsHTMLParts.h
layout/reftests/forms/input/datetime/from-time-to-other-type-unthemed-ref.html
layout/reftests/forms/input/datetime/from-time-to-other-type-unthemed.html
layout/reftests/forms/input/datetime/reftest.list
layout/reftests/forms/input/datetime/time-basic.html
layout/reftests/forms/input/datetime/time-border.html
layout/reftests/forms/input/datetime/time-large-font.html
layout/reftests/forms/input/datetime/time-simple-unthemed-ref.html
layout/reftests/forms/input/datetime/time-simple-unthemed.html
layout/reftests/forms/input/datetime/time-width-height.html
layout/reftests/forms/input/datetime/to-time-from-other-type-unthemed.html
layout/reftests/forms/input/reftest.list
layout/style/res/html.css
toolkit/components/satchel/test/test_form_autocomplete.html
toolkit/content/browser-content.js
toolkit/content/jar.mn
toolkit/content/widgets/browser.xml
toolkit/content/widgets/datetimebox.css
toolkit/content/widgets/datetimebox.xml
toolkit/modules/DateTimePickerHelper.jsm
toolkit/modules/moz.build
toolkit/themes/shared/icons/input-clear.svg
toolkit/themes/shared/jar.inc.mn
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -149,16 +149,22 @@
     <!-- for url bar autocomplete -->
     <panel type="autocomplete-richlistbox"
            id="PopupAutoCompleteRichResult"
            noautofocus="true"
            hidden="true"
            flip="none"
            level="parent"/>
 
+    <panel id="DateTimePickerPanel"
+           hidden="true"
+           noautofocus="true"
+           consumeoutsideclicks="false"
+           level="parent"/>
+
     <!-- for select dropdowns. The menupopup is what shows the list of options,
          and the popuponly menulist makes things like the menuactive attributes
          work correctly on the menupopup. ContentSelectDropdown expects the
          popuponly menulist to be its immediate parent. -->
     <menulist popuponly="true" id="ContentSelectDropdown" hidden="true">
       <menupopup rolluponmousewheel="true"
                  activateontab="true" position="after_start"
 #ifdef XP_WIN
@@ -1051,17 +1057,18 @@
       <splitter id="sidebar-splitter" class="chromeclass-extrachrome sidebar-splitter" hidden="true"/>
       <vbox id="appcontent" flex="1">
         <notificationbox id="high-priority-global-notificationbox" notificationside="top"/>
         <tabbrowser id="content"
                     flex="1" contenttooltip="aHTMLTooltip"
                     tabcontainer="tabbrowser-tabs"
                     contentcontextmenu="contentAreaContextMenu"
                     autocompletepopup="PopupAutoComplete"
-                    selectmenulist="ContentSelectDropdown"/>
+                    selectmenulist="ContentSelectDropdown"
+                    datetimepicker="DateTimePickerPanel"/>
       </vbox>
       <vbox id="browser-border-end" hidden="true" layer="true"/>
     </hbox>
 #include ../../components/customizableui/content/customizeMode.inc.xul
   </deck>
 
   <html:div id="fullscreen-warning" class="pointerlockfswarning" hidden="true">
     <html:div class="pointerlockfswarning-domain-text">
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -20,17 +20,17 @@
                   flex="1" eventnode="document" xbl:inherits="handleCtrlPageUpDown"
                   onselect="if (event.target.localName == 'tabpanels') this.parentNode.updateCurrentBrowser();">
         <xul:tabpanels flex="1" class="plain" selectedIndex="0" anonid="panelcontainer">
           <xul:notificationbox flex="1" notificationside="top">
             <xul:hbox flex="1" class="browserSidebarContainer">
               <xul:vbox flex="1" class="browserContainer">
                 <xul:stack flex="1" class="browserStack" anonid="browserStack">
                   <xul:browser anonid="initialBrowser" type="content-primary" message="true" messagemanagergroup="browsers"
-                               xbl:inherits="tooltip=contenttooltip,contextmenu=contentcontextmenu,autocompletepopup,selectmenulist"/>
+                               xbl:inherits="tooltip=contenttooltip,contextmenu=contentcontextmenu,autocompletepopup,selectmenulist,datetimepicker"/>
                 </xul:stack>
               </xul:vbox>
             </xul:hbox>
           </xul:notificationbox>
         </xul:tabpanels>
       </xul:tabbox>
       <children/>
     </content>
@@ -1881,16 +1881,20 @@
 
             if (!aParams.isPreloadBrowser && this.hasAttribute("autocompletepopup")) {
               b.setAttribute("autocompletepopup", this.getAttribute("autocompletepopup"));
             }
 
             if (this.hasAttribute("selectmenulist"))
               b.setAttribute("selectmenulist", this.getAttribute("selectmenulist"));
 
+            if (this.hasAttribute("datetimepicker")) {
+              b.setAttribute("datetimepicker", this.getAttribute("datetimepicker"));
+            }
+
             b.setAttribute("autoscrollpopup", this._autoScrollPopup.id);
 
             if (aParams.relatedBrowser) {
               b.relatedBrowser = aParams.relatedBrowser;
             }
 
             // Create the browserStack container
             var stack = document.createElementNS(NS_XUL, "stack");
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -28,16 +28,17 @@ XPCOMUtils.defineLazyServiceGetter(this,
   ["BookmarkHTMLUtils", "resource://gre/modules/BookmarkHTMLUtils.jsm"],
   ["BookmarkJSONUtils", "resource://gre/modules/BookmarkJSONUtils.jsm"],
   ["BrowserUITelemetry", "resource:///modules/BrowserUITelemetry.jsm"],
   ["BrowserUsageTelemetry", "resource:///modules/BrowserUsageTelemetry.jsm"],
   ["CaptivePortalWatcher", "resource:///modules/CaptivePortalWatcher.jsm"],
   ["ContentClick", "resource:///modules/ContentClick.jsm"],
   ["ContentPrefServiceParent", "resource://gre/modules/ContentPrefServiceParent.jsm"],
   ["ContentSearch", "resource:///modules/ContentSearch.jsm"],
+  ["DateTimePickerHelper", "resource://gre/modules/DateTimePickerHelper.jsm"],
   ["DirectoryLinksProvider", "resource:///modules/DirectoryLinksProvider.jsm"],
   ["Feeds", "resource:///modules/Feeds.jsm"],
   ["FileUtils", "resource://gre/modules/FileUtils.jsm"],
   ["FormValidationHandler", "resource:///modules/FormValidationHandler.jsm"],
   ["Integration", "resource://gre/modules/Integration.jsm"],
   ["LightweightThemeManager", "resource://gre/modules/LightweightThemeManager.jsm"],
   ["LoginHelper", "resource://gre/modules/LoginHelper.jsm"],
   ["LoginManagerParent", "resource://gre/modules/LoginManagerParent.jsm"],
@@ -1017,16 +1018,17 @@ BrowserGlue.prototype = {
       }
     }
 
     this._checkForOldBuildUpdates();
 
     CaptivePortalWatcher.init();
 
     AutoCompletePopup.init();
+    DateTimePickerHelper.init();
 
     this._firstWindowTelemetry(aWindow);
     this._firstWindowLoaded();
   },
 
   /**
    * Application shutdown handler.
    */
@@ -1048,16 +1050,17 @@ BrowserGlue.prototype = {
     BrowserUsageTelemetry.uninit();
     SelfSupportBackend.uninit();
     NewTabMessages.uninit();
     CaptivePortalWatcher.uninit();
     AboutNewTab.uninit();
     webrtcUI.uninit();
     FormValidationHandler.uninit();
     AutoCompletePopup.uninit();
+    DateTimePickerHelper.uninit();
     if (AppConstants.NIGHTLY_BUILD) {
       AddonWatcher.uninit();
     }
   },
 
   _initServiceDiscovery: function () {
     if (!Services.prefs.getBoolPref("browser.casting.enabled")) {
       return;
--- a/dom/base/nsGkAtomList.h
+++ b/dom/base/nsGkAtomList.h
@@ -267,16 +267,17 @@ GK_ATOM(current, "current")
 GK_ATOM(cutoutregion, "cutoutregion")
 GK_ATOM(cycler, "cycler")
 GK_ATOM(data, "data")
 GK_ATOM(datalist, "datalist")
 GK_ATOM(dataType, "data-type")
 GK_ATOM(dateTime, "date-time")
 GK_ATOM(datasources, "datasources")
 GK_ATOM(datetime, "datetime")
+GK_ATOM(datetimebox, "datetimebox")
 GK_ATOM(dblclick, "dblclick")
 GK_ATOM(dd, "dd")
 GK_ATOM(debug, "debug")
 GK_ATOM(decimalFormat, "decimal-format")
 GK_ATOM(decimalSeparator, "decimal-separator")
 GK_ATOM(deck, "deck")
 GK_ATOM(declare, "declare")
 GK_ATOM(decoderDoctor, "decoder-doctor")
@@ -1980,16 +1981,17 @@ GK_ATOM(bcTableCellFrame, "BCTableCellFr
 GK_ATOM(blockFrame, "BlockFrame")
 GK_ATOM(boxFrame, "BoxFrame")
 GK_ATOM(brFrame, "BRFrame")
 GK_ATOM(bulletFrame, "BulletFrame")
 GK_ATOM(colorControlFrame, "colorControlFrame")
 GK_ATOM(columnSetFrame, "ColumnSetFrame")
 GK_ATOM(comboboxControlFrame, "ComboboxControlFrame")
 GK_ATOM(comboboxDisplayFrame, "ComboboxDisplayFrame")
+GK_ATOM(dateTimeControlFrame, "DateTimeControlFrame")
 GK_ATOM(deckFrame, "DeckFrame")
 GK_ATOM(detailsFrame, "DetailsFrame")
 GK_ATOM(fieldSetFrame, "FieldSetFrame")
 GK_ATOM(flexContainerFrame, "FlexContainerFrame")
 GK_ATOM(formControlFrame, "FormControlFrame") // radio or checkbox
 GK_ATOM(frameSetFrame, "FrameSetFrame")
 GK_ATOM(gfxButtonControlFrame, "gfxButtonControlFrame")
 GK_ATOM(gridContainerFrame, "GridContainerFrame")
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -48,16 +48,17 @@
 #include "nsIFrame.h"
 #include "nsRangeFrame.h"
 #include "nsIServiceManager.h"
 #include "nsError.h"
 #include "nsIEditor.h"
 #include "nsIIOService.h"
 #include "nsDocument.h"
 #include "nsAttrValueOrString.h"
+#include "nsDateTimeControlFrame.h"
 
 #include "nsPresState.h"
 #include "nsIDOMEvent.h"
 #include "nsIDOMNodeList.h"
 #include "nsIDOMHTMLCollection.h"
 #include "nsLinebreakConverter.h" //to strip out carriage returns
 #include "nsReadableUtils.h"
 #include "nsUnicharUtils.h"
@@ -2702,16 +2703,92 @@ HTMLInputElement::MozSetDirectory(const 
 
   nsTArray<OwningFileOrDirectory> array;
   OwningFileOrDirectory* element = array.AppendElement();
   element->SetAsDirectory() = directory;
 
   SetFilesOrDirectories(array, true);
 }
 
+void HTMLInputElement::GetDateTimeInputBoxValue(DateTimeValue& aValue)
+{
+  if (NS_WARN_IF(!IsDateTimeInputType(mType)) || !mDateTimeInputBoxValue) {
+    return;
+  }
+
+  aValue = *mDateTimeInputBoxValue;
+}
+
+void
+HTMLInputElement::UpdateDateTimeInputBox(const DateTimeValue& aValue)
+{
+  if (NS_WARN_IF(!IsDateTimeInputType(mType))) {
+    return;
+  }
+
+  nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame());
+  if (frame) {
+    frame->SetValueFromPicker(aValue);
+  }
+}
+
+void
+HTMLInputElement::SetDateTimePickerState(bool aOpen)
+{
+  if (NS_WARN_IF(!IsDateTimeInputType(mType))) {
+    return;
+  }
+
+  nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame());
+  if (frame) {
+    frame->SetPickerState(aOpen);
+  }
+}
+
+void
+HTMLInputElement::OpenDateTimePicker(const DateTimeValue& aInitialValue)
+{
+  if (NS_WARN_IF(!IsDateTimeInputType(mType))) {
+    return;
+  }
+
+  mDateTimeInputBoxValue = new DateTimeValue(aInitialValue);
+  nsContentUtils::DispatchChromeEvent(OwnerDoc(),
+                                      static_cast<nsIDOMHTMLInputElement*>(this),
+                                      NS_LITERAL_STRING("MozOpenDateTimePicker"),
+                                      true, true);
+}
+
+void
+HTMLInputElement::UpdateDateTimePicker(const DateTimeValue& aValue)
+{
+  if (NS_WARN_IF(!IsDateTimeInputType(mType))) {
+    return;
+  }
+
+  mDateTimeInputBoxValue = new DateTimeValue(aValue);
+  nsContentUtils::DispatchChromeEvent(OwnerDoc(),
+                                      static_cast<nsIDOMHTMLInputElement*>(this),
+                                      NS_LITERAL_STRING("MozUpdateDateTimePicker"),
+                                      true, true);
+}
+
+void
+HTMLInputElement::CloseDateTimePicker()
+{
+  if (NS_WARN_IF(!IsDateTimeInputType(mType))) {
+    return;
+  }
+
+  nsContentUtils::DispatchChromeEvent(OwnerDoc(),
+                                      static_cast<nsIDOMHTMLInputElement*>(this),
+                                      NS_LITERAL_STRING("MozCloseDateTimePicker"),
+                                      true, true);
+}
+
 bool
 HTMLInputElement::MozIsTextField(bool aExcludePassword)
 {
   // TODO: temporary until bug 888320 is fixed.
   if (IsExperimentalMobileType(mType) || IsDateTimeInputType(mType)) {
     return false;
   }
 
@@ -2728,23 +2805,58 @@ HTMLInputElement::GetOwnerNumberControl(
       HTMLInputElement::FromContentOrNull(GetParent()->GetParent());
     if (grandparent && grandparent->mType == NS_FORM_INPUT_NUMBER) {
       return grandparent;
     }
   }
   return nullptr;
 }
 
+HTMLInputElement*
+HTMLInputElement::GetOwnerDateTimeControl()
+{
+  if (IsInNativeAnonymousSubtree() &&
+      mType == NS_FORM_INPUT_TEXT &&
+      GetParent() &&
+      GetParent()->GetParent() &&
+      GetParent()->GetParent()->GetParent() &&
+      GetParent()->GetParent()->GetParent()->GetParent()) {
+    // Yes, this is very very deep.
+    HTMLInputElement* ownerDateTimeControl =
+      HTMLInputElement::FromContentOrNull(
+        GetParent()->GetParent()->GetParent()->GetParent());
+    if (ownerDateTimeControl &&
+        ownerDateTimeControl->mType == NS_FORM_INPUT_TIME) {
+      return ownerDateTimeControl;
+    }
+  }
+  return nullptr;
+}
+
+
 NS_IMETHODIMP
 HTMLInputElement::MozIsTextField(bool aExcludePassword, bool* aResult)
 {
   *aResult = MozIsTextField(aExcludePassword);
   return NS_OK;
 }
 
+void
+HTMLInputElement::SetUserInput(const nsAString& aInput,
+                               const mozilla::Maybe<nsIPrincipal*>& aPrincipal) {
+  MOZ_ASSERT(aPrincipal.isSome());
+
+  if (mType == NS_FORM_INPUT_FILE &&
+      !nsContentUtils::IsSystemPrincipal(aPrincipal.value())) {
+    return;
+  }
+
+  SetUserInput(aInput);
+}
+
 NS_IMETHODIMP
 HTMLInputElement::SetUserInput(const nsAString& aValue)
 {
   if (mType == NS_FORM_INPUT_FILE)
   {
     Sequence<nsString> list;
     if (!list.AppendElement(aValue, fallible)) {
       return NS_ERROR_OUT_OF_MEMORY;
@@ -3159,16 +3271,22 @@ HTMLInputElement::SetValueInternal(const
           if (numberControlFrame) {
             numberControlFrame->SetValueOfAnonTextControl(value);
           }
         } else if (mType == NS_FORM_INPUT_RANGE) {
           nsRangeFrame* frame = do_QueryFrame(GetPrimaryFrame());
           if (frame) {
             frame->UpdateForValueChange();
           }
+        } else if (mType == NS_FORM_INPUT_TIME &&
+                   !IsExperimentalMobileType(mType)) {
+          nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame());
+          if (frame) {
+            frame->UpdateInputBoxValue();
+          }
         }
         if (!mParserCreating) {
           OnValueChanged(/* aNotify = */ true,
                          /* aWasInteractiveUserChange = */ false);
         }
         // else DoneCreatingElement calls us again once mParserCreating is false
       }
 
@@ -3461,16 +3579,25 @@ HTMLInputElement::Blur(ErrorResult& aErr
     if (numberControlFrame) {
       HTMLInputElement* textControl = numberControlFrame->GetAnonTextControl();
       if (textControl) {
         textControl->Blur(aError);
         return;
       }
     }
   }
+
+  if (mType == NS_FORM_INPUT_TIME && !IsExperimentalMobileType(mType)) {
+    nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame());
+    if (frame) {
+      frame->HandleBlurEvent();
+      return;
+    }
+  }
+
   nsGenericHTMLElement::Blur(aError);
 }
 
 void
 HTMLInputElement::Focus(ErrorResult& aError)
 {
   if (mType == NS_FORM_INPUT_NUMBER) {
     // Focus our anonymous text control, if we have one.
@@ -3480,16 +3607,24 @@ HTMLInputElement::Focus(ErrorResult& aEr
       HTMLInputElement* textControl = numberControlFrame->GetAnonTextControl();
       if (textControl) {
         textControl->Focus(aError);
         return;
       }
     }
   }
 
+  if (mType == NS_FORM_INPUT_TIME && !IsExperimentalMobileType(mType)) {
+    nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame());
+    if (frame) {
+      frame->HandleFocusEvent();
+      return;
+    }
+  }
+
   if (mType != NS_FORM_INPUT_FILE) {
     nsGenericHTMLElement::Focus(aError);
     return;
   }
 
   // For file inputs, focus the first button instead. In the case of there
   // being two buttons (when the picker is a directory picker) the user can
   // tab to the next one.
@@ -3783,17 +3918,17 @@ HTMLInputElement::PreHandleEvent(EventCh
     GetValue(mFocusedValue);
   }
 
   // Fire onchange (if necessary), before we do the blur, bug 357684.
   if (aVisitor.mEvent->mMessage == eBlur) {
     // Experimental mobile types rely on the system UI to prevent users to not
     // set invalid values but we have to be extra-careful. Especially if the
     // option has been enabled on desktop.
-    if (IsExperimentalMobileType(mType) || IsDateTimeInputType(mType)) {
+    if (IsExperimentalMobileType(mType)) {
       nsAutoString aValue;
       GetValueInternal(aValue);
       nsresult rv =
         SetValueInternal(aValue, nsTextEditorState::eSetValue_Internal);
       NS_ENSURE_SUCCESS(rv, rv);
     }
     FireChangeEventIfNeeded();
   }
@@ -3804,16 +3939,28 @@ HTMLInputElement::PreHandleEvent(EventCh
     // Just as nsGenericHTMLFormElementWithState::PreHandleEvent calls
     // nsIFormControlFrame::SetFocus, we handle focus here.
     nsIFrame* frame = GetPrimaryFrame();
     if (frame) {
       frame->InvalidateFrameSubtree();
     }
   }
 
+  if (mType == NS_FORM_INPUT_TIME &&
+      !IsExperimentalMobileType(mType) &&
+      aVisitor.mEvent->mMessage == eFocus &&
+      aVisitor.mEvent->mOriginalTarget == this) {
+    // If original target is this and not the anonymous text control, we should
+    // pass the focus to the anonymous text control.
+    nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame());
+    if (frame) {
+      frame->HandleFocusEvent();
+    }
+  }
+
   if (mType == NS_FORM_INPUT_NUMBER && aVisitor.mEvent->IsTrusted()) {
     if (mNumberControlSpinnerIsSpinning) {
       // If the timer is running the user has depressed the mouse on one of the
       // spin buttons. If the mouse exits the button we either want to reverse
       // the direction of spin if it has moved over the other button, or else
       // we want to end the spin. We do this here (rather than in
       // PostHandleEvent) because we don't want to let content preventDefault()
       // the end of the spin.
@@ -6677,32 +6824,42 @@ HTMLInputElement::AddStates(EventStates 
 {
   if (mType == NS_FORM_INPUT_TEXT) {
     EventStates focusStates(aStates & (NS_EVENT_STATE_FOCUS |
                                        NS_EVENT_STATE_FOCUSRING));
     if (!focusStates.IsEmpty()) {
       HTMLInputElement* ownerNumberControl = GetOwnerNumberControl();
       if (ownerNumberControl) {
         ownerNumberControl->AddStates(focusStates);
+      } else {
+        HTMLInputElement* ownerDateTimeControl = GetOwnerDateTimeControl();
+        if (ownerDateTimeControl) {
+          ownerDateTimeControl->AddStates(focusStates);
+        }
       }
     }
   }
   nsGenericHTMLFormElementWithState::AddStates(aStates);
 }
 
 void
 HTMLInputElement::RemoveStates(EventStates aStates)
 {
   if (mType == NS_FORM_INPUT_TEXT) {
     EventStates focusStates(aStates & (NS_EVENT_STATE_FOCUS |
                                        NS_EVENT_STATE_FOCUSRING));
     if (!focusStates.IsEmpty()) {
       HTMLInputElement* ownerNumberControl = GetOwnerNumberControl();
       if (ownerNumberControl) {
         ownerNumberControl->RemoveStates(focusStates);
+      } else {
+        HTMLInputElement* ownerDateTimeControl = GetOwnerDateTimeControl();
+        if (ownerDateTimeControl) {
+          ownerDateTimeControl->RemoveStates(focusStates);
+        }
       }
     }
   }
   nsGenericHTMLFormElementWithState::RemoveStates(aStates);
 }
 
 bool
 HTMLInputElement::RestoreState(nsPresState* aState)
@@ -6870,22 +7027,24 @@ HTMLInputElement::IsHTMLFocusable(bool a
 
 #ifdef XP_MACOSX
   const bool defaultFocusable = !aWithMouse || nsFocusManager::sMouseFocusesFormControl;
 #else
   const bool defaultFocusable = true;
 #endif
 
   if (mType == NS_FORM_INPUT_FILE ||
-      mType == NS_FORM_INPUT_NUMBER) {
+      mType == NS_FORM_INPUT_NUMBER ||
+      mType == NS_FORM_INPUT_TIME) {
     if (aTabIndex) {
       // We only want our native anonymous child to be tabable to, not ourself.
       *aTabIndex = -1;
     }
-    if (mType == NS_FORM_INPUT_NUMBER) {
+    if (mType == NS_FORM_INPUT_NUMBER ||
+        mType == NS_FORM_INPUT_TIME) {
       *aIsFocusable = true;
     } else {
       *aIsFocusable = defaultFocusable;
     }
     return true;
   }
 
   if (mType == NS_FORM_INPUT_HIDDEN) {
--- a/dom/html/HTMLInputElement.h
+++ b/dom/html/HTMLInputElement.h
@@ -767,17 +767,34 @@ public:
   int32_t GetTextLength(ErrorResult& aRv);
 
   void MozGetFileNameArray(nsTArray<nsString>& aFileNames, ErrorResult& aRv);
 
   void MozSetFileNameArray(const Sequence< nsString >& aFileNames, ErrorResult& aRv);
   void MozSetFileArray(const Sequence<OwningNonNull<File>>& aFiles);
   void MozSetDirectory(const nsAString& aDirectoryPath, ErrorResult& aRv);
 
+  /*
+   * The following functions are called from datetime picker to let input box
+   * know the current state of the picker or to update the input box on changes.
+   */
+  void GetDateTimeInputBoxValue(DateTimeValue& aValue);
+  void UpdateDateTimeInputBox(const DateTimeValue& aValue);
+  void SetDateTimePickerState(bool aOpen);
+
+  /*
+   * The following functions are called from datetime input box XBL to control
+   * and update the picker.
+   */
+  void OpenDateTimePicker(const DateTimeValue& aInitialValue);
+  void UpdateDateTimePicker(const DateTimeValue& aValue);
+  void CloseDateTimePicker();
+
   HTMLInputElement* GetOwnerNumberControl();
+  HTMLInputElement* GetOwnerDateTimeControl();
 
   void StartNumberControlSpinnerSpin();
   enum SpinnerStopState {
     eAllowDispatchingEvents,
     eDisallowDispatchingEvents
   };
   void StopNumberControlSpinnerSpin(SpinnerStopState aState =
                                       eAllowDispatchingEvents);
@@ -798,17 +815,18 @@ public:
   {
     return mNumberControlSpinnerIsSpinning && !mNumberControlSpinnerSpinsUp;
   }
 
   bool MozIsTextField(bool aExcludePassword);
 
   nsIEditor* GetEditor();
 
-  // XPCOM SetUserInput() is OK
+  void SetUserInput(const nsAString& aInput,
+                    const mozilla::Maybe<nsIPrincipal*>& aPrincipal);
 
   // XPCOM GetPhonetic() is OK
 
   /**
    * If aValue contains a valid floating-point number in the format specified
    * by the HTML 5 spec:
    *
    *   http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#floating-point-numbers
@@ -1467,16 +1485,22 @@ protected:
   /**
    * If mIsDraggingRange is true, this is the value that the input had before
    * the drag started. Used to reset the input to its old value if the drag is
    * canceled.
    */
   Decimal mRangeThumbDragStartValue;
 
   /**
+   * Current value in the input box, in DateTimeValue dictionary format, see
+   * HTMLInputElement.webidl for details.
+   */
+  nsAutoPtr<DateTimeValue> mDateTimeInputBoxValue;
+
+  /**
    * The selection properties cache for number controls.  This is needed because
    * the number controls don't recycle their text field, so the normal cache in
    * nsTextEditorState cannot do its job.
    */
   nsTextEditorState::SelectionProperties mSelectionProperties;
 
   // Step scale factor values, for input types that have one.
   static const Decimal kStepScaleFactorDate;
@@ -1556,17 +1580,18 @@ private:
     return mType == NS_FORM_INPUT_TEXT || mType == NS_FORM_INPUT_SEARCH ||
            mType == NS_FORM_INPUT_URL || mType == NS_FORM_INPUT_TEL ||
            mType == NS_FORM_INPUT_PASSWORD;
   }
 
   static bool MayFireChangeOnBlur(uint8_t aType) {
     return IsSingleLineTextControl(false, aType) ||
            aType == NS_FORM_INPUT_RANGE ||
-           aType == NS_FORM_INPUT_NUMBER;
+           aType == NS_FORM_INPUT_NUMBER ||
+           aType == NS_FORM_INPUT_TIME;
   }
 
   struct nsFilePickerFilter {
     nsFilePickerFilter()
       : mFilterMask(0) {}
 
     explicit nsFilePickerFilter(int32_t aFilterMask)
       : mFilterMask(aFilterMask) {}
--- a/dom/html/moz.build
+++ b/dom/html/moz.build
@@ -13,16 +13,17 @@ MOCHITEST_MANIFESTS += [
 MOCHITEST_CHROME_MANIFESTS += [
     'test/chrome.ini',
     'test/forms/chrome.ini',
 ]
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 
 XPIDL_SOURCES += [
+    'nsIDateTimeInputArea.idl',
     'nsIFormSubmitObserver.idl',
     'nsIHTMLMenu.idl',
     'nsIImageDocument.idl',
     'nsIMenuBuilder.idl',
     'nsIPhonetic.idl',
 ]
 
 XPIDL_MODULE = 'content_html'
new file mode 100644
--- /dev/null
+++ b/dom/html/nsIDateTimeInputArea.idl
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(465c0cc3-24cb-48ce-af1a-b18402326b05)]
+interface nsIDateTimeInputArea : nsISupports
+{
+  /**
+   * Called from DOM/Layout when input element value has changed.
+   */
+  void notifyInputElementValueChanged();
+
+  /**
+   * Called when date/time picker value has changed.
+   */
+  void setValueFromPicker(in jsval value);
+
+  /**
+   * Called from DOM/Layout to set focus on inner text box.
+   */
+  void focusInnerTextBox();
+
+  /**
+   * Called from DOM/Layout to blur inner text box.
+   */
+  void blurInnerTextBox();
+
+  /**
+   * Set the current state of the picker, true if it's opened, false otherwise.
+   */
+  void setPickerState(in boolean isOpen);
+};
--- a/dom/html/nsIFormControl.h
+++ b/dom/html/nsIFormControl.h
@@ -261,18 +261,21 @@ bool
 nsIFormControl::IsSingleLineTextControl(bool aExcludePassword, uint32_t aType)
 {
   return aType == NS_FORM_INPUT_TEXT ||
          aType == NS_FORM_INPUT_EMAIL ||
          aType == NS_FORM_INPUT_SEARCH ||
          aType == NS_FORM_INPUT_TEL ||
          aType == NS_FORM_INPUT_URL ||
          // TODO: those are temporary until bug 773205 is fixed.
+#if defined(MOZ_WIDGET_ANDROID) || defined(MOZ_WIDGET_GONK)
+         // On Android/B2G, date/time input appears as a normal text box.
+         aType == NS_FORM_INPUT_TIME ||
+#endif
          aType == NS_FORM_INPUT_DATE ||
-         aType == NS_FORM_INPUT_TIME ||
          aType == NS_FORM_INPUT_MONTH ||
          aType == NS_FORM_INPUT_WEEK ||
          (!aExcludePassword && aType == NS_FORM_INPUT_PASSWORD);
 }
 
 bool
 nsIFormControl::IsSubmittableControl() const
 {
new file mode 100644
--- /dev/null
+++ b/dom/html/reftests/autofocus/input-time-ref.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+  <!-- In this case we're using reftest-wait to make sure the test doesn't
+       get snapshotted before it's been focused. We're not testing
+       invalidation so we don't need to listen for MozReftestInvalidate.
+  -->
+  <head>
+    <script>
+      function focusHandler() {
+        setTimeout(function() {
+          document.documentElement.removeAttribute("class");
+        }, 0);
+      }
+    </script>
+  </head>
+  <body onload="document.getElementById('t').focus();">
+    <input type="time" id="t" onfocus="focusHandler();"
+           style="-moz-appearance: none;">
+  </body>
+</html>
+
+
new file mode 100644
--- /dev/null
+++ b/dom/html/reftests/autofocus/input-time.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+  <!-- In this case we're using reftest-wait to make sure the test doesn't
+       get snapshotted before it's been focused. We're not testing
+       invalidation so we don't need to listen for MozReftestInvalidate.
+  -->
+  <head>
+    <script>
+      function focusHandler() {
+        setTimeout(function() {
+          document.documentElement.removeAttribute("class");
+        }, 0);
+      }
+    </script>
+  </head>
+  <body>
+    <input type="time" autofocus onfocus="focusHandler();"
+           style="-moz-appearance: none;">
+  </body>
+</html>
+
+
--- a/dom/html/reftests/autofocus/reftest.list
+++ b/dom/html/reftests/autofocus/reftest.list
@@ -1,12 +1,13 @@
-default-preferences pref(dom.forms.number,true)
+default-preferences pref(dom.forms.number,true) pref(dom.forms.datetime,true)
 skip-if(B2G||Mulet) fuzzy-if(skiaContent,1,3) needs-focus == input-load.html input-ref.html # B2G timed out waiting for reftest-wait to be removed # Initial mulet triage: parity with B2G/B2G Desktop
 skip-if(B2G||Mulet) fuzzy-if(skiaContent,1,3) needs-focus == input-create.html input-ref.html # B2G timed out waiting for reftest-wait to be removed # Initial mulet triage: parity with B2G/B2G Desktop
 skip-if(B2G||Mulet) fuzzy-if(skiaContent,1,3) needs-focus == input-number.html input-number-ref.html # B2G timed out waiting for reftest-wait to be removed # Initial mulet triage: parity with B2G/B2G Desktop
+skip-if(B2G||Mulet) fuzzy-if(skiaContent,1,3) needs-focus == input-time.html input-time-ref.html # B2G timed out waiting for reftest-wait to be removed # Initial mulet triage: parity with B2G/B2G Desktop
 skip-if(B2G||Mulet) fuzzy-if(skiaContent,1,3) needs-focus == button-load.html button-ref.html # B2G timed out waiting for reftest-wait to be removed # Initial mulet triage: parity with B2G/B2G Desktop
 skip-if(B2G||Mulet) fuzzy-if(skiaContent,1,3) needs-focus == button-create.html button-ref.html # B2G timed out waiting for reftest-wait to be removed # Initial mulet triage: parity with B2G/B2G Desktop
 skip-if(B2G||Mulet) fuzzy-if(skiaContent,1,3) needs-focus == textarea-load.html textarea-ref.html # B2G timed out waiting for reftest-wait to be removed # Initial mulet triage: parity with B2G/B2G Desktop
 skip-if(B2G||Mulet) fuzzy-if(skiaContent,1,3) needs-focus == textarea-create.html textarea-ref.html # B2G timed out waiting for reftest-wait to be removed # Initial mulet triage: parity with B2G/B2G Desktop
 skip-if(B2G||Mulet) fuzzy-if(skiaContent,9,6) needs-focus == select-load.html select-ref.html # B2G timed out waiting for reftest-wait to be removed # Initial mulet triage: parity with B2G/B2G Desktop
 skip-if(B2G||Mulet) fuzzy-if(skiaContent,2,4) needs-focus == select-create.html select-ref.html # B2G timed out waiting for reftest-wait to be removed # Initial mulet triage: parity with B2G/B2G Desktop
 needs-focus == autofocus-after-load.html autofocus-after-load-ref.html
 fails-if(B2G||Mulet) fuzzy-if(skiaContent,2,5) needs-focus == autofocus-leaves-iframe.html autofocus-leaves-iframe-ref.html # B2G focus difference between test and reference # Initial mulet triage: parity with B2G/B2G Desktop
--- a/dom/html/test/forms/mochitest.ini
+++ b/dom/html/test/forms/mochitest.ini
@@ -30,16 +30,20 @@ skip-if = buildapp == 'mulet'
 [test_input_color_input_change_events.html]
 skip-if = buildapp == 'mulet'
 [test_input_color_picker_initial.html]
 skip-if = buildapp == 'mulet'
 [test_input_color_picker_popup.html]
 skip-if = android_version == '18' # Android, bug 1147974
 [test_input_color_picker_update.html]
 skip-if = android_version == '18' # Android, bug 1147974
+[test_input_datetime_focus_blur.html]
+skip-if = os == "android" || appname == "b2g"
+[test_input_datetime_tabindex.html]
+skip-if = os == "android" || appname == "b2g"
 [test_input_defaultValue.html]
 [test_input_email.html]
 [test_input_event.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') || android_version == '18' #Bug 931116, b2g desktop and mulet specific, initial triage; on Android, bug 1147974
 [test_input_file_picker.html]
 skip-if = buildapp == 'b2g' # b2g(5 failures out of 139 and timing out, bug 901581) b2g-debug(5 failures out of 139 and timing out, bug 901581) b2g-desktop(5 failures out of 139 and timing out, bug 901581)
 [test_input_list_attribute.html]
 [test_input_number_l10n.html]
@@ -59,16 +63,18 @@ skip-if = os == "android" || appname == 
 [test_input_range_key_events.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') #Bug 931116, b2g desktop specific, initial triage
 [test_input_range_mouse_and_touch_events.html]
 skip-if = (toolkit == 'gonk' && debug) #debug-only failure; bug 926546
 [test_input_range_rounding.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') #Bug 931116, b2g desktop specific, initial triage
 [test_input_sanitization.html]
 [test_input_textarea_set_value_no_scroll.html]
+[test_input_time_key_events.html]
+skip-if = os == "android" || appname == "b2g"
 [test_input_types_pref.html]
 [test_input_typing_sanitization.html]
 skip-if = buildapp == 'mulet'
 [test_input_untrusted_key_events.html]
 [test_input_url.html]
 [test_interactive_content_in_label.html]
 [test_label_control_attribute.html]
 [test_label_input_controls.html]
new file mode 100644
--- /dev/null
+++ b/dom/html/test/forms/test_input_datetime_focus_blur.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1288591
+-->
+<head>
+  <title>Test focus/blur behaviour for &lt;input type='time'&gt;</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1288591">Mozilla Bug 1288591</a>
+<p id="display"></p>
+<div id="content">
+  <input id="input" type="time">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * Test for Bug 1288591.
+ * This test checks whether date/time input types' .focus()/.blur() works
+ * correctly. This test also checks when focusing on an date/time input element,
+ * the focus is redirected to the anonymous text control, but the
+ * document.activeElement still returns date/time input element.
+ **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+  test();
+  SimpleTest.finish();
+});
+
+function test() {
+  let time = document.getElementById("input");
+  time.focus();
+
+  // The active element returns the input type=time.
+  let activeElement = document.activeElement;
+  is(activeElement, time, "activeElement should be the time element");
+  is(activeElement.localName, "input", "activeElement should be an input element");
+  is(activeElement.type, "time", "activeElement should be of type time");
+
+  // Use FocusManager to check that the actual focus is on the anonymous
+  // text control.
+  let fm = SpecialPowers.Cc["@mozilla.org/focus-manager;1"]
+                        .getService(SpecialPowers.Ci.nsIFocusManager);
+  let focusedElement = fm.focusedElement;
+  is(focusedElement.localName, "input", "focusedElement should be an input element");
+  is(focusedElement.type, "text", "focusedElement should be of type text");
+
+  time.blur();
+  isnot(document.activeElement, time, "activeElement should no longer be the time element");
+}
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/html/test/forms/test_input_datetime_tabindex.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1288591
+-->
+<head>
+  <title>Test tabindex attribute for &lt;input type='time'&gt;</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1288591">Mozilla Bug 1288591</a>
+<p id="display"></p>
+<div id="content">
+  <input id="time1" type="time" tabindex="0">
+  <input id="time2" type="time" tabindex="-1">
+  <input id="time3" type="time" tabindex="0">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * Test for Bug 1288591.
+ * This test checks whether date/time input types' tabindex attribute works
+ * correctly.
+ **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+  test();
+  SimpleTest.finish();
+});
+
+function test() {
+  let time1 = document.getElementById("time1");
+  let time2 = document.getElementById("time2");
+  let time3 = document.getElementById("time3");
+
+  time1.focus();
+  is(document.activeElement, time1,
+     "input element with tabindex=0 is focusable");
+
+  // Advance to time1 minute field
+  synthesizeKey("VK_TAB", {});
+  is(document.activeElement, time1,
+     "input element with tabindex=0 is tabbable");
+
+  // Advance to time1 AM/PM field
+  synthesizeKey("VK_TAB", {});
+  is(document.activeElement, time1,
+     "input element with tabindex=0 is tabbable");
+
+  // Advance to next element
+  synthesizeKey("VK_TAB", {});
+  is(document.activeElement, time3,
+     "input element with tabindex=-1 is not tabbable");
+
+  time2.focus();
+  is(document.activeElement, time2,
+     "input element with tabindex=-1 is still focusable");
+
+  // Changing the tabindex attribute dynamically.
+  time3.setAttribute("tabindex", "-1");
+  synthesizeKey("VK_TAB", {}); // need only one TAB since time2 is not tabbable
+  isnot(document.activeElement, time3,
+        "element with tabindex changed to -1 should not be tabbable");
+}
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/html/test/forms/test_input_time_key_events.html
@@ -0,0 +1,197 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1288591
+-->
+<head>
+  <title>Test key events for time control</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <meta charset="UTF-8">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1288591">Mozilla Bug 1288591</a>
+<p id="display"></p>
+<div id="content">
+  <input id="input" type="time">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+// Turn off Spatial Navigation because it hijacks arrow keydown events:
+SimpleTest.waitForFocus(function() {
+  SpecialPowers.pushPrefEnv({"set":[["snav.enabled", false]]}, function() {
+    test();
+    SimpleTest.finish();
+  });
+});
+
+var testData = [
+  /**
+   * keys: keys to send to the input element.
+   * initialVal: initial value set to the input element.
+   * expectedVal: expected value of the input element after sending the keys.
+   */
+  {
+    // Type 1030 and select AM.
+    keys: ["1030", "VK_DOWN"],
+    initialVal: "",
+    expectedVal: "10:30"
+  },
+  {
+    // Type 3 in the hour field will automatically advance to the minute field.
+    keys: ["330", "VK_DOWN"],
+    initialVal: "",
+    expectedVal: "03:30"
+  },
+  {
+    // Type 5 in the hour field will automatically advance to the minute field.
+    // Type 7 in the minute field will automatically advance to the AM/PM field.
+    keys: ["57", "VK_DOWN"],
+    initialVal: "",
+    expectedVal: "05:07"
+  },
+  {
+    // Advance to AM/PM field and change to PM.
+    keys: ["VK_TAB", "VK_TAB", "VK_DOWN"],
+    initialVal: "10:30",
+    expectedVal: "22:30"
+  },
+  {
+    // Right key should do the same thing as TAB key.
+    keys: ["VK_RIGHT", "VK_RIGHT", "VK_DOWN"],
+    initialVal: "10:30",
+    expectedVal: "22:30"
+  },
+  {
+    // Advance to minute field then back to hour field and decrement.
+    keys: ["VK_RIGHT", "VK_LEFT", "VK_DOWN"],
+    initialVal: "10:30",
+    expectedVal: "09:30"
+  },
+  {
+    // Focus starts on the first field, hour in this case, and increment.
+    keys: ["VK_UP"],
+    initialVal: "16:00",
+    expectedVal: "17:00"
+  },
+  {
+    // Advance to minute field and decrement.
+    keys: ["VK_TAB", "VK_DOWN"],
+    initialVal: "16:00",
+    expectedVal: "16:59"
+  },
+  {
+    // Advance to minute field and increment.
+    keys: ["VK_TAB", "VK_UP"],
+    initialVal: "16:59",
+    expectedVal: "16:00"
+  },
+  {
+    // PageUp on hour field increments hour by 3.
+    keys: ["VK_PAGE_UP"],
+    initialVal: "05:00",
+    expectedVal: "08:00"
+  },
+  {
+    // PageDown on hour field decrements hour by 3.
+    keys: ["VK_PAGE_DOWN"],
+    initialVal: "05:00",
+    expectedVal: "02:00"
+  },
+  {
+    // PageUp on minute field increments minute by 10.
+    keys: ["VK_TAB", "VK_PAGE_UP"],
+    initialVal: "14:00",
+    expectedVal: "14:10"
+  },
+  {
+    // PageDown on minute field decrements minute by 10.
+    keys: ["VK_TAB", "VK_PAGE_DOWN"],
+    initialVal: "14:00",
+    expectedVal: "14:50"
+  },
+  {
+    // Home key on hour field sets it to the minimum hour, which is 1 in 12-hour
+    // clock.
+    keys: ["VK_HOME"],
+    initialVal: "03:10",
+    expectedVal: "01:10"
+  },
+  {
+    // End key on hour field sets it to the maximum hour, which is 12 in 12-hour
+    // clock.
+    keys: ["VK_END"],
+    initialVal: "03:10",
+    expectedVal: "00:10"
+  },
+  {
+    // Home key on minute field sets it to the minimum minute, which is 0.
+    keys: ["VK_TAB", "VK_HOME"],
+    initialVal: "19:30",
+    expectedVal: "19:00"
+  },
+  {
+    // End key on minute field sets it to the minimum minute, which is 59.
+    keys: ["VK_TAB", "VK_END"],
+    initialVal: "19:30",
+    expectedVal: "19:59"
+  },
+  // Second field will show up when needed.
+  {
+    // PageUp on second field increments second by 10.
+    keys: ["VK_TAB", "VK_TAB", "VK_PAGE_UP"],
+    initialVal: "08:10:10",
+    expectedVal: "08:10:20"
+  },
+  {
+    // PageDown on second field increments second by 10.
+    keys: ["VK_TAB", "VK_TAB", "VK_PAGE_DOWN"],
+    initialVal: "08:10:10",
+    expectedVal: "08:10:00"
+  },
+  {
+    // Home key on second field sets it to the minimum second, which is 0.
+    keys: ["VK_TAB", "VK_TAB", "VK_HOME"],
+    initialVal: "16:00:30",
+    expectedVal: "16:00:00"
+  },
+  {
+    // End key on second field sets it to the minimum second, which is 59.
+    keys: ["VK_TAB", "VK_TAB", "VK_END"],
+    initialVal: "16:00:30",
+    expectedVal: "16:00:59"
+  },
+];
+
+function sendKeys(aKeys) {
+  for (let i = 0; i < aKeys.length; i++) {
+    let key = aKeys[i];
+    if (key.startsWith("VK")) {
+      synthesizeKey(key, {});
+    } else {
+      sendString(key);
+    }
+  }
+}
+
+function test() {
+  var elem = document.getElementById("input");
+
+  for (let { keys, initialVal, expectedVal } of testData) {
+    elem.focus();
+    elem.value = initialVal;
+    sendKeys(keys);
+    elem.blur();
+    is(elem.value, expectedVal,
+       "Test with " + keys + ", result should be " + expectedVal);
+    elem.value = "";
+  }
+}
+
+</script>
+</pre>
+</body>
+</html>
--- a/dom/html/test/forms/test_input_typing_sanitization.html
+++ b/dom/html/test/forms/test_input_typing_sanitization.html
@@ -156,37 +156,16 @@ function runTest()
         '-',
         '2012-01',
         '2013-01-1',
         '1011-23-21',
         '1000-12-99',
       ]
     },
     {
-      type: 'time',
-      validData: [
-        '00:00',
-        '09:09:00',
-        '08:23:23.1',
-        '21:43:56.12',
-        '23:12:45.100',
-      ],
-      invalidData: [
-        '00:',
-        '00:00:',
-        '25:00',
-        '-00:00',
-        '00:00:00.',
-        '00:60',
-        '10:58:99',
-        ':19:10',
-        '23:08:09.1012',
-      ]
-    },
-    {
       type: 'month',
       validData: [
         '0001-01',
         '2012-12',
         '100000-01',
       ],
       invalidData: [
         '1-01',
--- a/dom/webidl/HTMLInputElement.webidl
+++ b/dom/webidl/HTMLInputElement.webidl
@@ -191,17 +191,17 @@ partial interface HTMLInputElement {
   // to update this list if nsIDOMNSEditableElement changes.
 
   [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.
-  [ChromeOnly]
+  [Func="IsChromeOrXBL", NeedsSubjectPrincipal]
   void setUserInput(DOMString input);
 };
 
 partial interface HTMLInputElement {
   [Pref="dom.input.dirpicker", SetterThrows]
   attribute boolean allowdirs;
 
   [Pref="dom.input.dirpicker"]
@@ -229,8 +229,33 @@ HTMLInputElement implements MozPhonetic;
 // Webkit/Blink
 partial interface HTMLInputElement {
   [Pref="dom.webkitBlink.filesystem.enabled", Frozen, Cached, Pure]
   readonly attribute sequence<FileSystemEntry> webkitEntries;
 
   [Pref="dom.webkitBlink.dirPicker.enabled", BinaryName="WebkitDirectoryAttr", SetterThrows]
           attribute boolean webkitdirectory;
 };
+
+dictionary DateTimeValue {
+  long hour;
+  long minute;
+};
+
+partial interface HTMLInputElement {
+  [Pref="dom.forms.datetime", ChromeOnly]
+  DateTimeValue getDateTimeInputBoxValue();
+
+  [Pref="dom.forms.datetime", ChromeOnly]
+  void updateDateTimeInputBox(optional DateTimeValue value);
+
+  [Pref="dom.forms.datetime", ChromeOnly]
+  void setDateTimePickerState(boolean open);
+
+  [Pref="dom.forms.datetime", Func="IsChromeOrXBL"]
+  void openDateTimePicker(optional DateTimeValue initialValue);
+
+  [Pref="dom.forms.datetime", Func="IsChromeOrXBL"]
+  void updateDateTimePicker(optional DateTimeValue value);
+
+  [Pref="dom.forms.datetime", Func="IsChromeOrXBL"]
+  void closeDateTimePicker();
+};
--- a/dom/xul/nsXULElement.cpp
+++ b/dom/xul/nsXULElement.cpp
@@ -856,17 +856,18 @@ nsXULElement::BindToTree(nsIDocument* aD
       // an <audio> or <video> element. This assertion is here to make sure
       // that we don't fail to notice if a change to bindings causes us to
       // start pulling in xul.css much more frequently. If this assertion
       // fails then we need to figure out why, and how we can continue to avoid
       // pulling in xul.css.
       // Note that add-ons may introduce bindings that cause this assertion to
       // fire.
       NS_ASSERTION(IsInVideoControls(this) ||
-                   IsInFeedSubscribeLine(this),
+                   IsInFeedSubscribeLine(this) ||
+                   IsXULElement(nsGkAtoms::datetimebox),
                    "Unexpected XUL element in non-XUL doc");
     }
   }
 
   if (aDocument) {
       NS_ASSERTION(!nsContentUtils::IsSafeToRunScript(),
                    "Missing a script blocker!");
       // We're in a document now.  Kick off the frame load.
--- a/layout/base/nsCSSFrameConstructor.cpp
+++ b/layout/base/nsCSSFrameConstructor.cpp
@@ -3649,18 +3649,22 @@ nsCSSFrameConstructor::FindInputData(Ele
     SIMPLE_INT_CREATE(NS_FORM_INPUT_PASSWORD, NS_NewTextControlFrame),
     { NS_FORM_INPUT_COLOR,
       FCDATA_WITH_WRAPPING_BLOCK(0, NS_NewColorControlFrame,
                                  nsCSSAnonBoxes::buttonContent) },
     // TODO: this is temporary until a frame is written: bug 635240.
     SIMPLE_INT_CREATE(NS_FORM_INPUT_NUMBER, NS_NewNumberControlFrame),
     // TODO: this is temporary until a frame is written: bug 888320.
     SIMPLE_INT_CREATE(NS_FORM_INPUT_DATE, NS_NewTextControlFrame),
-    // TODO: this is temporary until a frame is written: bug 888320
+#if defined(MOZ_WIDGET_ANDROID) || defined(MOZ_WIDGET_GONK)
+    // On Android/B2G, date/time input appears as a normal text box.
     SIMPLE_INT_CREATE(NS_FORM_INPUT_TIME, NS_NewTextControlFrame),
+#else
+    SIMPLE_INT_CREATE(NS_FORM_INPUT_TIME, NS_NewDateTimeControlFrame),
+#endif
     // TODO: this is temporary until a frame is written: bug 888320
     SIMPLE_INT_CREATE(NS_FORM_INPUT_MONTH, NS_NewTextControlFrame),
     // TODO: this is temporary until a frame is written: bug 888320
     SIMPLE_INT_CREATE(NS_FORM_INPUT_WEEK, NS_NewTextControlFrame),
     { NS_FORM_INPUT_SUBMIT,
       FCDATA_WITH_WRAPPING_BLOCK(0, NS_NewGfxButtonControlFrame,
                                  nsCSSAnonBoxes::buttonContent) },
     { NS_FORM_INPUT_RESET,
--- a/layout/forms/moz.build
+++ b/layout/forms/moz.build
@@ -17,16 +17,17 @@ EXPORTS += [
     'nsISelectControlFrame.h',
     'nsITextControlFrame.h',
 ]
 
 UNIFIED_SOURCES += [
     'nsButtonFrameRenderer.cpp',
     'nsColorControlFrame.cpp',
     'nsComboboxControlFrame.cpp',
+    'nsDateTimeControlFrame.cpp',
     'nsFieldSetFrame.cpp',
     'nsFileControlFrame.cpp',
     'nsFormControlFrame.cpp',
     'nsGfxButtonControlFrame.cpp',
     'nsGfxCheckboxControlFrame.cpp',
     'nsGfxRadioControlFrame.cpp',
     'nsHTMLButtonControlFrame.cpp',
     'nsImageControlFrame.cpp',
new file mode 100644
--- /dev/null
+++ b/layout/forms/nsDateTimeControlFrame.cpp
@@ -0,0 +1,414 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 frame type is used for input type=date, time, month, week, and
+ * datetime-local.
+ */
+
+#include "nsDateTimeControlFrame.h"
+
+#include "nsContentUtils.h"
+#include "nsFormControlFrame.h"
+#include "nsGkAtoms.h"
+#include "nsContentUtils.h"
+#include "nsContentCreatorFunctions.h"
+#include "nsContentList.h"
+#include "mozilla/dom/HTMLInputElement.h"
+#include "nsNodeInfoManager.h"
+#include "nsIDateTimeInputArea.h"
+#include "nsIObserverService.h"
+#include "nsIDOMHTMLInputElement.h"
+#include "nsIDOMMutationEvent.h"
+#include "jsapi.h"
+#include "nsJSUtils.h"
+#include "nsThreadUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+nsIFrame*
+NS_NewDateTimeControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext)
+{
+  return new (aPresShell) nsDateTimeControlFrame(aContext);
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsDateTimeControlFrame)
+
+NS_QUERYFRAME_HEAD(nsDateTimeControlFrame)
+  NS_QUERYFRAME_ENTRY(nsDateTimeControlFrame)
+  NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator)
+NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame)
+
+nsDateTimeControlFrame::nsDateTimeControlFrame(nsStyleContext* aContext)
+  : nsContainerFrame(aContext)
+{
+}
+
+void
+nsDateTimeControlFrame::DestroyFrom(nsIFrame* aDestructRoot)
+{
+  nsContentUtils::DestroyAnonymousContent(&mInputAreaContent);
+  nsContainerFrame::DestroyFrom(aDestructRoot);
+}
+
+void
+nsDateTimeControlFrame::UpdateInputBoxValue()
+{
+  nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
+    do_QueryInterface(mInputAreaContent);
+  if (inputAreaContent) {
+    inputAreaContent->NotifyInputElementValueChanged();
+  }
+}
+
+void
+nsDateTimeControlFrame::SetValueFromPicker(const DateTimeValue& aValue)
+{
+  nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
+    do_QueryInterface(mInputAreaContent);
+  if (inputAreaContent) {
+    AutoJSAPI api;
+    if (!api.Init(mContent->OwnerDoc()->GetScopeObject())) {
+      return;
+    }
+
+    JSObject* wrapper = mContent->GetWrapper();
+    if (!wrapper) {
+      return;
+    }
+
+    JSObject* scope = xpc::GetXBLScope(api.cx(), wrapper);
+    AutoJSAPI jsapi;
+    if (!scope || !jsapi.Init(scope)) {
+      return;
+    }
+
+    JS::Rooted<JS::Value> jsValue(jsapi.cx());
+    if (!ToJSValue(jsapi.cx(), aValue, &jsValue)) {
+      return;
+    }
+
+    inputAreaContent->SetValueFromPicker(jsValue);
+  }
+}
+
+void
+nsDateTimeControlFrame::SetPickerState(bool aOpen)
+{
+  nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
+    do_QueryInterface(mInputAreaContent);
+  if (inputAreaContent) {
+    inputAreaContent->SetPickerState(aOpen);
+  }
+}
+
+void
+nsDateTimeControlFrame::HandleFocusEvent()
+{
+  nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
+    do_QueryInterface(mInputAreaContent);
+  if (inputAreaContent) {
+    inputAreaContent->FocusInnerTextBox();
+  }
+}
+
+void
+nsDateTimeControlFrame::HandleBlurEvent()
+{
+  nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
+    do_QueryInterface(mInputAreaContent);
+  if (inputAreaContent) {
+    inputAreaContent->BlurInnerTextBox();
+  }
+}
+
+nscoord
+nsDateTimeControlFrame::GetMinISize(nsRenderingContext* aRenderingContext)
+{
+  nscoord result;
+  DISPLAY_MIN_WIDTH(this, result);
+
+  nsIFrame* kid = mFrames.FirstChild();
+  if (kid) { // display:none?
+    result = nsLayoutUtils::IntrinsicForContainer(aRenderingContext,
+                                                  kid,
+                                                  nsLayoutUtils::MIN_ISIZE);
+  } else {
+    result = 0;
+  }
+
+  return result;
+}
+
+nscoord
+nsDateTimeControlFrame::GetPrefISize(nsRenderingContext* aRenderingContext)
+{
+  nscoord result;
+  DISPLAY_PREF_WIDTH(this, result);
+
+  nsIFrame* kid = mFrames.FirstChild();
+  if (kid) { // display:none?
+    result = nsLayoutUtils::IntrinsicForContainer(aRenderingContext,
+                                                  kid,
+                                                  nsLayoutUtils::PREF_ISIZE);
+  } else {
+    result = 0;
+  }
+
+  return result;
+}
+
+void
+nsDateTimeControlFrame::Reflow(nsPresContext* aPresContext,
+                               ReflowOutput& aDesiredSize,
+                               const ReflowInput& aReflowInput,
+                               nsReflowStatus& aStatus)
+{
+  MarkInReflow();
+
+  DO_GLOBAL_REFLOW_COUNT("nsDateTimeControlFrame");
+  DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus);
+  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!");
+
+  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();
+
+  // Figure out our border-box sizes as well (by adding borderPadding to
+  // content-box sizes):
+  const nscoord borderBoxISize = contentBoxISize +
+    aReflowInput.ComputedLogicalBorderPadding().IStartEnd(myWM);
+
+  nscoord borderBoxBSize;
+  if (contentBoxBSize != NS_INTRINSICSIZE) {
+    borderBoxBSize = contentBoxBSize +
+      aReflowInput.ComputedLogicalBorderPadding().BStartEnd(myWM);
+  } // else, we'll figure out borderBoxBSize after we resolve contentBoxBSize.
+
+  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);
+
+    // Convert input area margin into my own writing-mode (in case it differs):
+    LogicalMargin childMargin =
+      childReflowOuput.ComputedLogicalMargin().ConvertTo(myWM, wm);
+
+    // offsets of input area frame within this frame:
+    LogicalPoint
+      childOffset(myWM,
+                  aReflowInput.ComputedLogicalBorderPadding().IStart(myWM) +
+                  childMargin.IStart(myWM),
+                  aReflowInput.ComputedLogicalBorderPadding().BStart(myWM) +
+                  childMargin.BStart(myWM));
+
+    nsReflowStatus childStatus;
+    // We initially reflow the child with a dummy containerSize; positioning
+    // will be fixed later.
+    const nsSize dummyContainerSize;
+    ReflowChild(inputAreaFrame, aPresContext, childDesiredSize,
+                childReflowOuput, myWM, childOffset, dummyContainerSize, 0,
+                childStatus);
+    MOZ_ASSERT(NS_FRAME_IS_FULLY_COMPLETE(childStatus),
+               "We gave our child unconstrained available block-size, "
+               "so it should be complete");
+
+    nscoord childMarginBoxBSize =
+      childDesiredSize.BSize(myWM) + childMargin.BStartEnd(myWM);
+
+    if (contentBoxBSize == NS_INTRINSICSIZE) {
+      // We are intrinsically sized -- we should shrinkwrap the input area's
+      // block-size:
+      contentBoxBSize = childMarginBoxBSize;
+
+      // Make sure we obey min/max-bsize in the case when we're doing intrinsic
+      // sizing (we get it for free when we have a non-intrinsic
+      // aReflowInput.ComputedBSize()).  Note that we do this before
+      // adjusting for borderpadding, since ComputedMaxBSize and
+      // ComputedMinBSize are content heights.
+      contentBoxBSize =
+        NS_CSS_MINMAX(contentBoxBSize,
+                      aReflowInput.ComputedMinBSize(),
+                      aReflowInput.ComputedMaxBSize());
+
+      borderBoxBSize = contentBoxBSize +
+        aReflowInput.ComputedLogicalBorderPadding().BStartEnd(myWM);
+    }
+
+    // Center child in block axis
+    nscoord extraSpace = contentBoxBSize - childMarginBoxBSize;
+    childOffset.B(myWM) += std::max(0, extraSpace / 2);
+
+    // Needed in FinishReflowChild, for logical-to-physical conversion:
+    nsSize borderBoxSize = LogicalSize(myWM, borderBoxISize, borderBoxBSize).
+                           GetPhysicalSize(myWM);
+
+    // Place the child
+    FinishReflowChild(inputAreaFrame, aPresContext, childDesiredSize,
+                      &childReflowOuput, myWM, childOffset, borderBoxSize, 0);
+
+    nsSize contentBoxSize =
+      LogicalSize(myWM, contentBoxISize, contentBoxBSize).
+        GetPhysicalSize(myWM);
+    aDesiredSize.SetBlockStartAscent(
+      childDesiredSize.BlockStartAscent() +
+      inputAreaFrame->BStart(aReflowInput.GetWritingMode(),
+                             contentBoxSize));
+  }
+
+  LogicalSize logicalDesiredSize(myWM, borderBoxISize, borderBoxBSize);
+  aDesiredSize.SetSize(myWM, logicalDesiredSize);
+
+  aDesiredSize.SetOverflowAreasToDesiredBounds();
+
+  if (inputAreaFrame) {
+    ConsiderChildOverflow(aDesiredSize.mOverflowAreas, inputAreaFrame);
+  }
+
+  FinishAndStoreOverflow(&aDesiredSize);
+
+  aStatus = NS_FRAME_COMPLETE;
+
+  NS_FRAME_TRACE(NS_FRAME_TRACE_CALLS,
+                 ("exit nsDateTimeControlFrame::Reflow: size=%d,%d",
+                  aDesiredSize.Width(), aDesiredSize.Height()));
+  NS_FRAME_SET_TRUNCATION(aStatus, aReflowInput, aDesiredSize);
+}
+
+nsresult
+nsDateTimeControlFrame::CreateAnonymousContent(nsTArray<ContentInfo>& aElements)
+{
+  // 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, nsIDOMNode::ELEMENT_NODE);
+  NS_ENSURE_TRUE(nodeInfo, NS_ERROR_OUT_OF_MEMORY);
+
+  NS_TrustedNewXULElement(getter_AddRefs(mInputAreaContent), nodeInfo.forget());
+  aElements.AppendElement(mInputAreaContent);
+
+  // Propogate our tabindex.
+  nsAutoString tabIndexStr;
+  if (mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::tabindex, tabIndexStr)) {
+    mInputAreaContent->SetAttr(kNameSpaceID_None, nsGkAtoms::tabindex,
+                               tabIndexStr, false);
+  }
+
+  // Propagate our readonly state.
+  nsAutoString readonly;
+  if (mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::readonly, readonly)) {
+    mInputAreaContent->SetAttr(kNameSpaceID_None, nsGkAtoms::readonly, readonly,
+                               false);
+  }
+
+  SyncDisabledState();
+
+  return NS_OK;
+}
+
+void
+nsDateTimeControlFrame::AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements,
+                                                 uint32_t aFilter)
+{
+  if (mInputAreaContent) {
+    aElements.AppendElement(mInputAreaContent);
+  }
+}
+
+void
+nsDateTimeControlFrame::SyncDisabledState()
+{
+  EventStates eventStates = mContent->AsElement()->State();
+  if (eventStates.HasState(NS_EVENT_STATE_DISABLED)) {
+    mInputAreaContent->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled,
+                               EmptyString(), true);
+  } else {
+    mInputAreaContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, true);
+  }
+}
+
+nsresult
+nsDateTimeControlFrame::AttributeChanged(int32_t aNameSpaceID,
+                                         nsIAtom* 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*>(mContent);
+      // 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->GetType() == NS_FORM_INPUT_TIME) {
+        if (aAttribute == nsGkAtoms::value) {
+          nsCOMPtr<nsIDateTimeInputArea> inputAreaContent =
+            do_QueryInterface(mInputAreaContent);
+          if (inputAreaContent) {
+            nsContentUtils::AddScriptRunner(NewRunnableMethod(inputAreaContent,
+              &nsIDateTimeInputArea::NotifyInputElementValueChanged));
+          }
+        } else {
+          if (aModType == nsIDOMMutationEvent::REMOVAL) {
+            mInputAreaContent->UnsetAttr(aNameSpaceID, aAttribute, true);
+          } else {
+            MOZ_ASSERT(aModType == nsIDOMMutationEvent::ADDITION ||
+                       aModType == nsIDOMMutationEvent::MODIFICATION);
+            nsAutoString value;
+            mContent->GetAttr(aNameSpaceID, aAttribute, value);
+            mInputAreaContent->SetAttr(aNameSpaceID, aAttribute, value, true);
+          }
+        }
+      }
+    }
+  }
+
+  return nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute,
+                                            aModType);
+}
+
+void
+nsDateTimeControlFrame::ContentStatesChanged(EventStates aStates)
+{
+  if (aStates.HasState(NS_EVENT_STATE_DISABLED)) {
+    nsContentUtils::AddScriptRunner(new SyncDisabledStateEvent(this));
+  }
+}
+
+nsIAtom*
+nsDateTimeControlFrame::GetType() const
+{
+  return nsGkAtoms::dateTimeControlFrame;
+}
new file mode 100644
--- /dev/null
+++ b/layout/forms/nsDateTimeControlFrame.h
@@ -0,0 +1,119 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 frame type is used for input type=date, time, month, week, and
+ * datetime-local.
+ *
+ * NOTE: some of the above-mentioned input types are still to-be-implemented.
+ * See nsCSSFrameConstructor::FindInputData, as well as bug 1286182 (date),
+ * bug 1306215 (month), bug 1306216 (week) and bug 1306217 (datetime-local).
+ */
+
+#ifndef nsDateTimeControlFrame_h__
+#define nsDateTimeControlFrame_h__
+
+#include "mozilla/Attributes.h"
+#include "nsContainerFrame.h"
+#include "nsIAnonymousContentCreator.h"
+#include "nsCOMPtr.h"
+
+namespace mozilla {
+namespace dom {
+struct DateTimeValue;
+} // namespace dom
+} // namespace mozilla
+
+class nsDateTimeControlFrame final : public nsContainerFrame,
+                                     public nsIAnonymousContentCreator
+{
+  typedef mozilla::dom::DateTimeValue DateTimeValue;
+
+  explicit nsDateTimeControlFrame(nsStyleContext* aContext);
+
+public:
+  friend nsIFrame* NS_NewDateTimeControlFrame(nsIPresShell* aPresShell,
+                                              nsStyleContext* aContext);
+
+  void ContentStatesChanged(mozilla::EventStates aStates) override;
+  void DestroyFrom(nsIFrame* aDestructRoot) override;
+
+  NS_DECL_QUERYFRAME_TARGET(nsDateTimeControlFrame)
+  NS_DECL_QUERYFRAME
+  NS_DECL_FRAMEARENA_HELPERS
+
+#ifdef DEBUG_FRAME_DUMP
+  nsresult GetFrameName(nsAString& aResult) const override {
+    return MakeFrameName(NS_LITERAL_STRING("DateTimeControl"), aResult);
+  }
+#endif
+
+  nsIAtom* GetType() const override;
+
+  bool IsFrameOfType(uint32_t aFlags) const override
+  {
+    return nsContainerFrame::IsFrameOfType(aFlags &
+      ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock));
+  }
+
+  // Reflow
+  nscoord GetMinISize(nsRenderingContext* aRenderingContext) override;
+
+  nscoord GetPrefISize(nsRenderingContext* aRenderingContext) override;
+
+  void Reflow(nsPresContext* aPresContext,
+              ReflowOutput& aDesiredSize,
+              const ReflowInput& aReflowState,
+              nsReflowStatus& aStatus) override;
+
+  // nsIAnonymousContentCreator
+  nsresult CreateAnonymousContent(nsTArray<ContentInfo>& aElements) override;
+  void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements,
+                                uint32_t aFilter) override;
+
+  nsresult AttributeChanged(int32_t aNameSpaceID, nsIAtom* aAttribute,
+                            int32_t aModType) override;
+
+  void UpdateInputBoxValue();
+  void SetValueFromPicker(const DateTimeValue& aValue);
+  void HandleFocusEvent();
+  void HandleBlurEvent();
+  void SetPickerState(bool aOpen);
+
+private:
+  class SyncDisabledStateEvent;
+  friend class SyncDisabledStateEvent;
+  class SyncDisabledStateEvent : public mozilla::Runnable
+  {
+  public:
+    explicit SyncDisabledStateEvent(nsDateTimeControlFrame* aFrame)
+    : mFrame(aFrame)
+    {}
+
+    NS_IMETHOD Run() override
+    {
+      nsDateTimeControlFrame* frame =
+        static_cast<nsDateTimeControlFrame*>(mFrame.GetFrame());
+      NS_ENSURE_STATE(frame);
+
+      frame->SyncDisabledState();
+      return NS_OK;
+    }
+
+  private:
+    nsWeakFrame mFrame;
+  };
+
+  /**
+   * Sync the disabled state of the anonymous children up with our content's.
+   */
+  void SyncDisabledState();
+
+  // Anonymous child which is bound via XBL to an element that wraps the input
+  // area and reset button.
+  nsCOMPtr<nsIContent> mInputAreaContent;
+};
+
+#endif // nsDateTimeControlFrame_h__
--- a/layout/generic/nsFrameIdList.h
+++ b/layout/generic/nsFrameIdList.h
@@ -14,16 +14,17 @@ FRAME_ID(nsBulletFrame)
 FRAME_ID(nsButtonBoxFrame)
 FRAME_ID(nsCanvasFrame)
 FRAME_ID(nsColorControlFrame)
 FRAME_ID(nsColumnSetFrame)
 FRAME_ID(nsComboboxControlFrame)
 FRAME_ID(nsComboboxDisplayFrame)
 FRAME_ID(nsContainerFrame)
 FRAME_ID(nsContinuingTextFrame)
+FRAME_ID(nsDateTimeControlFrame)
 FRAME_ID(nsDeckFrame)
 FRAME_ID(nsDocElementBoxFrame)
 FRAME_ID(nsFieldSetFrame)
 FRAME_ID(nsFileControlFrame)
 FRAME_ID(nsFirstLetterFrame)
 FRAME_ID(nsFirstLineFrame)
 FRAME_ID(nsFlexContainerFrame)
 FRAME_ID(nsFormControlFrame)
--- a/layout/generic/nsHTMLParts.h
+++ b/layout/generic/nsHTMLParts.h
@@ -166,16 +166,18 @@ NS_NewComboboxControlFrame(nsIPresShell*
 nsIFrame*
 NS_NewProgressFrame(nsIPresShell* aPresShell, nsStyleContext* aContext);
 nsIFrame*
 NS_NewMeterFrame(nsIPresShell* aPresShell, nsStyleContext* aContext);
 nsIFrame*
 NS_NewRangeFrame(nsIPresShell* aPresShell, nsStyleContext* aContext);
 nsIFrame*
 NS_NewNumberControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext);
+nsIFrame*
+NS_NewDateTimeControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext);
 nsBlockFrame*
 NS_NewDetailsFrame(nsIPresShell* aPresShell, nsStyleContext* aContext);
 
 // Table frame factories
 class nsTableWrapperFrame;
 nsTableWrapperFrame*
 NS_NewTableWrapperFrame(nsIPresShell* aPresShell, nsStyleContext* aContext);
 class nsTableFrame;
new file mode 100644
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/from-time-to-other-type-unthemed-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+  <body>
+    <input type="checkbox" style="-moz-appearance:none;">
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/from-time-to-other-type-unthemed.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+  <!-- Test: when switching to another type, the input element should look
+             like that type (not like an input time element) -->
+  <script type="text/javascript">
+    function setToCheckbox()
+    {
+      document.getElementById("i").type = "checkbox";
+      document.documentElement.className = "";
+    }
+    document.addEventListener("MozReftestInvalidate", setToCheckbox);
+  </script>
+  <body>
+    <input type="time" id="i" style="-moz-appearance:none;">
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/reftest.list
@@ -0,0 +1,13 @@
+default-preferences pref(dom.forms.datetime,true)
+
+# not valid on Android/B2G where type=time looks like type=text
+skip-if(Android||B2G||Mulet) != time-simple-unthemed.html time-simple-unthemed-ref.html
+skip-if(Android||B2G||Mulet) != time-large-font.html time-basic.html
+skip-if(Android||B2G||Mulet) != time-width-height.html time-basic.html
+skip-if(Android||B2G||Mulet) != time-border.html time-basic.html
+# only valid on Android/B2G where type=number looks the same as type=text
+skip-if(!Android&&!B2G&&!Mulet) == time-simple-unthemed.html time-simple-unthemed-ref.html
+
+# type change
+skip-if(Android||B2G||Mulet) == to-time-from-other-type-unthemed.html time-simple-unthemed.html
+skip-if(Android||B2G||Mulet) == from-time-to-other-type-unthemed.html from-time-to-other-type-unthemed-ref.html
new file mode 100644
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/time-basic.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+  <body>
+    <input type="time" value="12:30">
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/time-border.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+  <body>
+    <input type="time" value="12:30" style="border:10px solid blue">
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/time-large-font.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+  <body>
+    <input type="time" value="12:30" style="font-size: 32px;">
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/time-simple-unthemed-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+  <body>
+    <input type="text" style="-moz-appearance:none;">
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/time-simple-unthemed.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+  <body>
+    <input type="time" style="-moz-appearance:none;">
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/time-width-height.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+  <body>
+    <input type="time" style="width:200px; height:50px">
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/forms/input/datetime/to-time-from-other-type-unthemed.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+  <!-- Test: input element changed to time state doesn't look like checkbox state -->
+  <script type="text/javascript">
+    function setToTime()
+    {
+      document.getElementById("i").type = "time";
+      document.documentElement.className = "";
+    }
+    document.addEventListener("MozReftestInvalidate", setToTime);
+  </script>
+  <body>
+    <input type="checkbox" id="i" style="-moz-appearance:none;">
+  </body>
+</html>
--- a/layout/reftests/forms/input/reftest.list
+++ b/layout/reftests/forms/input/reftest.list
@@ -6,8 +6,9 @@ include url/reftest.list
 include number/reftest.list
 include file/reftest.list
 include radio/reftest.list
 include range/reftest.list
 include text/reftest.list
 include percentage/reftest.list
 include hidden/reftest.list
 include color/reftest.list
+include datetime/reftest.list
--- a/layout/style/res/html.css
+++ b/layout/style/res/html.css
@@ -762,16 +762,23 @@ audio:not([controls]) {
   transform: translate(0) !important;
 }
 
 video > .caption-box {
   position: relative;
   overflow: hidden;
 }
 
+/* datetime elements */
+
+input[type="time"] > xul|datetimebox {
+  display: flex;
+  -moz-binding: url("chrome://global/content/bindings/datetimebox.xml#time-input");
+}
+
 /* details & summary */
 /* Need to revert Bug 1259889 Part 2 when removing details preference. */
 @supports -moz-bool-pref("dom.details_element.enabled") {
   details > summary:first-of-type,
   details > summary:-moz-native-anonymous {
     display: list-item;
     list-style: disclosure-closed inside;
   }
--- a/toolkit/components/satchel/test/test_form_autocomplete.html
+++ b/toolkit/components/satchel/test/test_form_autocomplete.html
@@ -162,17 +162,17 @@ function setupFormHistory(aCallback) {
     { op : "add", fieldname : "field5", value : "123" },
     { op : "add", fieldname : "field5", value : "1234" },
     { op : "add", fieldname : "field6", value : "value" },
     { op : "add", fieldname : "field7", value : "value" },
     { op : "add", fieldname : "field8", value : "value" },
     { op : "add", fieldname : "field9", value : "value" },
     { op : "add", fieldname : "field10", value : "42" },
     { op : "add", fieldname : "field11", value : "2010-10-10" },
-    { op : "add", fieldname : "field12", value : "21:21" },
+    { op : "add", fieldname : "field12", value : "21:21" }, // not used, since type=time doesn't have autocomplete currently
     { op : "add", fieldname : "field13", value : "32" },  // not used, since type=range doesn't have a drop down menu
     { op : "add", fieldname : "field14", value : "#ffffff" }, // not used, since type=color doesn't have autocomplete currently
     { op : "add", fieldname : "field15", value : "2016-08" },
     { op : "add", fieldname : "field16", value : "2016-W32" },
     { op : "add", fieldname : "searchbar-history", value : "blacklist test" },
   ], aCallback);
 }
 
@@ -908,25 +908,23 @@ function runTest() {
     case 405:
         checkMenuEntries(["2010-10-10"]);
         doKey("down");
         doKey("return");
         checkForm("2010-10-10");
 
         input = $_(15, "field12");
         restoreForm();
-        expectPopup();
-        doKey("down");
+        waitForMenuChange(0);
         break;
 
     case 406:
-        checkMenuEntries(["21:21"]);
-        doKey("down");
-        doKey("return");
-        checkForm("21:21");
+        checkMenuEntries([]); // type=time with it's own control frame does not
+                              // have a drop down menu for now
+        checkForm("");
 
         input = $_(16, "field13");
         restoreForm();
         doKey("down");
         waitForMenuChange(0);
         break;
 
     case 407:
--- a/toolkit/content/browser-content.js
+++ b/toolkit/content/browser-content.js
@@ -1504,8 +1504,156 @@ let AutoCompletePopup = {
       results.push(result);
     }
 
     return results;
   }
 }
 
 AutoCompletePopup.init();
+
+/**
+ * DateTimePickerListener is the communication channel between the input box
+ * (content) for date/time input types and its picker (chrome).
+ */
+let DateTimePickerListener = {
+  /**
+   * On init, just listen for the event to open the picker, once the picker is
+   * opened, we'll listen for update and close events.
+   */
+  init: function() {
+    addEventListener("MozOpenDateTimePicker", this);
+    this._inputElement = null;
+
+    addEventListener("unload", () => {
+      this.uninit();
+    });
+  },
+
+  uninit: function() {
+    removeEventListener("MozOpenDateTimePicker", this);
+    this._inputElement = null;
+  },
+
+  /**
+   * Cleanup function called when picker is closed.
+   */
+  close: function() {
+    this.removeListeners();
+    this._inputElement.setDateTimePickerState(false);
+    this._inputElement = null;
+  },
+
+  /**
+   * Called after picker is opened to start listening for input box update
+   * events.
+   */
+  addListeners: function() {
+    addEventListener("MozUpdateDateTimePicker", this);
+    addEventListener("MozCloseDateTimePicker", this);
+    addEventListener("pagehide", this);
+
+    addMessageListener("FormDateTime:PickerValueChanged", this);
+    addMessageListener("FormDateTime:PickerClosed", this);
+  },
+
+  /**
+   * Stop listeneing for events when picker is closed.
+   */
+  removeListeners: function() {
+    removeEventListener("MozUpdateDateTimePicker", this);
+    removeEventListener("MozCloseDateTimePicker", this);
+    removeEventListener("pagehide", this);
+
+    removeMessageListener("FormDateTime:PickerValueChanged", this);
+    removeMessageListener("FormDateTime:PickerClosed", this);
+  },
+
+  /**
+   * Helper function that returns the CSS direction property of the element.
+   */
+  getComputedDirection: function(aElement) {
+    return aElement.ownerDocument.defaultView.getComputedStyle(aElement)
+      .getPropertyValue("direction");
+  },
+
+  /**
+   * Helper function that returns the rect of the element, which is the position
+   * in "screen" coordinates.
+   */
+  getBoundingContentRect: function(aElement) {
+    return BrowserUtils.getElementBoundingScreenRect(aElement);
+  },
+
+  /**
+   * nsIMessageListener.
+   */
+  receiveMessage: function(aMessage) {
+    switch (aMessage.name) {
+      case "FormDateTime:PickerClosed": {
+        this.close();
+        break;
+      }
+      case "FormDateTime:PickerValueChanged": {
+        this._inputElement.updateDateTimeInputBox(aMessage.data);
+        break;
+      }
+      default:
+        break;
+    }
+  },
+
+  /**
+   * nsIDOMEventListener, for chrome events sent by the input element and other
+   * DOM events.
+   */
+  handleEvent: function(aEvent) {
+    switch (aEvent.type) {
+      case "MozOpenDateTimePicker": {
+        if (!(aEvent.originalTarget instanceof content.HTMLInputElement)) {
+          return;
+        }
+        this._inputElement = aEvent.originalTarget;
+        this._inputElement.setDateTimePickerState(true);
+        this.addListeners();
+
+        let value = this._inputElement.getDateTimeInputBoxValue();
+        sendAsyncMessage("FormDateTime:OpenPicker", {
+          rect: this.getBoundingContentRect(this._inputElement),
+          dir: this.getComputedDirection(this._inputElement),
+          type: this._inputElement.type,
+          detail: {
+            // Pass partial value if it's available, otherwise pass input
+            // element's value.
+            value: Object.keys(value).length > 0 ? value
+                                                 : this._inputElement.value,
+            step: this._inputElement.step,
+            min: this._inputElement.min,
+            max: this._inputElement.max,
+          },
+        });
+        break;
+      }
+      case "MozUpdateDateTimePicker": {
+        let value = this._inputElement.getDateTimeInputBoxValue();
+        sendAsyncMessage("FormDateTime:UpdatePicker", { value });
+        break;
+      }
+      case "MozCloseDateTimePicker": {
+        sendAsyncMessage("FormDateTime:ClosePicker");
+        this.close();
+        break;
+      }
+      case "pagehide": {
+        if (this._inputElement &&
+            this._inputElement.ownerDocument == aEvent.target) {
+          sendAsyncMessage("FormDateTime:ClosePicker");
+          this.close();
+        }
+        break;
+      }
+      default:
+        break;
+    }
+  },
+}
+
+DateTimePickerListener.init();
--- a/toolkit/content/jar.mn
+++ b/toolkit/content/jar.mn
@@ -66,16 +66,18 @@ toolkit.jar:
    content/global/treeUtils.js
    content/global/viewZoomOverlay.js
    content/global/bindings/autocomplete.xml    (widgets/autocomplete.xml)
    content/global/bindings/browser.xml         (widgets/browser.xml)
    content/global/bindings/button.xml          (widgets/button.xml)
    content/global/bindings/checkbox.xml        (widgets/checkbox.xml)
    content/global/bindings/colorpicker.xml     (widgets/colorpicker.xml)
    content/global/bindings/datetimepicker.xml  (widgets/datetimepicker.xml)
+   content/global/bindings/datetimebox.xml     (widgets/datetimebox.xml)
+   content/global/bindings/datetimebox.css     (widgets/datetimebox.css)
 *  content/global/bindings/dialog.xml          (widgets/dialog.xml)
    content/global/bindings/editor.xml          (widgets/editor.xml)
    content/global/bindings/expander.xml        (widgets/expander.xml)
    content/global/bindings/filefield.xml       (widgets/filefield.xml)
 *  content/global/bindings/findbar.xml         (widgets/findbar.xml)
    content/global/bindings/general.xml         (widgets/general.xml)
    content/global/bindings/groupbox.xml        (widgets/groupbox.xml)
    content/global/bindings/listbox.xml         (widgets/listbox.xml)
--- a/toolkit/content/widgets/browser.xml
+++ b/toolkit/content/widgets/browser.xml
@@ -276,16 +276,20 @@
           return this._loadContext;
         ]]></getter>
       </property>
 
       <property name="autoCompletePopup"
                 onget="return document.getElementById(this.getAttribute('autocompletepopup'))"
                 readonly="true"/>
 
+      <property name="dateTimePicker"
+                onget="return document.getElementById(this.getAttribute('datetimepicker'))"
+                readonly="true"/>
+
       <property name="docShellIsActive">
         <getter>
           <![CDATA[
             return this.docShell && this.docShell.isActive;
           ]]>
         </getter>
         <setter>
           <![CDATA[
new file mode 100644
--- /dev/null
+++ b/toolkit/content/widgets/datetimebox.css
@@ -0,0 +1,44 @@
+/* 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");
+@namespace xul url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+.datetime-input-box-wrapper {
+  -moz-appearance: none;
+  display: inline-flex;
+  cursor: default;
+  background-color: inherit;
+  color: inherit;
+}
+
+.datetime-input {
+  -moz-appearance: none;
+  text-align: center;
+  padding: 0;
+  border: 0;
+  margin: 0;
+  ime-mode: disabled;
+}
+
+.datetime-separator {
+  margin: 0 !important;
+}
+
+.datetime-input[readonly],
+.datetime-input[disabled] {
+  color: GrayText;
+  -moz-user-select: none;
+}
+
+.datetime-reset-button {
+  background-image: url(chrome://global/skin/icons/input-clear.svg);
+  background-repeat: no-repeat;
+  background-size: 12px, 12px;
+  border: none;
+  height: 12px;
+  width: 12px;
+  align-self: center;
+  justify-content: flex-end;
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/content/widgets/datetimebox.xml
@@ -0,0 +1,806 @@
+<?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/. -->
+
+<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">
+
+  <binding id="time-input"
+           extends="chrome://global/content/bindings/datetimebox.xml#datetime-input-base">
+    <resources>
+      <stylesheet src="chrome://global/content/textbox.css"/>
+      <stylesheet src="chrome://global/skin/textbox.css"/>
+      <stylesheet src="chrome://global/content/bindings/datetimebox.css"/>
+    </resources>
+
+    <implementation>
+      <constructor>
+      <![CDATA[
+        // TODO: Bug 1301312 - localization for input type=time input.
+        this.mHour12 = true;
+        this.mAMIndicator = "AM";
+        this.mPMIndicator = "PM";
+        this.mPlaceHolder = "--";
+        this.mSeparatorText = ":";
+        this.mMillisecSeparatorText = ".";
+        this.mMaxLength = 2;
+        this.mMillisecMaxLength = 3;
+        this.mDefaultStep = 60 * 1000; // in milliseconds
+
+        this.mMinHourInHour12 = 1;
+        this.mMaxHourInHour12 = 12;
+        this.mMinMinute = 0;
+        this.mMaxMinute = 59;
+        this.mMinSecond = 0;
+        this.mMaxSecond = 59;
+        this.mMinMillisecond = 0;
+        this.mMaxMillisecond = 999;
+
+        this.mHourPageUpDownInterval = 3;
+        this.mMinSecPageUpDownInterval = 10;
+
+        this.mHourField =
+          document.getAnonymousElementByAttribute(this, "anonid", "input-one");
+        this.mHourField.setAttribute("typeBuffer", "");
+        this.mMinuteField =
+          document.getAnonymousElementByAttribute(this, "anonid", "input-two");
+        this.mMinuteField.setAttribute("typeBuffer", "");
+        this.mDayPeriodField =
+          document.getAnonymousElementByAttribute(this, "anonid", "input-three");
+        this.mDayPeriodField.classList.remove("numeric");
+
+        this.mHourField.placeholder = this.mPlaceHolder;
+        this.mMinuteField.placeholder = this.mPlaceHolder;
+        this.mDayPeriodField.placeholder = this.mPlaceHolder;
+
+        this.mHourField.setAttribute("min", this.mMinHourInHour12);
+        this.mHourField.setAttribute("max", this.mMaxHourInHour12);
+        this.mMinuteField.setAttribute("min", this.mMinMinute);
+        this.mMinuteField.setAttribute("max", this.mMaxMinute);
+
+        this.mMinuteSeparator =
+           document.getAnonymousElementByAttribute(this, "anonid", "sep-first");
+        this.mMinuteSeparator.textContent = this.mSeparatorText;
+        this.mSpaceSeparator =
+          document.getAnonymousElementByAttribute(this, "anonid", "sep-second");
+        // space between time and am/pm field
+        this.mSpaceSeparator.textContent = " ";
+
+        this.mSecondSeparator = null;
+        this.mSecondField = null;
+        this.mMillisecSeparator = null;
+        this.mMillisecField = null;
+
+        if (this.mInputElement.value) {
+          this.setFieldsFromInputValue();
+        }
+        ]]>
+      </constructor>
+
+      <method name="insertSeparator">
+        <parameter name="aSeparatorText"/>
+        <body>
+        <![CDATA[
+          let container = this.mHourField.parentNode;
+          const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+          let separator = document.createElementNS(HTML_NS, "span");
+          separator.textContent = aSeparatorText;
+          separator.setAttribute("class", "datetime-separator");
+          container.insertBefore(separator, this.mSpaceSeparator);
+
+          return separator;
+        ]]>
+        </body>
+      </method>
+
+      <method name="insertAdditionalField">
+        <parameter name="aPlaceHolder"/>
+        <parameter name="aMin"/>
+        <parameter name="aMax"/>
+        <parameter name="aSize"/>
+        <parameter name="aMaxLength"/>
+        <body>
+        <![CDATA[
+          let container = this.mHourField.parentNode;
+          const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+          let field = document.createElementNS(HTML_NS, "input");
+          field.classList.add("textbox-input", "datetime-input", "numeric");
+          field.setAttribute("size", aSize);
+          field.setAttribute("maxlength", aMaxLength);
+          field.setAttribute("min", aMin);
+          field.setAttribute("max", aMax);
+          field.setAttribute("typeBuffer", "");
+          field.disabled = this.mInputElement.disabled;
+          field.readOnly = this.mInputElement.readOnly;
+          field.tabIndex = this.mInputElement.tabIndex;
+          field.placeholder = aPlaceHolder;
+          container.insertBefore(field, this.mSpaceSeparator);
+
+          return field;
+        ]]>
+        </body>
+      </method>
+
+      <method name="setFieldsFromInputValue">
+        <body>
+        <![CDATA[
+          let value = this.mInputElement.value;
+          if (!value) {
+            this.clearInputFields(true);
+            return;
+          }
+
+          this.log("setFieldsFromInputValue: " + value);
+          let [hour, minute, second] = value.split(':');
+
+          this.setFieldValue(this.mHourField, hour);
+          this.setFieldValue(this.mMinuteField, minute);
+          if (this.mHour12) {
+            this.mDayPeriodField.value = (hour >= this.mMaxHourInHour12) ?
+              this.mPMIndicator : this.mAMIndicator;
+          }
+
+          if (!this.isEmpty(second)) {
+            let index = second.indexOf(".");
+            let millisecond;
+            if (index != -1) {
+              millisecond = second.substring(index + 1);
+              second = second.substring(0, index);
+            }
+
+            if (!this.mSecondField) {
+              this.mSecondSeparator = this.insertSeparator(this.mSeparatorText);
+              this.mSecondField = this.insertAdditionalField(this.mPlaceHolder,
+                this.mMinSecond, this.mMaxSecond, this.mMaxLength,
+                this.mMaxLength);
+            }
+            this.setFieldValue(this.mSecondField, second);
+
+            if (!this.isEmpty(millisecond)) {
+              if (!this.mMillisecField) {
+                this.mMillisecSeparator = this.insertSeparator(
+                  this.mMillisecSeparatorText);
+                this.mMillisecField = this.insertAdditionalField(
+                  this.mPlaceHolder, this.mMinMillisecond, this.mMaxMillisecond,
+                  this.mMillisecMaxLength, this.mMillisecMaxLength);
+              }
+              this.setFieldValue(this.mMillisecField, millisecond);
+            } else if (this.mMillisecField) {
+              this.mMillisecField.remove();
+              this.mMillisecField = null;
+
+              this.mMillisecSeparator.remove();
+              this.mMillisecSeparator = null;
+            }
+          } else {
+            if (this.mSecondField) {
+              this.mSecondField.remove();
+              this.mSecondField = null;
+
+              this.mSecondSeparator.remove();
+              this.mSecondSeparator = null;
+            }
+
+            if (this.mMillisecField) {
+              this.mMillisecField.remove();
+              this.mMillisecField = null;
+
+              this.mMillisecSeparator.remove();
+              this.mMillisecSeparator = null;
+            }
+          }
+          this.notifyPicker();
+        ]]>
+        </body>
+      </method>
+
+      <method name="setInputValueFromFields">
+        <body>
+        <![CDATA[
+          if (this.isEmpty(this.mHourField.value) ||
+              this.isEmpty(this.mMinuteField.value) ||
+              (this.mDayPeriodField && this.isEmpty(this.mDayPeriodField.value)) ||
+              (this.mSecondField && this.isEmpty(this.mSecondField.value))) {
+            // We still need to notify picker in case any of the field has
+            // changed. If we can set input element value, then notifyPicker
+            // will be called in setFieldsFromInputValue().
+            this.notifyPicker();
+            return;
+          }
+
+          let hour = Number(this.mHourField.value);
+          if (this.mHour12) {
+            let dayPeriod = this.mDayPeriodField.value;
+            if (dayPeriod == this.mPMIndicator &&
+                hour < this.mMaxHourInHour12) {
+              hour += this.mMaxHourInHour12;
+            } else if (dayPeriod == this.mAMIndicator &&
+                       hour == this.mMaxHourInHour12) {
+              hour = 0;
+            }
+          }
+
+          hour = (hour < 10) ? ("0" + hour) : hour;
+
+          let time = hour + ":" + this.mMinuteField.value;
+          if (this.mSecondField) {
+            time += ":" + this.mSecondField.value;
+          }
+
+          if (this.mMillisecField) {
+            time += "." + this.mMillisecField.value;
+          }
+
+          this.log("setInputValueFromFields: " + time);
+          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.mDayPeriodField.value =
+                (hour >= this.mMaxHourInHour12) ? this.mPMIndicator
+                                                : this.mAMIndicator;
+            }
+          }
+
+          if (!this.isEmpty(minute)) {
+            this.setFieldValue(this.mMinuteField, minute);
+          }
+        ]]>
+        </body>
+       </method>
+
+      <method name="clearInputFields">
+        <parameter name="aFromInputElement"/>
+        <body>
+        <![CDATA[
+          this.log("clearInputFields");
+
+          if (this.isDisabled() || this.isReadonly()) {
+            return;
+          }
+
+          if (this.mHourField && !this.mHourField.disabled &&
+              !this.mHourField.readOnly) {
+            this.mHourField.value = "";
+          }
+
+          if (this.mMinuteField && !this.mMinuteField.disabled &&
+              !this.mMinuteField.readOnly) {
+            this.mMinuteField.value = "";
+          }
+
+          if (this.mSecondField && !this.mSecondField.disabled &&
+              !this.mSecondField.readOnly) {
+            this.mSecondField.value = "";
+          }
+
+          if (this.mMillisecField && !this.mMillisecField.disabled &&
+              !this.mMillisecField.readOnly) {
+            this.mMillisecField.value = "";
+          }
+
+          if (this.mDayPeriodField && !this.mDayPeriodField.disabled &&
+              !this.mDayPeriodField.readOnly) {
+            this.mDayPeriodField.value = "";
+          }
+
+          if (!aFromInputElement) {
+            this.mInputElement.setUserInput("");
+          }
+        ]]>
+        </body>
+      </method>
+
+      <method name="incrementFieldValue">
+        <parameter name="aTargetField"/>
+        <parameter name="aTimes"/>
+        <body>
+        <![CDATA[
+          let value;
+
+          // Use current time if field is empty.
+          if (this.isEmpty(aTargetField.value)) {
+            let now = new Date();
+
+            if (aTargetField == this.mHourField) {
+              value = now.getHours() % this.mMaxHourInHour12 ||
+                this.mMaxHourInHour12;
+            } else if (aTargetField == this.mMinuteField) {
+              value = now.getMinutes();
+            } else if (aTargetField == this.mSecondField) {
+              value = now.getSeconds();
+            } else if (aTargetField == this.mMillisecondsField) {
+              value = now.getMilliseconds();
+            } else {
+              this.log("Field not supported in incrementFieldValue.");
+              return;
+            }
+          } else {
+            value = Number(aTargetField.value);
+          }
+
+          let min = aTargetField.getAttribute("min");
+          let max = aTargetField.getAttribute("max");
+
+          value += aTimes;
+          if (value > max) {
+            value -= (max - min + 1);
+          } else if (value < min) {
+            value += (max - min + 1);
+          }
+          this.setFieldValue(aTargetField, value);
+          aTargetField.select();
+        ]]>
+        </body>
+      </method>
+
+      <method name="handleKeyboardNav">
+        <parameter name="aEvent"/>
+        <body>
+        <![CDATA[
+          if (this.isDisabled() || this.isReadonly()) {
+            return;
+          }
+
+          let targetField = aEvent.originalTarget;
+          let key = aEvent.key;
+
+          if (this.mDayPeriodField &&
+              targetField == this.mDayPeriodField) {
+            // Home/End key does nothing on AM/PM field.
+            if (key == "Home" || key == "End") {
+              return;
+            }
+
+            this.mDayPeriodField.value =
+              this.mDayPeriodField.value == this.mAMIndicator ?
+                this.mPMIndicator : this.mAMIndicator;
+            this.mDayPeriodField.select();
+            this.setInputValueFromFields();
+            return;
+          }
+
+          switch (key) {
+            case "ArrowUp":
+              this.incrementFieldValue(targetField, 1);
+              break;
+            case "ArrowDown":
+              this.incrementFieldValue(targetField, -1);
+              break;
+            case "PageUp":
+              this.incrementFieldValue(targetField,
+                targetField == this.mHourField ? this.mHourPageUpDownInterval
+                                               : this.mMinSecPageUpDownInterval);
+              break;
+            case "PageDown":
+              this.incrementFieldValue(targetField,
+                targetField == this.mHourField ? (0 - this.mHourPageUpDownInterval)
+                                               : (0 - this.mMinSecPageUpDownInterval));
+              break;
+            case "Home":
+              let min = targetField.getAttribute("min");
+              this.setFieldValue(targetField, min);
+              targetField.select();
+              break;
+            case "End":
+              let max = targetField.getAttribute("max");
+              this.setFieldValue(targetField, max);
+              targetField.select();
+              break;
+          }
+          this.setInputValueFromFields();
+        ]]>
+        </body>
+      </method>
+
+      <method name="handleKeypress">
+        <parameter name="aEvent"/>
+        <body>
+        <![CDATA[
+          if (this.isDisabled() || this.isReadonly()) {
+            return;
+          }
+
+          let targetField = aEvent.originalTarget;
+          let key = aEvent.key;
+
+          if (this.mDayPeriodField &&
+              targetField == this.mDayPeriodField) {
+            if (key == "a" || key == "A") {
+              this.mDayPeriodField.value = this.mAMIndicator;
+              this.mDayPeriodField.select();
+            } else if (key == "p" || key == "P") {
+              this.mDayPeriodField.value = this.mPMIndicator;
+              this.mDayPeriodField.select();
+            }
+            return;
+          }
+
+          if (targetField.classList.contains("numeric") && key.match(/[0-9]/)) {
+            let buffer = targetField.getAttribute("typeBuffer") || "";
+
+            buffer = buffer.concat(key);
+            this.setFieldValue(targetField, buffer);
+            targetField.select();
+
+            let n = Number(buffer);
+            let max = targetField.getAttribute("max");
+            if (buffer.length >= targetField.maxLength || n * 10 > max) {
+              buffer = "";
+              this.advanceToNextField();
+            }
+            targetField.setAttribute("typeBuffer", buffer);
+          }
+        ]]>
+        </body>
+      </method>
+
+      <method name="setFieldValue">
+       <parameter name="aField"/>
+       <parameter name="aValue"/>
+        <body>
+        <![CDATA[
+          let value = Number(aValue);
+          if (isNaN(value)) {
+            this.log("NaN on setFieldValue!");
+            return;
+          }
+
+          if (aField.maxLength == this.mMaxLength) { // For hour, minute and second
+            if (aField == this.mHourField && this.mHour12) {
+              value = (value > this.mMaxHourInHour12) ?
+                value - this.mMaxHourInHour12 : value;
+              if (aValue == "00") {
+                value = this.mMaxHourInHour12;
+              }
+            }
+            // prepend zero
+            if (value < 10) {
+              value = "0" + value;
+            }
+          } else if (aField.maxLength == this.mMillisecMaxLength) {
+            // prepend zeroes
+            if (value < 10) {
+              value = "00" + value;
+            } else if (value < 100) {
+              value = "0" + value;
+            }
+          }
+
+          aField.value = value;
+        ]]>
+        </body>
+      </method>
+
+      <method name="isValueAvailable">
+        <body>
+        <![CDATA[
+          // Picker only cares about hour:minute.
+          return !this.isEmpty(this.mHourField.value) ||
+                 !this.isEmpty(this.mMinuteField.value);
+        ]]>
+        </body>
+      </method>
+
+      <method name="getCurrentValue">
+        <body>
+        <![CDATA[
+          let hour;
+          if (!this.isEmpty(this.mHourField.value)) {
+            hour = Number(this.mHourField.value);
+            if (this.mHour12) {
+              let dayPeriod = this.mDayPeriodField.value;
+              if (dayPeriod == this.mPMIndicator &&
+                  hour < this.mMaxHourInHour12) {
+                hour += this.mMaxHourInHour12;
+              } else if (dayPeriod == this.mAMIndicator &&
+                         hour == this.mMaxHourInHour12) {
+                hour = 0;
+              }
+            }
+           }
+
+          let minute;
+          if (!this.isEmpty(this.mMinuteField.value)) {
+            minute = Number(this.mMinuteField.value);
+          }
+
+          // Picker only needs hour/minute.
+          let time = { hour, minute };
+
+          this.log("getCurrentValue: " + JSON.stringify(time));
+          return time;
+        ]]>
+        </body>
+      </method>
+    </implementation>
+  </binding>
+
+  <binding id="datetime-input-base">
+    <resources>
+      <stylesheet src="chrome://global/content/textbox.css"/>
+      <stylesheet src="chrome://global/skin/textbox.css"/>
+      <stylesheet src="chrome://global/content/bindings/datetimebox.css"/>
+    </resources>
+
+    <content>
+      <html:div class="datetime-input-box-wrapper"
+                xbl:inherits="context,disabled,readonly">
+        <html:span>
+          <html:input anonid="input-one"
+                      class="textbox-input datetime-input numeric"
+                      size="2" maxlength="2"
+                      xbl:inherits="disabled,readonly,tabindex"/>
+          <html:span anonid="sep-first" class="datetime-separator"></html:span>
+          <html:input anonid="input-two"
+                      class="textbox-input datetime-input numeric"
+                      size="2" maxlength="2"
+                      xbl:inherits="disabled,readonly,tabindex"/>
+          <html:span anonid="sep-second" class="datetime-separator"></html:span>
+          <html:input anonid="input-three"
+                      class="textbox-input datetime-input numeric"
+                      size="2" maxlength="2"
+                      xbl:inherits="disabled,readonly,tabindex"/>
+        </html:span>
+
+        <html:button class="datetime-reset-button" anoid="reset-button"
+                     tabindex="-1" xbl:inherits="disabled"
+                     onclick="document.getBindingParent(this).clearInputFields(false);"/>
+      </html:div>
+    </content>
+
+    <implementation implements="nsIDateTimeInputArea">
+      <constructor>
+      <![CDATA[
+        this.DEBUG = false;
+        this.mInputElement = this.parentNode;
+
+        this.mMin = this.mInputElement.min;
+        this.mMax = this.mInputElement.max;
+        this.mStep = this.mInputElement.step;
+        this.mIsPickerOpen = false;
+      ]]>
+      </constructor>
+
+      <method name="log">
+        <parameter name="aMsg"/>
+        <body>
+        <![CDATA[
+          if (this.DEBUG) {
+            dump("[DateTimeBox] " + aMsg + "\n");
+          }
+        ]]>
+        </body>
+      </method>
+
+      <method name="focusInnerTextBox">
+        <body>
+        <![CDATA[
+          this.log("focusInnerTextBox");
+          document.getAnonymousElementByAttribute(this, "anonid", "input-one").focus();
+        ]]>
+        </body>
+      </method>
+
+      <method name="blurInnerTextBox">
+        <body>
+        <![CDATA[
+          this.log("blurInnerTextBox");
+          if (this.mLastFocusedField) {
+            this.mLastFocusedField.blur();
+          }
+        ]]>
+        </body>
+      </method>
+
+      <method name="notifyInputElementValueChanged">
+        <body>
+        <![CDATA[
+          this.log("inputElementValueChanged");
+          this.setFieldsFromInputValue();
+        ]]>
+        </body>
+      </method>
+
+      <method name="setValueFromPicker">
+        <parameter name="aValue"/>
+        <body>
+        <![CDATA[
+          this.setFieldsFromPicker(aValue);
+        ]]>
+        </body>
+      </method>
+
+      <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;
+          }
+
+          while (next) {
+            if (next.type == "text" && !next.disabled) {
+              next.focus();
+              break;
+            }
+            next = aReverse ? next.previousElementSibling
+                            : next.nextElementSibling;
+          }
+        ]]>
+        </body>
+      </method>
+
+      <method name="setPickerState">
+        <parameter name="aIsOpen"/>
+        <body>
+        <![CDATA[
+          this.log("picker is now " + (aIsOpen ? "opened" : "closed"));
+          this.mIsPickerOpen = aIsOpen;
+        ]]>
+        </body>
+      </method>
+
+      <method name="isEmpty">
+        <parameter name="aValue"/>
+        <body>
+          return (aValue == undefined || 0 === aValue.length);
+        </body>
+      </method>
+
+      <method name="clearInputFields">
+        <body>
+          throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+        </body>
+      </method>
+
+      <method name="setFieldsFromInputValue">
+        <body>
+          throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+        </body>
+      </method>
+
+      <method name="setInputValueFromFields">
+        <body>
+          throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+        </body>
+      </method>
+
+      <method name="setFieldsFromPicker">
+        <body>
+          throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+        </body>
+      </method>
+
+      <method name="handleKeypress">
+        <body>
+          throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+        </body>
+      </method>
+
+      <method name="handleKeyboardNav">
+        <body>
+          throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+        </body>
+      </method>
+
+      <method name="notifyPicker">
+        <body>
+        <![CDATA[
+          if (this.mIsPickerOpen && this.isValueAvailable()) {
+            this.mInputElement.updateDateTimePicker(this.getCurrentValue());
+          }
+        ]]>
+        </body>
+      </method>
+
+      <method name="isDisabled">
+        <body>
+        <![CDATA[
+          return this.hasAttribute("disabled");
+        ]]>
+        </body>
+      </method>
+
+      <method name="isReadonly">
+        <body>
+        <![CDATA[
+          return this.hasAttribute("readonly");
+        ]]>
+        </body>
+      </method>
+
+    </implementation>
+
+    <handlers>
+      <handler event="focus">
+      <![CDATA[
+        this.log("focus on: " + event.originalTarget);
+
+        let target = event.originalTarget;
+        if (target.type == "text") {
+          this.mLastFocusedField = target;
+          target.select();
+        }
+      ]]>
+      </handler>
+
+      <handler event="blur">
+      <![CDATA[
+        this.setInputValueFromFields();
+      ]]>
+      </handler>
+
+      <handler event="click">
+      <![CDATA[
+        // XXX: .originalTarget is not expected.
+        // When clicking on one of the inner text boxes, the .originalTarget is
+        // a HTMLDivElement and when clicking on the reset button, it's a
+        // HTMLButtonElement but it's not equal to our reset-button.
+        this.log("click on: " + event.originalTarget);
+        if (event.defaultPrevented || this.isDisabled() || this.isReadonly()) {
+          return;
+        }
+
+        if (!(event.originalTarget instanceof HTMLButtonElement)) {
+          this.mInputElement.openDateTimePicker(this.getCurrentValue());
+        }
+      ]]>
+      </handler>
+
+      <handler event="keypress" phase="capturing">
+      <![CDATA[
+        let key = event.key;
+        this.log("keypress: " + key);
+
+        if (key == "Backspace" || key == "Tab") {
+          return;
+        }
+
+        if (key == "Enter" || key == " ") {
+          // Close picker on Enter and Space.
+          this.mInputElement.closeDateTimePicker();
+        }
+
+        if (key == "ArrowUp" || key == "ArrowDown" ||
+            key == "PageUp" || key == "PageDown" ||
+            key == "Home" || key == "End") {
+          this.handleKeyboardNav(event);
+        } else if (key == "ArrowRight" || key == "ArrowLeft") {
+          this.advanceToNextField((key == "ArrowRight" ? false : true));
+        } else {
+          this.handleKeypress(event);
+        }
+
+        event.preventDefault();
+      ]]>
+      </handler>
+    </handlers>
+  </binding>
+
+</bindings>
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/DateTimePickerHelper.jsm
@@ -0,0 +1,165 @@
+/* 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";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+const DEBUG = false;
+function debug(aStr) {
+  if (DEBUG) {
+    dump("-*- DateTimePickerHelper: " + aStr + "\n");
+  }
+}
+
+this.EXPORTED_SYMBOLS = [
+  "DateTimePickerHelper"
+];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+/*
+ * DateTimePickerHelper receives message from content side (input box) and
+ * is reposible for opening, closing and updating the picker. Similary,
+ * DateTimePickerHelper listens for picker's events and notifies the content
+ * side (input box) about them.
+ */
+this.DateTimePickerHelper = {
+  picker: null,
+  weakBrowser: null,
+
+  MESSAGES: [
+    "FormDateTime:OpenPicker",
+    "FormDateTime:ClosePicker",
+    "FormDateTime:UpdatePicker"
+  ],
+
+  init: function() {
+    for (let msg of this.MESSAGES) {
+      Services.mm.addMessageListener(msg, this);
+    }
+  },
+
+  uninit: function() {
+    for (let msg of this.MESSAGES) {
+      Services.mm.removeMessageListener(msg, this);
+    }
+  },
+
+  // nsIMessageListener
+  receiveMessage: function(aMessage) {
+    debug("receiveMessage: " + aMessage.name);
+    switch (aMessage.name) {
+      case "FormDateTime:OpenPicker": {
+        this.showPicker(aMessage.target, aMessage.data);
+        break;
+      }
+      case "FormDateTime:ClosePicker": {
+        if (!this.picker) {
+          return;
+        }
+        this.picker.hidePopup();
+        break;
+      }
+      case "FormDateTime:UpdatePicker": {
+        let value = aMessage.data.value;
+        debug("Input box value is now: " + value.hour + ":" + value.minute);
+        // TODO: updating picker will be handled in Bug 1283384.
+        break;
+      }
+      default:
+        break;
+    }
+  },
+
+  // nsIDOMEventListener
+  handleEvent: function(aEvent) {
+    debug("handleEvent: " + aEvent.type);
+    switch (aEvent.type) {
+      case "DateTimePickerValueChanged": {
+        this.updateInputBoxValue(aEvent);
+        break;
+      }
+      case "popuphidden": {
+        let browser = this.weakBrowser ? this.weakBrowser.get() : null;
+        if (browser) {
+          browser.messageManager.sendAsyncMessage("FormDateTime:PickerClosed");
+        }
+        this.close();
+        break;
+      }
+      default:
+        break;
+    }
+  },
+
+  // Called when picker value has changed, notify input box about it.
+  updateInputBoxValue: function(aEvent) {
+    // TODO: parse data based on input type.
+    const { hour, minute } = aEvent.detail;
+    debug("hour: " + hour + ", minute: " + minute);
+    let browser = this.weakBrowser ? this.weakBrowser.get() : null;
+    if (browser) {
+      browser.messageManager.sendAsyncMessage(
+        "FormDateTime:PickerValueChanged", { hour, minute });
+    }
+  },
+
+  // Get picker from browser and show it anchored to the input box.
+  showPicker: function(aBrowser, aData) {
+    let rect = aData.rect;
+    let dir = aData.dir;
+    let type = aData.type;
+    let detail = aData.detail;
+    debug("Opening picker with details: " + JSON.stringify(detail));
+
+    let window = aBrowser.ownerDocument.defaultView;
+    let tabbrowser = window.gBrowser;
+    if (Services.focus.activeWindow != window ||
+        tabbrowser.selectedBrowser != aBrowser) {
+      // We were sent a message from a window or tab that went into the
+      // background, so we'll ignore it for now.
+      return;
+    }
+
+    this.weakBrowser = Cu.getWeakReference(aBrowser);
+    this.picker = aBrowser.dateTimePicker;
+    if (!this.picker) {
+      debug("aBrowser.dateTimePicker not found, exiting now.");
+      return;
+    }
+    this.picker.hidden = false;
+    this.picker.openPopupAtScreenRect("after_start", rect.left, rect.top,
+                                      rect.width, rect.height, false, false);
+    this.addPickerListeners();
+  },
+
+  // Picker is closed, do some cleanup.
+  close: function() {
+    this.removePickerListeners();
+    this.picker = null;
+    this.weakBrowser = null;
+  },
+
+  // Listen to picker's event.
+  addPickerListeners: function() {
+    if (!this.picker) {
+      return;
+    }
+    this.picker.addEventListener("popuphidden", this);
+    this.picker.addEventListener("DateTimePickerValueChanged", this);
+  },
+
+  // Stop listening to picker's event.
+  removePickerListeners: function() {
+    if (!this.picker) {
+      return;
+    }
+    this.picker.removeEventListener("popuphidden", this);
+    this.picker.removeEventListener("DateTimePickerValueChanged", this);
+  },
+};
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -30,16 +30,17 @@ EXTRA_JS_MODULES += [
     'BinarySearch.jsm',
     'BrowserUtils.jsm',
     'CanonicalJSON.jsm',
     'CertUtils.jsm',
     'CharsetMenu.jsm',
     'ClientID.jsm',
     'Color.jsm',
     'Console.jsm',
+    'DateTimePickerHelper.jsm',
     'debug.js',
     'DeferredTask.jsm',
     'Deprecated.jsm',
     'FileUtils.jsm',
     'Finder.jsm',
     'FinderHighlighter.jsm',
     'FinderIterator.jsm',
     'Geometry.jsm',
new file mode 100644
--- /dev/null
+++ b/toolkit/themes/shared/icons/input-clear.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 12 12" style="enable-background:new 0 0 12 12;" xml:space="preserve">
+<path id="Combined-Shape" d="M6,12c3.3,0,6-2.7,6-6S9.3,0,6,0S0,2.7,0,6S2.7,12,6,12z M9,8.1L8.1,9L6,6.9L3.9,9L3,8.1L5.1,6L3,3.9
+	L3.9,3L6,5.1L8.1,3L9,3.9L6.9,6L9,8.1z"/>
+</svg>
--- a/toolkit/themes/shared/jar.inc.mn
+++ b/toolkit/themes/shared/jar.inc.mn
@@ -18,16 +18,17 @@ toolkit.jar:
   skin/classic/global/aboutReader.css                      (../../shared/aboutReader.css)
   skin/classic/global/aboutReaderContent.css               (../../shared/aboutReaderContent.css)
 * skin/classic/global/aboutReaderControls.css              (../../shared/aboutReaderControls.css)
   skin/classic/global/aboutSupport.css                     (../../shared/aboutSupport.css)
   skin/classic/global/appPicker.css                        (../../shared/appPicker.css)
   skin/classic/global/config.css                           (../../shared/config.css)
   skin/classic/global/icons/find-arrows.svg                (../../shared/icons/find-arrows.svg)
   skin/classic/global/icons/info.svg                       (../../shared/incontent-icons/info.svg)
+  skin/classic/global/icons/input-clear.svg                (../../shared/icons/input-clear.svg)
   skin/classic/global/icons/loading.png                    (../../shared/icons/loading.png)
   skin/classic/global/icons/loading@2x.png                 (../../shared/icons/loading@2x.png)
   skin/classic/global/icons/warning.svg                    (../../shared/incontent-icons/warning.svg)
   skin/classic/global/icons/blocked.svg                    (../../shared/incontent-icons/blocked.svg)
   skin/classic/global/alerts/alert-common.css              (../../shared/alert-common.css)
   skin/classic/global/narrate.css                          (../../shared/narrate.css)
   skin/classic/global/narrateControls.css                  (../../shared/narrateControls.css)
   skin/classic/global/narrate/arrow.svg                    (../../shared/narrate/arrow.svg)