mirror of
https://github.com/lexmount/moli.git
synced 2026-09-27 16:01:31 +00:00
fix(events): retarget sources across composed dispatch
This commit is contained in:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user