Merge pull request #1600 from lightpanda-io/formdata_disabled_fieldset

FormData recognizes (and skips over) disabled fieldsets
This commit is contained in:
Pierre Tachoire
2026-02-20 15:35:03 +01:00
committed by GitHub
2 changed files with 149 additions and 0 deletions

View File

@@ -602,6 +602,114 @@
} }
</script> </script>
<script id=disabledFieldset>
{
// Elements inside a disabled fieldset should not be included
const form = document.createElement('form');
const fieldset = document.createElement('fieldset');
fieldset.disabled = true;
const inside = document.createElement('input');
inside.name = 'inside';
inside.value = 'nope';
fieldset.appendChild(inside);
const outside = document.createElement('input');
outside.name = 'outside';
outside.value = 'yes';
form.appendChild(fieldset);
form.appendChild(outside);
const fd = new FormData(form);
testing.expectEqual(null, fd.get('inside'));
testing.expectEqual('yes', fd.get('outside'));
}
</script>
<script id=disabledFieldsetLegendExemption>
{
// Elements inside the FIRST legend of a disabled fieldset are NOT disabled
const form = document.createElement('form');
const fieldset = document.createElement('fieldset');
fieldset.disabled = true;
const legend = document.createElement('legend');
const inLegend = document.createElement('input');
inLegend.name = 'in-legend';
inLegend.value = 'exempt';
legend.appendChild(inLegend);
const notInLegend = document.createElement('input');
notInLegend.name = 'not-in-legend';
notInLegend.value = 'nope';
fieldset.appendChild(legend);
fieldset.appendChild(notInLegend);
form.appendChild(fieldset);
const fd = new FormData(form);
testing.expectEqual('exempt', fd.get('in-legend'));
testing.expectEqual(null, fd.get('not-in-legend'));
}
</script>
<script id=disabledFieldsetSecondLegend>
{
// Only the FIRST legend gets the exemption; second legend inputs are still disabled
const form = document.createElement('form');
const fieldset = document.createElement('fieldset');
fieldset.disabled = true;
const legend1 = document.createElement('legend');
const inLegend1 = document.createElement('input');
inLegend1.name = 'first-legend';
inLegend1.value = 'exempt';
legend1.appendChild(inLegend1);
const legend2 = document.createElement('legend');
const inLegend2 = document.createElement('input');
inLegend2.name = 'second-legend';
inLegend2.value = 'nope';
legend2.appendChild(inLegend2);
fieldset.appendChild(legend1);
fieldset.appendChild(legend2);
form.appendChild(fieldset);
const fd = new FormData(form);
testing.expectEqual('exempt', fd.get('first-legend'));
testing.expectEqual(null, fd.get('second-legend'));
}
</script>
<script id=disabledFieldsetNested>
{
// Outer fieldset disabled: inner enabled fieldset's elements are still disabled
const form = document.createElement('form');
const outer = document.createElement('fieldset');
outer.disabled = true;
const inner = document.createElement('fieldset');
// inner is NOT disabled itself
const input = document.createElement('input');
input.name = 'deep';
input.value = 'nope';
inner.appendChild(input);
outer.appendChild(inner);
form.appendChild(outer);
const fd = new FormData(form);
testing.expectEqual(null, fd.get('deep'));
}
</script>
<script id=imageWithoutName> <script id=imageWithoutName>
{ {
// Test that image input without name still submits x and y coordinates // Test that image input without name still submits x and y coordinates

View File

@@ -22,6 +22,7 @@ const log = @import("../../../log.zig");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const Node = @import("../Node.zig");
const Form = @import("../element/html/Form.zig"); const Form = @import("../element/html/Form.zig");
const Element = @import("../Element.zig"); const Element = @import("../Element.zig");
const KeyValueList = @import("../KeyValueList.zig"); const KeyValueList = @import("../KeyValueList.zig");
@@ -124,12 +125,17 @@ fn collectForm(arena: Allocator, form_: ?*Form, submitter_: ?*Element, page: *Pa
var list: KeyValueList = .empty; var list: KeyValueList = .empty;
const form = form_ orelse return list; const form = form_ orelse return list;
const form_node = form.asNode();
var elements = try form.getElements(page); var elements = try form.getElements(page);
var it = try elements.iterator(); var it = try elements.iterator();
while (it.next()) |element| { while (it.next()) |element| {
if (element.getAttributeSafe(comptime .wrap("disabled")) != null) { if (element.getAttributeSafe(comptime .wrap("disabled")) != null) {
continue; continue;
} }
if (isDisabledByFieldset(element, form_node)) {
continue;
}
// Handle image submitters first - they can submit without a name // Handle image submitters first - they can submit without a name
if (element.is(Form.Input)) |input| { if (element.is(Form.Input)) |input| {
@@ -196,6 +202,41 @@ fn collectForm(arena: Allocator, form_: ?*Form, submitter_: ?*Element, page: *Pa
return list; return list;
} }
// Returns true if `element` is disabled by an ancestor <fieldset disabled>,
// stopping the upward walk when the form node is reached.
// Per spec, elements inside the first <legend> child of a disabled fieldset
// are NOT disabled by that fieldset.
fn isDisabledByFieldset(element: *Element, form_node: *Node) bool {
const element_node = element.asNode();
var current: ?*Node = element_node._parent;
while (current) |node| {
// Stop at the form boundary (common case optimisation)
if (node == form_node) {
return false;
}
current = node._parent;
const el = node.is(Element) orelse continue;
if (el.getTag() == .fieldset and el.getAttributeSafe(comptime .wrap("disabled")) != null) {
// Check if `element` is inside the first <legend> child of this fieldset
var child = el.firstElementChild();
while (child) |c| {
if (c.getTag() == .legend) {
// Found the first legend; exempt if element is a descendant
if (c.asNode().contains(element_node)) {
return false;
}
break;
}
child = c.nextElementSibling();
}
return true;
}
}
return false;
}
pub const JsApi = struct { pub const JsApi = struct {
pub const bridge = js.Bridge(FormData); pub const bridge = js.Bridge(FormData);