fix(events): retarget sources across composed dispatch

This commit is contained in:
ldm0
2026-09-27 19:28:50 +08:00
parent f3fa0c8cde
commit e8fbd0471a
5 changed files with 118 additions and 11 deletions
@@ -22,8 +22,8 @@ use super::{
event_time_stamp_getter_function, event_type_getter_function,
focus_event_related_target_getter_function, form_data_event_form_data_getter_function,
mouse_event_related_target_getter_function, pointer_event_get_predicted_events_callback,
submit_event_submitter_getter_function, track_event_track_getter_function,
ui_event_pseudo_target_getter_function,
submit_event_submitter_getter_function, toggle_event_source_getter_function,
track_event_track_getter_function, ui_event_pseudo_target_getter_function,
},
selection_surface::document_get_selection_callback,
specs::{ConstructorKind, ConstructorSpec},
@@ -188,6 +188,13 @@ struct CommandEventTemplateAccessorsDeclaration {
command: (),
}
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::ToggleEvent, enumerable)]
struct ToggleEventTemplateAccessorsDeclaration {
#[webapi(accessor_property, getter = toggle_event_source_getter_function)]
source: (),
}
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::UIEvent, enumerable)]
struct UiEventTemplateMethodsDeclaration {
@@ -428,6 +435,10 @@ pub(super) fn install_event_template_bindings<'s>(
let proto = template.prototype_template(scope);
CommandEventTemplateAccessorsDeclaration::initialize_prototype_template(scope, proto);
}
"ToggleEvent" => {
let proto = template.prototype_template(scope);
ToggleEventTemplateAccessorsDeclaration::initialize_prototype_template(scope, proto);
}
"EventTarget" => {
let prototype = template.prototype_template(scope);
EventTargetTemplateMethodsDeclaration::initialize_prototype_template(scope, prototype);
@@ -17,6 +17,7 @@ const FORM_DATA_EVENT_FORM_DATA_SLOT: &str = "__moliFormDataEventFormData";
const TRACK_EVENT_TRACK_SLOT: &str = "__moliTrackEventTrack";
const COMMAND_EVENT_SOURCE_SLOT: &str = "__moliCommandEventSource";
const COMMAND_EVENT_COMMAND_SLOT: &str = "__moliCommandEventCommand";
const TOGGLE_EVENT_SOURCE_SLOT: &str = "__moliToggleEventSource";
const EVENT_SUBCLASS_KIND_SLOT: &str = "__moliEventSubclassKind";
const BEFORE_UNLOAD_EVENT_RETURN_VALUE_SLOT: &str = "__moliBeforeUnloadEventReturnValue";
#[derive(WebApiObject)]
@@ -367,10 +368,16 @@ pub(crate) fn set_event_source_value<'s>(
value: v8::Local<'s, v8::Value>,
) {
let event = event_backing(scope, event);
if event_subclass_kind(scope, event) == Some(EventSubclassKind::CommandEvent) {
set_private_value(scope, event, COMMAND_EVENT_SOURCE_SLOT, value);
} else {
let _ = event.set(scope, v8str(scope, "source").into(), value);
match event_subclass_kind(scope, event) {
Some(EventSubclassKind::CommandEvent) => {
set_private_value(scope, event, COMMAND_EVENT_SOURCE_SLOT, value);
}
Some(EventSubclassKind::ToggleEvent) => {
set_private_value(scope, event, TOGGLE_EVENT_SOURCE_SLOT, value);
}
_ => {
let _ = event.set(scope, v8str(scope, "source").into(), value);
}
}
}
@@ -594,6 +601,25 @@ pub(super) fn command_event_command_getter_function<'s>(
rv.set(value);
}
pub(super) fn toggle_event_source_getter_function<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if event_subclass_kind(scope, args.this()) != Some(EventSubclassKind::ToggleEvent) {
throw_type_error(scope, "Illegal invocation");
return;
}
let value = event_private_value(scope, args.this(), TOGGLE_EVENT_SOURCE_SLOT)
.unwrap_or_else(|| v8::null(scope).into());
let Some(context) = args.this().get_creation_context(scope) else {
return;
};
if let Some(value) = super::platform_object_worlds::in_realm(scope, value, context) {
rv.set(value);
}
}
pub(super) fn form_data_event_form_data_getter_function<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -173,13 +173,11 @@ struct InterestEventInitDeclaration<'scope> {
#[derive(WebApiObject)]
#[webapi(plain)]
struct ToggleEventStateDeclaration<'scope> {
struct ToggleEventStateDeclaration {
#[webapi(data_property = "oldState", readonly, dont_delete)]
old_state: String,
#[webapi(data_property = "newState", readonly, dont_delete)]
new_state: String,
#[webapi(data_property, readonly, dont_delete)]
source: v8::Local<'scope, v8::Value>,
}
#[derive(WebApiObject)]
@@ -572,8 +570,9 @@ pub(in crate::context_bootstrap::events::subclasses) fn initialize_toggle_event<
},
};
let source = parsed.source.unwrap_or_else(|| v8::null(scope).into());
let _ = ToggleEventStateDeclaration::new(parsed.old_state, parsed.new_state, source)
let _ = ToggleEventStateDeclaration::new(parsed.old_state, parsed.new_state)
.initialize(scope, event);
set_private_value(scope, event, TOGGLE_EVENT_SOURCE_SLOT, source);
true
}
@@ -539,7 +539,12 @@ impl DocumentRuntime {
let composed = crate::context_bootstrap::event_backing(scope, event)
.get(scope, v8str(scope, "composed").into())
.is_some_and(|value| value.boolean_value(scope));
let mut path = if let Some(source_target) =
// A caller-created composed event is not confined to the tree that
// contains its reference source. Its path crosses shadow boundaries
// normally while `source` is retargeted for each listener.
let mut path = if composed {
self.build_propagation_path(dispatch_target, composed)
} else if let Some(source_target) =
source_target_for_reference_event(scope, host_ptr, event)
{
self.build_source_scoped_propagation_path(dispatch_target, source_target)
@@ -4882,6 +4882,72 @@ target:true:true:true:targetDiv,#document-fragment,middleF,#document-fragment|mi
target:true:true:true:targetForm,#document-fragment,middleI,#document-fragment|middle:true:true"
);
}
#[test]
fn composed_reference_events_cross_shadow_roots_and_retarget_source() {
let mut vm = new_storage_test_vm("https://composed-reference-event-source.test/");
let result = vm
.eval(
r#"
(() => {
if (!document.documentElement) {
document.appendChild(document.createElement('html'));
}
if (!document.body) {
document.documentElement.appendChild(document.createElement('body'));
}
const outerHost = document.createElement('div');
const outerRoot = outerHost.attachShadow({ mode: 'open' });
const source = document.createElement('button');
const innerHost = document.createElement('section');
const innerRoot = innerHost.attachShadow({ mode: 'open' });
const target = document.createElement('span');
innerRoot.appendChild(target);
outerRoot.append(source, innerHost);
document.body.appendChild(outerHost);
const targets = [outerHost, innerHost, target];
const dispatch = (event) => {
const captureSources = new Map();
const bubbleSources = new Map();
for (const currentTarget of targets) {
currentTarget.addEventListener(event.type, currentEvent => {
bubbleSources.set(currentTarget, currentEvent.source);
}, { once: true });
currentTarget.addEventListener(event.type, currentEvent => {
captureSources.set(currentTarget, currentEvent.source);
}, { capture: true, once: true });
}
target.dispatchEvent(event);
return targets.every(currentTarget => {
const expectedSource = currentTarget === outerHost ? outerHost : source;
return captureSources.get(currentTarget) === expectedSource &&
bubbleSources.get(currentTarget) === expectedSource;
}) && event.source === outerHost;
};
const command = new CommandEvent('command', { composed: true, source });
const toggle = new ToggleEvent('toggle', { composed: true, source });
const toggleSourceDescriptor = Object.getOwnPropertyDescriptor(
ToggleEvent.prototype,
'source'
);
return [
dispatch(command),
dispatch(toggle),
!Object.hasOwn(toggle, 'source'),
typeof toggleSourceDescriptor.get,
toggleSourceDescriptor.set === undefined,
toggleSourceDescriptor.enumerable
].join('|');
})()
"#,
)
.expect("composed reference events should cross shadow roots");
assert_eq!(result, "true|true|true|function|true|true");
}
#[tokio::test]
async fn toggle_and_interest_events_follow_reference_target_source_path() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");