Skip to content

Commit 15df9a4

Browse files
committed
Property HTMLFieldSetElement.type added
Method HTMLFieldSetElement.elements() added Fixed DisabledElement.isDisabled(): a disabled fieldset no longer disables descendants of its first legend element, matching the spec's exemption for controls placed inside a fieldset's legend.
1 parent e649584 commit 15df9a4

7 files changed

Lines changed: 1021 additions & 52 deletions

File tree

src/changes/changes.xml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@
88

99
<body>
1010
<release version="5.4.0" date="August xx, 2026" description="Firefox 153, Bugfixes">
11+
<action type="add" dev="rbri">
12+
Property HTMLFieldSetElement.type added.
13+
</action>
14+
<action type="add" dev="rbri">
15+
Method HTMLFieldSetElement.elements() added.
16+
</action>
17+
<action type="fix" dev="rbri">
18+
Fixed DisabledElement.isDisabled(): a disabled fieldset no longer disables descendants of its
19+
first legend element, matching the spec's exemption for controls placed inside a fieldset's legend.
20+
</action>
1121
<action type="fix" dev="rbri">
1222
Fixed checkValidity()/reportValidity() and the CSS :valid/:invalid pseudo-classes
1323
for HtmlButtons, HtmlTextarea.
@@ -17,8 +27,8 @@
1727
'value' attribute; it now simply clears the selected files, without firing a change event.
1828
</action>
1929
<action type="fix" dev="rbri">
20-
HtmlFileInput.isValid(): a disabled file input is no longer incorrectly reported as invalid
21-
by checkValidity() when 'required' is set and no file is selected
30+
HtmlFileInput.isValid(): a disabled file input is no longer incorrectly reported as invalid
31+
by checkValidity() when 'required' is set and no file is selected
2232
</action>
2333
<action type="add" dev="rbri">
2434
Method reportValidity() support to various form controls added.

src/main/java/org/htmlunit/html/DisabledElement.java

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,25 +30,71 @@ public interface DisabledElement extends Element {
3030

3131
/**
3232
* Returns {@code true} if the disabled attribute is set for this element.
33+
* <p>
34+
* Per spec, a disabled {@link HtmlFieldSet} ancestor disables its
35+
* descendants EXCEPT for descendants of that fieldset's first
36+
* {@code <legend>} child, if any. This is checked cheaply while walking
37+
* up the ancestor chain: at each step, the node we just came from
38+
* (tracked as {@code previous}) is by construction a direct child of the
39+
* node we're currently examining, so when that node turns out to be a
40+
* disabled fieldset we already know its relevant direct child without
41+
* any second traversal -- see {@link #isFirstLegendChild(HtmlFieldSet, Node)}.
42+
* </p>
43+
*
3344
* @return {@code true} if the disabled attribute is set for this element
3445
*/
3546
default boolean isDisabled() {
3647
if (hasAttribute(ATTRIBUTE_DISABLED)) {
3748
return true;
3849
}
3950

51+
Node previous = this;
4052
Node node = getParentNode();
4153
while (node != null) {
42-
if (node instanceof DisabledElement element
43-
&& element.isDisabled()) {
54+
if (node instanceof HtmlFieldSet fieldSet) {
55+
if (fieldSet.hasAttribute(ATTRIBUTE_DISABLED)
56+
&& !isFirstLegendChild(fieldSet, previous)) {
57+
return true;
58+
}
59+
}
60+
else if (node instanceof DisabledElement element
61+
&& element.hasAttribute(ATTRIBUTE_DISABLED)) {
4462
return true;
4563
}
64+
previous = node;
4665
node = node.getParentNode();
4766
}
4867

4968
return false;
5069
}
5170

71+
/**
72+
* Checks whether {@code candidate} -- the direct child of {@code fieldSet}
73+
* that lies on the path up from this element -- is {@code fieldSet}'s
74+
* FIRST {@code <legend>} child. The exemption from fieldset disabling
75+
* applies only to the first legend -- a control inside a second,
76+
* non-conforming {@code <legend>} is still disabled. Bounded by how many
77+
* children precede the first legend (in practice O(1), since a legend is
78+
* almost always the first child), not by how deep this element is nested
79+
* inside it.
80+
*
81+
* @param fieldSet the disabled fieldset ancestor to check the exemption against
82+
* @param candidate the direct child of {@code fieldSet} on the path up from this element
83+
* @return {@code true} if {@code candidate} is {@code fieldSet}'s first {@code <legend>} child
84+
*/
85+
private static boolean isFirstLegendChild(final HtmlFieldSet fieldSet, final Node candidate) {
86+
if (!(candidate instanceof HtmlLegend)) {
87+
return false;
88+
}
89+
90+
for (final DomNode child : fieldSet.getChildren()) {
91+
if (child instanceof HtmlLegend) {
92+
return child == candidate;
93+
}
94+
}
95+
return false;
96+
}
97+
5298
/**
5399
* Returns the value of the attribute {@code disabled}. Refer to the
54100
* <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
@@ -57,5 +103,4 @@ default boolean isDisabled() {
57103
* @return the value of the attribute {@code disabled} or an empty string if that attribute isn't defined
58104
*/
59105
String getDisabledAttribute();
60-
61106
}

src/main/java/org/htmlunit/javascript/host/html/HTMLFieldSetElement.java

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,19 @@
1414
*/
1515
package org.htmlunit.javascript.host.html;
1616

17+
import java.io.Serializable;
18+
import java.util.function.Predicate;
19+
1720
import org.htmlunit.html.DomElement;
21+
import org.htmlunit.html.DomNode;
22+
import org.htmlunit.html.HtmlButton;
1823
import org.htmlunit.html.HtmlFieldSet;
1924
import org.htmlunit.html.HtmlForm;
25+
import org.htmlunit.html.HtmlInput;
26+
import org.htmlunit.html.HtmlObject;
27+
import org.htmlunit.html.HtmlOutput;
28+
import org.htmlunit.html.HtmlSelect;
29+
import org.htmlunit.html.HtmlTextArea;
2030
import org.htmlunit.javascript.configuration.JsxClass;
2131
import org.htmlunit.javascript.configuration.JsxConstructor;
2232
import org.htmlunit.javascript.configuration.JsxFunction;
@@ -78,13 +88,31 @@ public HTMLFormElement getForm() {
7888
return (HTMLFormElement) getScriptableFor(form);
7989
}
8090

91+
/**
92+
* Returns the {@code type} property; always "fieldset".
93+
* @return the {@code type} property
94+
*/
95+
@JsxGetter
96+
public String getType() {
97+
return "fieldset";
98+
}
99+
100+
/**
101+
* {@inheritDoc}
102+
*/
103+
@Override
104+
public HtmlFieldSet getDomNodeOrDie() {
105+
return (HtmlFieldSet) super.getDomNodeOrDie();
106+
}
107+
81108
/**
82109
* Checks whether the element has any constraints and whether it satisfies them.
83110
* @return {@code true} if the element is valid
84111
*/
85112
@JsxFunction
86113
public boolean checkValidity() {
87-
return getDomNodeOrDie().isValid();
114+
final HtmlFieldSet fieldSet = getDomNodeOrDie();
115+
return !fieldSet.willValidate() || fieldSet.isValid();
88116
}
89117

90118
/**
@@ -133,7 +161,7 @@ public ValidityState getValidity() {
133161
*/
134162
@JsxGetter
135163
public boolean isWillValidate() {
136-
return ((HtmlFieldSet) getDomNodeOrDie()).willValidate();
164+
return getDomNodeOrDie().willValidate();
137165
}
138166

139167
/**
@@ -142,6 +170,51 @@ public boolean isWillValidate() {
142170
*/
143171
@JsxFunction
144172
public void setCustomValidity(final String message) {
145-
((HtmlFieldSet) getDomNodeOrDie()).setCustomValidity(message);
173+
getDomNodeOrDie().setCustomValidity(message);
174+
}
175+
176+
/**
177+
* Returns the fieldset's associated form controls -- listed elements whose
178+
* closest fieldset element ancestor is this fieldset.
179+
* <p>
180+
* Per spec, this is purely a tree-position question: it does not consult
181+
* the 'form' attribute or form ownership at all (a control physically
182+
* inside this fieldset is included even if 'form' points elsewhere), and a
183+
* nested inner &lt;fieldset&gt;'s own descendants are excluded here even
184+
* though they're still tree-descendants of this fieldset -- only the inner
185+
* fieldset ELEMENT itself counts, since it is the listed element whose
186+
* closest fieldset ancestor is this one.
187+
* </p>
188+
*
189+
* @return the fieldset's associated form controls
190+
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLFieldSetElement/elements">MDN Documentation</a>
191+
*/
192+
@JsxGetter
193+
public HTMLCollection getElements() {
194+
final DomElement elt = getDomNodeOrDie();
195+
196+
final HTMLCollection elements = new HTMLCollection(elt, true);
197+
198+
elements.setIsMatchingPredicate((Predicate<DomNode> & Serializable) node -> isListedElement(node));
199+
200+
return elements;
201+
}
202+
203+
/**
204+
* Checks whether {@code node} belongs to the HTML "listed" category of
205+
* form-associated elements (button, fieldset, input except type=image,
206+
* object, output, select, textarea).
207+
*
208+
* @param node the node to check
209+
* @return {@code true} if {@code node} is a listed element
210+
*/
211+
private static boolean isListedElement(final DomNode node) {
212+
return node instanceof HtmlInput
213+
|| node instanceof HtmlButton
214+
|| node instanceof HtmlFieldSet
215+
|| node instanceof HtmlObject
216+
|| node instanceof HtmlOutput
217+
|| node instanceof HtmlSelect
218+
|| node instanceof HtmlTextArea;
146219
}
147220
}

src/test/java/org/htmlunit/general/ElementOwnPropertiesTest.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5528,14 +5528,14 @@ public void em() throws Exception {
55285528
FF_ESR = "checkValidity(),constructor(),disabled[GSCE],elements[GCE],form[GCE],name[GSCE],reportValidity(),"
55295529
+ "setCustomValidity(),type[GCE],validationMessage[GCE],validity[GCE],"
55305530
+ "willValidate[GCE]")
5531-
@HtmlUnitNYI(CHROME = "checkValidity(),constructor(),disabled[GSCE],form[GCE],name[GSCE],"
5532-
+ "reportValidity(),setCustomValidity(),validity[GCE],willValidate[GCE]",
5533-
EDGE = "checkValidity(),constructor(),disabled[GSCE],form[GCE],name[GSCE],"
5534-
+ "reportValidity(),setCustomValidity(),validity[GCE],willValidate[GCE]",
5535-
FF_ESR = "checkValidity(),constructor(),disabled[GSCE],form[GCE],name[GSCE],"
5536-
+ "reportValidity(),setCustomValidity(),validity[GCE],willValidate[GCE]",
5537-
FF = "checkValidity(),constructor(),disabled[GSCE],form[GCE],name[GSCE],"
5538-
+ "reportValidity(),setCustomValidity(),validity[GCE],willValidate[GCE]")
5531+
@HtmlUnitNYI(CHROME = "checkValidity(),constructor(),disabled[GSCE],elements[GCE],form[GCE],name[GSCE],"
5532+
+ "reportValidity(),setCustomValidity(),type[GCE],validity[GCE],willValidate[GCE]",
5533+
EDGE = "checkValidity(),constructor(),disabled[GSCE],elements[GCE],form[GCE],name[GSCE],"
5534+
+ "reportValidity(),setCustomValidity(),type[GCE],validity[GCE],willValidate[GCE]",
5535+
FF_ESR = "checkValidity(),constructor(),disabled[GSCE],elements[GCE],form[GCE],name[GSCE],"
5536+
+ "reportValidity(),setCustomValidity(),type[GCE],validity[GCE],willValidate[GCE]",
5537+
FF = "checkValidity(),constructor(),disabled[GSCE],elements[GCE],form[GCE],name[GSCE],"
5538+
+ "reportValidity(),setCustomValidity(),type[GCE],validity[GCE],willValidate[GCE]")
55395539
public void fieldset() throws Exception {
55405540
test("fieldset");
55415541
}

src/test/java/org/htmlunit/general/ElementPropertiesTest.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1865,10 +1865,15 @@ public void em() throws Exception {
18651865
@Test
18661866
@Alerts("checkValidity(),disabled,elements,form,name,reportValidity(),setCustomValidity(),type,"
18671867
+ "validationMessage,validity,willValidate")
1868-
@HtmlUnitNYI(CHROME = "checkValidity(),disabled,form,name,reportValidity(),setCustomValidity(),validity,willValidate",
1869-
EDGE = "checkValidity(),disabled,form,name,reportValidity(),setCustomValidity(),validity,willValidate",
1870-
FF_ESR = "checkValidity(),disabled,form,name,reportValidity(),setCustomValidity(),validity,willValidate",
1871-
FF = "checkValidity(),disabled,form,name,reportValidity(),setCustomValidity(),validity,willValidate")
1868+
@HtmlUnitNYI(
1869+
CHROME = "checkValidity(),disabled,elements,form,name,reportValidity(),setCustomValidity(),"
1870+
+ "type,validity,willValidate",
1871+
EDGE = "checkValidity(),disabled,elements,form,name,reportValidity(),setCustomValidity(),"
1872+
+ "type,validity,willValidate",
1873+
FF_ESR = "checkValidity(),disabled,elements,form,name,reportValidity(),setCustomValidity(),"
1874+
+ "type,validity,willValidate",
1875+
FF = "checkValidity(),disabled,elements,form,name,reportValidity(),setCustomValidity(),"
1876+
+ "type,validity,willValidate")
18721877
public void fieldset() throws Exception {
18731878
test("fieldset");
18741879
}

0 commit comments

Comments
 (0)