mirror of
https://github.com/lexmount/moli.git
synced 2026-10-06 16:01:00 +00:00
fix(dialog): implement requestClose lifecycle
This commit is contained in:
@@ -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/");
|
||||
|
||||
Reference in New Issue
Block a user