From e8fbd0471aac8a7481c827ccb459945a71ca01d3 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 2 Sep 2026 12:57:59 +0800 Subject: [PATCH] fix(events): retarget sources across composed dispatch --- .../src/context_bootstrap/event_template.rs | 15 ++++- .../src/context_bootstrap/events.rs | 34 ++++++++-- .../events/subclasses/data.rs | 7 +- .../src/document_runtime/events.rs | 7 +- .../src/script_vm/tests/dom_xhr/shadow_dom.rs | 66 +++++++++++++++++++ 5 files changed, 118 insertions(+), 11 deletions(-) diff --git a/moli-renderer-v8/src/context_bootstrap/event_template.rs b/moli-renderer-v8/src/context_bootstrap/event_template.rs index 9995fa0a2..c511cbef0 100644 --- a/moli-renderer-v8/src/context_bootstrap/event_template.rs +++ b/moli-renderer-v8/src/context_bootstrap/event_template.rs @@ -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); diff --git a/moli-renderer-v8/src/context_bootstrap/events.rs b/moli-renderer-v8/src/context_bootstrap/events.rs index 3f6418419..aaf5d8e8b 100644 --- a/moli-renderer-v8/src/context_bootstrap/events.rs +++ b/moli-renderer-v8/src/context_bootstrap/events.rs @@ -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>, diff --git a/moli-renderer-v8/src/context_bootstrap/events/subclasses/data.rs b/moli-renderer-v8/src/context_bootstrap/events/subclasses/data.rs index 50df8d6ce..d6cfad217 100644 --- a/moli-renderer-v8/src/context_bootstrap/events/subclasses/data.rs +++ b/moli-renderer-v8/src/context_bootstrap/events/subclasses/data.rs @@ -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 } diff --git a/moli-renderer-v8/src/document_runtime/events.rs b/moli-renderer-v8/src/document_runtime/events.rs index 46a64893a..89ddfd084 100644 --- a/moli-renderer-v8/src/document_runtime/events.rs +++ b/moli-renderer-v8/src/document_runtime/events.rs @@ -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) diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/shadow_dom.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/shadow_dom.rs index 84a47f439..aaa293710 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/shadow_dom.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/shadow_dom.rs @@ -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");