forms: close the ancestor dialog on method=dialog submission

`Form.normalizeMethod` already canonicalizes `method="dialog"`, but
`Frame.submitForm` only branched on `"post"`, so a dialog submission fell
through to the GET path: the document was reloaded, the dialog kept its
`open` attribute, `returnValue` was never set, and no `close` event fired.
A promise awaiting `close` — the idiomatic way to await a confirm dialog —
hung forever while the page reloaded underneath it.

Add the branch the HTML form-submission algorithm calls for: walk up from
the form to the nearest ancestor `<dialog>` and close it with the
submitter's value through the existing `Dialog.close()`, then return
without scheduling a navigation. With no ancestor dialog the submission is
dropped, which is also what the spec asks for — and still not a navigation.

Completes the `<dialog>` support started in #2435, which implemented
`show`/`showModal`/`close`/`returnValue` but left the form-submission
integration that drives them unwired.

Closes #3053
This commit is contained in:
Navid EMAD
2026-07-24 21:23:52 +02:00
parent 044d5c9186
commit 77bbbf5e84
2 changed files with 121 additions and 0 deletions

View File

@@ -3404,6 +3404,31 @@ pub fn submitForm(self: *Frame, submitter_: ?*Element, form_: ?*Element.Html.For
const method = Element.Html.Form.normalizeMethod(method_attr, "get");
const is_post = std.mem.eql(u8, method, "post");
// Per the HTML form-submission algorithm, the dialog method closes the
// form's nearest ancestor dialog with the submitter's value and performs no
// navigation. Falling through here would submit the form as a GET, which
// both reloads the page and leaves the dialog open forever.
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm
if (std.mem.eql(u8, method, "dialog")) {
var ancestor: ?*Element = form_element;
while (ancestor) |el| : (ancestor = el.parentElement()) {
const dialog = el.is(Element.Html.Dialog) orelse continue;
// A submit button always has a value (empty when the attribute is
// absent); with no submitter at all the dialog's existing
// returnValue is left untouched.
const result: ?[]const u8 = if (submit_button) |s|
s.getAttributeSafe(comptime .wrap("value")) orelse ""
else
null;
try dialog.close(result, self);
break;
}
// Nothing is navigated, so the arena reserved for the request body goes
// unused — the errdefer above only covers the error path.
self._session.releaseArena(arena);
return;
}
// Get charset from accept-charset attribute or fall back to document charset
const charset: []const u8 = blk: {
if (form_element.getAttributeSafe(.wrap("accept-charset"))) |ac| {

View File

@@ -198,3 +198,99 @@
testing.expectEqual(0, fired)
}
</script>
<script id="dialog_method_closes_with_submitter_value">
{
document.body.insertAdjacentHTML('beforeend',
'<dialog id="d_submit"><form method="dialog">' +
'<button type="submit" value="confirm" id="b_submit">ok</button>' +
'</form></dialog>')
const dialog = $('#d_submit')
dialog.showModal()
let fired = 0
dialog.addEventListener('close', () => fired++)
$('#b_submit').click()
testing.expectEqual(1, fired)
testing.expectEqual(false, dialog.open)
testing.expectEqual('confirm', dialog.returnValue)
}
</script>
<script id="dialog_method_uses_empty_value_when_submitter_has_none">
{
document.body.insertAdjacentHTML('beforeend',
'<dialog id="d_novalue"><form method="dialog">' +
'<button type="submit" id="b_novalue">ok</button>' +
'</form></dialog>')
const dialog = $('#d_novalue')
dialog.returnValue = 'preset'
dialog.showModal()
$('#b_novalue').click()
testing.expectEqual(false, dialog.open)
testing.expectEqual('', dialog.returnValue)
}
</script>
<script id="dialog_method_closes_nearest_ancestor_dialog">
{
document.body.insertAdjacentHTML('beforeend',
'<dialog id="d_outer"><dialog id="d_inner"><form method="dialog">' +
'<button type="submit" value="inner" id="b_nested">ok</button>' +
'</form></dialog></dialog>')
const outer = $('#d_outer')
const inner = $('#d_inner')
outer.show()
inner.show()
$('#b_nested').click()
testing.expectEqual(false, inner.open)
testing.expectEqual('inner', inner.returnValue)
testing.expectEqual(true, outer.open)
}
</script>
<script id="dialog_method_is_inert_without_ancestor_dialog">
{
// No ancestor dialog: the submission is dropped. It must not fall through
// to a GET submission either, so the form's own submit handler is the only
// thing that runs.
document.body.insertAdjacentHTML('beforeend',
'<form method="dialog" id="f_orphan">' +
'<button type="submit" value="x" id="b_orphan">ok</button>' +
'</form>')
let submits = 0
$('#f_orphan').addEventListener('submit', () => submits++)
$('#b_orphan').click()
testing.expectEqual(1, submits)
}
</script>
<script id="dialog_method_honors_formmethod_on_submitter">
{
// formmethod on the submit button overrides the form's method, so a GET
// form can still close its dialog.
document.body.insertAdjacentHTML('beforeend',
'<dialog id="d_formmethod"><form method="get">' +
'<button type="submit" formmethod="dialog" value="via-formmethod" id="b_formmethod">ok</button>' +
'</form></dialog>')
const dialog = $('#d_formmethod')
dialog.showModal()
$('#b_formmethod').click()
testing.expectEqual(false, dialog.open)
testing.expectEqual('via-formmethod', dialog.returnValue)
}
</script>