fix(dialog): implement requestClose lifecycle

This commit is contained in:
ldm0
2026-09-09 06:38:21 +08:00
parent 2f07646414
commit e16919df78
7 changed files with 191 additions and 12 deletions
@@ -3155,7 +3155,6 @@ html/semantics/interactive-elements/the-dialog-element/dialog-focusing-steps-ine
html/semantics/interactive-elements/the-dialog-element/dialog-focusing-steps-prevent-autofocus.html
html/semantics/interactive-elements/the-dialog-element/dialog-inert.html
html/semantics/interactive-elements/the-dialog-element/dialog-overlay.html
html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-recurse.html
html/semantics/interactive-elements/the-dialog-element/dialog-showModal.html
html/semantics/interactive-elements/the-dialog-element/inertness-with-modal-dialogs-and-iframes.html
html/semantics/interactive-elements/the-dialog-element/modal-dialog-in-visibility-hidden.html
@@ -5951,6 +5951,7 @@ html/semantics/interactive-elements/the-dialog-element/dialog-form-submission-un
html/semantics/interactive-elements/the-dialog-element/dialog-no-throw-requested-state.html
html/semantics/interactive-elements/the-dialog-element/dialog-open-2.html
html/semantics/interactive-elements/the-dialog-element/dialog-open.html
html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-recurse.html
html/semantics/interactive-elements/the-dialog-element/dialog-return-value.html
html/semantics/interactive-elements/the-dialog-element/dialog-showModal-remove.html
html/semantics/interactive-elements/the-dialog-element/form-submit-dialog-shadow.html
@@ -387,6 +387,7 @@ impl JsContextHost {
button_element_targets: HashMap::new(),
constructing_form_data_forms: Vec::new(),
active_form_submission_forms: Vec::new(),
active_dialog_request_closes: HashSet::new(),
pending_form_submission_child_targets: HashMap::new(),
active_image_submitter_coordinate: None,
current_inline_script_stack: Vec::new(),
@@ -1704,6 +1705,14 @@ impl JsContextHost {
}
}
pub(crate) fn begin_dialog_request_close(&mut self, dialog: DomHandle) -> bool {
self.active_dialog_request_closes.insert(dialog)
}
pub(crate) fn end_dialog_request_close(&mut self, dialog: DomHandle) {
self.active_dialog_request_closes.remove(&dialog);
}
pub(crate) fn observers_mut(
&mut self,
_access: &ObserverStoreAccessToken,
@@ -993,6 +993,7 @@ pub(crate) struct JsContextHost {
button_element_targets: HashMap<(DomHandle, String), DomHandle>,
constructing_form_data_forms: Vec<DomHandle>,
active_form_submission_forms: Vec<DomHandle>,
active_dialog_request_closes: HashSet<DomHandle>,
pending_form_submission_child_targets: HashMap<DomHandle, Vec<DomHandle>>,
active_image_submitter_coordinate: Option<(DomHandle, u32, u32)>,
current_inline_script_stack: Vec<DomHandle>,
@@ -178,8 +178,9 @@ pub(super) use dataset::{build_dom_string_map_wrapper_template, node_dataset_get
use details_dialog::{
close_dialog_element, details_open_getter_function, details_open_setter_function,
dialog_close_callback, dialog_open_getter_function, dialog_open_setter_function,
dialog_return_value_getter_function, dialog_return_value_setter_function, dialog_show_callback,
dialog_show_modal_callback, perform_summary_click_default_action,
dialog_request_close_callback, dialog_return_value_getter_function,
dialog_return_value_setter_function, dialog_show_callback, dialog_show_modal_callback,
perform_summary_click_default_action,
};
pub(crate) use details_dialog::{
queue_details_toggle_event_for_attribute_change, queue_parser_details_toggle_event,
@@ -4378,6 +4379,8 @@ struct HtmlDialogElementPrototypeDeclaration {
show_modal: (),
#[webapi(method, length = 1, callback = dialog_close_callback)]
close: (),
#[webapi(method, length = 1, callback = dialog_request_close_callback)]
request_close: (),
}
#[derive(WebApiFunctionTemplate)]
@@ -16,8 +16,9 @@ use super::super::{
use super::popover::popover_is_open;
use super::toggle_event::queue_element_toggle_event;
use super::{
dispatch_public_event, element_has_attribute, html_element_getter_receiver,
html_element_setter_receiver, property_dom_string_value, set_reflected_boolean_attribute,
construct_simple_event, dispatch_public_event, element_has_attribute,
html_element_getter_receiver, html_element_setter_receiver, property_dom_string_value,
set_reflected_boolean_attribute,
};
pub(crate) fn queue_details_toggle_event_for_attribute_change(
@@ -101,6 +102,13 @@ struct DialogCloseArgs {
return_value: Option<String>,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "HTMLDialogElement.requestClose")]
struct DialogRequestCloseArgs {
#[webidl(with = dialog_request_close_return_value_arg)]
return_value: Option<String>,
}
pub(super) fn main_summary_child(runtime: &JsContextHost, details: DomHandle) -> Option<DomHandle> {
let details_element = runtime
.dom_host()
@@ -180,17 +188,29 @@ fn dialog_close_return_value_arg<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
index: i32,
) -> Result<Option<String>, webidl::WebIdlError> {
dialog_optional_return_value_arg(scope, args, index, "HTMLDialogElement.close")
}
fn dialog_request_close_return_value_arg<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
index: i32,
) -> Result<Option<String>, webidl::WebIdlError> {
dialog_optional_return_value_arg(scope, args, index, "HTMLDialogElement.requestClose")
}
fn dialog_optional_return_value_arg<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
index: i32,
prefix: &'static str,
) -> Result<Option<String>, webidl::WebIdlError> {
if args.length() <= index || args.get(index).is_undefined() {
return Ok(None);
}
webidl::argument::<webidl::DomString>(
scope,
args,
index,
webidl::Context::argument("HTMLDialogElement.close", 1),
)
.map(|value| Some(value.0))
webidl::argument::<webidl::DomString>(scope, args, index, webidl::Context::argument(prefix, 1))
.map(|value| Some(value.0))
}
fn dialog_runtime_and_handle_from_object<'s>(
@@ -509,6 +529,38 @@ pub(super) fn dialog_close_callback<'s>(
close_dialog_element(scope, runtime_ptr, handle, parsed.return_value.as_deref());
}
pub(super) fn dialog_request_close_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'_, v8::Value>,
) {
let object = args.this();
let Some(parsed) = webidl::parse_args::<DialogRequestCloseArgs>(scope, &args) else {
return;
};
let Ok((runtime_ptr, handle)) = dialog_runtime_and_handle_from_object(scope, object) else {
return;
};
let runtime = unsafe { &*runtime_ptr };
if !runtime.dom_host().is_html_element_named(handle, "dialog")
|| !runtime.dom_host().is_connected(handle)
|| !element_has_attribute(runtime, handle, "open")
{
return;
}
let Some(event) = construct_simple_event(scope, "cancel", false, true, false) else {
return;
};
if !unsafe { &mut *runtime_ptr }.begin_dialog_request_close(handle) {
return;
}
let allows_close = dispatch_public_event(scope, runtime_ptr, handle, event).allows_default();
if allows_close {
close_dialog_element(scope, runtime_ptr, handle, parsed.return_value.as_deref());
}
unsafe { &mut *runtime_ptr }.end_dialog_request_close(handle);
}
pub(in crate::native_bridge::element) fn close_dialog_element(
scope: &mut v8::PinScope<'_, '_>,
runtime_ptr: *mut JsContextHost,
@@ -70,6 +70,120 @@ async fn dialog_toggle_events_cancel_opening_and_coalesce_state_changes() {
);
}
#[tokio::test]
async fn dialog_request_close_ignores_recursive_cancel_and_queues_close() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
let mut vm = new_storage_page_task_executor_test_vm_with_loader(
"https://dialog-request-close-recursive.test/",
&loader,
);
let before_close = vm
.eval(
r#"
(() => {
const host = document.body || document.documentElement || document;
const dialog = document.createElement('dialog');
host.appendChild(dialog);
globalThis.__dialogRequestCloseEvents = [];
let cancelCount = 0;
dialog.addEventListener('cancel', event => {
cancelCount += 1;
__dialogRequestCloseEvents.push(`${event.type}:${event.bubbles}:${event.cancelable}`);
dialog.requestClose('nested');
});
dialog.addEventListener('close', event => {
__dialogRequestCloseEvents.push(`${event.type}:${event.bubbles}:${event.cancelable}`);
});
dialog.showModal();
dialog.requestClose('outer');
return JSON.stringify({
length: dialog.requestClose.length,
cancelCount,
open: dialog.open,
returnValue: dialog.returnValue,
events: __dialogRequestCloseEvents
});
})()
"#,
)
.expect("recursive dialog requestClose setup should evaluate");
assert_eq!(
before_close,
r#"{"length":1,"cancelCount":1,"open":false,"returnValue":"outer","events":["cancel:false:true"]}"#
);
assert!(
!vm.has_ready_timeout(),
"dialog requestClose must not create synthetic Page timers"
);
assert!(
vm.run_one_dom_manipulation_task_executor_turn(
PageDomManipulationTestFamily::ElementToggle,
&loader,
)
.await
.expect("coalesced dialog toggle event should run")
);
assert!(
vm.run_one_user_interaction_executor_turn(&loader)
.await
.expect("queued dialog close event should run")
);
let after_close = vm
.eval("JSON.stringify(__dialogRequestCloseEvents)")
.expect("dialog requestClose event log should evaluate");
assert_eq!(after_close, r#"["cancel:false:true","close:false:false"]"#);
}
#[test]
fn dialog_request_close_honors_cancellation_and_active_document() {
let mut vm = new_storage_test_vm("https://dialog-request-close-state.test/");
let result = vm
.eval(
r#"
(() => {
const host = document.body || document.documentElement || document;
const dialog = document.createElement('dialog');
host.appendChild(dialog);
dialog.returnValue = 'seed';
dialog.show();
dialog.addEventListener('cancel', event => event.preventDefault(), { once: true });
dialog.requestClose('blocked');
const canceled = [dialog.open, dialog.returnValue];
dialog.requestClose('accepted');
const accepted = [dialog.open, dialog.returnValue];
const disconnected = document.createElement('dialog');
disconnected.open = true;
disconnected.requestClose('disconnected');
const inactiveDocument = document.implementation.createHTMLDocument('');
const inactive = inactiveDocument.createElement('dialog');
inactiveDocument.body.appendChild(inactive);
inactive.open = true;
inactive.requestClose('inactive');
return JSON.stringify({
canceled,
accepted,
disconnected: [disconnected.open, disconnected.returnValue],
inactive: [inactive.open, inactive.returnValue]
});
})()
"#,
)
.expect("dialog requestClose state probe should evaluate");
assert_eq!(
result,
r#"{"canceled":[true,"seed"],"accepted":[false,"accepted"],"disconnected":[true,""],"inactive":[true,""]}"#
);
}
#[test]
fn dialog_show_methods_enforce_requested_state_and_active_document() {
let mut vm = new_storage_test_vm("https://dialog-requested-state.test/");