Bug 635240 - Implementation of the layout and anonymous shadow tree portion of <input type=number>. r=dholbert
authorJonathan Watt <jwatt@jwatt.org>
Wed, 04 Sep 2013 11:30:36 +0100
changeset 157383 435d9468467c92027872cbc08164fce966124c0d
parent 157382 20d8b156a140d0f5ed64891a4a0dc0b99e23a42d
child 157384 4c2b12f35738354caa3df66f8e8f4962a7a46dd4
push id270
push userpvanderbeken@mozilla.com
push dateThu, 06 Mar 2014 09:24:21 +0000
reviewersdholbert
bugs635240
milestone28.0a1
Bug 635240 - Implementation of the layout and anonymous shadow tree portion of <input type=number>. r=dholbert
b2g/chrome/content/content.css
content/base/src/nsGkAtomList.h
content/html/content/public/nsIFormControl.h
content/html/content/src/HTMLInputElement.cpp
content/html/content/src/HTMLInputElement.h
layout/base/RestyleManager.cpp
layout/base/nsCSSFrameConstructor.cpp
layout/forms/moz.build
layout/forms/nsNumberControlFrame.cpp
layout/forms/nsNumberControlFrame.h
layout/generic/nsFrameIdList.h
layout/generic/nsHTMLParts.h
layout/reftests/forms/input/number/from-number-to-other-type-unthemed-1-ref.html
layout/reftests/forms/input/number/from-number-to-other-type-unthemed-1.html
layout/reftests/forms/input/number/not-other-type-unthemed-1.html
layout/reftests/forms/input/number/not-other-type-unthemed-1a-notref.html
layout/reftests/forms/input/number/not-other-type-unthemed-1b-notref.html
layout/reftests/forms/input/number/number-same-as-text-unthemed-ref.html
layout/reftests/forms/input/number/number-same-as-text-unthemed.html
layout/reftests/forms/input/number/number-similar-to-text-unthemed-ref.html
layout/reftests/forms/input/number/number-similar-to-text-unthemed.html
layout/reftests/forms/input/number/reftest.list
layout/reftests/forms/input/number/to-number-from-other-type-unthemed-1-ref.html
layout/reftests/forms/input/number/to-number-from-other-type-unthemed-1.html
layout/reftests/forms/input/reftest.list
layout/style/forms.css
layout/style/nsCSSPseudoElementList.h
mobile/android/themes/core/content.css
--- a/b2g/chrome/content/content.css
+++ b/b2g/chrome/content/content.css
@@ -279,15 +279,19 @@ button:active,
 input:active,
 option:active,
 select:active,
 label:active,
 textarea:active {
   background-color: rgba(141, 184, 216, 0.5);
 }
 
+input[type=number]::-moz-number-spin-box {
+  display: none;
+}
+
 %ifdef MOZ_WIDGET_GONK
 /* This binding only provide key shortcuts that we can't use on devices */
 input,
 textarea {
 -moz-binding: none !important;
 }
 %endif
--- a/content/base/src/nsGkAtomList.h
+++ b/content/base/src/nsGkAtomList.h
@@ -1821,16 +1821,17 @@ GK_ATOM(imageControlFrame, "ImageControl
 GK_ATOM(inlineFrame, "InlineFrame")
 GK_ATOM(leafBoxFrame, "LeafBoxFrame")
 GK_ATOM(legendFrame, "LegendFrame")
 GK_ATOM(letterFrame, "LetterFrame")
 GK_ATOM(lineFrame, "LineFrame")
 GK_ATOM(listControlFrame,"ListControlFrame")
 GK_ATOM(menuFrame,"MenuFrame")
 GK_ATOM(menuPopupFrame,"MenuPopupFrame")
+GK_ATOM(numberControlFrame, "NumberControlFrame")
 GK_ATOM(objectFrame, "ObjectFrame")
 GK_ATOM(pageFrame, "PageFrame")
 GK_ATOM(pageBreakFrame, "PageBreakFrame")
 GK_ATOM(pageContentFrame, "PageContentFrame")
 GK_ATOM(placeholderFrame, "PlaceholderFrame")
 GK_ATOM(popupSetFrame, "PopupSetFrame")
 GK_ATOM(canvasFrame, "CanvasFrame")
 GK_ATOM(rangeFrame, "RangeFrame")
--- a/content/html/content/public/nsIFormControl.h
+++ b/content/html/content/public/nsIFormControl.h
@@ -245,18 +245,16 @@ nsIFormControl::IsSingleLineTextControl(
 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: this is temporary until bug 635240 is fixed.
-         aType == NS_FORM_INPUT_NUMBER ||
          // TODO: those are temporary until bug 773205 is fixed.
          aType == NS_FORM_INPUT_DATE ||
          aType == NS_FORM_INPUT_TIME ||
          (!aExcludePassword && aType == NS_FORM_INPUT_PASSWORD);
 }
 
 bool
 nsIFormControl::IsSubmittableControl() const
--- a/content/html/content/src/HTMLInputElement.cpp
+++ b/content/html/content/src/HTMLInputElement.cpp
@@ -2138,16 +2138,24 @@ HTMLInputElement::StepDown(int32_t n, ui
 
 NS_IMETHODIMP
 HTMLInputElement::StepUp(int32_t n, uint8_t optional_argc)
 {
   return ApplyStep(optional_argc ? n : 1);
 }
 
 void
+HTMLInputElement::FlushFrames()
+{
+  if (GetCurrentDoc()) {
+    GetCurrentDoc()->FlushPendingNotifications(Flush_Frames);
+  }
+}
+
+void
 HTMLInputElement::MozGetFileNameArray(nsTArray< nsString >& aArray)
 {
   for (uint32_t i = 0; i < mFiles.Length(); i++) {
     nsString str;
     mFiles[i]->GetMozFullPathInternal(str);
     aArray.AppendElement(str);
   }
 }
@@ -2228,17 +2236,17 @@ HTMLInputElement::MozSetFileNameArray(co
 
   MozSetFileNameArray(list);
   return NS_OK;
 }
 
 bool
 HTMLInputElement::MozIsTextField(bool aExcludePassword)
 {
-  // TODO: temporary until bug 635240 and 773205 are fixed.
+  // TODO: temporary until bug 773205 is fixed.
   if (IsExperimentalMobileType(mType)) {
     return false;
   }
 
   return IsSingleLineTextControl(aExcludePassword);
 }
 
 HTMLInputElement*
@@ -3416,17 +3424,18 @@ HTMLInputElement::PostHandleEvent(nsEven
   // Ideally we would make the default action for click and space just dispatch
   // DOMActivate, and the default action for DOMActivate flip the checkbox/
   // radio state and fire onchange.  However, for backwards compatibility, we
   // need to flip the state before firing click, and we need to fire click
   // when space is pressed.  So, we just nest the firing of DOMActivate inside
   // the click event handling, and allow cancellation of DOMActivate to cancel
   // the click.
   if (aVisitor.mEventStatus != nsEventStatus_eConsumeNoDefault &&
-      !IsSingleLineTextControl(true)) {
+      !IsSingleLineTextControl(true) &&
+      mType != NS_FORM_INPUT_NUMBER) {
     WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent();
     if (mouseEvent && mouseEvent->IsLeftClickEvent() &&
         !ShouldPreventDOMActivateDispatch(aVisitor.mEvent->originalTarget)) {
       InternalUIEvent actEvent(aVisitor.mEvent->mFlags.mIsTrusted,
                                NS_UI_ACTIVATE, 1);
 
       nsCOMPtr<nsIPresShell> shell = aVisitor.mPresContext->GetPresShell();
       if (shell) {
@@ -5511,17 +5520,18 @@ HTMLInputElement::IsHTMLFocusable(bool a
   }
 
   if (IsDisabled()) {
     *aIsFocusable = false;
     return true;
   }
 
   if (IsSingleLineTextControl(false) ||
-      mType == NS_FORM_INPUT_RANGE) {
+      mType == NS_FORM_INPUT_RANGE ||
+      mType == NS_FORM_INPUT_NUMBER) {
     *aIsFocusable = true;
     return false;
   }
 
 #ifdef XP_MACOSX
   const bool defaultFocusable = !aWithMouse || nsFocusManager::sMouseFocusesFormControl;
 #else
   const bool defaultFocusable = true;
@@ -5722,17 +5732,17 @@ HTMLInputElement::PlaceholderApplies() c
   }
 
   return IsSingleLineTextControl(false);
 }
 
 bool
 HTMLInputElement::DoesPatternApply() const
 {
-  // TODO: temporary until bug 635240 and bug 773205 are fixed.
+  // TODO: temporary until bug 773205 is fixed.
   if (IsExperimentalMobileType(mType)) {
     return false;
   }
 
   return IsSingleLineTextControl(false);
 }
 
 bool
--- a/content/html/content/src/HTMLInputElement.h
+++ b/content/html/content/src/HTMLInputElement.h
@@ -569,16 +569,23 @@ public:
   {
     SetHTMLAttr(nsGkAtoms::step, aValue, aRv);
   }
 
   // XPCOM GetType() is OK
   void SetType(const nsAString& aValue, ErrorResult& aRv)
   {
     SetHTMLAttr(nsGkAtoms::type, aValue, aRv);
+    if (aValue.Equals(NS_LITERAL_STRING("number"))) {
+      // For NS_FORM_INPUT_NUMBER we rely on having frames to process key
+      // events. Make sure we have them in case someone changes the type of
+      // this element to "number" and then expects to be able to send key
+      // events to it (type changes are rare, so not a big perf issue):
+      FlushFrames();
+    }
   }
 
   // XPCOM GetDefaultValue() is OK
   void SetDefaultValue(const nsAString& aValue, ErrorResult& aRv)
   {
     SetHTMLAttr(nsGkAtoms::value, aValue, aRv);
   }
 
@@ -1083,21 +1090,25 @@ protected:
    */
   nsresult ApplyStep(int32_t aStep);
 
   /**
    * Returns if the current type is an experimental mobile type.
    */
   static bool IsExperimentalMobileType(uint8_t aType)
   {
-    return aType == NS_FORM_INPUT_NUMBER || aType == NS_FORM_INPUT_DATE ||
-           aType == NS_FORM_INPUT_TIME;
+    return aType == NS_FORM_INPUT_DATE || aType == NS_FORM_INPUT_TIME;
   }
 
   /**
+   * Flushes the layout frame tree to make sure we have up-to-date frames.
+   */
+  void FlushFrames();
+
+  /**
    * Returns true if the element should prevent dispatching another DOMActivate.
    * This is used in situations where the anonymous subtree should already have
    * sent a DOMActivate and prevents firing more than once.
    */
   bool ShouldPreventDOMActivateDispatch(EventTarget* aOriginalTarget);
 
   /**
    * Some input type (color and file) let user choose a value using a picker:
@@ -1238,17 +1249,18 @@ private:
   bool SupportsSetRangeText() const {
     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_RANGE ||
+           aType == NS_FORM_INPUT_NUMBER;
   }
 
   struct nsFilePickerFilter {
     nsFilePickerFilter()
       : mFilterMask(0), mIsTrusted(false) {}
 
     nsFilePickerFilter(int32_t aFilterMask)
       : mFilterMask(aFilterMask), mIsTrusted(true) {}
--- a/layout/base/RestyleManager.cpp
+++ b/layout/base/RestyleManager.cpp
@@ -1709,16 +1709,31 @@ ElementForStyleContext(nsIContent* aPare
 
     nsIFrame* grandparentFrame = aFrame->GetParent()->GetParent();
     MOZ_ASSERT(grandparentFrame->GetType() == nsGkAtoms::colorControlFrame,
                "Color swatch's grandparent should be nsColorControlFrame");
 
     return grandparentFrame->GetContent()->AsElement();
   }
 
+  if (aPseudoType == nsCSSPseudoElements::ePseudo_mozNumberText ||
+      aPseudoType == nsCSSPseudoElements::ePseudo_mozNumberWrapper ||
+      aPseudoType == nsCSSPseudoElements::ePseudo_mozNumberSpinBox ||
+      aPseudoType == nsCSSPseudoElements::ePseudo_mozNumberSpinUp ||
+      aPseudoType == nsCSSPseudoElements::ePseudo_mozNumberSpinDown) {
+    // Get content for nearest nsNumberControlFrame:
+    nsIFrame* f = aFrame->GetParent();
+    MOZ_ASSERT(f);
+    while (f->GetType() != nsGkAtoms::numberControlFrame) {
+      f = f->GetParent();
+      MOZ_ASSERT(f);
+    }
+    return f->GetContent()->AsElement();
+  }
+
   nsIContent* content = aParentContent ? aParentContent : aFrame->GetContent();
   return content->AsElement();
 }
 
 /**
  * FIXME: Temporary.  Should merge with following function.
  */
 static nsIFrame*
--- a/layout/base/nsCSSFrameConstructor.cpp
+++ b/layout/base/nsCSSFrameConstructor.cpp
@@ -3388,17 +3388,17 @@ nsCSSFrameConstructor::FindInputData(Ele
     SIMPLE_INT_CREATE(NS_FORM_INPUT_TEL, NS_NewTextControlFrame),
     SIMPLE_INT_CREATE(NS_FORM_INPUT_URL, NS_NewTextControlFrame),
     SIMPLE_INT_CREATE(NS_FORM_INPUT_RANGE, NS_NewRangeFrame),
     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_NewTextControlFrame),
+    SIMPLE_INT_CREATE(NS_FORM_INPUT_NUMBER, NS_NewNumberControlFrame),
     // TODO: this is temporary until a frame is written: bug 773205.
     SIMPLE_INT_CREATE(NS_FORM_INPUT_DATE, NS_NewTextControlFrame),
     // TODO: this is temporary until a frame is written: bug 773205
     SIMPLE_INT_CREATE(NS_FORM_INPUT_TIME, 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
@@ -24,16 +24,17 @@ UNIFIED_SOURCES += [
     'nsGfxButtonControlFrame.cpp',
     'nsGfxCheckboxControlFrame.cpp',
     'nsGfxRadioControlFrame.cpp',
     'nsHTMLButtonControlFrame.cpp',
     'nsImageControlFrame.cpp',
     'nsLegendFrame.cpp',
     'nsListControlFrame.cpp',
     'nsMeterFrame.cpp',
+    'nsNumberControlFrame.cpp',
     'nsProgressFrame.cpp',
     'nsRangeFrame.cpp',
     'nsSelectsAreaFrame.cpp',
     'nsTextControlFrame.cpp',
 ]
 
 FAIL_ON_WARNINGS = True
 
new file mode 100644
--- /dev/null
+++ b/layout/forms/nsNumberControlFrame.cpp
@@ -0,0 +1,260 @@
+/* -*- 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 "nsNumberControlFrame.h"
+
+#include "nsIPresShell.h"
+#include "nsFontMetrics.h"
+#include "nsFormControlFrame.h"
+#include "nsGkAtoms.h"
+#include "nsINodeInfo.h"
+#include "nsINameSpaceManager.h"
+#include "nsContentUtils.h"
+#include "nsContentCreatorFunctions.h"
+#include "nsContentList.h"
+#include "nsStyleSet.h"
+
+using namespace mozilla;
+
+nsIFrame*
+NS_NewNumberControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext)
+{
+  return new (aPresShell) nsNumberControlFrame(aContext);
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsNumberControlFrame)
+
+NS_QUERYFRAME_HEAD(nsNumberControlFrame)
+  NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator)
+NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame)
+
+nsNumberControlFrame::nsNumberControlFrame(nsStyleContext* aContext)
+  : nsContainerFrame(aContext)
+{
+}
+
+void
+nsNumberControlFrame::DestroyFrom(nsIFrame* aDestructRoot)
+{
+  NS_ASSERTION(!GetPrevContinuation() && !GetNextContinuation(),
+               "nsNumberControlFrame should not have continuations; if it does we "
+               "need to call RegUnregAccessKey only for the first");
+  nsFormControlFrame::RegUnRegAccessKey(static_cast<nsIFrame*>(this), false);
+  nsContentUtils::DestroyAnonymousContent(&mOuterWrapper);
+  nsContainerFrame::DestroyFrom(aDestructRoot);
+}
+
+NS_IMETHODIMP
+nsNumberControlFrame::Reflow(nsPresContext* aPresContext,
+                             nsHTMLReflowMetrics& aDesiredSize,
+                             const nsHTMLReflowState& aReflowState,
+                             nsReflowStatus& aStatus)
+{
+  DO_GLOBAL_REFLOW_COUNT("nsNumberControlFrame");
+  DISPLAY_REFLOW(aPresContext, this, aReflowState, aDesiredSize, aStatus);
+
+  NS_ASSERTION(mOuterWrapper, "Outer wrapper div must exist!");
+
+  NS_ASSERTION(!GetPrevContinuation() && !GetNextContinuation(),
+               "nsNumberControlFrame should not have continuations; if it does we "
+               "need to call RegUnregAccessKey only for the first");
+
+  NS_ASSERTION(!mFrames.FirstChild() ||
+               !mFrames.FirstChild()->GetNextSibling(),
+               "We expect at most one direct child frame");
+
+  if (mState & NS_FRAME_FIRST_REFLOW) {
+    nsFormControlFrame::RegUnRegAccessKey(this, true);
+  }
+
+  nsHTMLReflowMetrics wrappersDesiredSize;
+  nsIFrame* outerWrapperFrame = mOuterWrapper->GetPrimaryFrame();
+  if (outerWrapperFrame) { // display:none?
+    NS_ASSERTION(outerWrapperFrame == mFrames.FirstChild(), "huh?");
+    nsresult rv =
+      ReflowAnonymousContent(aPresContext, wrappersDesiredSize,
+                             aReflowState, outerWrapperFrame);
+    NS_ENSURE_SUCCESS(rv, rv);
+    ConsiderChildOverflow(aDesiredSize.mOverflowAreas, outerWrapperFrame);
+  }
+
+  nscoord computedHeight = aReflowState.ComputedHeight();
+  if (computedHeight == NS_AUTOHEIGHT) {
+    computedHeight =
+      outerWrapperFrame ? outerWrapperFrame->GetSize().height : 0;
+  }
+  aDesiredSize.width = aReflowState.ComputedWidth() +
+                         aReflowState.mComputedBorderPadding.LeftRight();
+  aDesiredSize.height = computedHeight +
+                          aReflowState.mComputedBorderPadding.TopBottom();
+
+  if (outerWrapperFrame) {
+    aDesiredSize.ascent = wrappersDesiredSize.ascent +
+                            outerWrapperFrame->GetPosition().y;
+  }
+
+  aDesiredSize.SetOverflowAreasToDesiredBounds();
+
+  FinishAndStoreOverflow(&aDesiredSize);
+
+  aStatus = NS_FRAME_COMPLETE;
+
+  NS_FRAME_SET_TRUNCATION(aStatus, aReflowState, aDesiredSize);
+
+  return NS_OK;
+}
+
+nsresult
+nsNumberControlFrame::
+  ReflowAnonymousContent(nsPresContext* aPresContext,
+                         nsHTMLReflowMetrics& aWrappersDesiredSize,
+                         const nsHTMLReflowState& aParentReflowState,
+                         nsIFrame* aOuterWrapperFrame)
+{
+  MOZ_ASSERT(aOuterWrapperFrame);
+
+  // The width of our content box, which is the available width
+  // for our anonymous content:
+  nscoord inputFrameContentBoxWidth = aParentReflowState.ComputedWidth();
+
+  nsHTMLReflowState wrapperReflowState(aPresContext, aParentReflowState,
+                                       aOuterWrapperFrame,
+                                       nsSize(inputFrameContentBoxWidth,
+                                              NS_UNCONSTRAINEDSIZE));
+
+  nscoord xoffset = aParentReflowState.mComputedBorderPadding.left +
+                      wrapperReflowState.mComputedMargin.left;
+  nscoord yoffset = aParentReflowState.mComputedBorderPadding.top +
+                      wrapperReflowState.mComputedMargin.top;
+
+  nsReflowStatus childStatus;
+  nsresult rv = ReflowChild(aOuterWrapperFrame, aPresContext,
+                            aWrappersDesiredSize, wrapperReflowState,
+                            xoffset, yoffset, 0, childStatus);
+  NS_ENSURE_SUCCESS(rv, rv);
+  MOZ_ASSERT(NS_FRAME_IS_FULLY_COMPLETE(childStatus),
+             "We gave our child unconstrained height, so it should be complete");
+  return FinishReflowChild(aOuterWrapperFrame, aPresContext,
+                           &wrapperReflowState, aWrappersDesiredSize,
+                           xoffset, yoffset, 0);
+}
+
+nsresult
+nsNumberControlFrame::MakeAnonymousElement(nsIContent** aResult,
+                                           nsTArray<ContentInfo>& aElements,
+                                           nsIAtom* aTagName,
+                                           nsCSSPseudoElements::Type aPseudoType,
+                                           nsStyleContext* aParentContext)
+{
+  // Get the NodeInfoManager and tag necessary to create the anonymous divs.
+  nsCOMPtr<nsIDocument> doc = mContent->GetDocument();
+
+  nsCOMPtr<nsINodeInfo> nodeInfo;
+  nodeInfo = doc->NodeInfoManager()->GetNodeInfo(aTagName, nullptr,
+                                                 kNameSpaceID_XHTML,
+                                                 nsIDOMNode::ELEMENT_NODE);
+  NS_ENSURE_TRUE(nodeInfo, NS_ERROR_OUT_OF_MEMORY);
+  nsresult rv = NS_NewHTMLElement(aResult, nodeInfo.forget(),
+                                  dom::NOT_FROM_PARSER);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // If we legitimately fail this assertion and need to allow
+  // non-pseudo-element anonymous children, then we'll need to add a branch
+  // that calls ResolveStyleFor((*aResult)->AsElement(), aParentContext)") to
+  // set newStyleContext.
+  NS_ASSERTION(aPseudoType != nsCSSPseudoElements::ePseudo_NotPseudoElement,
+               "Expecting anonymous children to all be pseudo-elements");
+  // Associate the pseudo-element with the anonymous child
+  nsRefPtr<nsStyleContext> newStyleContext =
+    PresContext()->StyleSet()->ResolvePseudoElementStyle(mContent->AsElement(),
+                                                         aPseudoType,
+                                                         aParentContext);
+
+  if (!aElements.AppendElement(ContentInfo(*aResult, newStyleContext))) {
+    return NS_ERROR_OUT_OF_MEMORY;
+  }
+  return NS_OK;
+}
+
+nsresult
+nsNumberControlFrame::CreateAnonymousContent(nsTArray<ContentInfo>& aElements)
+{
+  nsresult rv;
+
+  // We create an anonymous tree for our input element that is structured as
+  // follows:
+  //
+  // input
+  //   div      - outer wrapper with "display:flex" by default
+  //     input  - text input field
+  //     div    - spin box wrapping up/down arrow buttons
+  //       div  - spin up (up arrow button)
+  //       div  - spin down (down arrow button)
+  //
+  // If you change this, be careful to change the destruction order in
+  // nsNumberControlFrame::DestroyFrom.
+
+
+  // Create the anonymous outer wrapper:
+  rv = MakeAnonymousElement(getter_AddRefs(mOuterWrapper),
+                            aElements,
+                            nsGkAtoms::div,
+                            nsCSSPseudoElements::ePseudo_mozNumberWrapper,
+                            mStyleContext);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  ContentInfo& outerWrapperCI = aElements.LastElement();
+
+  // Create the ::-moz-number-text pseudo-element:
+  rv = MakeAnonymousElement(getter_AddRefs(mTextField),
+                            outerWrapperCI.mChildren,
+                            nsGkAtoms::input,
+                            nsCSSPseudoElements::ePseudo_mozNumberText,
+                            outerWrapperCI.mStyleContext);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  mTextField->SetAttr(kNameSpaceID_None, nsGkAtoms::type,
+                      NS_LITERAL_STRING("text"), PR_FALSE);
+
+  // Create the ::-moz-number-spin-box pseudo-element:
+  rv = MakeAnonymousElement(getter_AddRefs(mSpinBox),
+                            outerWrapperCI.mChildren,
+                            nsGkAtoms::div,
+                            nsCSSPseudoElements::ePseudo_mozNumberSpinBox,
+                            outerWrapperCI.mStyleContext);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  ContentInfo& spinBoxCI = outerWrapperCI.mChildren.LastElement();
+
+  // Create the ::-moz-number-spin-up pseudo-element:
+  rv = MakeAnonymousElement(getter_AddRefs(mSpinUp),
+                            spinBoxCI.mChildren,
+                            nsGkAtoms::div,
+                            nsCSSPseudoElements::ePseudo_mozNumberSpinUp,
+                            spinBoxCI.mStyleContext);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // Create the ::-moz-number-spin-down pseudo-element:
+  rv = MakeAnonymousElement(getter_AddRefs(mSpinDown),
+                            spinBoxCI.mChildren,
+                            nsGkAtoms::div,
+                            nsCSSPseudoElements::ePseudo_mozNumberSpinDown,
+                            spinBoxCI.mStyleContext);
+  return rv;
+}
+
+nsIAtom*
+nsNumberControlFrame::GetType() const
+{
+  return nsGkAtoms::numberControlFrame;
+}
+
+void
+nsNumberControlFrame::AppendAnonymousContentTo(nsBaseContentList& aElements,
+                                               uint32_t aFilter)
+{
+  // Only one direct anonymous child:
+  aElements.MaybeAppendElement(mOuterWrapper);
+}
new file mode 100644
--- /dev/null
+++ b/layout/forms/nsNumberControlFrame.h
@@ -0,0 +1,83 @@
+/* -*- 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/. */
+
+#ifndef nsNumberControlFrame_h__
+#define nsNumberControlFrame_h__
+
+#include "nsContainerFrame.h"
+#include "nsIFormControlFrame.h"
+#include "nsIAnonymousContentCreator.h"
+#include "nsCOMPtr.h"
+
+class nsPresContext;
+
+/**
+ * This frame type is used for <input type=number>.
+ */
+class nsNumberControlFrame MOZ_FINAL : public nsContainerFrame
+                                     , public nsIAnonymousContentCreator
+{
+  friend nsIFrame*
+  NS_NewNumberControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext);
+
+  nsNumberControlFrame(nsStyleContext* aContext);
+
+public:
+  NS_DECL_QUERYFRAME
+  NS_DECL_FRAMEARENA_HELPERS
+
+  virtual void DestroyFrom(nsIFrame* aDestructRoot) MOZ_OVERRIDE;
+
+  virtual bool IsLeaf() const MOZ_OVERRIDE { return true; }
+
+  NS_IMETHOD Reflow(nsPresContext*           aPresContext,
+                    nsHTMLReflowMetrics&     aDesiredSize,
+                    const nsHTMLReflowState& aReflowState,
+                    nsReflowStatus&          aStatus) MOZ_OVERRIDE;
+
+  // nsIAnonymousContentCreator
+  virtual nsresult CreateAnonymousContent(nsTArray<ContentInfo>& aElements) MOZ_OVERRIDE;
+  virtual void AppendAnonymousContentTo(nsBaseContentList& aElements,
+                                        uint32_t aFilter) MOZ_OVERRIDE;
+
+#ifdef NS_DEBUG
+  NS_IMETHOD GetFrameName(nsAString& aResult) const MOZ_OVERRIDE {
+    return MakeFrameName(NS_LITERAL_STRING("NumberControl"), aResult);
+  }
+#endif
+
+  virtual nsIAtom* GetType() const MOZ_OVERRIDE;
+
+  virtual bool IsFrameOfType(uint32_t aFlags) const MOZ_OVERRIDE
+  {
+    return nsContainerFrame::IsFrameOfType(aFlags &
+      ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock));
+  }
+
+private:
+
+  nsresult MakeAnonymousElement(nsIContent** aResult,
+                                nsTArray<ContentInfo>& aElements,
+                                nsIAtom* aTagName,
+                                nsCSSPseudoElements::Type aPseudoType,
+                                nsStyleContext* aParentContext);
+
+  nsresult ReflowAnonymousContent(nsPresContext* aPresContext,
+                                  nsHTMLReflowMetrics& aWrappersDesiredSize,
+                                  const nsHTMLReflowState& aReflowState,
+                                  nsIFrame* aOuterWrapperFrame);
+
+  /**
+   * The text field used to edit and show the number.
+   * @see nsNumberControlFrame::CreateAnonymousContent.
+   */
+  nsCOMPtr<nsIContent> mOuterWrapper;
+  nsCOMPtr<nsIContent> mTextField;
+  nsCOMPtr<nsIContent> mSpinBox;
+  nsCOMPtr<nsIContent> mSpinUp;
+  nsCOMPtr<nsIContent> mSpinDown;
+};
+
+#endif // nsNumberControlFrame_h__
--- a/layout/generic/nsFrameIdList.h
+++ b/layout/generic/nsFrameIdList.h
@@ -97,16 +97,17 @@ FRAME_ID(nsMathMLmtrFrame)
 FRAME_ID(nsMathMLmunderFrame)
 FRAME_ID(nsMathMLmunderoverFrame)
 FRAME_ID(nsMathMLsemanticsFrame)
 FRAME_ID(nsMathMLTokenFrame)
 FRAME_ID(nsMenuBarFrame)
 FRAME_ID(nsMenuFrame)
 FRAME_ID(nsMenuPopupFrame)
 FRAME_ID(nsMeterFrame)
+FRAME_ID(nsNumberControlFrame)
 FRAME_ID(nsObjectFrame)
 FRAME_ID(nsPageBreakFrame)
 FRAME_ID(nsPageContentFrame)
 FRAME_ID(nsPageFrame)
 FRAME_ID(nsPlaceholderFrame)
 FRAME_ID(nsPopupSetFrame)
 FRAME_ID(nsProgressFrame)
 FRAME_ID(nsProgressMeterFrame)
--- a/layout/generic/nsHTMLParts.h
+++ b/layout/generic/nsHTMLParts.h
@@ -188,16 +188,18 @@ NS_NewListControlFrame(nsIPresShell* aPr
 nsIFrame*
 NS_NewComboboxControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext, uint32_t aFlags);
 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);
 
 // Table frame factories
 nsIFrame*
 NS_NewTableOuterFrame(nsIPresShell* aPresShell, nsStyleContext* aContext);
 nsIFrame*
 NS_NewTableFrame(nsIPresShell* aPresShell, nsStyleContext* aContext);
 nsIFrame*
 NS_NewTableCaptionFrame(nsIPresShell* aPresShell, nsStyleContext* aContext);
new file mode 100644
--- /dev/null
+++ b/layout/reftests/forms/input/number/from-number-to-other-type-unthemed-1-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/number/from-number-to-other-type-unthemed-1.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 number element) -->
+  <script type="text/javascript">
+    function setToCheckbox()
+    {
+      document.getElementById('i').type='checkbox';
+      document.documentElement.className = '';
+    }
+    document.addEventListener("MozReftestInvalidate", setToCheckbox);
+  </script>
+  <body>
+    <input type='number' id='i' style="-moz-appearance:none;">
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/forms/input/number/not-other-type-unthemed-1.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+  <body>
+    <input type="number" value="1" style="-moz-appearance:none;">
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/forms/input/number/not-other-type-unthemed-1a-notref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+  <body>
+    <input type="text" value="1" style="-moz-appearance:none;">
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/forms/input/number/not-other-type-unthemed-1b-notref.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/number/number-same-as-text-unthemed-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+  <body>
+    <input type="text" style="-moz-appearance:none; width:200px;">
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/forms/input/number/number-same-as-text-unthemed.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+  <body>
+    <input type="number" style="-moz-appearance:none; width:200px;">
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/forms/input/number/number-similar-to-text-unthemed-ref.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+  <body>
+    <input type="text" style="-moz-appearance:none; width:200px;">
+    <!-- div to cover spin box area -->
+    <div style="display:block; position:absolute; background-color:black; width:200px; height:100px; top:0px; left:100px;">
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/forms/input/number/number-similar-to-text-unthemed.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+  <body>
+    <input type="number" style="-moz-appearance:none; width:200px;">
+    <!-- div to cover spin box area -->
+    <div style="display:block; position:absolute; background-color:black; width:200px; height:100px; top:0px; left:100px;">
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/forms/input/number/reftest.list
@@ -0,0 +1,16 @@
+default-preferences pref(dom.forms.number,true)
+
+# sanity checks:
+# not valid on Android/B2G where type=number looks like type=text
+skip-if(Android||B2G) != not-other-type-unthemed-1.html not-other-type-unthemed-1a-notref.html
+skip-if(Android||B2G) != not-other-type-unthemed-1.html not-other-type-unthemed-1b-notref.html
+# only valid on Android/B2G where type=number looks the same as type=text
+skip-if(!Android&&!B2G) == number-same-as-text-unthemed.html number-same-as-text-unthemed-ref.html
+
+# should look the same as type=text, except for the spin box
+== number-similar-to-text-unthemed.html number-similar-to-text-unthemed-ref.html
+
+# dynamic type changes:
+fuzzy-if(/^Windows\x20NT\x205\.1/.test(http.oscpu),64,4) fuzzy-if(cocoaWidget,63,4) == to-number-from-other-type-unthemed-1.html to-number-from-other-type-unthemed-1-ref.html
+== from-number-to-other-type-unthemed-1.html from-number-to-other-type-unthemed-1-ref.html
+
new file mode 100644
--- /dev/null
+++ b/layout/reftests/forms/input/number/to-number-from-other-type-unthemed-1-ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+  <body>
+    <input type="number" style="-moz-appearance:none;">
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/forms/input/number/to-number-from-other-type-unthemed-1.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+  <!-- Test: input element changed to number state doesn't look like checkbox state -->
+  <script type="text/javascript">
+    function setToNumber()
+    {
+      document.getElementById('i').type='number';
+      document.documentElement.className = '';
+    }
+    document.addEventListener("MozReftestInvalidate", setToNumber);
+  </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
@@ -1,12 +1,13 @@
 include checkbox/reftest.list
 include email/reftest.list
 include tel/reftest.list
 include search/reftest.list
 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
--- a/layout/style/forms.css
+++ b/layout/style/forms.css
@@ -876,16 +876,85 @@ input[type=range]::-moz-range-thumb {
   height: 1em;
   border: 0.1em solid grey;
   border-radius: 0.5em;
   background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'><linearGradient id='g' x2='0' y2='100%'><stop stop-color='%23ddd'/><stop offset='100%' stop-color='white'/></linearGradient><rect fill='url(%23g)' width='100%' height='100%'/></svg>");
   /* Prevent nsFrame::HandlePress setting mouse capture to this element. */
   -moz-user-select: none ! important;
 }
 
+input[type="number"] {
+  /* Has to revert some properties applied by the generic input rule. */
+  -moz-binding: none;
+  width: 149px; /* to match type=text */
+}
+
+input[type=number]::-moz-number-wrapper {
+  /* Prevent styling that would change the type of frame we construct. */
+  display: flex;
+  float: none !important;
+  position: static !important;
+  -moz-box-sizing: border-box;
+  width: 100%;
+  height: 100%;
+}
+
+input[type=number]::-moz-number-text {
+  -moz-appearance: none;
+  flex: 1;
+  padding: 0;
+  border: 0;
+  margin: 0;
+}
+
+input[type=number]::-moz-number-spin-box {
+  display: flex;
+  flex-direction: column;
+  flex: 0 8px;
+  cursor: default;
+  padding: 1px;
+}
+
+input[type=number]::-moz-number-spin-up {
+  /* We should be "display:block" so that we don't get wrapped in an anonymous
+   * flex item that will prevent the setting of the "flex" property below from
+   * working.
+   */
+  display: block;
+  flex: 1;
+  background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="6" height="5"><path d="M1,4 L3,0 5,4" fill="dimgrey"/></svg>');
+  background-repeat: no-repeat;
+  background-position: center bottom;
+  border: 1px solid darkgray;
+  border-bottom: none;
+  border-top-left-radius: 4px;
+  border-top-right-radius: 4px;
+}
+
+input[type=number]::-moz-number-spin-down {
+  /* We should be "display:block" so that we don't get wrapped in an anonymous
+   * flex item that will prevent the setting of the "flex" property below from
+   * working.
+   */
+  display: block;
+  flex: 1;
+  background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="6" height="5"><path d="M1,1 L3,5 5,1" fill="dimgrey"/></svg>');
+  background-repeat: no-repeat;
+  background-position: center top;
+  border: 1px solid darkgray;
+  border-top: none;
+  border-bottom-left-radius: 4px;
+  border-bottom-right-radius: 4px;
+}
+
+input[type="number"] > div > div > div:hover {
+  /* give some indication of hover state for the up/down buttons */
+  background-color: lightblue;
+}
+
 %ifdef XP_OS2
 input {
   font: medium serif; font-family: inherit
 }
 
 select {
   font: medium serif; font-family: inherit
 }
--- a/layout/style/nsCSSPseudoElementList.h
+++ b/layout/style/nsCSSPseudoElementList.h
@@ -47,16 +47,21 @@ CSS_PSEUDO_ELEMENT(mozFocusOuter, ":-moz
 // use our flags to prevent that?
 CSS_PSEUDO_ELEMENT(mozListBullet, ":-moz-list-bullet", 0)
 CSS_PSEUDO_ELEMENT(mozListNumber, ":-moz-list-number", 0)
 
 CSS_PSEUDO_ELEMENT(mozMathStretchy, ":-moz-math-stretchy", 0)
 CSS_PSEUDO_ELEMENT(mozMathAnonymous, ":-moz-math-anonymous", 0)
 
 // HTML5 Forms pseudo elements
+CSS_PSEUDO_ELEMENT(mozNumberWrapper, ":-moz-number-wrapper", 0)
+CSS_PSEUDO_ELEMENT(mozNumberText, ":-moz-number-text", 0)
+CSS_PSEUDO_ELEMENT(mozNumberSpinBox, ":-moz-number-spin-box", 0)
+CSS_PSEUDO_ELEMENT(mozNumberSpinUp, ":-moz-number-spin-up", 0)
+CSS_PSEUDO_ELEMENT(mozNumberSpinDown, ":-moz-number-spin-down", 0)
 CSS_PSEUDO_ELEMENT(mozProgressBar, ":-moz-progress-bar", 0)
 CSS_PSEUDO_ELEMENT(mozRangeTrack, ":-moz-range-track", 0)
 CSS_PSEUDO_ELEMENT(mozRangeProgress, ":-moz-range-progress", 0)
 CSS_PSEUDO_ELEMENT(mozRangeThumb, ":-moz-range-thumb", 0)
 CSS_PSEUDO_ELEMENT(mozMeterBar, ":-moz-meter-bar", 0)
 CSS_PSEUDO_ELEMENT(mozPlaceholder, ":-moz-placeholder", 0)
 CSS_PSEUDO_ELEMENT(mozColorSwatch, ":-moz-color-swatch",
                    CSS_PSEUDO_ELEMENT_SUPPORTS_STYLE_ATTRIBUTE)
--- a/mobile/android/themes/core/content.css
+++ b/mobile/android/themes/core/content.css
@@ -303,8 +303,13 @@ button:not([disabled]):active,
 input:not([disabled]):active,
 select:not([disabled]):active,
 textarea:not([disabled]):active,
 option:active,
 label:active,
 xul|menulist:active {
   background-color: @color_background_highlight_overlay@;
 }
+
+input[type=number]::-moz-number-spin-box {
+  display: none;
+}
+