Bug 1375599 - Change IsDisabled() to look at NS_EVENT_STATE_DISABLED instead of the "disabled" attribute. r=bz
authorJessica Jong <jjong@mozilla.com>
Thu, 20 Jul 2017 02:15:00 -0400
changeset 369875 c72f79ecb0dda81850fd2434db9f58b60da51d29
parent 369874 4194f4a318defa521543768acf09e19efba899ac
child 369876 1440d943e924df9a132d1d1e73974cc9bfc8695f
push id92764
push userryanvm@gmail.com
push dateThu, 20 Jul 2017 12:59:34 +0000
treeherdermozilla-inbound@c72f79ecb0dd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbz
bugs1375599
milestone56.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1375599 - Change IsDisabled() to look at NS_EVENT_STATE_DISABLED instead of the "disabled" attribute. r=bz In order to speed up IsDisabled(), instead of querying for the @disabled attribute, we're now using the NS_EVENT_STATE_DISABLED flag to know whether an element is disabled. It is safe to use the NS_EVENT_STATE_DISABLED flag for the following reasons: - For form elements, nsGenericHTMLFormElement::IsDisabled() is only called on form elements that can be disabled; form elements that can't be disabled overrides IsDisabled() to return false directly. And, before this patch, NS_EVENT_STATE_DISABLED flag is set by nsGenericHTMLFormElement::IntrinsicState() if and only if IsDisabled() in all cases when CanBeDisabled() is true, and when CanBeDisabled() is false then IsDisabled() is always false and the flag is not set. - For non form elements, optgroup and option have the flag matching IsDisabled(). Note that option's IsDisabled() should also refer to optgroup's (if it exists) disabled state, which was not done before this patch. For this to work correctly, we need to set NS_EVENT_STATE_DISABLED earlier, that is, in AfterSetAttr(), before any consumer of IsDisabled(). We also need to update the flag whenever the element's parent (e.g. fieldset or optgroup) disabled state changes and when moving into/out of a parent container. Note that NS_EVENT_STATE_DISABLED/ENABLED is now part of the EXTERNALLY_MANAGED_STATES. MozReview-Commit-ID: KSceikeqvvU
dom/events/EventStates.h
dom/html/HTMLButtonElement.cpp
dom/html/HTMLFieldSetElement.cpp
dom/html/HTMLInputElement.cpp
dom/html/HTMLLabelElement.h
dom/html/HTMLOptGroupElement.cpp
dom/html/HTMLOptGroupElement.h
dom/html/HTMLOptionElement.cpp
dom/html/HTMLOptionElement.h
dom/html/HTMLSelectElement.cpp
dom/html/HTMLTextAreaElement.cpp
dom/html/nsGenericHTMLElement.cpp
dom/html/nsGenericHTMLElement.h
--- a/dom/events/EventStates.h
+++ b/dom/events/EventStates.h
@@ -323,16 +323,18 @@ private:
 
 #define DIRECTION_STATES (NS_EVENT_STATE_LTR | NS_EVENT_STATE_RTL)
 
 #define DIR_ATTR_STATES (NS_EVENT_STATE_HAS_DIR_ATTR |          \
                          NS_EVENT_STATE_DIR_ATTR_LTR |          \
                          NS_EVENT_STATE_DIR_ATTR_RTL |          \
                          NS_EVENT_STATE_DIR_ATTR_LIKE_AUTO)
 
+#define DISABLED_STATES (NS_EVENT_STATE_DISABLED | NS_EVENT_STATE_ENABLED)
+
 // Event states that can be added and removed through
 // Element::{Add,Remove}ManuallyManagedStates.
 //
 // Take care when manually managing state bits.  You are responsible for
 // setting or clearing the bit when an Element is added or removed from a
 // document (e.g. in BindToTree and UnbindFromTree), if that is an
 // appropriate thing to do for your state bit.
 #define MANUALLY_MANAGED_STATES (             \
@@ -342,16 +344,17 @@ private:
 
 // Event states that are managed externally to an element (by the
 // EventStateManager, or by other code).  As opposed to those in
 // INTRINSIC_STATES, which are are computed by the element itself
 // and returned from Element::IntrinsicState.
 #define EXTERNALLY_MANAGED_STATES (           \
   MANUALLY_MANAGED_STATES |                   \
   DIR_ATTR_STATES |                           \
+  DISABLED_STATES |                           \
   NS_EVENT_STATE_ACTIVE |                     \
   NS_EVENT_STATE_DRAGOVER |                   \
   NS_EVENT_STATE_FOCUS |                      \
   NS_EVENT_STATE_FOCUSRING |                  \
   NS_EVENT_STATE_FOCUS_WITHIN |               \
   NS_EVENT_STATE_FULL_SCREEN |                \
   NS_EVENT_STATE_HOVER |                      \
   NS_EVENT_STATE_UNRESOLVED |                 \
--- a/dom/html/HTMLButtonElement.cpp
+++ b/dom/html/HTMLButtonElement.cpp
@@ -101,19 +101,24 @@ HTMLButtonElement::UpdateBarredFromConst
   SetBarredFromConstraintValidation(mType == NS_FORM_BUTTON_BUTTON ||
                                     mType == NS_FORM_BUTTON_RESET ||
                                     IsDisabled());
 }
 
 void
 HTMLButtonElement::FieldSetDisabledChanged(bool aNotify)
 {
-  UpdateBarredFromConstraintValidation();
 
+  // FieldSetDisabledChanged *has* to be called *before*
+  // UpdateBarredFromConstraintValidation, because the latter depends on our
+  // disabled state.
   nsGenericHTMLFormElementWithState::FieldSetDisabledChanged(aNotify);
+
+  UpdateBarredFromConstraintValidation();
+  UpdateState(aNotify);
 }
 
 // nsIDOMHTMLButtonElement
 
 NS_IMPL_ELEMENT_CLONE(HTMLButtonElement)
 
 
 // nsIDOMHTMLButtonElement
@@ -431,16 +436,22 @@ HTMLButtonElement::AfterSetAttr(int32_t 
       if (aValue) {
         mType = aValue->GetEnumValue();
       } else {
         mType = kButtonDefaultType->value;
       }
     }
 
     if (aName == nsGkAtoms::type || aName == nsGkAtoms::disabled) {
+      if (aName == nsGkAtoms::disabled) {
+        // This *has* to be called *before* validity state check because
+        // UpdateBarredFromConstraintValidation depends on our disabled state.
+        UpdateDisabledState(aNotify);
+      }
+
       UpdateBarredFromConstraintValidation();
     }
   }
 
   return nsGenericHTMLFormElementWithState::AfterSetAttr(aNameSpaceID, aName,
                                                          aValue, aOldValue,
                                                          aNotify);
 }
--- a/dom/html/HTMLFieldSetElement.cpp
+++ b/dom/html/HTMLFieldSetElement.cpp
@@ -78,27 +78,32 @@ HTMLFieldSetElement::GetEventTargetParen
   return nsGenericHTMLFormElement::GetEventTargetParent(aVisitor);
 }
 
 nsresult
 HTMLFieldSetElement::AfterSetAttr(int32_t aNameSpaceID, nsIAtom* aName,
                                   const nsAttrValue* aValue,
                                   const nsAttrValue* aOldValue, bool aNotify)
 {
-  if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::disabled &&
-      nsINode::GetFirstChild()) {
-    if (!mElements) {
-      mElements = new nsContentList(this, MatchListedElements, nullptr, nullptr,
-                                    true);
-    }
+  if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::disabled) {
+    // This *has* to be called *before* calling FieldSetDisabledChanged on our
+    // controls, as they may depend on our disabled state.
+    UpdateDisabledState(aNotify);
 
-    uint32_t length = mElements->Length(true);
-    for (uint32_t i=0; i<length; ++i) {
-      static_cast<nsGenericHTMLFormElement*>(mElements->Item(i))
-        ->FieldSetDisabledChanged(aNotify);
+    if (nsINode::GetFirstChild()) {
+      if (!mElements) {
+        mElements = new nsContentList(this, MatchListedElements, nullptr, nullptr,
+                                      true);
+      }
+
+      uint32_t length = mElements->Length(true);
+      for (uint32_t i=0; i<length; ++i) {
+        static_cast<nsGenericHTMLFormElement*>(mElements->Item(i))
+          ->FieldSetDisabledChanged(aNotify);
+      }
     }
   }
 
   return nsGenericHTMLFormElement::AfterSetAttr(aNameSpaceID, aName,
                                                 aValue, aOldValue, aNotify);
 }
 
 // nsIDOMHTMLFieldSetElement
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -1441,16 +1441,23 @@ HTMLInputElement::AfterSetAttr(int32_t a
       }
       if (newType != mType) {
         HandleTypeChange(newType, aNotify);
       }
     }
 
     if (aName == nsGkAtoms::required || aName == nsGkAtoms::disabled ||
         aName == nsGkAtoms::readonly) {
+      if (aName == nsGkAtoms::disabled) {
+        // This *has* to be called *before* validity state check because
+        // UpdateBarredFromConstraintValidation and
+        // UpdateValueMissingValidityState depend on our disabled state.
+        UpdateDisabledState(aNotify);
+      }
+
       UpdateValueMissingValidityState();
 
       // This *has* to be called *after* validity has changed.
       if (aName == nsGkAtoms::readonly || aName == nsGkAtoms::disabled) {
         UpdateBarredFromConstraintValidation();
       }
     } else if (aName == nsGkAtoms::maxlength) {
       UpdateTooLongValidityState();
@@ -7458,20 +7465,24 @@ HTMLInputElement::HasCachedSelection()
     }
   }
   return isCached;
 }
 
 void
 HTMLInputElement::FieldSetDisabledChanged(bool aNotify)
 {
+  // This *has* to be called *before* UpdateBarredFromConstraintValidation and
+  // UpdateValueMissingValidityState because these two functions depend on our
+  // disabled state.
+  nsGenericHTMLFormElementWithState::FieldSetDisabledChanged(aNotify);
+
   UpdateValueMissingValidityState();
   UpdateBarredFromConstraintValidation();
-
-  nsGenericHTMLFormElementWithState::FieldSetDisabledChanged(aNotify);
+  UpdateState(aNotify);
 }
 
 void
 HTMLInputElement::SetFilePickerFiltersFromAccept(nsIFilePicker* filePicker)
 {
   // We always add |filterAll|
   filePicker->AppendFilters(nsIFilePicker::filterAll);
 
--- a/dom/html/HTMLLabelElement.h
+++ b/dom/html/HTMLLabelElement.h
@@ -54,18 +54,16 @@ public:
   nsGenericHTMLElement* GetControl() const
   {
     return GetLabeledElement();
   }
 
   using nsGenericHTMLElement::Focus;
   virtual void Focus(mozilla::ErrorResult& aError) override;
 
-  virtual bool IsDisabled() const override { return false; }
-
   // nsIContent
   virtual nsresult PostHandleEvent(
                      EventChainPostVisitor& aVisitor) override;
   virtual bool PerformAccesskey(bool aKeyCausesActivation,
                                 bool aIsTrustedEvent) override;
   virtual nsresult Clone(mozilla::dom::NodeInfo *aNodeInfo, nsINode **aResult,
                          bool aPreallocateChildren) const override;
 
--- a/dom/html/HTMLOptGroupElement.cpp
+++ b/dom/html/HTMLOptGroupElement.cpp
@@ -97,47 +97,45 @@ HTMLOptGroupElement::RemoveChildAt(uint3
 }
 
 nsresult
 HTMLOptGroupElement::AfterSetAttr(int32_t aNameSpaceID, nsIAtom* aName,
                                   const nsAttrValue* aValue,
                                   const nsAttrValue* aOldValue, bool aNotify)
 {
   if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::disabled) {
-    // All our children <option> have their :disabled state depending on our
-    // disabled attribute. We should make sure their state is updated.
-    for (nsIContent* child = nsINode::GetFirstChild(); child;
-         child = child->GetNextSibling()) {
-      if (child->IsHTMLElement(nsGkAtoms::option)) {
-        // No need to call |IsElement()| because it's an HTML element.
-        child->AsElement()->UpdateState(true);
+
+    EventStates disabledStates;
+    if (aValue) {
+      disabledStates |= NS_EVENT_STATE_DISABLED;
+    } else {
+      disabledStates |= NS_EVENT_STATE_ENABLED;
+    }
+
+    EventStates oldDisabledStates = State() & DISABLED_STATES;
+    EventStates changedStates = disabledStates ^ oldDisabledStates;
+
+    if (!changedStates.IsEmpty()) {
+      ToggleStates(changedStates, aNotify);
+
+      // All our children <option> have their :disabled state depending on our
+      // disabled attribute. We should make sure their state is updated.
+      for (nsIContent* child = nsINode::GetFirstChild(); child;
+           child = child->GetNextSibling()) {
+        if (auto optElement = HTMLOptionElement::FromContent(child)) {
+          optElement->OptGroupDisabledChanged(true);
+        }
       }
     }
   }
 
   return nsGenericHTMLElement::AfterSetAttr(aNameSpaceID, aName, aValue,
                                             aOldValue, aNotify);
 }
 
-EventStates
-HTMLOptGroupElement::IntrinsicState() const
-{
-  EventStates state = nsGenericHTMLElement::IntrinsicState();
-
-  if (HasAttr(kNameSpaceID_None, nsGkAtoms::disabled)) {
-    state |= NS_EVENT_STATE_DISABLED;
-    state &= ~NS_EVENT_STATE_ENABLED;
-  } else {
-    state &= ~NS_EVENT_STATE_DISABLED;
-    state |= NS_EVENT_STATE_ENABLED;
-  }
-
-  return state;
-}
-
 JSObject*
 HTMLOptGroupElement::WrapNode(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
 {
   return HTMLOptGroupElementBinding::Wrap(aCx, this, aGivenProto);
 }
 
 } // namespace dom
 } // namespace mozilla
--- a/dom/html/HTMLOptGroupElement.h
+++ b/dom/html/HTMLOptGroupElement.h
@@ -33,30 +33,28 @@ public:
   virtual nsresult InsertChildAt(nsIContent* aKid, uint32_t aIndex,
                                  bool aNotify) override;
   virtual void RemoveChildAt(uint32_t aIndex, bool aNotify) override;
 
   // nsIContent
   virtual nsresult GetEventTargetParent(
                      EventChainPreVisitor& aVisitor) override;
 
-  virtual EventStates IntrinsicState() const override;
-
   virtual nsresult Clone(mozilla::dom::NodeInfo* aNodeInfo, nsINode** aResult,
                          bool aPreallocateChildren) const override;
 
   virtual nsresult AfterSetAttr(int32_t aNameSpaceID, nsIAtom* aName,
                                 const nsAttrValue* aValue,
                                 const nsAttrValue* aOldValue,
                                 bool aNotify) override;
 
   virtual nsIDOMNode* AsDOMNode() override { return this; }
 
   virtual bool IsDisabled() const override {
-    return HasAttr(kNameSpaceID_None, nsGkAtoms::disabled);
+    return State().HasState(NS_EVENT_STATE_DISABLED);
   }
 
   bool Disabled() const
   {
     return GetBoolAttr(nsGkAtoms::disabled);
   }
   void SetDisabled(bool aValue, ErrorResult& aError)
   {
--- a/dom/html/HTMLOptionElement.cpp
+++ b/dom/html/HTMLOptionElement.cpp
@@ -79,16 +79,49 @@ HTMLOptionElement::SetSelectedInternal(b
 
   // When mIsInSetDefaultSelected is true, the state change will be handled by
   // SetAttr/UnsetAttr.
   if (!mIsInSetDefaultSelected) {
     UpdateState(aNotify);
   }
 }
 
+void
+HTMLOptionElement::OptGroupDisabledChanged(bool aNotify)
+{
+  UpdateDisabledState(aNotify);
+}
+
+void
+HTMLOptionElement::UpdateDisabledState(bool aNotify)
+{
+  bool isDisabled = HasAttr(kNameSpaceID_None, nsGkAtoms::disabled);
+
+  if (!isDisabled) {
+    nsIContent* parent = GetParent();
+    if (auto optGroupElement = HTMLOptGroupElement::FromContentOrNull(parent)) {
+      isDisabled = optGroupElement->IsDisabled();
+    }
+  }
+
+  EventStates disabledStates;
+  if (isDisabled) {
+    disabledStates |= NS_EVENT_STATE_DISABLED;
+  } else {
+    disabledStates |= NS_EVENT_STATE_ENABLED;
+  }
+
+  EventStates oldDisabledStates = State() & DISABLED_STATES;
+  EventStates changedStates = disabledStates ^ oldDisabledStates;
+
+  if (!changedStates.IsEmpty()) {
+    ToggleStates(changedStates, aNotify);
+  }
+}
+
 NS_IMETHODIMP
 HTMLOptionElement::GetSelected(bool* aValue)
 {
   NS_ENSURE_ARG_POINTER(aValue);
   *aValue = Selected();
   return NS_OK;
 }
 
@@ -230,24 +263,29 @@ HTMLOptionElement::BeforeSetAttr(int32_t
   return NS_OK;
 }
 
 nsresult
 HTMLOptionElement::AfterSetAttr(int32_t aNameSpaceID, nsIAtom* aName,
                                 const nsAttrValue* aValue,
                                 const nsAttrValue* aOldValue, bool aNotify)
 {
-  if (aNameSpaceID == kNameSpaceID_None &&
-      aName == nsGkAtoms::value && Selected()) {
-    // Since this option is selected, changing value
-    // may have changed missing validity state of the
-    // Select element
-    HTMLSelectElement* select = GetSelect();
-    if (select) {
-      select->UpdateValueMissingValidityState();
+  if (aNameSpaceID == kNameSpaceID_None) {
+    if (aName == nsGkAtoms::disabled) {
+      UpdateDisabledState(aNotify);
+    }
+
+    if (aName == nsGkAtoms::value && Selected()) {
+      // Since this option is selected, changing value
+      // may have changed missing validity state of the
+      // Select element
+      HTMLSelectElement* select = GetSelect();
+      if (select) {
+        select->UpdateValueMissingValidityState();
+      }
     }
   }
 
   return nsGenericHTMLElement::AfterSetAttr(aNameSpaceID, aName,
                                             aValue, aOldValue, aNotify);
 }
 
 NS_IMETHODIMP
@@ -288,58 +326,41 @@ HTMLOptionElement::BindToTree(nsIDocumen
                               bool aCompileEventHandlers)
 {
   nsresult rv = nsGenericHTMLElement::BindToTree(aDocument, aParent,
                                                  aBindingParent,
                                                  aCompileEventHandlers);
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Our new parent might change :disabled/:enabled state.
-  UpdateState(false);
+  UpdateDisabledState(false);
 
   return NS_OK;
 }
 
 void
 HTMLOptionElement::UnbindFromTree(bool aDeep, bool aNullParent)
 {
   nsGenericHTMLElement::UnbindFromTree(aDeep, aNullParent);
 
   // Our previous parent could have been involved in :disabled/:enabled state.
-  UpdateState(false);
+  UpdateDisabledState(false);
 }
 
 EventStates
 HTMLOptionElement::IntrinsicState() const
 {
   EventStates state = nsGenericHTMLElement::IntrinsicState();
   if (Selected()) {
     state |= NS_EVENT_STATE_CHECKED;
   }
   if (DefaultSelected()) {
     state |= NS_EVENT_STATE_DEFAULT;
   }
 
-  // An <option> is disabled if it has @disabled set or if it's <optgroup> has
-  // @disabled set.
-  if (HasAttr(kNameSpaceID_None, nsGkAtoms::disabled)) {
-    state |= NS_EVENT_STATE_DISABLED;
-    state &= ~NS_EVENT_STATE_ENABLED;
-  } else {
-    nsIContent* parent = GetParent();
-    if (parent && parent->IsHTMLElement(nsGkAtoms::optgroup) &&
-        parent->HasAttr(kNameSpaceID_None, nsGkAtoms::disabled)) {
-      state |= NS_EVENT_STATE_DISABLED;
-      state &= ~NS_EVENT_STATE_ENABLED;
-    } else {
-      state &= ~NS_EVENT_STATE_DISABLED;
-      state |= NS_EVENT_STATE_ENABLED;
-    }
-  }
-
   return state;
 }
 
 // Get the select content element that contains this option
 HTMLSelectElement*
 HTMLOptionElement::GetSelect()
 {
   nsIContent* parent = GetParent();
--- a/dom/html/HTMLOptionElement.h
+++ b/dom/html/HTMLOptionElement.h
@@ -57,32 +57,45 @@ public:
                                  bool aNotify) override;
   virtual nsresult AfterSetAttr(int32_t aNameSpaceID, nsIAtom* aName,
                                 const nsAttrValue* aValue,
                                 const nsAttrValue* aOldValue,
                                 bool aNotify) override;
 
   void SetSelectedInternal(bool aValue, bool aNotify);
 
+  /**
+   * This callback is called by an optgroup on all its option elements whenever
+   * its disabled state is changed so that option elements can know their
+   * disabled state might have changed.
+   */
+  void OptGroupDisabledChanged(bool aNotify);
+
+  /**
+   * Check our disabled content attribute and optgroup's (if it exists) disabled
+   * state to decide whether our disabled flag should be toggled.
+   */
+  void UpdateDisabledState(bool aNotify);
+
   virtual nsresult BindToTree(nsIDocument* aDocument, nsIContent* aParent,
                               nsIContent* aBindingParent,
                               bool aCompileEventHandlers) override;
   virtual void UnbindFromTree(bool aDeep = true,
                               bool aNullParent = true) override;
 
   // nsIContent
   virtual EventStates IntrinsicState() const override;
 
   virtual nsresult Clone(mozilla::dom::NodeInfo* aNodeInfo, nsINode** aResult,
                          bool aPreallocateChildren) const override;
 
   nsresult CopyInnerTo(mozilla::dom::Element* aDest, bool aPreallocateChildren);
 
   virtual bool IsDisabled() const override {
-    return HasAttr(kNameSpaceID_None, nsGkAtoms::disabled);
+    return State().HasState(NS_EVENT_STATE_DISABLED);
   }
 
   bool Disabled() const
   {
     return GetBoolAttr(nsGkAtoms::disabled);
   }
 
   void SetDisabled(bool aValue, ErrorResult& aRv)
--- a/dom/html/HTMLSelectElement.cpp
+++ b/dom/html/HTMLSelectElement.cpp
@@ -1320,16 +1320,22 @@ HTMLSelectElement::BeforeSetAttr(int32_t
 
 nsresult
 HTMLSelectElement::AfterSetAttr(int32_t aNameSpaceID, nsIAtom* aName,
                                 const nsAttrValue* aValue,
                                 const nsAttrValue* aOldValue, bool aNotify)
 {
   if (aNameSpaceID == kNameSpaceID_None) {
     if (aName == nsGkAtoms::disabled) {
+      // This *has* to be called *before* validity state check because
+      // UpdateBarredFromConstraintValidation and
+      // UpdateValueMissingValidityState depend on our disabled state.
+      UpdateDisabledState(aNotify);
+
+      UpdateValueMissingValidityState();
       UpdateBarredFromConstraintValidation();
     } else if (aName == nsGkAtoms::required) {
       UpdateValueMissingValidityState();
     } else if (aName == nsGkAtoms::autocomplete) {
       // Clear the cached @autocomplete attribute and autocompleteInfo state.
       mAutocompleteAttrState = nsContentUtils::eAutocompleteAttrState_Unknown;
       mAutocompleteInfoState = nsContentUtils::eAutocompleteAttrState_Unknown;
     } else if (aName == nsGkAtoms::multiple) {
@@ -1864,19 +1870,24 @@ void
 HTMLSelectElement::UpdateBarredFromConstraintValidation()
 {
   SetBarredFromConstraintValidation(IsDisabled());
 }
 
 void
 HTMLSelectElement::FieldSetDisabledChanged(bool aNotify)
 {
-  UpdateBarredFromConstraintValidation();
+  // This *has* to be called before UpdateBarredFromConstraintValidation and
+  // UpdateValueMissingValidityState because these two functions depend on our
+  // disabled state.
+  nsGenericHTMLFormElementWithState::FieldSetDisabledChanged(aNotify);
 
-  nsGenericHTMLFormElementWithState::FieldSetDisabledChanged(aNotify);
+  UpdateValueMissingValidityState();
+  UpdateBarredFromConstraintValidation();
+  UpdateState(aNotify);
 }
 
 void
 HTMLSelectElement::SetSelectionChanged(bool aValue, bool aNotify)
 {
   if (!mDefaultSelectionSet) {
     return;
   }
--- a/dom/html/HTMLTextAreaElement.cpp
+++ b/dom/html/HTMLTextAreaElement.cpp
@@ -1102,16 +1102,23 @@ HTMLTextAreaElement::ContentChanged(nsIC
 nsresult
 HTMLTextAreaElement::AfterSetAttr(int32_t aNameSpaceID, nsIAtom* aName,
                                   const nsAttrValue* aValue,
                                   const nsAttrValue* aOldValue, bool aNotify)
 {
   if (aNameSpaceID == kNameSpaceID_None) {
     if (aName == nsGkAtoms::required || aName == nsGkAtoms::disabled ||
         aName == nsGkAtoms::readonly) {
+      if (aName == nsGkAtoms::disabled) {
+        // This *has* to be called *before* validity state check because
+        // UpdateBarredFromConstraintValidation and
+        // UpdateValueMissingValidityState depend on our disabled state.
+        UpdateDisabledState(aNotify);
+      }
+
       UpdateValueMissingValidityState();
 
       // This *has* to be called *after* validity has changed.
       if (aName == nsGkAtoms::readonly || aName == nsGkAtoms::disabled) {
         UpdateBarredFromConstraintValidation();
       }
     } else if (aName == nsGkAtoms::maxlength) {
       UpdateTooLongValidityState();
@@ -1415,20 +1422,24 @@ NS_IMETHODIMP_(bool)
 HTMLTextAreaElement::HasCachedSelection()
 {
   return mState.IsSelectionCached();
 }
 
 void
 HTMLTextAreaElement::FieldSetDisabledChanged(bool aNotify)
 {
+  // This *has* to be called before UpdateBarredFromConstraintValidation and
+  // UpdateValueMissingValidityState because these two functions depend on our
+  // disabled state.
+  nsGenericHTMLFormElementWithState::FieldSetDisabledChanged(aNotify);
+
   UpdateValueMissingValidityState();
   UpdateBarredFromConstraintValidation();
-
-  nsGenericHTMLFormElementWithState::FieldSetDisabledChanged(aNotify);
+  UpdateState(aNotify);
 }
 
 JSObject*
 HTMLTextAreaElement::WrapNode(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
 {
   return HTMLTextAreaElementBinding::Wrap(aCx, this, aGivenProto);
 }
 
--- a/dom/html/nsGenericHTMLElement.cpp
+++ b/dom/html/nsGenericHTMLElement.cpp
@@ -2086,18 +2086,17 @@ nsGenericHTMLFormElement::PreHandleEvent
   }
   return nsGenericHTMLElement::PreHandleEvent(aVisitor);
 }
 
 /* virtual */
 bool
 nsGenericHTMLFormElement::IsDisabled() const
 {
-  return HasAttr(kNameSpaceID_None, nsGkAtoms::disabled) ||
-         (mFieldSet && mFieldSet->IsDisabled());
+  return State().HasState(NS_EVENT_STATE_DISABLED);
 }
 
 void
 nsGenericHTMLFormElement::ForgetFieldSet(nsIContent* aFieldset)
 {
   if (mFieldSet == aFieldset) {
     mFieldSet = nullptr;
   }
@@ -2132,27 +2131,16 @@ nsGenericHTMLFormElement::IsHTMLFocusabl
 EventStates
 nsGenericHTMLFormElement::IntrinsicState() const
 {
   // If you add attribute-dependent states here, you need to add them them to
   // AfterSetAttr too.  And add them to AfterSetAttr for all subclasses that
   // implement IntrinsicState() and are affected by that attribute.
   EventStates state = nsGenericHTMLElement::IntrinsicState();
 
-  if (CanBeDisabled()) {
-    // :enabled/:disabled
-    if (IsDisabled()) {
-      state |= NS_EVENT_STATE_DISABLED;
-      state &= ~NS_EVENT_STATE_ENABLED;
-    } else {
-      state &= ~NS_EVENT_STATE_DISABLED;
-      state |= NS_EVENT_STATE_ENABLED;
-    }
-  }
-
   if (mForm && mForm->IsDefaultSubmitElement(this)) {
       NS_ASSERTION(IsSubmitControl(),
                    "Default submit element that isn't a submit control.");
       // We are the default submit element (:default)
       state |= NS_EVENT_STATE_DEFAULT;
   }
 
   // Make the text controls read-write
@@ -2404,20 +2392,47 @@ nsGenericHTMLFormElement::UpdateFieldSet
   if (mFieldSet) {
     mFieldSet->RemoveElement(this);
     mFieldSet = nullptr;
     // The disabled state may have changed
     FieldSetDisabledChanged(aNotify);
   }
 }
 
+void nsGenericHTMLFormElement::UpdateDisabledState(bool aNotify)
+{
+  if (!CanBeDisabled()) {
+    return;
+  }
+
+  bool isDisabled = HasAttr(kNameSpaceID_None, nsGkAtoms::disabled);
+
+  if (!isDisabled && mFieldSet) {
+    isDisabled = mFieldSet->IsDisabled();
+  }
+
+  EventStates disabledStates;
+  if (isDisabled) {
+    disabledStates |= NS_EVENT_STATE_DISABLED;
+  } else {
+    disabledStates |= NS_EVENT_STATE_ENABLED;
+  }
+
+  EventStates oldDisabledStates = State() & DISABLED_STATES;
+  EventStates changedStates = disabledStates ^ oldDisabledStates;
+
+  if (!changedStates.IsEmpty()) {
+    ToggleStates(changedStates, aNotify);
+  }
+}
+
 void
 nsGenericHTMLFormElement::FieldSetDisabledChanged(bool aNotify)
 {
-  UpdateState(aNotify);
+  UpdateDisabledState(aNotify);
 }
 
 bool
 nsGenericHTMLFormElement::IsLabelable() const
 {
   // TODO: keygen should be in that list, see bug 101019.
   uint32_t type = ControlType();
   return (type & NS_FORM_INPUT_ELEMENT && type != NS_FORM_INPUT_HIDDEN) ||
--- a/dom/html/nsGenericHTMLElement.h
+++ b/dom/html/nsGenericHTMLElement.h
@@ -1092,16 +1092,22 @@ public:
    * might have changed.
    *
    * @note Classes redefining this method should not do any content
    * state updates themselves but should just make sure to call into
    * nsGenericHTMLFormElement::FieldSetDisabledChanged.
    */
   virtual void FieldSetDisabledChanged(bool aNotify);
 
+  /**
+   * Check our disabled content attribute and fieldset's (if it exists) disabled
+   * state to decide whether our disabled flag should be toggled.
+   */
+  void UpdateDisabledState(bool aNotify);
+
   void FieldSetFirstLegendChanged(bool aNotify) {
     UpdateFieldSet(aNotify);
   }
 
   /**
    * This callback is called by a fieldset on all it's elements when it's being
    * destroyed. When called, the elements should check that aFieldset is there
    * first parent fieldset and null mFieldset in that case only.