Merge inbound to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Fri, 07 Oct 2016 09:42:25 -0400
changeset 422188 ea8624a9b11e89b831b830cef70c47ae67ccead1
parent 422187 9c11fedf43675388a547f09cc4a64e78739f5220 (current diff)
parent 422127 01fd505d5cc2621881fa344d977b5054ad055f3b (diff)
child 422189 70758fd844ecca6f084414a52406dd58795d0715
child 422193 fc8b4536625e320d88539a81c48f9da53d24158d
child 422194 7b9d4fbab1beaf1539f8793d1ff4247350bde0fd
child 422256 49fe455cac957808ed4a5d1685c3a1938dac1d31
child 422295 0a08efd9a350d1979a532d223443555a7b924828
child 422564 6ec07eafa41f4eb5d8a3fc9ed8a165b46603b216
child 422821 bc9ffa3b7540da1ef8ab4981bf0e377307f39e11
child 425513 a37a32c3f44d4e5a88bfe7e3dbdac878baa47628
push id31701
push userbmo:james@hoppipolla.co.uk
push dateFri, 07 Oct 2016 14:21:46 +0000
reviewersmerge
milestone52.0a1
Merge inbound to m-c. a=merge
testing/web-platform/meta/encrypted-media/Google/encrypted-media-keystatuses.html.ini
--- 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/browser/components/originattributes/test/browser/browser.ini
+++ b/browser/components/originattributes/test/browser/browser.ini
@@ -24,8 +24,9 @@ support-files =
   worker_blobify.js
   worker_deblobify.js
 
 [browser_firstPartyIsolation.js]
 [browser_localStorageIsolation.js]
 [browser_blobURLIsolation.js]
 [browser_imageCacheIsolation.js]
 [browser_sharedworker.js]
+[browser_httpauth.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/originattributes/test/browser/browser_httpauth.js
@@ -0,0 +1,54 @@
+let Cu = Components.utils;
+let {HttpServer} = Cu.import("resource://testing-common/httpd.js", {});
+
+let server = new HttpServer();
+server.registerPathHandler('/file.html', fileHandler);
+server.start(-1);
+
+let BASE_URI = 'http://localhost:' + server.identity.primaryPort;
+let FILE_URI = BASE_URI + '/file.html';
+
+let credentialQueue = [];
+
+// Ask the user agent for authorization.
+function fileHandler(metadata, response) {
+  if (!metadata.hasHeader("Authorization")) {
+    response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+    response.setHeader("WWW-Authenticate", "Basic realm=\"User Visible Realm\"");
+    return;
+  }
+
+  // This will be "account:password" encoded in base64.
+  credentialQueue.push(metadata.getHeader("Authorization"));
+
+  response.setStatusLine(metadata.httpVersion, 200, "OK");
+  response.setHeader("Content-Type", "text/html", false);
+  let body = "<html><body></body></html>";
+  response.bodyOutputStream.write(body, body.length);
+}
+
+function onCommonDialogLoaded(subject) {
+  // Submit random account and password
+  let dialog = subject.Dialog;
+  dialog.ui.loginTextbox.setAttribute("value", Math.random());
+  dialog.ui.password1Textbox.setAttribute("value", Math.random());
+  dialog.ui.button0.click();
+}
+
+Services.obs.addObserver(onCommonDialogLoaded, "common-dialog-loaded", false);
+
+registerCleanupFunction(() => {
+  Services.obs.removeObserver(onCommonDialogLoaded, "common-dialog-loaded");
+  server.stop(() => {
+    server = null;
+  });
+});
+
+function getResult() {
+  // If two targets are isolated, they should get different credentials.
+  // Otherwise, the credentials will be cached and therefore the same.
+  return credentialQueue.shift();
+}
+
+IsolationTestTools.runTests(FILE_URI, getResult);
+
--- 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")
new file mode 100644
--- /dev/null
+++ b/dom/canvas/test/reftest/mozCurrentTransform-ref.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+<canvas id="canvas" width="150" height="150"></canvas>
+
+<script>
+
+var canvas = document.getElementById('canvas');
+var ctx = canvas.getContext('2d');
+ctx.transform(1,0.5,-0.5,1,30,10);
+ctx.fillStyle = '#f00';
+ctx.fillRect(0, 0, 100, 100);
+
+</script>
+</body></html>
new file mode 100644
--- /dev/null
+++ b/dom/canvas/test/reftest/mozCurrentTransform.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<canvas id="canvas" width="150" height="150"></canvas>
+
+<script>
+
+var canvas = document.getElementById('canvas');
+var ctx = canvas.getContext('2d');
+ctx.transform(1,0.5,-0.5,1,30,10);
+setTimeout(function() {
+  var canvas = document.getElementById('canvas');
+  var ctx = canvas.getContext('2d');
+  var transform = ctx.mozCurrentTransform;
+  ctx.mozCurrentTransform = transform;
+  ctx.fillStyle = '#f00';
+  ctx.fillRect(0, 0, 100, 100);
+  document.documentElement.removeAttribute("class");
+}, 10)
+
+</script>
+</body></html>
new file mode 100644
--- /dev/null
+++ b/dom/canvas/test/reftest/mozCurrentTransformInverse.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<canvas id="canvas" width="150" height="150"></canvas>
+
+<script>
+
+var canvas = document.getElementById('canvas');
+var ctx = canvas.getContext('2d');
+ctx.transform(1,0.5,-0.5,1,30,10);
+setTimeout(function() {
+  var canvas = document.getElementById('canvas');
+  var ctx = canvas.getContext('2d');
+  var transform = ctx.mozCurrentTransformInverse;
+  ctx.mozCurrentTransformInverse = transform;
+  ctx.fillStyle = '#f00';
+  ctx.fillRect(0, 0, 100, 100);
+  document.documentElement.removeAttribute("class");
+}, 10)
+
+</script>
+</body></html>
--- a/dom/canvas/test/reftest/reftest.list
+++ b/dom/canvas/test/reftest/reftest.list
@@ -161,8 +161,12 @@ pref(canvas.customfocusring.enabled,true
 
 # Check that captureStream() displays in a local video element
 == capturestream.html wrapper.html?green.png
 
 fuzzy-if(azureSkia,16,2) fuzzy-if(Android,3,40) fuzzy-if(/^Windows\x20NT\x2010\.0/.test(http.oscpu),1,1) == 1177726-text-stroke-bounds.html 1177726-text-stroke-bounds-ref.html
 
 # Canvas Filter Reftests
 include filters/reftest.list
+
+# Bug 1305963
+== mozCurrentTransform.html mozCurrentTransform-ref.html
+== mozCurrentTransformInverse.html mozCurrentTransform-ref.html
--- a/dom/html/HTMLImageElement.cpp
+++ b/dom/html/HTMLImageElement.cpp
@@ -644,16 +644,18 @@ HTMLImageElement::UnbindFromTree(bool aD
     nsIDocument* doc = GetOurOwnerDoc();
     MOZ_ASSERT(doc);
     if (doc) {
       doc->RemoveResponsiveContent(this);
       mInDocResponsiveContent = false;
     }
   }
 
+  mLastSelectedSource = nullptr;
+
   nsImageLoadingContent::UnbindFromTree(aDeep, aNullParent);
   nsGenericHTMLElement::UnbindFromTree(aDeep, aNullParent);
 }
 
 void
 HTMLImageElement::UpdateFormOwner()
 {
   if (!mForm) {
--- 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
new file mode 100644
--- /dev/null
+++ b/dom/html/reftests/image-load-shortcircuit-ref.html
@@ -0,0 +1,1 @@
+<div><img src=pass.png></div>
new file mode 100644
--- /dev/null
+++ b/dom/html/reftests/image-load-shortcircuit.html
@@ -0,0 +1,8 @@
+<html>
+<div></div>
+<script>
+  var d = (new DOMParser()).parseFromString("<img src=pass.png>", "text/html");
+  var n = d.adoptNode(d.querySelector('img'));
+  document.querySelector('div').appendChild(n);
+</script>
+</html>
--- a/dom/html/reftests/reftest.list
+++ b/dom/html/reftests/reftest.list
@@ -30,16 +30,17 @@ skip-if(Android||B2G) == 649134-2.html 6
 
 == bug448564-1_malformed.html bug448564-1_well-formed.html
 == bug448564-1_malformed.html bug448564-1_ideal.html
 
 == bug448564-4a.html          bug448564-4b.html
 == bug502168-1_malformed.html bug502168-1_well-formed.html
 
 == responsive-image-load-shortcircuit.html responsive-image-load-shortcircuit-ref.html
+== image-load-shortcircuit.html image-load-shortcircuit-ref.html
 
 # Test that image documents taken into account CSS properties like
 # image-orientation when determining the size of the image.
 # (Fuzzy necessary due to pixel-wise comparison of different JPEGs.
 # The vast majority of the fuzziness comes from Linux and WinXP.)
 fuzzy(1,149) == bug917595-iframe-1.html    bug917595-1-ref.html
 skip-if(B2G||Mulet) fuzzy-if((!B2G&&!Mulet),3,640) == bug917595-exif-rotated.jpg bug917595-pixel-rotated.jpg # bug 1060869 # Bug 1150490 disabling on Mulet as on B2G
 
--- 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/media/eme/CDMProxy.h
+++ b/dom/media/eme/CDMProxy.h
@@ -29,16 +29,43 @@ struct DecryptResult {
   DecryptResult(DecryptStatus aStatus, MediaRawData* aSample)
     : mStatus(aStatus)
     , mSample(aSample)
   {}
   DecryptStatus mStatus;
   RefPtr<MediaRawData> mSample;
 };
 
+class CDMKeyInfo {
+public:
+  explicit CDMKeyInfo(const nsTArray<uint8_t>& aKeyId)
+    : mKeyId(aKeyId)
+    , mStatus()
+  {}
+
+  CDMKeyInfo(const nsTArray<uint8_t>& aKeyId,
+             const dom::Optional<dom::MediaKeyStatus>& aStatus)
+    : mKeyId(aKeyId)
+    , mStatus(aStatus.Value())
+  {}
+
+  // The copy-ctor and copy-assignment operator for Optional<T> are declared as
+  // delete, so override CDMKeyInfo copy-ctor for nsTArray operations.
+  CDMKeyInfo(const CDMKeyInfo& aKeyInfo)
+  {
+    mKeyId = aKeyInfo.mKeyId;
+    if (aKeyInfo.mStatus.WasPassed()) {
+      mStatus.Construct(aKeyInfo.mStatus.Value());
+    }
+  }
+
+  nsTArray<uint8_t> mKeyId;
+  dom::Optional<dom::MediaKeyStatus> mStatus;
+};
+
 typedef int64_t UnixTime;
 
 // Proxies calls CDM, and proxies calls back.
 // Note: Promises are passed in via a PromiseId, so that the ID can be
 // passed via IPC to the CDM, which can then signal when to reject or
 // resolve the promise using its PromiseId.
 class CDMProxy {
 protected:
--- a/dom/media/eme/DecryptorProxyCallback.h
+++ b/dom/media/eme/DecryptorProxyCallback.h
@@ -36,21 +36,17 @@ public:
 
   virtual void SessionClosed(const nsCString& aSessionId) = 0;
 
   virtual void SessionError(const nsCString& aSessionId,
                             nsresult aException,
                             uint32_t aSystemCode,
                             const nsCString& aMessage) = 0;
 
-  virtual void KeyStatusChanged(const nsCString& aSessionId,
-                                const nsTArray<uint8_t>& aKeyId,
-                                mozilla::dom::MediaKeyStatus aStatus) = 0;
-
-  virtual void ForgetKeyStatus(const nsCString& aSessionId,
-                               const nsTArray<uint8_t>& aKeyId) = 0;
-
   virtual void Decrypted(uint32_t aId,
                          mozilla::DecryptStatus aResult,
                          const nsTArray<uint8_t>& aDecryptedData) = 0;
+
+  virtual void BatchedKeyStatusChanged(const nsCString& aSessionId,
+                                       const nsTArray<mozilla::CDMKeyInfo>& aKeyInfos) = 0;
 };
 
-#endif
\ No newline at end of file
+#endif
--- a/dom/media/gmp/GMPCDMCallbackProxy.cpp
+++ b/dom/media/gmp/GMPCDMCallbackProxy.cpp
@@ -275,49 +275,36 @@ GMPCDMCallbackProxy::SessionError(const 
                               aSessionId,
                               aException,
                               aSystemCode,
                               aMessage);
   NS_DispatchToMainThread(task);
 }
 
 void
-GMPCDMCallbackProxy::KeyStatusChanged(const nsCString& aSessionId,
-                                      const nsTArray<uint8_t>& aKeyId,
-                                      dom::MediaKeyStatus aStatus)
+GMPCDMCallbackProxy::BatchedKeyStatusChanged(const nsCString& aSessionId,
+                                             const nsTArray<CDMKeyInfo>& aKeyInfos)
 {
   MOZ_ASSERT(mProxy->IsOnOwnerThread());
-
-  KeyStatusChangedInternal(aSessionId,
-                           aKeyId,
-                           dom::Optional<dom::MediaKeyStatus>(aStatus));
+  BatchedKeyStatusChangedInternal(aSessionId, aKeyInfos);
 }
 
 void
-GMPCDMCallbackProxy::ForgetKeyStatus(const nsCString& aSessionId,
-                                     const nsTArray<uint8_t>& aKeyId)
-{
-  MOZ_ASSERT(mProxy->IsOnOwnerThread());
-
-  KeyStatusChangedInternal(aSessionId,
-                           aKeyId,
-                           dom::Optional<dom::MediaKeyStatus>());
-}
-
-void
-GMPCDMCallbackProxy::KeyStatusChangedInternal(const nsCString& aSessionId,
-                                              const nsTArray<uint8_t>& aKeyId,
-                                              const dom::Optional<dom::MediaKeyStatus>& aStatus)
+GMPCDMCallbackProxy::BatchedKeyStatusChangedInternal(const nsCString& aSessionId,
+                                                     const nsTArray<CDMKeyInfo>& aKeyInfos)
 {
   bool keyStatusesChange = false;
   {
     CDMCaps::AutoLock caps(mProxy->Capabilites());
-    keyStatusesChange = caps.SetKeyStatus(aKeyId,
-                                          NS_ConvertUTF8toUTF16(aSessionId),
-                                          aStatus);
+    for (size_t i = 0; i < aKeyInfos.Length(); i++) {
+      keyStatusesChange |=
+        caps.SetKeyStatus(aKeyInfos[i].mKeyId,
+                          NS_ConvertUTF8toUTF16(aSessionId),
+                          aKeyInfos[i].mStatus);
+    }
   }
   if (keyStatusesChange) {
     nsCOMPtr<nsIRunnable> task;
     task = NewRunnableMethod<nsString>(mProxy,
                                        &CDMProxy::OnKeyStatusesChange,
                                        NS_ConvertUTF8toUTF16(aSessionId));
     NS_DispatchToMainThread(task);
   }
--- a/dom/media/gmp/GMPCDMCallbackProxy.h
+++ b/dom/media/gmp/GMPCDMCallbackProxy.h
@@ -38,37 +38,32 @@ public:
 
   void SessionClosed(const nsCString& aSessionId) override;
 
   void SessionError(const nsCString& aSessionId,
                     nsresult aException,
                     uint32_t aSystemCode,
                     const nsCString& aMessage) override;
 
-  void KeyStatusChanged(const nsCString& aSessionId,
-                        const nsTArray<uint8_t>& aKeyId,
-                        dom::MediaKeyStatus aStatus) override;
-
-  void ForgetKeyStatus(const nsCString& aSessionId,
-                       const nsTArray<uint8_t>& aKeyId) override;
-
   void Decrypted(uint32_t aId,
                  DecryptStatus aResult,
                  const nsTArray<uint8_t>& aDecryptedData) override;
 
+  void BatchedKeyStatusChanged(const nsCString& aSessionId,
+                               const nsTArray<CDMKeyInfo>& aKeyInfos) override;
+
   void Terminated() override;
 
   ~GMPCDMCallbackProxy() {}
 
 private:
   friend class GMPCDMProxy;
   explicit GMPCDMCallbackProxy(CDMProxy* aProxy);
-  void KeyStatusChangedInternal(const nsCString& aSessionId,
-                                const nsTArray<uint8_t>& aKeyId,
-                                const dom::Optional<dom::MediaKeyStatus>& aStatus);
 
+  void BatchedKeyStatusChangedInternal(const nsCString& aSessionId,
+                                       const nsTArray<CDMKeyInfo>& aKeyInfos);
   // Warning: Weak ref.
   CDMProxy* mProxy;
 };
 
 } // namespace mozilla
 
 #endif // GMPCDMCallbackProxy_h_
--- a/dom/media/gmp/GMPDecryptorChild.cpp
+++ b/dom/media/gmp/GMPDecryptorChild.cpp
@@ -156,26 +156,39 @@ void
 GMPDecryptorChild::KeyStatusChanged(const char* aSessionId,
                                     uint32_t aSessionIdLength,
                                     const uint8_t* aKeyId,
                                     uint32_t aKeyIdLength,
                                     GMPMediaKeyStatus aStatus)
 {
   AutoTArray<uint8_t, 16> kid;
   kid.AppendElements(aKeyId, aKeyIdLength);
-  if (aStatus == kGMPUnknown) {
-    CALL_ON_GMP_THREAD(SendForgetKeyStatus,
-                       nsCString(aSessionId, aSessionIdLength),
-                       kid);
-  } else {
-    CALL_ON_GMP_THREAD(SendKeyStatusChanged,
-                       nsCString(aSessionId, aSessionIdLength),
-                       kid,
-                       aStatus);
+
+  nsTArray<GMPKeyInformation> keyInfos;
+  keyInfos.AppendElement(GMPKeyInformation(kid, aStatus));
+  CALL_ON_GMP_THREAD(SendBatchedKeyStatusChanged,
+                     nsCString(aSessionId, aSessionIdLength),
+                     keyInfos);
+}
+
+void
+GMPDecryptorChild::BatchedKeyStatusChanged(const char* aSessionId,
+                                           uint32_t aSessionIdLength,
+                                           const GMPMediaKeyInfo* aKeyInfos,
+                                           uint32_t aKeyInfosLength)
+{
+  nsTArray<GMPKeyInformation> keyInfos;
+  for (uint32_t i = 0; i < aKeyInfosLength; i++) {
+    nsTArray<uint8_t> keyId;
+    keyId.AppendElements(aKeyInfos[i].keyid, aKeyInfos[i].keyid_size);
+    keyInfos.AppendElement(GMPKeyInformation(keyId, aKeyInfos[i].status));
   }
+  CALL_ON_GMP_THREAD(SendBatchedKeyStatusChanged,
+                     nsCString(aSessionId, aSessionIdLength),
+                     keyInfos);
 }
 
 void
 GMPDecryptorChild::Decrypted(GMPBuffer* aBuffer, GMPErr aResult)
 {
   if (!ON_GMP_THREAD()) {
     // We should run this whole method on the GMP thread since the buffer needs
     // to be deleted after the SendDecrypted call.
--- a/dom/media/gmp/GMPDecryptorChild.h
+++ b/dom/media/gmp/GMPDecryptorChild.h
@@ -68,16 +68,21 @@ public:
                         const uint8_t* aKeyId,
                         uint32_t aKeyIdLength,
                         GMPMediaKeyStatus aStatus) override;
 
   void SetCapabilities(uint64_t aCaps) override;
 
   void Decrypted(GMPBuffer* aBuffer, GMPErr aResult) override;
 
+  void BatchedKeyStatusChanged(const char* aSessionId,
+                               uint32_t aSessionIdLength,
+                               const GMPMediaKeyInfo* aKeyInfos,
+                               uint32_t aKeyInfosLength) override;
+
   // GMPDecryptorHost
   void GetSandboxVoucher(const uint8_t** aVoucher,
                          uint32_t* aVoucherLength) override;
 
   void GetPluginVoucher(const uint8_t** aVoucher,
                         uint32_t* aVoucherLength) override;
 private:
   ~GMPDecryptorChild();
--- a/dom/media/gmp/GMPDecryptorParent.cpp
+++ b/dom/media/gmp/GMPDecryptorParent.cpp
@@ -353,38 +353,37 @@ ToMediaKeyStatus(GMPMediaKeyStatus aStat
     case kGMPInternalError: return dom::MediaKeyStatus::Internal_error;
     case kGMPReleased: return dom::MediaKeyStatus::Released;
     case kGMPStatusPending: return dom::MediaKeyStatus::Status_pending;
     default: return dom::MediaKeyStatus::Internal_error;
   }
 }
 
 bool
-GMPDecryptorParent::RecvKeyStatusChanged(const nsCString& aSessionId,
-                                         InfallibleTArray<uint8_t>&& aKeyId,
-                                         const GMPMediaKeyStatus& aStatus)
+GMPDecryptorParent::RecvBatchedKeyStatusChanged(const nsCString& aSessionId,
+                                                InfallibleTArray<GMPKeyInformation>&& aKeyInfos)
 {
-  LOGD(("GMPDecryptorParent[%p]::RecvKeyStatusChanged(sessionId='%s', keyId=%s, status=%d)",
-        this, aSessionId.get(), ToBase64(aKeyId).get(), aStatus));
+  LOGD(("GMPDecryptorParent[%p]::RecvBatchedKeyStatusChanged(sessionId='%s', KeyInfos len='%d')",
+        this, aSessionId.get(), aKeyInfos.Length()));
 
   if (mIsOpen) {
-    mCallback->KeyStatusChanged(aSessionId, aKeyId, ToMediaKeyStatus(aStatus));
-  }
-  return true;
-}
-
-bool
-GMPDecryptorParent::RecvForgetKeyStatus(const nsCString& aSessionId,
-                                        InfallibleTArray<uint8_t>&& aKeyId)
-{
-  LOGD(("GMPDecryptorParent[%p]::RecvForgetKeyStatus(sessionId='%s', keyId=%s)",
-        this, aSessionId.get(), ToBase64(aKeyId).get()));
-
-  if (mIsOpen) {
-    mCallback->ForgetKeyStatus(aSessionId, aKeyId);
+    nsTArray<CDMKeyInfo> cdmKeyInfos(aKeyInfos.Length());
+    for (uint32_t i = 0; i < aKeyInfos.Length(); i++) {
+      LOGD(("GMPDecryptorParent[%p]::RecvBatchedKeyStatusChanged(keyId=%s, gmp-status=%d)",
+            this, ToBase64(aKeyInfos[i].keyId()).get(), aKeyInfos[i].status()));
+      // If the status is kGMPUnknown, we're going to forget(remove) that key info.
+      if (aKeyInfos[i].status() != kGMPUnknown) {
+        auto status = ToMediaKeyStatus(aKeyInfos[i].status());
+        cdmKeyInfos.AppendElement(CDMKeyInfo(aKeyInfos[i].keyId(),
+                                             dom::Optional<dom::MediaKeyStatus>(status)));
+      } else {
+        cdmKeyInfos.AppendElement(CDMKeyInfo(aKeyInfos[i].keyId()));
+      }
+    }
+    mCallback->BatchedKeyStatusChanged(aSessionId, cdmKeyInfos);
   }
   return true;
 }
 
 DecryptStatus
 ToDecryptStatus(GMPErr aError)
 {
   switch (aError) {
--- a/dom/media/gmp/GMPDecryptorParent.h
+++ b/dom/media/gmp/GMPDecryptorParent.h
@@ -93,27 +93,23 @@ private:
 
   bool RecvSessionClosed(const nsCString& aSessionId) override;
 
   bool RecvSessionError(const nsCString& aSessionId,
                         const GMPDOMException& aException,
                         const uint32_t& aSystemCode,
                         const nsCString& aMessage) override;
 
-  bool RecvKeyStatusChanged(const nsCString& aSessionId,
-                            InfallibleTArray<uint8_t>&& aKeyId,
-                            const GMPMediaKeyStatus& aStatus) override;
-
-  bool RecvForgetKeyStatus(const nsCString& aSessionId,
-                           InfallibleTArray<uint8_t>&& aKeyId) override;
-
   bool RecvDecrypted(const uint32_t& aId,
                      const GMPErr& aErr,
                      InfallibleTArray<uint8_t>&& aBuffer) override;
 
+  bool RecvBatchedKeyStatusChanged(const nsCString& aSessionId,
+                                   InfallibleTArray<GMPKeyInformation>&& aKeyInfos) override;
+
   bool RecvShutdown() override;
 
   void ActorDestroy(ActorDestroyReason aWhy) override;
   bool Recv__delete__() override;
 
   bool mIsOpen;
   bool mShuttingDown;
   bool mActorDestroyed;
--- a/dom/media/gmp/GMPTypes.ipdlh
+++ b/dom/media/gmp/GMPTypes.ipdlh
@@ -1,15 +1,16 @@
 /* -*- 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/. */
 
 using GMPBufferType from "gmp-video-codec.h";
 using GMPAudioCodecType from "gmp-audio-codec.h";
+using GMPMediaKeyStatus from "gmp-decryption.h";
 
 namespace mozilla {
 namespace gmp {
 
 struct GMPDecryptionData {
   uint8_t[] mKeyId;
   uint8_t[] mIV;
   uint16_t[] mClearBytes;
@@ -71,10 +72,15 @@ struct GMPAudioEncodedSampleData
 struct GMPAudioDecodedSampleData
 {
   int16_t[] mData;
   uint64_t mTimeStamp; // microseconds.
   uint32_t mChannelCount;
   uint32_t mSamplesPerSecond;
 };
 
+struct GMPKeyInformation {
+  uint8_t[] keyId;
+  GMPMediaKeyStatus status;
+};
+
 }
 }
--- a/dom/media/gmp/PGMPDecryptor.ipdl
+++ b/dom/media/gmp/PGMPDecryptor.ipdl
@@ -2,17 +2,16 @@
 /* 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 protocol PGMPContent;
 include GMPTypes;
 
 using GMPSessionMessageType from  "gmp-decryption.h";
-using GMPMediaKeyStatus from  "gmp-decryption.h";
 using GMPSessionType from  "gmp-decryption.h";
 using GMPDOMException from "gmp-decryption.h";
 using GMPErr from "gmp-errors.h";
 
 namespace mozilla {
 namespace gmp {
 
 async protocol PGMPDecryptor
@@ -74,20 +73,18 @@ parent:
 
   async SessionClosed(nsCString aSessionId);
 
   async SessionError(nsCString aSessionId,
                      GMPDOMException aDOMExceptionCode,
                      uint32_t aSystemCode,
                      nsCString aMessage);
 
-  async KeyStatusChanged(nsCString aSessionId, uint8_t[] aKey,
-                         GMPMediaKeyStatus aStatus);
-
-  async ForgetKeyStatus(nsCString aSessionId, uint8_t[] aKey);
-
   async Decrypted(uint32_t aId, GMPErr aResult, uint8_t[] aBuffer);
 
   async Shutdown();
+
+  async BatchedKeyStatusChanged(nsCString aSessionId,
+                                GMPKeyInformation[] aKeyInfos);
 };
 
 } // namespace gmp
 } // namespace mozilla
--- a/dom/media/gmp/gmp-api/gmp-decryption.h
+++ b/dom/media/gmp/gmp-api/gmp-decryption.h
@@ -97,16 +97,30 @@ enum GMPMediaKeyStatus {
   kGMPOutputRestricted = 3,
   kGMPInternalError = 4,
   kGMPUnknown = 5, // Removes key from MediaKeyStatusMap
   kGMPReleased = 6,
   kGMPStatusPending = 7,
   kGMPMediaKeyStatusInvalid = 8 // Must always be last.
 };
 
+struct GMPMediaKeyInfo {
+  GMPMediaKeyInfo() {}
+  GMPMediaKeyInfo(const uint8_t* aKeyId,
+                  uint32_t aKeyIdSize,
+                  GMPMediaKeyStatus aStatus)
+    : keyid(aKeyId)
+    , keyid_size(aKeyIdSize)
+    , status(aStatus)
+  {}
+  const uint8_t* keyid;
+  uint32_t keyid_size;
+  GMPMediaKeyStatus status;
+};
+
 // Time in milliseconds, as offset from epoch, 1 Jan 1970.
 typedef int64_t GMPTimestamp;
 
 // Callbacks to be called from the CDM. Threadsafe.
 class GMPDecryptorCallback {
 public:
 
   // The GMPDecryptor should call this in response to a call to
@@ -187,16 +201,22 @@ public:
                                 GMPMediaKeyStatus aStatus) = 0;
 
   // DEPRECATED; this function has no affect.
   virtual void SetCapabilities(uint64_t aCaps) = 0;
 
   // Returns decrypted buffer to Gecko, or reports failure.
   virtual void Decrypted(GMPBuffer* aBuffer, GMPErr aResult) = 0;
 
+  // To aggregate KeyStatusChanged into single callback per session id.
+  virtual void BatchedKeyStatusChanged(const char* aSessionId,
+                                       uint32_t aSessionIdLength,
+                                       const GMPMediaKeyInfo* aKeyInfos,
+                                       uint32_t aKeyInfosLength) = 0;
+
   virtual ~GMPDecryptorCallback() {}
 };
 
 // Host interface, passed to GetAPIFunc(), with "decrypt".
 class GMPDecryptorHost {
 public:
   virtual void GetSandboxVoucher(const uint8_t** aVoucher,
                                  uint32_t* aVoucherLength) = 0;
--- a/dom/media/gmp/widevine-adapter/WidevineDecryptor.cpp
+++ b/dom/media/gmp/widevine-adapter/WidevineDecryptor.cpp
@@ -404,23 +404,25 @@ WidevineDecryptor::OnSessionKeysChange(c
                                        const KeyInformation* aKeysInfo,
                                        uint32_t aKeysInfoCount)
 {
   if (!mCallback) {
     Log("Decryptor::OnSessionKeysChange() FAIL; !mCallback");
     return;
   }
   Log("Decryptor::OnSessionKeysChange()");
+
+  nsTArray<GMPMediaKeyInfo> key_infos;
   for (uint32_t i = 0; i < aKeysInfoCount; i++) {
-    mCallback->KeyStatusChanged(aSessionId,
-                                aSessionIdSize,
-                                aKeysInfo[i].key_id,
-                                aKeysInfo[i].key_id_size,
-                                ToGMPKeyStatus(aKeysInfo[i].status));
+    key_infos.AppendElement(GMPMediaKeyInfo(aKeysInfo[i].key_id,
+                                            aKeysInfo[i].key_id_size,
+                                            ToGMPKeyStatus(aKeysInfo[i].status)));
   }
+  mCallback->BatchedKeyStatusChanged(aSessionId, aSessionIdSize,
+                                     key_infos.Elements(), key_infos.Length());
 }
 
 static GMPTimestamp
 ToGMPTime(Time aCDMTime)
 {
   return static_cast<GMPTimestamp>(aCDMTime * 1000);
 }
 
--- a/dom/media/gtest/TestGMPCrossOrigin.cpp
+++ b/dom/media/gtest/TestGMPCrossOrigin.cpp
@@ -1383,26 +1383,23 @@ class GMPStorageTest : public GMPDecrypt
                      const nsCString& aSessionId) override { }
   void ExpirationChange(const nsCString& aSessionId,
                         UnixTime aExpiryTime) override {}
   void SessionClosed(const nsCString& aSessionId) override {}
   void SessionError(const nsCString& aSessionId,
                     nsresult aException,
                     uint32_t aSystemCode,
                     const nsCString& aMessage) override {}
-  void KeyStatusChanged(const nsCString& aSessionId,
-                        const nsTArray<uint8_t>& aKeyId,
-                        mozilla::dom::MediaKeyStatus aStatus) override { }
-
-  void ForgetKeyStatus(const nsCString& aSessionId,
-                       const nsTArray<uint8_t>& aKeyId) override { }
-
   void Decrypted(uint32_t aId,
                  mozilla::DecryptStatus aResult,
                  const nsTArray<uint8_t>& aDecryptedData) override { }
+
+  void BatchedKeyStatusChanged(const nsCString& aSessionId,
+                               const nsTArray<CDMKeyInfo>& aKeyInfos) override { }
+
   void Terminated() override {
     if (mDecryptor) {
       mDecryptor->Close();
       mDecryptor = nullptr;
     }
   }
 
 private:
--- a/dom/media/webrtc/MediaEngineWebRTCAudio.cpp
+++ b/dom/media/webrtc/MediaEngineWebRTCAudio.cpp
@@ -293,16 +293,20 @@ MediaEngineWebRTCMicrophoneSource::Updat
   switch (mState) {
     case kReleased:
       MOZ_ASSERT(aHandle);
       if (sChannelsOpen == 0) {
         if (!InitEngine()) {
           LOG(("Audio engine is not initalized"));
           return NS_ERROR_FAILURE;
         }
+      } else {
+        // Until we fix (or wallpaper) support for multiple mic input
+        // (Bug 1238038) fail allocation for a second device
+        return NS_ERROR_FAILURE;
       }
       if (!AllocChannel()) {
         LOG(("Audio device is not initalized"));
         return NS_ERROR_FAILURE;
       }
       if (mAudioInput->SetRecordingDevice(mCapIndex)) {
         FreeChannel();
         return NS_ERROR_FAILURE;
--- 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/gfx/ipc/GPUProcessManager.cpp
+++ b/gfx/ipc/GPUProcessManager.cpp
@@ -58,24 +58,28 @@ GPUProcessManager::Shutdown()
 
 GPUProcessManager::GPUProcessManager()
  : mTaskFactory(this),
    mNextLayerTreeId(0),
    mNumProcessAttempts(0),
    mProcess(nullptr),
    mGPUChild(nullptr)
 {
+  MOZ_COUNT_CTOR(GPUProcessManager);
+
   mObserver = new Observer(this);
   nsContentUtils::RegisterShutdownObserver(mObserver);
 
   LayerTreeOwnerTracker::Initialize();
 }
 
 GPUProcessManager::~GPUProcessManager()
 {
+  MOZ_COUNT_DTOR(GPUProcessManager);
+
   LayerTreeOwnerTracker::Shutdown();
 
   // The GPU process should have already been shut down.
   MOZ_ASSERT(!mProcess && !mGPUChild);
 
   // We should have already removed observers.
   MOZ_ASSERT(!mObserver);
 }
@@ -367,24 +371,18 @@ GPUProcessManager::NotifyRemoteActorDest
   // can happen if the ActorDestroy for a bridged protocol fires
   // before the ActorDestroy for PGPUChild.
   OnProcessUnexpectedShutdown(mProcess);
 }
 
 void
 GPUProcessManager::CleanShutdown()
 {
-  if (!mProcess) {
-    return;
-  }
-
-#ifdef NS_FREE_PERMANENT_DATA
-  mVsyncBridge->Close();
-#endif
   DestroyProcess();
+  mVsyncIOThread = nullptr;
 }
 
 void
 GPUProcessManager::DestroyProcess()
 {
   if (!mProcess) {
     return;
   }
--- a/gfx/ipc/VsyncIOThreadHolder.cpp
+++ b/gfx/ipc/VsyncIOThreadHolder.cpp
@@ -6,25 +6,32 @@
 
 #include "VsyncIOThreadHolder.h"
 
 namespace mozilla {
 namespace gfx {
 
 VsyncIOThreadHolder::VsyncIOThreadHolder()
 {
+  MOZ_COUNT_CTOR(VsyncIOThreadHolder);
 }
 
 VsyncIOThreadHolder::~VsyncIOThreadHolder()
 {
+  MOZ_COUNT_DTOR(VsyncIOThreadHolder);
+
   if (!mThread) {
     return;
   }
 
-  NS_DispatchToMainThread(NewRunnableMethod(mThread, &nsIThread::AsyncShutdown));
+  if (NS_IsMainThread()) {
+    mThread->AsyncShutdown();
+  } else {
+    NS_DispatchToMainThread(NewRunnableMethod(mThread, &nsIThread::AsyncShutdown));
+  }
 }
 
 bool
 VsyncIOThreadHolder::Start()
 {
   nsresult rv = NS_NewNamedThread("VsyncIOThread", getter_AddRefs(mThread));
   return NS_SUCCEEDED(rv);
 }
--- a/gfx/layers/composite/TextureHost.cpp
+++ b/gfx/layers/composite/TextureHost.cpp
@@ -726,17 +726,20 @@ BufferTextureHost::PrepareTextureSource(
   if (mFirstSource && mFirstSource->IsOwnedBy(this)) {
     // We are already attached to a TextureSource, nothing to do except tell
     // the compositable to use it.
     aTexture = mFirstSource.get();
     return;
   }
 
   // We don't own it, apparently.
-  mFirstSource = nullptr;
+  if (mFirstSource) {
+    mNeedsFullUpdate = true;
+    mFirstSource = nullptr;
+  }
 
   DataTextureSource* texture = aTexture.get() ? aTexture->AsDataTextureSource() : nullptr;
 
   bool compatibleFormats = texture && IsCompatibleTextureSource(texture,
                                                                 mDescriptor,
                                                                 mCompositor);
 
   bool shouldCreateTexture = !compatibleFormats
--- 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/forms/test/mochitest.ini
+++ b/layout/forms/test/mochitest.ini
@@ -60,9 +60,11 @@ skip-if = (buildapp == 'b2g' && (toolkit
 [test_textarea_resize.html]
 skip-if = buildapp == 'mulet' || buildapp == 'b2g' || toolkit == 'android' # b2g(resizing textarea not available in b2g) b2g-debug(resizing textarea not available in b2g) b2g-desktop(resizing textarea not available in b2g)
 [test_bug961363.html]
 skip-if = toolkit == 'android' # Bug 1021644 - Fails when pushed into a different chunk on Android
 [test_bug1111995.html]
 skip-if = buildapp == 'mulet' || buildapp == 'b2g'
 [test_select_vertical.html]
 skip-if = e10s || buildapp == 'mulet' || buildapp == 'b2g' || toolkit == 'android' # Bug 1170129 - vertical <select> popup not implemented for e10s # <select> elements don't use an in-page popup on B2G/Android
+[test_bug1301290.html]
+skip-if = buildapp == 'mulet' || buildapp == 'b2g' || toolkit == 'android' # b2g(resizing textarea not available in b2g) b2g-debug(resizing textarea not available in b2g) b2g-desktop(resizing textarea not available in b2g)
 [test_bug1305282.html]
new file mode 100644
--- /dev/null
+++ b/layout/forms/test/test_bug1301290.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>Test for Bug 1301290</title>
+        <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+        <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+        <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+        <style type="text/css">
+            .blue, .green {
+                border: none;
+                box-sizing: border-box;
+                display: block;
+                width: 200px;
+                height: 100px;
+                overflow: scroll;
+                resize: both;
+            }
+
+            .blue {
+                background: blue;
+            }
+
+            .green {
+                background: green;
+                margin-top: -100px;
+            }
+        </style>
+    </head>
+    <body>
+        <div class="blue"></div>
+        <textarea class="green" id="textarea"></textarea>
+        <script type="application/javascript">
+            SimpleTest.waitForExplicitFinish();
+            addLoadEvent(() => SimpleTest.executeSoon(function() {
+                var textarea = $("textarea");
+                var rect = textarea.getBoundingClientRect();
+
+                synthesizeMouse(textarea, rect.width - 10, rect.height - 10, { type: "mousedown" });
+                synthesizeMouse(textarea, rect.width + 40, rect.height + 40, { type: "mousemove" });
+                synthesizeMouse(textarea, rect.width + 40, rect.height + 40, { type: "mouseup" });
+
+                var newrect = textarea.getBoundingClientRect();
+                ok(newrect.width > rect.width, "width did not increase");
+                ok(newrect.height > rect.height, "height did not increase");
+                SimpleTest.finish();
+            }));
+        </script>
+    </body>
+</html>
--- 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/nsGfxScrollFrame.cpp
+++ b/layout/generic/nsGfxScrollFrame.cpp
@@ -2932,17 +2932,17 @@ ScrollFrameHelper::ScrollToImpl(nsPoint 
   if (docShell) {
     docShell->NotifyScrollObservers();
   }
 }
 
 static int32_t
 MaxZIndexInList(nsDisplayList* aList, nsDisplayListBuilder* aBuilder)
 {
-  int32_t maxZIndex = 0;
+  int32_t maxZIndex = -1;
   for (nsDisplayItem* item = aList->GetBottom(); item; item = item->GetAbove()) {
     maxZIndex = std::max(maxZIndex, item->ZIndex());
   }
   return maxZIndex;
 }
 
 // Finds the max z-index of the items in aList that meet the following conditions
 //   1) have z-index auto or z-index >= 0.
@@ -2961,16 +2961,30 @@ MaxZIndexInListOfItemsContainedInFrame(n
     }
     if (nsLayoutUtils::IsProperAncestorFrame(aFrame, itemFrame)) {
       maxZIndex = std::max(maxZIndex, item->ZIndex());
     }
   }
   return maxZIndex;
 }
 
+template<class T>
+static void
+AppendInternalItemToTop(const nsDisplayListSet& aLists,
+                        T* aItem,
+                        int32_t aZIndex)
+{
+  if (aZIndex >= 0) {
+    aItem->SetOverrideZIndex(aZIndex);
+    aLists.PositionedDescendants()->AppendNewToTop(aItem);
+  } else {
+    aLists.Content()->AppendNewToTop(aItem);
+  }
+}
+
 static const uint32_t APPEND_OWN_LAYER = 0x1;
 static const uint32_t APPEND_POSITIONED = 0x2;
 static const uint32_t APPEND_SCROLLBAR_CONTAINER = 0x4;
 
 static void
 AppendToTop(nsDisplayListBuilder* aBuilder, const nsDisplayListSet& aLists,
             nsDisplayList* aSource, nsIFrame* aSourceFrame, uint32_t aFlags)
 {
@@ -2986,23 +3000,18 @@ AppendToTop(nsDisplayListBuilder* aBuild
   } else {
     newItem = new (aBuilder) nsDisplayWrapList(aBuilder, aSourceFrame, aSource);
   }
 
   if (aFlags & APPEND_POSITIONED) {
     // We want overlay scrollbars to always be on top of the scrolled content,
     // but we don't want them to unnecessarily cover overlapping elements from
     // outside our scroll frame.
-    nsDisplayList* positionedDescendants = aLists.PositionedDescendants();
-    if (!positionedDescendants->IsEmpty()) {
-      newItem->SetOverrideZIndex(MaxZIndexInList(positionedDescendants, aBuilder));
-      positionedDescendants->AppendNewToTop(newItem);
-    } else {
-      aLists.Outlines()->AppendNewToTop(newItem);
-    }
+    int32_t zIndex = MaxZIndexInList(aLists.PositionedDescendants(), aBuilder);
+    AppendInternalItemToTop(aLists, newItem, zIndex);
   } else {
     aLists.BorderBackground()->AppendNewToTop(newItem);
   }
 }
 
 struct HoveredStateComparator
 {
   bool Equals(nsIFrame* A, nsIFrame* B) const {
@@ -3568,27 +3577,19 @@ ScrollFrameHelper::BuildDisplayList(nsDi
         !mWillBuildScrollableLayer &&
         aBuilder->IsBuildingLayerEventRegions())
     {
       inactiveRegionItem = new (aBuilder) nsDisplayLayerEventRegions(aBuilder, mScrolledFrame);
       inactiveRegionItem->AddInactiveScrollPort(mScrollPort + aBuilder->ToReferenceFrame(mOuter));
     }
 
     if (inactiveRegionItem) {
-      nsDisplayList* positionedDescendants = scrolledContent.PositionedDescendants();
-      nsDisplayList* destinationList = nullptr;
-      int32_t zindex =
-        MaxZIndexInListOfItemsContainedInFrame(positionedDescendants, mOuter);
-      if (zindex >= 0) {
-        destinationList = positionedDescendants;
-        inactiveRegionItem->SetOverrideZIndex(zindex);
-      } else {
-        destinationList = scrolledContent.Outlines();
-      }
-      destinationList->AppendNewToTop(inactiveRegionItem);
+      int32_t zIndex =
+        MaxZIndexInListOfItemsContainedInFrame(scrolledContent.PositionedDescendants(), mOuter);
+      AppendInternalItemToTop(scrolledContent, inactiveRegionItem, zIndex);
     }
 
     if (aBuilder->ShouldBuildScrollInfoItemsForHoisting()) {
       aBuilder->AppendNewScrollInfoItemForHoisting(
         new (aBuilder) nsDisplayScrollInfoLayer(aBuilder, mScrolledFrame,
                                                 mOuter));
     }
   }
--- 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/media/gmp-clearkey/0.1/ClearKeySession.cpp
+++ b/media/gmp-clearkey/0.1/ClearKeySession.cpp
@@ -36,25 +36,24 @@ ClearKeySession::ClearKeySession(const s
 {
   CK_LOGD("ClearKeySession ctor %p", this);
 }
 
 ClearKeySession::~ClearKeySession()
 {
   CK_LOGD("ClearKeySession dtor %p", this);
 
-  auto& keyIds = GetKeyIds();
-  for (auto it = keyIds.begin(); it != keyIds.end(); it++) {
-    assert(ClearKeyDecryptionManager::Get()->HasSeenKeyId(*it));
-
-    ClearKeyDecryptionManager::Get()->ReleaseKeyId(*it);
-    mCallback->KeyStatusChanged(&mSessionId[0], mSessionId.size(),
-                                &(*it)[0], it->size(),
-                                kGMPUnknown);
+  std::vector<GMPMediaKeyInfo> key_infos;
+  for (const KeyId& keyId : mKeyIds) {
+    assert(ClearKeyDecryptionManager::Get()->HasSeenKeyId(keyId));
+    ClearKeyDecryptionManager::Get()->ReleaseKeyId(keyId);
+    key_infos.push_back(GMPMediaKeyInfo(&keyId[0], keyId.size(), kGMPUnknown));
   }
+  mCallback->BatchedKeyStatusChanged(&mSessionId[0], mSessionId.size(),
+                                     key_infos.data(), key_infos.size());
 }
 
 void
 ClearKeySession::Init(uint32_t aCreateSessionToken,
                       uint32_t aPromiseId,
                       const std::string& aInitDataType,
                       const uint8_t* aInitData, uint32_t aInitDataSize)
 {
--- a/media/gmp-clearkey/0.1/ClearKeySessionManager.cpp
+++ b/media/gmp-clearkey/0.1/ClearKeySessionManager.cpp
@@ -163,34 +163,43 @@ ClearKeySessionManager::PersistentSessio
   }
 
   ClearKeySession* session = new ClearKeySession(aSessionId,
                                                  mCallback,
                                                  kGMPPersistentSession);
   mSessions[aSessionId] = session;
 
   uint32_t numKeys = aKeyDataSize / (2 * CLEARKEY_KEY_LEN);
+
+  vector<GMPMediaKeyInfo> key_infos;
+  vector<KeyIdPair> keyPairs;
   for (uint32_t i = 0; i < numKeys; i ++) {
     const uint8_t* base = aKeyData + 2 * CLEARKEY_KEY_LEN * i;
 
-    KeyId keyId(base, base + CLEARKEY_KEY_LEN);
-    assert(keyId.size() == CLEARKEY_KEY_LEN);
+    KeyIdPair keyPair;
+
+    keyPair.mKeyId = KeyId(base, base + CLEARKEY_KEY_LEN);
+    assert(keyPair.mKeyId.size() == CLEARKEY_KEY_LEN);
 
-    Key key(base + CLEARKEY_KEY_LEN, base + 2 * CLEARKEY_KEY_LEN);
-    assert(key.size() == CLEARKEY_KEY_LEN);
+    keyPair.mKey = Key(base + CLEARKEY_KEY_LEN, base + 2 * CLEARKEY_KEY_LEN);
+    assert(keyPair.mKey.size() == CLEARKEY_KEY_LEN);
 
-    session->AddKeyId(keyId);
+    session->AddKeyId(keyPair.mKeyId);
 
-    mDecryptionManager->ExpectKeyId(keyId);
-    mDecryptionManager->InitKey(keyId, key);
-    mKeyIds.insert(key);
-    mCallback->KeyStatusChanged(&aSessionId[0], aSessionId.size(),
-                                &keyId[0], keyId.size(),
-                                kGMPUsable);
+    mDecryptionManager->ExpectKeyId(keyPair.mKeyId);
+    mDecryptionManager->InitKey(keyPair.mKeyId, keyPair.mKey);
+    mKeyIds.insert(keyPair.mKey);
+
+    keyPairs.push_back(keyPair);
+    key_infos.push_back(GMPMediaKeyInfo(&keyPairs[i].mKeyId[0],
+                                        keyPairs[i].mKeyId.size(),
+                                        kGMPUsable));
   }
+  mCallback->BatchedKeyStatusChanged(&aSessionId[0], aSessionId.size(),
+                                     key_infos.data(), key_infos.size());
 
   mCallback->ResolveLoadSessionPromise(aPromiseId, true);
 }
 
 void
 ClearKeySessionManager::UpdateSession(uint32_t aPromiseId,
                                       const char* aSessionId,
                                       uint32_t aSessionIdLength,
@@ -211,23 +220,27 @@ ClearKeySessionManager::UpdateSession(ui
   // Parse the response for any (key ID, key) pairs.
   vector<KeyIdPair> keyPairs;
   if (!ClearKeyUtils::ParseJWK(aResponse, aResponseSize, keyPairs, session->Type())) {
     CK_LOGW("ClearKey CDM failed to parse JSON Web Key.");
     mCallback->RejectPromise(aPromiseId, kGMPInvalidAccessError, nullptr, 0);
     return;
   }
 
-  for (auto it = keyPairs.begin(); it != keyPairs.end(); it++) {
-    mDecryptionManager->InitKey(it->mKeyId, it->mKey);
-    mKeyIds.insert(it->mKeyId);
-    mCallback->KeyStatusChanged(aSessionId, aSessionIdLength,
-                                &it->mKeyId[0], it->mKeyId.size(),
-                                kGMPUsable);
+  vector<GMPMediaKeyInfo> key_infos;
+  for (size_t i = 0; i < keyPairs.size(); i++) {
+    KeyIdPair& keyPair = keyPairs[i];
+    mDecryptionManager->InitKey(keyPair.mKeyId, keyPair.mKey);
+    mKeyIds.insert(keyPair.mKeyId);
+    key_infos.push_back(GMPMediaKeyInfo(&keyPair.mKeyId[0],
+                                        keyPair.mKeyId.size(),
+                                        kGMPUsable));
   }
+  mCallback->BatchedKeyStatusChanged(aSessionId, aSessionIdLength,
+                                     key_infos.data(), key_infos.size());
 
   if (session->Type() != kGMPPersistentSession) {
     mCallback->ResolvePromise(aPromiseId);
     return;
   }
 
   // Store the keys on disk. We store a record whose name is the sessionId,
   // and simply append each keyId followed by its key.
deleted file mode 100644
--- a/testing/web-platform/meta/encrypted-media/Google/encrypted-media-keystatuses.html.ini
+++ /dev/null
@@ -1,5 +0,0 @@
-[encrypted-media-keystatuses.html]
-  type: testharness
-  [Verify MediaKeySession.keyStatuses.]
-    expected: FAIL
-
--- 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)
--- a/widget/nsBaseWidget.cpp
+++ b/widget/nsBaseWidget.cpp
@@ -607,25 +607,25 @@ double nsIWidget::DefaultScaleOverride()
 
 //-------------------------------------------------------------------------
 //
 // Add a child to the list of children
 //
 //-------------------------------------------------------------------------
 void nsBaseWidget::AddChild(nsIWidget* aChild)
 {
-  MOZ_RELEASE_ASSERT(!aChild->GetNextSibling() && !aChild->GetPrevSibling(),
-                     "aChild not properly removed from its old child list");
+  MOZ_ASSERT(!aChild->GetNextSibling() && !aChild->GetPrevSibling(),
+             "aChild not properly removed from its old child list");
 
   if (!mFirstChild) {
     mFirstChild = mLastChild = aChild;
   } else {
     // append to the list
-    MOZ_RELEASE_ASSERT(mLastChild);
-    MOZ_RELEASE_ASSERT(!mLastChild->GetNextSibling());
+    MOZ_ASSERT(mLastChild);
+    MOZ_ASSERT(!mLastChild->GetNextSibling());
     mLastChild->SetNextSibling(aChild);
     aChild->SetPrevSibling(mLastChild);
     mLastChild = aChild;
   }
 }
 
 
 //-------------------------------------------------------------------------