Bug 759666 - Make :disabled apply on <option> when their parent <optgroup> has @disabled set. r=bz
authorMounir Lamouri <mounir.lamouri@gmail.com>
Fri, 01 Jun 2012 11:46:43 +0200
changeset 95521 654fb530a96894be08e35b9eb713dea2b7d18be8
parent 95520 68f264782f5eaad4611915c0eec00be7ab6f246d
child 95522 3e88a83dcb3743381c37a281be935fe9882b6a61
push id22819
push usereakhgari@mozilla.com
push dateSat, 02 Jun 2012 18:40:08 +0000
treeherdermozilla-central@f4a7c1a1f514 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbz
bugs759666
milestone15.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 759666 - Make :disabled apply on <option> when their parent <optgroup> has @disabled set. r=bz
content/html/content/src/nsHTMLOptGroupElement.cpp
content/html/content/src/nsHTMLOptionElement.cpp
content/html/content/src/nsHTMLOptionElement.h
content/html/content/test/forms/Makefile.in
content/html/content/test/forms/test_option_disabled.html
--- a/content/html/content/src/nsHTMLOptGroupElement.cpp
+++ b/content/html/content/src/nsHTMLOptGroupElement.cpp
@@ -49,16 +49,19 @@ public:
   virtual nsresult PreHandleEvent(nsEventChainPreVisitor& aVisitor);
 
   virtual nsEventStates IntrinsicState() const;
  
   virtual nsresult Clone(nsINodeInfo *aNodeInfo, nsINode **aResult) const;
 
   virtual nsXPCClassInfo* GetClassInfo();
 
+  virtual nsresult AfterSetAttr(PRInt32 aNameSpaceID, nsIAtom* aName,
+                                const nsAttrValue* aValue, bool aNotify);
+
   virtual nsIDOMNode* AsDOMNode() { return this; }
 
   virtual bool IsDisabled() const {
     return HasAttr(kNameSpaceID_None, nsGkAtoms::disabled);
   }
 protected:
 
   /**
@@ -160,16 +163,36 @@ nsHTMLOptGroupElement::InsertChildAt(nsI
 void
 nsHTMLOptGroupElement::RemoveChildAt(PRUint32 aIndex, bool aNotify)
 {
   nsSafeOptionListMutation safeMutation(GetSelect(), this, nsnull, aIndex,
                                         aNotify);
   nsGenericHTMLElement::RemoveChildAt(aIndex, aNotify);
 }
 
+nsresult
+nsHTMLOptGroupElement::AfterSetAttr(PRInt32 aNameSpaceID, nsIAtom* aName,
+                                    const nsAttrValue* aValue, 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->IsHTML(nsGkAtoms::option)) {
+        // No need to call |IsElement()| because it's an HTML element.
+        child->AsElement()->UpdateState(true);
+      }
+    }
+  }
+
+  return nsGenericHTMLElement::AfterSetAttr(aNameSpaceID, aName, aValue,
+                                            aNotify);
+}
+
 nsEventStates
 nsHTMLOptGroupElement::IntrinsicState() const
 {
   nsEventStates state = nsGenericHTMLElement::IntrinsicState();
 
   if (HasAttr(kNameSpaceID_None, nsGkAtoms::disabled)) {
     state |= NS_EVENT_STATE_DISABLED;
     state &= ~NS_EVENT_STATE_ENABLED;
--- a/content/html/content/src/nsHTMLOptionElement.cpp
+++ b/content/html/content/src/nsHTMLOptionElement.cpp
@@ -272,33 +272,67 @@ nsHTMLOptionElement::GetText(nsAString& 
 }
 
 NS_IMETHODIMP
 nsHTMLOptionElement::SetText(const nsAString& aText)
 {
   return nsContentUtils::SetNodeTextContent(this, aText, true);
 }
 
+nsresult
+nsHTMLOptionElement::BindToTree(nsIDocument* aDocument, nsIContent* aParent,
+                                nsIContent* aBindingParent,
+                                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);
+
+  return NS_OK;
+}
+
+void
+nsHTMLOptionElement::UnbindFromTree(bool aDeep, bool aNullParent)
+{
+  nsGenericHTMLElement::UnbindFromTree(aDeep, aNullParent);
+
+  // Our previous parent could have been involved in :disabled/:enabled state.
+  UpdateState(false);
+}
+
 nsEventStates
 nsHTMLOptionElement::IntrinsicState() const
 {
   nsEventStates 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 {
-    state &= ~NS_EVENT_STATE_DISABLED;
-    state |= NS_EVENT_STATE_ENABLED;
+    nsIContent* parent = GetParent();
+    if (parent && parent->IsHTML(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
 nsHTMLSelectElement*
 nsHTMLOptionElement::GetSelect()
--- a/content/html/content/src/nsHTMLOptionElement.h
+++ b/content/html/content/src/nsHTMLOptionElement.h
@@ -57,16 +57,22 @@ public:
                                               PRInt32 aModType) const;
 
   virtual nsresult BeforeSetAttr(PRInt32 aNamespaceID, nsIAtom* aName,
                                  const nsAttrValueOrString* aValue,
                                  bool aNotify);
 
   void SetSelectedInternal(bool aValue, bool aNotify);
 
+  virtual nsresult BindToTree(nsIDocument* aDocument, nsIContent* aParent,
+                              nsIContent* aBindingParent,
+                              bool aCompileEventHandlers);
+  virtual void UnbindFromTree(bool aDeep = true,
+                              bool aNullParent = true);
+
   // nsIContent
   virtual nsEventStates IntrinsicState() const;
 
   virtual nsresult Clone(nsINodeInfo *aNodeInfo, nsINode **aResult) const;
 
   nsresult CopyInnerTo(nsGenericElement* aDest) const;
 
   virtual nsXPCClassInfo* GetClassInfo();
--- a/content/html/content/test/forms/Makefile.in
+++ b/content/html/content/test/forms/Makefile.in
@@ -34,13 +34,14 @@ include $(topsrcdir)/config/rules.mk
 		test_datalist_element.html \
 		test_form_attributes_reflection.html \
 		test_option_index_attribute.html \
 		test_progress_element.html \
 		test_form_attribute-1.html \
 		test_form_attribute-2.html \
 		test_form_attribute-3.html \
 		test_form_attribute-4.html \
+		test_option_disabled.html \
 		$(NULL)
 
 libs:: $(_TEST_FILES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir)
 
new file mode 100644
--- /dev/null
+++ b/content/html/content/test/forms/test_option_disabled.html
@@ -0,0 +1,123 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=759666
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for HTMLOptionElement disabled attribute and pseudo-class</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=759666">Mozilla Bug 759666</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+  
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLOptionElement disabled attribute and pseudo-class **/
+
+var testCases = [
+  // Static checks.
+  { html: "<option></option>",
+    result: { attr: null, idl: false, pseudo: false } },
+  { html: "<option disabled></option>",
+    result: { attr: "", idl: true, pseudo: true } },
+  { html: "<optgroup><option></option></otpgroup>",
+    result: { attr: null, idl: false, pseudo: false } },
+  { html: "<optgroup><option disabled></option></optgroup>",
+    result: { attr: "", idl: true, pseudo: true } },
+  { html: "<optgroup disabled><option disabled></option></optgroup>",
+    result: { attr: "", idl: true, pseudo: true } },
+  { html: "<optgroup disabled><option></option></optgroup>",
+    result: { attr: null, idl: false, pseudo: true } },
+  { html: "<optgroup><optgroup disabled><option></option></optgroup></optgroup>",
+    result: { attr: null, idl: false, pseudo: true } },
+  { html: "<optgroup disabled><optgroup><option></option></optgroup></optgroup>",
+    result: { attr: null, idl: false, pseudo: false } },
+  { html: "<optgroup disabled><optgroup><option disabled></option></optgroup></optgroup>",
+    result: { attr: "", idl: true, pseudo: true } },
+
+  // Dynamic checks: changing disable value.
+  { html: "<option></option>",
+    modifier: function(c) { c.querySelector('option').disabled = true; },
+    result: { attr: "", idl: true, pseudo: true } },
+  { html: "<option disabled></option>",
+    modifier: function(c) { c.querySelector('option').disabled = false; },
+    result: { attr: null, idl: false, pseudo: false } },
+  { html: "<optgroup><option></option></otpgroup>",
+    modifier: function(c) { c.querySelector('optgroup').disabled = true; },
+    result: { attr: null, idl: false, pseudo: true } },
+  { html: "<optgroup><option disabled></option></optgroup>",
+    modifier: function(c) { c.querySelector('option').disabled = false; },
+    result: { attr: null, idl: false, pseudo: false } },
+  { html: "<optgroup disabled><option disabled></option></optgroup>",
+    modifier: function(c) { c.querySelector('optgroup').disabled = false; },
+    result: { attr: "", idl: true, pseudo: true } },
+  { html: "<optgroup disabled><option disabled></option></optgroup>",
+    modifier: function(c) { c.querySelector('option').disabled = false; },
+    result: { attr: null, idl: false, pseudo: true } },
+  { html: "<optgroup disabled><option disabled></option></optgroup>",
+    modifier: function(c) { c.querySelector('optgroup').disabled = c.querySelector('option').disabled = false; },
+    result: { attr: null, idl: false, pseudo: false } },
+  { html: "<optgroup disabled><option></option></optgroup>",
+    modifier: function(c) { c.querySelector('optgroup').disabled = false; },
+    result: { attr: null, idl: false, pseudo: false } },
+  { html: "<optgroup><optgroup disabled><option></option></optgroup></optgroup>",
+    modifier: function(c) { c.querySelector('optgroup[disabled]').disabled = false; },
+    result: { attr: null, idl: false, pseudo: false } },
+  { html: "<optgroup disabled><optgroup><option></option></optgroup></optgroup>",
+    modifier: function(c) { c.querySelector('optgroup[disabled]').disabled = false; },
+    result: { attr: null, idl: false, pseudo: false } },
+  { html: "<optgroup disabled><optgroup><option disabled></option></optgroup></optgroup>",
+    modifier: function(c) { c.querySelector('optgroup').disabled = false; },
+    result: { attr: "", idl: true, pseudo: true } },
+  { html: "<optgroup disabled><optgroup><option disabled></option></optgroup></optgroup>",
+    modifier: function(c) { c.querySelector('option').disabled = false; },
+    result: { attr: null, idl: false, pseudo: false } },
+  { html: "<optgroup disabled><optgroup><option disabled></option></optgroup></optgroup>",
+    modifier: function(c) { c.querySelector('option').disabled = c.querySelector('option').disabled = false; },
+    result: { attr: null, idl: false, pseudo: false } },
+
+  // Dynamic checks: moving option element.
+  { html: "<optgroup id='a'><option></option></optgroup><optgroup id='b'></optgroup>",
+    modifier: function(c) { c.querySelector('#b').appendChild(c.querySelector('option')); },
+    result: { attr: null, idl: false, pseudo: false } },
+  { html: "<optgroup id='a'><option disabled></option></optgroup><optgroup id='b'></optgroup>",
+    modifier: function(c) { c.querySelector('#b').appendChild(c.querySelector('option')); },
+    result: { attr: "", idl: true, pseudo: true } },
+  { html: "<optgroup id='a'><option></option></optgroup><optgroup disabled id='b'></optgroup>",
+    modifier: function(c) { c.querySelector('#b').appendChild(c.querySelector('option')); },
+    result: { attr: null, idl: false, pseudo: true } },
+  { html: "<optgroup disabled id='a'><option></option></optgroup><optgroup id='b'></optgroup>",
+    modifier: function(c) { c.querySelector('#b').appendChild(c.querySelector('option')); },
+    result: { attr: null, idl: false, pseudo: false } },
+];
+
+var content = document.getElementById('content');
+
+testCases.forEach(function(testCase) {
+  var result = testCase.result;
+
+  content.innerHTML = testCase.html;
+
+  if (testCase.modifier !== undefined) {
+    testCase.modifier(content);
+  }
+
+  var option = content.querySelector('option');
+  is(option.getAttribute('disabled'), result.attr, "disabled content attribute value should be " + result.attr);
+  is(option.disabled, result.idl, "disabled idl attribute value should be " + result.idl);
+  is(option.mozMatchesSelector(":disabled"), result.pseudo, ":disabled state should be " + result.pseudo);
+  is(option.mozMatchesSelector(":enabled"), !result.pseudo, ":enabled state should be " + !result.pseudo);
+
+  content.innerHTML = "";
+});
+
+</script>
+</pre>
+</body>
+</html>