Bug 1460382: Make element-backed pseudos inherit from NAC subtree roots and other NAC inherit from their parents. r?heycam draft
authorEmilio Cobos Álvarez <emilio@crisal.io>
Thu, 10 May 2018 18:48:08 +0200
changeset 800558 585a76384beebeb753649e8e4916da77c536d804
parent 800557 19f877c30cb61e49ae6fee33972d3cf78e1fefe6
push id111397
push userbmo:emilio@crisal.io
push dateMon, 28 May 2018 13:32:28 +0000
reviewersheycam
bugs1460382
milestone62.0a1
Bug 1460382: Make element-backed pseudos inherit from NAC subtree roots and other NAC inherit from their parents. r?heycam Currently, NAC always inherits from the closest non-NAC ancestor element, regardless of whether it is for an element-backed pseudo or not. This patch changes the inheritance so that for element-backed pseudos, we inherit from the closest native anonymous root's parent, and for other NAC we inherit from the parent. This prevents the following two issues and allows us to remove the NODE_IS_NATIVE_ANONYMOUS flag: * Avoiding inheriting from the non-NAC ancestor in XBL bindings bound to NAC. - This is no longer a problem since we apply the rule only if we're a pseudo-element, and all pseudo-elements are in native anonymous subtrees. - This also allows to remove the hack that propagates the NODE_IS_NATIVE_ANONYMOUS flag from the ::cue pseudo-element from BindToTree. * Inheriting from the wrong thing if we're a nested NAC subtree. - We no longer look past our NAC subtree, with the exception of ::-moz-number-text's pseudo-elements, for which we do want to propagate ::placeholder to. A few rules from forms.css have been modified because they're useless or needed to propagate stuff to the anonymous form control in input[type="number"] which previously inherited from the input itself. MozReview-Commit-ID: IDKYt3EJtSH
dom/base/Element.cpp
dom/base/nsIContent.h
dom/base/nsINode.h
layout/base/nsCSSFrameConstructor.cpp
layout/generic/nsFrame.cpp
layout/style/res/forms.css
servo/components/style/dom.rs
servo/components/style/gecko/wrapper.rs
servo/components/style/matching.rs
servo/components/style/sharing/mod.rs
servo/components/style/style_resolver.rs
servo/components/style/traversal.rs
testing/web-platform/meta/MANIFEST.json
testing/web-platform/tests/css/css-pseudo/placeholder-input-number-notref.html
testing/web-platform/tests/css/css-pseudo/placeholder-input-number.html
--- a/dom/base/Element.cpp
+++ b/dom/base/Element.cpp
@@ -1752,39 +1752,24 @@ Element::BindToTree(nsIDocument* aDocume
          child = child->GetNextSibling()) {
       rv = child->BindToTree(nullptr, shadowRoot,
                              shadowRoot->GetBindingParent(),
                              aCompileEventHandlers);
       NS_ENSURE_SUCCESS(rv, rv);
     }
   }
 
-  // Pseudo-elements implemented by JS must have the NODE_IS_NATIVE_ANONYMOUS
-  // flag set on them. For C++-created pseudo-elements, this is done in
-  // nsCSSFrameConstructor::GetAnonymousContent, but any JS that creates
-  // pseudo-elements would run after that. So we set that flag here,
-  // when the element implementing the pseudo is inserted into the document.
-  // We maintain the invariant that any NAC-implemented pseudo-element's
-  // anonymous ancestors are also flagged as NAC, which the style system relies on.
-  if (aDocument) {
+  // FIXME(emilio): Why is this needed? The element shouldn't even be styled in
+  // the first place, we should style it properly eventually.
+  //
+  // Also, if this _is_ needed, then it's wrong and should use GetComposedDoc()
+  // to account for Shadow DOM.
+  if (aDocument && MayHaveAnimations()) {
     CSSPseudoElementType pseudoType = GetPseudoElementType();
-    if (pseudoType != CSSPseudoElementType::NotPseudo &&
-        nsCSSPseudoElements::PseudoElementIsJSCreatedNAC(pseudoType)) {
-      SetFlags(NODE_IS_NATIVE_ANONYMOUS);
-      nsIContent* parent = aParent;
-      while (parent && !parent->IsRootOfNativeAnonymousSubtree()) {
-        MOZ_ASSERT(parent->IsInNativeAnonymousSubtree());
-        parent->SetFlags(NODE_IS_NATIVE_ANONYMOUS);
-        parent = parent->GetParent();
-      }
-      MOZ_ASSERT(parent);
-    }
-
-    if (MayHaveAnimations() &&
-        (pseudoType == CSSPseudoElementType::NotPseudo ||
+    if ((pseudoType == CSSPseudoElementType::NotPseudo ||
          pseudoType == CSSPseudoElementType::before ||
          pseudoType == CSSPseudoElementType::after) &&
         EffectSet::GetEffectSet(this, pseudoType)) {
       if (nsPresContext* presContext = aDocument->GetPresContext()) {
         presContext->EffectCompositor()->
           RequestRestyle(this, pseudoType,
                          EffectCompositor::RestyleType::Standard,
                          EffectCompositor::CascadeLevel::Animations);
--- a/dom/base/nsIContent.h
+++ b/dom/base/nsIContent.h
@@ -197,17 +197,17 @@ public:
 
   /**
    * Makes this content anonymous
    * @see nsIAnonymousContentCreator
    */
   void SetIsNativeAnonymousRoot()
   {
     SetFlags(NODE_IS_ANONYMOUS_ROOT | NODE_IS_IN_NATIVE_ANONYMOUS_SUBTREE |
-             NODE_IS_NATIVE_ANONYMOUS_ROOT | NODE_IS_NATIVE_ANONYMOUS);
+             NODE_IS_NATIVE_ANONYMOUS_ROOT);
   }
 
   /**
    * Returns |this| if it is not chrome-only/native anonymous, otherwise
    * first non chrome-only/native anonymous ancestor.
    */
   nsIContent* FindFirstNonChromeOnlyAccessContent() const;
 
@@ -737,24 +737,16 @@ public:
     if (auto* lang = GetLang()) {
       aResult.Assign(nsDependentAtomString(lang));
       return true;
     }
 
     return false;
   }
 
-  // Returns true if this element is native-anonymous scrollbar content.
-  bool IsNativeScrollbarContent() const {
-    return IsNativeAnonymous() &&
-           IsAnyOfXULElements(nsGkAtoms::scrollbar,
-                              nsGkAtoms::resizer,
-                              nsGkAtoms::scrollcorner);
-  }
-
   // Overloaded from nsINode
   virtual already_AddRefed<nsIURI> GetBaseURI(bool aTryUseXHRDocBaseURI = false) const override;
 
   // Returns base URI for style attribute.
   nsIURI* GetBaseURIForStyleAttr() const;
 
   // Returns the URL data for style attribute.
   // If aSubjectPrincipal is passed, it should be the scripted principal
--- a/dom/base/nsINode.h
+++ b/dom/base/nsINode.h
@@ -126,38 +126,17 @@ enum {
   // NOTE: Should only be used on nsIContent nodes
   NODE_IS_NATIVE_ANONYMOUS_ROOT =         NODE_FLAG_BIT(4),
 
   // Whether a binding manager may have a pointer to this
   NODE_MAY_BE_IN_BINDING_MNGR =           NODE_FLAG_BIT(5),
 
   NODE_IS_EDITABLE =                      NODE_FLAG_BIT(6),
 
-  // This node was created by layout as native anonymous content. This
-  // generally corresponds to things created by nsIAnonymousContentCreator,
-  // though there are exceptions (svg:use content does not have this flag
-  // set, and any non-nsIAnonymousContentCreator callers of
-  // SetIsNativeAnonymousRoot also get this flag).
-  //
-  // One very important aspect here is that this node is not transitive over
-  // the subtree (if you want that, use NODE_IS_IN_NATIVE_ANONYMOUS_SUBTREE).
-  // If Gecko code somewhere attaches children to a node with this bit set,
-  // the children will not have the bit themselves unless the calling code sets
-  // it explicitly. This means that XBL content bound to NAC doesn't get this
-  // bit, nor do nodes inserted by editor.
-  //
-  // For now, this bit exists primarily to control style inheritance behavior,
-  // since the nodes for which we set it are often used to implement pseudo-
-  // elements, which need to inherit style from a script-visible element.
-  //
-  // A more general principle for this bit might be this: If the node is entirely
-  // a detail of layout, is not script-observable in any way, and other engines
-  // might accomplish the same task with a nodeless layout frame, then the node
-  // should have this bit set.
-  NODE_IS_NATIVE_ANONYMOUS =              NODE_FLAG_BIT(7),
+  // Free bit here.
 
   // Whether the node participates in a shadow tree.
   NODE_IS_IN_SHADOW_TREE =                NODE_FLAG_BIT(8),
 
   // Node has an :empty or :-moz-only-whitespace selector
   NODE_HAS_EMPTY_SELECTOR =               NODE_FLAG_BIT(9),
 
   // A child of the node has a selector such that any insertion,
@@ -1271,25 +1250,16 @@ public:
     else {
       UnsetFlags(NODE_IS_EDITABLE);
     }
   }
 
   bool IsEditable() const;
 
   /**
-   * Returns true if |this| is native anonymous (i.e. created by
-   * nsIAnonymousContentCreator);
-   */
-  bool IsNativeAnonymous() const
-  {
-    return HasFlag(NODE_IS_NATIVE_ANONYMOUS);
-  }
-
-  /**
    * Returns true if |this| or any of its ancestors is native anonymous.
    */
   bool IsInNativeAnonymousSubtree() const
   {
 #ifdef DEBUG
     if (HasFlag(NODE_IS_IN_NATIVE_ANONYMOUS_SUBTREE)) {
       return true;
     }
--- a/layout/base/nsCSSFrameConstructor.cpp
+++ b/layout/base/nsCSSFrameConstructor.cpp
@@ -4150,24 +4150,16 @@ ConnectAnonymousTreeDescendants(nsIConte
     NS_ASSERTION(content, "null anonymous content?");
 
     ConnectAnonymousTreeDescendants(content, aContent[i].mChildren);
 
     aParent->AppendChildTo(content, false);
   }
 }
 
-static void
-SetNativeAnonymousBitOnDescendants(nsIContent* aRoot)
-{
-  for (nsIContent* curr = aRoot; curr; curr = curr->GetNextNode(aRoot)) {
-    curr->SetFlags(NODE_IS_NATIVE_ANONYMOUS);
-  }
-}
-
 nsresult
 nsCSSFrameConstructor::GetAnonymousContent(nsIContent* aParent,
                                            nsIFrame* aParentFrame,
                                            nsTArray<nsIAnonymousContentCreator::ContentInfo>& aContent)
 {
   nsIAnonymousContentCreator* creator = do_QueryFrame(aParentFrame);
   if (!creator)
     return NS_OK;
@@ -4181,38 +4173,22 @@ nsCSSFrameConstructor::GetAnonymousConte
   uint32_t count = aContent.Length();
   for (uint32_t i=0; i < count; i++) {
     // get our child's content and set its parent to our content
     nsIContent* content = aContent[i].mContent;
     NS_ASSERTION(content, "null anonymous content?");
 
     ConnectAnonymousTreeDescendants(content, aContent[i].mChildren);
 
-    LayoutFrameType parentFrameType = aParentFrame->Type();
-    if (parentFrameType == LayoutFrameType::SVGUse) {
+    if (aParentFrame->IsSVGUseFrame()) {
       // least-surprise CSS binding until we do the SVG specified
       // cascading rules for <svg:use> - bug 265894
       content->SetFlags(NODE_IS_ANONYMOUS_ROOT);
     } else {
       content->SetIsNativeAnonymousRoot();
-      // Don't mark descendants of the custom content container
-      // as native anonymous.  When canvas custom content is initially
-      // created and appended to the custom content container, in
-      // nsIDocument::InsertAnonymousContent, it is not considered native
-      // anonymous content.  But if we end up reframing the root element,
-      // we will re-create the nsCanvasFrame, and we would end up in here,
-      // marking it as NAC.  Existing uses of canvas custom content would
-      // break if it becomes NAC (since each element starts inheriting
-      // styles from its closest non-NAC ancestor, rather than from its
-      // parent).
-      if (!(parentFrameType == LayoutFrameType::Canvas &&
-            content == static_cast<nsCanvasFrame*>(aParentFrame)
-                         ->GetCustomContentContainer())) {
-        SetNativeAnonymousBitOnDescendants(content);
-      }
     }
 
     bool anonContentIsEditable = content->HasFlag(NODE_IS_EDITABLE);
 
     // If the parent is in a shadow tree, make sure we don't
     // bind with a document because shadow roots and its descendants
     // are not in document.
     nsIDocument* bindDocument =
--- a/layout/generic/nsFrame.cpp
+++ b/layout/generic/nsFrame.cpp
@@ -9813,37 +9813,32 @@ GetCorrectedParent(const nsIFrame* aFram
   // Table wrappers are always anon boxes; if we're in here for an outer
   // table, that actually means its the _inner_ table that wants to
   // know its parent. So get the pseudo of the inner in that case.
   nsAtom* pseudo = aFrame->Style()->GetPseudo();
   if (pseudo == nsCSSAnonBoxes::tableWrapper) {
     pseudo = aFrame->PrincipalChildList().FirstChild()->Style()->GetPseudo();
   }
 
-  // Prevent NAC from inheriting NAC. This partially duplicates the logic
-  // implemented in nsCSSFrameConstructor::AddFCItemsForAnonymousContent, and is
-  // necessary so that restyle inherits style in the same way as the initial
-  // styling performed in frame construction.
-  //
-  // It would be nice to put it in CorrectStyleParentFrame and therefore share
-  // it, but that would lose the information of whether the _child_ is NAC,
-  // since CorrectStyleParentFrame only knows about the prospective _parent_.
-  // This duplication and complexity will go away when we fully switch to the
-  // Servo style system, where all this can be handled much more naturally.
-  //
-  // We need to take special care not to disrupt the style inheritance of frames
-  // whose content is NAC but who implement a pseudo (like an anonymous
-  // box, or a non-NAC-backed pseudo like ::first-line) that does not match the
-  // one that the NAC implements, if any.
-  nsIContent* content = aFrame->GetContent();
-  Element* element =
-    content && content->IsElement() ? content->AsElement() : nullptr;
-  if (element && element->IsNativeAnonymous() && !element->IsNativeScrollbarContent() &&
-      element->GetPseudoElementType() == aFrame->Style()->GetPseudoType()) {
-    while (parent->GetContent() && parent->GetContent()->IsNativeAnonymous()) {
+  // Prevent a NAC pseudo-element from inheriting from its NAC parent, and
+  // inherit from the NAC generator element instead.
+  if (pseudo) {
+    MOZ_ASSERT(aFrame->GetContent());
+    Element* element =
+      aFrame->GetContent()->IsElement()
+        ? aFrame->GetContent()->AsElement() : nullptr;
+    // Make sure to avoid doing the fixup for non-element-backed pseudos like
+    // ::first-line and such.
+    if (element &&
+        !element->IsRootOfNativeAnonymousSubtree() &&
+        element->GetPseudoElementType() == aFrame->Style()->GetPseudoType()) {
+      while (parent->GetContent() &&
+             !parent->GetContent()->IsRootOfAnonymousSubtree()) {
+        parent = parent->GetInFlowParent();
+      }
       parent = parent->GetInFlowParent();
     }
   }
 
   return nsFrame::CorrectStyleParentFrame(parent, pseudo);
 }
 
 /* static */
--- a/layout/style/res/forms.css
+++ b/layout/style/res/forms.css
@@ -155,18 +155,16 @@ input::placeholder,
 textarea::placeholder,
 input > .preview-div
 textarea > .preview-div {
   overflow: auto;
   border: 0px !important;
   padding: inherit !important;
   margin: 0px;
   text-decoration: inherit;
-  text-decoration-color: inherit;
-  text-decoration-style: inherit;
   display: inline-block;
   ime-mode: inherit;
   resize: inherit;
   -moz-control-character-visibility: visible;
   overflow-clip-box: inherit;
 }
 
 input > .anonymous-div,
@@ -1055,16 +1053,18 @@ input[type=number]::-moz-number-text {
   -moz-user-modify: read-write;
   /* This pseudo-element is also an 'input' element (nested inside and
    * distinct from the <input type=number> element) so we need to prevent the
    * explicit setting of 'text-align' by the general CSS rule for 'input'
    * above. We want to inherit its value from its <input type=number>
    * ancestor, not have that general CSS rule reset it.
    */
   text-align: inherit;
+  text-decoration: inherit;
+  ime-mode: inherit;
   flex: 1;
   min-inline-size: 0;
   padding: 0;
   border: 0;
   margin: 0;
 }
 
 input[type=number]::-moz-number-spin-box {
--- a/servo/components/style/dom.rs
+++ b/servo/components/style/dom.rs
@@ -461,22 +461,16 @@ pub trait TElement:
     /// Return whether this element is an element in the XUL namespace.
     fn is_xul_element(&self) -> bool { false }
 
     /// Return the list of slotted nodes of this node.
     fn slotted_nodes(&self) -> &[Self::ConcreteNode] {
         &[]
     }
 
-    /// For a given NAC element, return the closest non-NAC ancestor, which is
-    /// guaranteed to exist.
-    fn closest_non_native_anonymous_ancestor(&self) -> Option<Self> {
-        unreachable!("Servo doesn't know about NAC");
-    }
-
     /// Get this element's style attribute.
     fn style_attribute(&self) -> Option<ArcBorrow<Locked<PropertyDeclarationBlock>>>;
 
     /// Unset the style attribute's dirty bit.
     /// Servo doesn't need to manage ditry bit for style attribute.
     fn unset_dirty_style_attribute(&self) {}
 
     /// Get this element's SMIL override declarations.
@@ -652,19 +646,18 @@ pub trait TElement:
 
     /// Returns true if this element is a visited link.
     ///
     /// Servo doesn't support visited styles yet.
     fn is_visited_link(&self) -> bool {
         false
     }
 
-    /// Returns true if this element is native anonymous (only Gecko has native
-    /// anonymous content).
-    fn is_native_anonymous(&self) -> bool {
+    /// Returns true if this element is in a native anonymous subtree.
+    fn is_in_native_anonymous_subtree(&self) -> bool {
         false
     }
 
     /// Returns the pseudo-element implemented by this element, if any.
     ///
     /// Gecko traverses pseudo-elements during the style traversal, and we need
     /// to know this so we can properly grab the pseudo-element style from the
     /// parent element.
@@ -789,17 +782,17 @@ pub trait TElement:
     /// Return the element which we can use to look up rules in the selector
     /// maps.
     ///
     /// This is always the element itself, except in the case where we are an
     /// element-backed pseudo-element, in which case we return the originating
     /// element.
     fn rule_hash_target(&self) -> Self {
         if self.implemented_pseudo_element().is_some() {
-            self.closest_non_native_anonymous_ancestor()
+            self.pseudo_element_originating_element()
                 .expect("Trying to collect rules for a detached pseudo-element")
         } else {
             *self
         }
     }
 
     /// Implements Gecko's `nsBindingManager::WalkRules`.
     ///
--- a/servo/components/style/gecko/wrapper.rs
+++ b/servo/components/style/gecko/wrapper.rs
@@ -584,16 +584,30 @@ impl<'le> fmt::Debug for GeckoElement<'l
         }
 
         write!(f, "> ({:#x})", self.as_node().opaque().0)
     }
 }
 
 impl<'le> GeckoElement<'le> {
     #[inline]
+    fn closest_anon_subtree_root_parent(&self) -> Option<Self> {
+        debug_assert!(self.is_in_native_anonymous_subtree());
+        let mut current = *self;
+
+        loop {
+            if current.is_root_of_native_anonymous_subtree() {
+                return current.traversal_parent();
+            }
+
+            current = current.traversal_parent()?;
+        }
+    }
+
+    #[inline]
     fn may_have_anonymous_children(&self) -> bool {
         self.as_node()
             .get_bool_flag(nsINode_BooleanFlag::ElementMayHaveAnonymousChildren)
     }
 
     #[inline]
     fn flags(&self) -> u32 {
         self.as_node().flags()
@@ -808,23 +822,16 @@ impl<'le> GeckoElement<'le> {
 
     /// This logic is duplicated in Gecko's nsIContent::IsRootOfNativeAnonymousSubtree.
     #[inline]
     fn is_root_of_native_anonymous_subtree(&self) -> bool {
         use gecko_bindings::structs::NODE_IS_NATIVE_ANONYMOUS_ROOT;
         return self.flags() & (NODE_IS_NATIVE_ANONYMOUS_ROOT as u32) != 0;
     }
 
-    /// This logic is duplicated in Gecko's nsINode::IsInNativeAnonymousSubtree.
-    #[inline]
-    fn is_in_native_anonymous_subtree(&self) -> bool {
-        use gecko_bindings::structs::NODE_IS_IN_NATIVE_ANONYMOUS_SUBTREE;
-        self.flags() & (NODE_IS_IN_NATIVE_ANONYMOUS_SUBTREE as u32) != 0
-    }
-
     /// This logic is duplicated in Gecko's nsIContent::IsInAnonymousSubtree.
     #[inline]
     fn is_in_anonymous_subtree(&self) -> bool {
         if self.is_in_native_anonymous_subtree() {
             return true;
         }
 
         let binding_parent = match self.xbl_binding_parent() {
@@ -1033,23 +1040,21 @@ impl structs::FontSizePrefs {
 }
 
 impl<'le> TElement for GeckoElement<'le> {
     type ConcreteNode = GeckoNode<'le>;
     type FontMetricsProvider = GeckoFontMetricsProvider;
     type TraversalChildrenIterator = GeckoChildrenIterator<'le>;
 
     fn inheritance_parent(&self) -> Option<Self> {
-        if self.is_native_anonymous() {
-            self.closest_non_native_anonymous_ancestor()
-        } else {
-            self.as_node()
-                .flattened_tree_parent()
-                .and_then(|n| n.as_element())
+        if self.implemented_pseudo_element().is_some() {
+            return self.pseudo_element_originating_element()
         }
+
+        self.as_node().flattened_tree_parent().and_then(|n| n.as_element())
     }
 
     fn traversal_children(&self) -> LayoutIterator<GeckoChildrenIterator<'le>> {
         // This condition is similar to the check that
         // StyleChildrenIterator::IsNeeded does, except that it might return
         // true if we used to (but no longer) have anonymous content from
         // ::before/::after, XBL bindings, or nsIAnonymousContentCreators.
         if self.is_in_anonymous_subtree() || self.has_xbl_binding_with_content() ||
@@ -1169,29 +1174,16 @@ impl<'le> TElement for GeckoElement<'le>
             };
 
             f(element);
         }
 
         unsafe { bindings::Gecko_DestroyAnonymousContentList(array) };
     }
 
-    fn closest_non_native_anonymous_ancestor(&self) -> Option<Self> {
-        debug_assert!(self.is_native_anonymous());
-        let mut parent = self.traversal_parent()?;
-
-        loop {
-            if !parent.is_native_anonymous() {
-                return Some(parent);
-            }
-
-            parent = parent.traversal_parent()?;
-        }
-    }
-
     #[inline]
     fn as_node(&self) -> Self::ConcreteNode {
         unsafe { GeckoNode(&*(self.0 as *const _ as *const RawGeckoNode)) }
     }
 
     fn owner_doc_matches_for_testing(&self, device: &Device) -> bool {
         self.as_node().owner_doc().0 as *const structs::nsIDocument ==
             device
@@ -1349,29 +1341,30 @@ impl<'le> TElement for GeckoElement<'le>
                 NODE_DESCENDANTS_NEED_FRAMES as u32 | NODE_NEEDS_FRAME as u32,
         )
     }
 
     fn is_visited_link(&self) -> bool {
         self.state().intersects(ElementState::IN_VISITED_STATE)
     }
 
+    /// This logic is duplicated in Gecko's nsINode::IsInNativeAnonymousSubtree.
     #[inline]
-    fn is_native_anonymous(&self) -> bool {
-        use gecko_bindings::structs::NODE_IS_NATIVE_ANONYMOUS;
-        self.flags() & (NODE_IS_NATIVE_ANONYMOUS as u32) != 0
+    fn is_in_native_anonymous_subtree(&self) -> bool {
+        use gecko_bindings::structs::NODE_IS_IN_NATIVE_ANONYMOUS_SUBTREE;
+        self.flags() & (NODE_IS_IN_NATIVE_ANONYMOUS_SUBTREE as u32) != 0
     }
 
     #[inline]
     fn matches_user_and_author_rules(&self) -> bool {
         !self.is_in_native_anonymous_subtree()
     }
 
     fn implemented_pseudo_element(&self) -> Option<PseudoElement> {
-        if !self.is_native_anonymous() {
+        if !self.is_in_native_anonymous_subtree() {
             return None;
         }
 
         if !self.has_properties() {
             return None;
         }
 
         let pseudo_type = unsafe { bindings::Gecko_GetImplementedPseudo(self.0) };
@@ -1910,17 +1903,32 @@ impl<'le> ::selectors::Element for Gecko
     fn containing_shadow_host(&self) -> Option<Self> {
         let shadow = self.containing_shadow()?;
         Some(shadow.host())
     }
 
     #[inline]
     fn pseudo_element_originating_element(&self) -> Option<Self> {
         debug_assert!(self.implemented_pseudo_element().is_some());
-        self.closest_non_native_anonymous_ancestor()
+        let parent = self.closest_anon_subtree_root_parent()?;
+
+        // FIXME(emilio): Special-case for <input type="number">s
+        // pseudo-elements, which are nested NAC. Probably nsNumberControlFrame
+        // should instead inherit from nsTextControlFrame, and then this could
+        // go away.
+        if let Some(PseudoElement::MozNumberText) = parent.implemented_pseudo_element() {
+            debug_assert_eq!(
+                self.implemented_pseudo_element().unwrap(),
+                PseudoElement::Placeholder,
+                "You added a new pseudo, do you really want this?"
+            );
+            return parent.closest_anon_subtree_root_parent();
+        }
+
+        Some(parent)
     }
 
     #[inline]
     fn assigned_slot(&self) -> Option<Self> {
         let slot = self.extended_slots()?._base.mAssignedSlot.mRawPtr;
 
         unsafe { Some(GeckoElement(&slot.as_ref()?._base._base._base._base)) }
     }
--- a/servo/components/style/matching.rs
+++ b/servo/components/style/matching.rs
@@ -673,17 +673,17 @@ pub trait MatchMethods: TElement {
         );
 
         // First of all, update the styles.
         let old_styles = data.set_styles(new_styles);
 
         let new_primary_style = data.styles.primary.as_ref().unwrap();
 
         let mut cascade_requirement = ChildCascadeRequirement::CanSkipCascade;
-        if self.is_root() && !self.is_native_anonymous() {
+        if self.is_root() && !self.is_in_native_anonymous_subtree() {
             let device = context.shared.stylist.device();
             let new_font_size = new_primary_style.get_font().clone_font_size();
 
             if old_styles
                 .primary
                 .as_ref()
                 .map_or(true, |s| s.get_font().clone_font_size() != new_font_size)
             {
--- a/servo/components/style/sharing/mod.rs
+++ b/servo/components/style/sharing/mod.rs
@@ -560,17 +560,17 @@ impl<E: TElement> StyleSharingCache<E> {
         let parent = match element.traversal_parent() {
             Some(element) => element,
             None => {
                 debug!("Failing to insert to the cache: no parent element");
                 return;
             },
         };
 
-        if element.is_native_anonymous() {
+        if element.is_in_native_anonymous_subtree() {
             debug!("Failing to insert into the cache: NAC");
             return;
         }
 
         // We can't share style across shadow hosts right now, because they may
         // match different :host rules.
         //
         // TODO(emilio): We could share across the ones that don't have :host
@@ -651,17 +651,17 @@ impl<E: TElement> StyleSharingCache<E> {
         if target.inheritance_parent().is_none() {
             debug!(
                 "{:?} Cannot share style: element has no parent",
                 target.element
             );
             return None;
         }
 
-        if target.is_native_anonymous() {
+        if target.is_in_native_anonymous_subtree() {
             debug!("{:?} Cannot share style: NAC", target.element);
             return None;
         }
 
         self.cache_mut().entries.lookup(|candidate| {
             Self::test_candidate(
                 target,
                 candidate,
@@ -676,17 +676,17 @@ impl<E: TElement> StyleSharingCache<E> {
     fn test_candidate(
         target: &mut StyleSharingTarget<E>,
         candidate: &mut StyleSharingCandidate<E>,
         shared: &SharedStyleContext,
         bloom: &StyleBloom<E>,
         nth_index_cache: &mut NthIndexCache,
         selector_flags_map: &mut SelectorFlagsMap<E>,
     ) -> Option<ResolvedElementStyles> {
-        debug_assert!(!target.is_native_anonymous());
+        debug_assert!(!target.is_in_native_anonymous_subtree());
 
         // Check that we have the same parent, or at least that the parents
         // share styles and permit sharing across their children. The latter
         // check allows us to share style between cousins if the parents
         // shared style.
         if !checks::parents_allow_sharing(target, candidate) {
             trace!("Miss: Parent");
             return None;
--- a/servo/components/style/style_resolver.rs
+++ b/servo/components/style/style_resolver.rs
@@ -186,17 +186,19 @@ where
         &mut self,
         inputs: CascadeInputs,
         parent_style: Option<&ComputedValues>,
         layout_parent_style: Option<&ComputedValues>,
     ) -> PrimaryStyle {
         // Before doing the cascade, check the sharing cache and see if we can
         // reuse the style via rule node identity.
         let may_reuse =
-            !self.element.is_native_anonymous() && parent_style.is_some() && inputs.rules.is_some();
+            !self.element.is_in_native_anonymous_subtree() &&
+            parent_style.is_some() &&
+            inputs.rules.is_some();
 
         if may_reuse {
             let cached = self.context.thread_local.sharing_cache.lookup_by_rules(
                 self.context.shared,
                 parent_style.unwrap(),
                 inputs.rules.as_ref().unwrap(),
                 inputs.visited_rules.as_ref(),
                 self.element,
--- a/servo/components/style/traversal.rs
+++ b/servo/components/style/traversal.rs
@@ -428,19 +428,20 @@ pub fn recalc_style_at<E, D, F>(
     );
 
     let mut child_cascade_requirement = ChildCascadeRequirement::CanSkipCascade;
 
     // Compute style for this element if necessary.
     if compute_self {
         child_cascade_requirement = compute_style(traversal_data, context, element, data);
 
-        if element.is_native_anonymous() {
-            // We must always cascade native anonymous subtrees, since they inherit
-            // styles from their first non-NAC ancestor.
+        if element.is_in_native_anonymous_subtree() {
+            // We must always cascade native anonymous subtrees, since they
+            // may have pseudo-elements underneath that would inherit from the
+            // closest non-NAC ancestor instead of us.
             child_cascade_requirement = cmp::max(
                 child_cascade_requirement,
                 ChildCascadeRequirement::MustCascadeChildren,
             );
         }
 
         // If we're restyling this element to display:none, throw away all style
         // data in the subtree, notify the caller to early-return.
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -126196,16 +126196,28 @@
       [
        "/css/css-pseudo/marker-inherit-values-ref.html",
        "=="
       ]
      ],
      {}
     ]
    ],
+   "css/css-pseudo/placeholder-input-number.html": [
+    [
+     "/css/css-pseudo/placeholder-input-number.html",
+     [
+      [
+       "/css/css-pseudo/placeholder-input-number-notref.html",
+       "!="
+      ]
+     ],
+     {}
+    ]
+   ],
    "css/css-regions/contentEditable/contentEditable-001.html": [
     [
      "/css/css-regions/contentEditable/contentEditable-001.html",
      [
       [
        "/css/css-regions/contentEditable/reference/contentEditable-001-ref.html",
        "=="
       ]
@@ -255065,16 +255077,21 @@
      {}
     ]
    ],
    "css/css-pseudo/marker-inherit-values-ref.html": [
     [
      {}
     ]
    ],
+   "css/css-pseudo/placeholder-input-number-notref.html": [
+    [
+     {}
+    ]
+   ],
    "css/css-regions/OWNERS": [
     [
      {}
     ]
    ],
    "css/css-regions/animations/reference/animations-001-ref.html": [
     [
      {}
@@ -517432,16 +517449,24 @@
   "css/css-pseudo/marker-inherit-values-ref.html": [
    "92bdc9d8f482c34ad389f27c957d4024a7e05b43",
    "support"
   ],
   "css/css-pseudo/marker-inherit-values.html": [
    "f11765ff416808470d52dde2500106c294243469",
    "reftest"
   ],
+  "css/css-pseudo/placeholder-input-number-notref.html": [
+   "c005e33b7e50074d19c0afd7d9790a38b29ef52e",
+   "support"
+  ],
+  "css/css-pseudo/placeholder-input-number.html": [
+   "90f5c1a64e8171cfce64820b30ad7feafd6e5b06",
+   "reftest"
+  ],
   "css/css-regions/OWNERS": [
    "a10a15c87ddef84355e4bbc8b8966cf0738e9d69",
    "support"
   ],
   "css/css-regions/animations/animations-001.html": [
    "512b5cfa7abcb628d3642194b9a89e23761643d8",
    "manual"
   ],
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/css-pseudo/placeholder-input-number-notref.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<title>CSS Test Reference</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<style>
+#number::placeholder {
+  color: blue;
+}
+</style>
+<input id="number" type="number" placeholder="Placeholder">
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/css-pseudo/placeholder-input-number.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<title>CSS Test: ::placeholder applies to input type="number"</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="mismatch" href="placeholder-input-number-notref.html">
+<link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#placeholder-pseudo">
+<style>
+#number::placeholder {
+  color: green;
+}
+</style>
+<input id="number" type="number" placeholder="Placeholder">