From 3025bf2a4d7483ff06f0d49b38c2d2bf4710d69f Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Thu, 17 Mar 2022 18:57:01 +0100 Subject: [PATCH] Add test case for hierarchical event bubbling Async event dispatching is surprisingly complicated. Make sure to see #2510 for details, comments and discussion --- packages/yew/Makefile.toml | 2 + packages/yew/src/dom_bundle/btag/listeners.rs | 144 ++++-- packages/yew/src/dom_bundle/subtree_root.rs | 476 +++++++++++------- 3 files changed, 421 insertions(+), 201 deletions(-) diff --git a/packages/yew/Makefile.toml b/packages/yew/Makefile.toml index 1a4a0f505e0..7be704aa34f 100644 --- a/packages/yew/Makefile.toml +++ b/packages/yew/Makefile.toml @@ -13,6 +13,8 @@ args = [ "--", "--features", "${YEW_TEST_FEATURES}", + "--", + "portal_bubbling" ] [tasks.doc-test] diff --git a/packages/yew/src/dom_bundle/btag/listeners.rs b/packages/yew/src/dom_bundle/btag/listeners.rs index b558a69f4c9..f8cdc03d5c3 100644 --- a/packages/yew/src/dom_bundle/btag/listeners.rs +++ b/packages/yew/src/dom_bundle/btag/listeners.rs @@ -21,12 +21,12 @@ extern "C" { /// DOM-Types that can have listeners registered on them. /// Uses the duck-typed interface from above in impls. -pub trait EventTarget { +pub trait EventListening { fn listener_id(&self) -> Option; fn set_listener_id(&self, id: u32); } -impl EventTarget for Element { +impl EventListening for Element { fn listener_id(&self) -> Option { self.unchecked_ref::().listener_id() } @@ -202,16 +202,21 @@ impl Registry { } } - // Handle a single event, given the listener and event descriptor. + /// Check if this registry has any listeners for the given event descriptor + pub fn has_any_listeners(&self, desc: &EventDescriptor) -> bool { + self.global.handling.contains(desc) + } + + /// Handle a single event, given the listening element and event descriptor. pub fn get_handler( registry: &RefCell, - listener: &dyn EventTarget, + listening: &dyn EventListening, desc: &EventDescriptor, ) -> Option { // The tricky part is that we want to drop the reference to the registry before // calling any actual listeners (since that might end up running lifecycle methods // and modify the registry). So we clone the current listeners and return a closure - let listener_id = listener.listener_id()?; + let listener_id = listening.listener_id()?; let registry_ref = registry.borrow(); let handlers = registry_ref.by_id.get(&listener_id)?; let listeners = handlers.get(desc)?.clone(); @@ -261,14 +266,15 @@ impl Registry { let id = self.id_counter; self.id_counter += 1; - root.brand_element(el); + root.brand_element(el as &HtmlEventTarget); el.set_listener_id(id); id } } -#[cfg(all(test, feature = "wasm_test"))] +#[cfg(feature = "wasm_test")] +#[cfg(test)] mod tests { use std::marker::PhantomData; @@ -276,7 +282,10 @@ mod tests { use web_sys::{Event, EventInit, MouseEvent}; wasm_bindgen_test_configure!(run_in_browser); - use crate::{html, html::TargetCast, scheduler, AppHandle, Component, Context, Html}; + use crate::{ + create_portal, html, html::TargetCast, scheduler, virtual_dom::VNode, AppHandle, Component, + Context, Html, Properties, + }; use gloo_utils::document; use wasm_bindgen::JsCast; use yew::Callback; @@ -298,26 +307,7 @@ mod tests { trait Mixin { fn view(ctx: &Context, state: &State) -> Html where - C: Component, - { - let link = ctx.link().clone(); - let onclick = Callback::from(move |_| { - link.send_message(Message::Action); - scheduler::start_now(); - }); - - if state.stop_listening { - html! { - {state.action} - } - } else { - html! { - - {state.action} - - } - } - } + C: Component; } struct Comp @@ -330,10 +320,10 @@ mod tests { impl Component for Comp where - M: Mixin + 'static, + M: Mixin + Properties + 'static, { type Message = Message; - type Properties = (); + type Properties = M; fn create(_: &Context) -> Self { Comp { @@ -362,6 +352,7 @@ mod tests { } } + #[track_caller] fn assert_count(el: &web_sys::HtmlElement, count: isize) { assert_eq!(el.text_content(), Some(count.to_string())) } @@ -377,7 +368,7 @@ mod tests { fn init(tag: &str) -> (AppHandle>, web_sys::HtmlElement) where - M: Mixin, + M: Mixin + Properties + Default, { // Remove any existing elements if let Some(el) = document().query_selector(tag).unwrap() { @@ -394,9 +385,33 @@ mod tests { #[test] fn synchronous() { + #[derive(Default, PartialEq, Properties)] struct Synchronous; - impl Mixin for Synchronous {} + impl Mixin for Synchronous { + fn view(ctx: &Context, state: &State) -> Html + where + C: Component, + { + let link = ctx.link().clone(); + let onclick = Callback::from(move |_| { + link.send_message(Message::Action); + scheduler::start_now(); + }); + + if state.stop_listening { + html! { + {state.action} + } + } else { + html! { + + {state.action} + + } + } + } + } let (link, el) = init::("a"); @@ -417,6 +432,7 @@ mod tests { #[test] async fn non_bubbling_event() { + #[derive(Default, PartialEq, Properties)] struct NonBubbling; impl Mixin for NonBubbling { @@ -462,6 +478,7 @@ mod tests { #[test] fn bubbling() { + #[derive(Default, PartialEq, Properties)] struct Bubbling; impl Mixin for Bubbling { @@ -512,6 +529,7 @@ mod tests { #[test] fn cancel_bubbling() { + #[derive(Default, PartialEq, Properties)] struct CancelBubbling; impl Mixin for CancelBubbling { @@ -558,6 +576,7 @@ mod tests { // Here an event is being delivered to a DOM node which does // _not_ have a listener but which is contained within an // element that does and which cancels the bubble. + #[derive(Default, PartialEq, Properties)] struct CancelBubbling; impl Mixin for CancelBubbling { @@ -600,10 +619,71 @@ mod tests { assert_count(&el, 2); } + #[test] + fn portal_bubbling() { + // Here an event is being delivered to a DOM node which is contained + // in a portal. It should bubble through the portal and reach the containing + // element + #[derive(PartialEq, Properties)] + struct PortalBubbling { + host: web_sys::Element, + } + impl Default for PortalBubbling { + fn default() -> Self { + let host = document().create_element("div").unwrap(); + PortalBubbling { host } + } + } + + impl Mixin for PortalBubbling { + fn view(ctx: &Context, state: &State) -> Html + where + C: Component, + { + let portal_target = ctx.props().host.clone(); + let onclick = { + let link = ctx.link().clone(); + Callback::from(move |_| { + link.send_message(Message::Action); + scheduler::start_now(); + }) + }; + let portal = create_portal( + html! { + + {state.action} + + }, + portal_target.clone(), + ); + + html! { + <> +
+ {portal} +
+ {VNode::VRef(portal_target.into())} + + } + } + } + + let (_, el) = init::("a"); + + assert_count(&el, 0); + + el.click(); + assert_count(&el, 1); + + el.click(); + assert_count(&el, 2); + } + fn test_input_listener(make_event: impl Fn() -> E) where E: JsCast + std::fmt::Debug, { + #[derive(Default, PartialEq, Properties)] struct Input; impl Mixin for Input { diff --git a/packages/yew/src/dom_bundle/subtree_root.rs b/packages/yew/src/dom_bundle/subtree_root.rs index 568015758f1..31ce2eb59dd 100644 --- a/packages/yew/src/dom_bundle/subtree_root.rs +++ b/packages/yew/src/dom_bundle/subtree_root.rs @@ -2,38 +2,19 @@ use super::{test_log, EventDescriptor, Registry}; use std::cell::RefCell; -use std::rc::Rc; -use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::rc::{Rc, Weak}; +use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::JsCast; use web_sys::{Element, Event, EventTarget as HtmlEventTarget}; -/// Bubble events during delegation -static BUBBLE_EVENTS: AtomicBool = AtomicBool::new(true); - -/// Set, if events should bubble up the DOM tree, calling any matching callbacks. -/// -/// Bubbling is enabled by default. Disabling bubbling can lead to substantial improvements in event -/// handling performance. -/// -/// Note that yew uses event delegation and implements internal even bubbling for performance -/// reasons. Calling `Event.stopPropagation()` or `Event.stopImmediatePropagation()` in the event -/// handler has no effect. -/// -/// This function should be called before any component is mounted. -pub fn set_event_bubbling(bubble: bool) { - BUBBLE_EVENTS.store(bubble, Ordering::Relaxed); -} - -// The TreeId is an additional payload attached to each listening element -// It identifies the host responsible for the target. Events not matching -// are ignored during handling -type TreeId = u32; -/// DOM-Types that capture bubbling events. This generally includes event targets, +/// DOM-Types that capture (bubbling) events. This generally includes event targets, /// but also subtree roots. pub trait EventGrating { - fn responsible_tree_id(&self) -> Option; - fn set_responsible_tree_id(&self, tree_id: TreeId); + fn subtree_id(&self) -> Option; + fn set_subtree_id(&self, tree_id: TreeId); } #[wasm_bindgen] @@ -41,201 +22,363 @@ extern "C" { // Duck-typing, not a real class on js-side. On rust-side, use impls of EventGrating below type EventTargetable; #[wasm_bindgen(method, getter = __yew_subtree_root_id, structural)] - fn subtree_id(this: &EventTargetable) -> Option; + fn subtree_id(this: &EventTargetable) -> Option; #[wasm_bindgen(method, setter = __yew_subtree_root_id, structural)] - fn set_subtree_id(this: &EventTargetable, id: u32); + fn set_subtree_id(this: &EventTargetable, id: TreeId); } -impl EventGrating for Element { - fn responsible_tree_id(&self) -> Option { - self.unchecked_ref::().subtree_id() - } - fn set_responsible_tree_id(&self, tree_id: TreeId) { - self.unchecked_ref::() - .set_subtree_id(tree_id); +macro_rules! impl_event_grating { + ($($t:ty);* $(;)?) => { + $( + impl EventGrating for $t { + fn subtree_id(&self) -> Option { + self.unchecked_ref::().subtree_id() + } + fn set_subtree_id(&self, tree_id: TreeId) { + self.unchecked_ref::() + .set_subtree_id(tree_id); + } + } + )* } } -impl EventGrating for HtmlEventTarget { - fn responsible_tree_id(&self) -> Option { - self.unchecked_ref::().subtree_id() - } - fn set_responsible_tree_id(&self, tree_id: TreeId) { - self.unchecked_ref::() - .set_subtree_id(tree_id); - } -} +impl_event_grating!( + HtmlEventTarget; + Event; // We cache the found subtree id on the event. This should speed up repeated searches +); -/// We cache the found subtree id on the event. This should speed up repeated searches -impl EventGrating for Event { - fn responsible_tree_id(&self) -> Option { - self.unchecked_ref::().subtree_id() - } - fn set_responsible_tree_id(&self, tree_id: TreeId) { - self.unchecked_ref::() - .set_subtree_id(tree_id); - } -} +/// The TreeId is the additional payload attached to each listening element +/// It identifies the host responsible for the target. Events not matching +/// are ignored during handling +type TreeId = i32; -static NEXT_ROOT_ID: AtomicU32 = AtomicU32::new(1); // Skip 0, used for ssr +/// Special id for caching the fact that some event should not be handled +static NONE_TREE_ID: TreeId = 0; +static NEXT_ROOT_ID: AtomicI32 = AtomicI32::new(1); fn next_root_id() -> TreeId { NEXT_ROOT_ID.fetch_add(1, Ordering::SeqCst) } +type KnownSubtrees = HashMap>; +thread_local! { + static KNOWN_ROOTS: RefCell = RefCell::default(); +} + /// Data kept per controlled subtree. [Portal] and [AppHandle] serve as /// hosts. Two controlled subtrees should never overlap. /// /// [Portal]: super::bportal::BPortal /// [AppHandle]: super::app_handle::AppHandle #[derive(Debug, Clone)] -pub struct BSubtree(/* None during SSR */ Option>); +pub struct BSubtree( + Option>, // None during SSR +); // The parent is the logical location where a subtree is mounted // Used to bubble events through portals, which are physically somewhere else in the DOM tree // but should bubble to logical ancestors in the virtual DOM tree #[derive(Debug)] struct ParentingInformation { - parent_root: Option>, + parent_root: Option>, + // Logical parent of the subtree. Might be the host element of another subtree, + // if mounted as a direct child, or a controlled element. mount_element: Element, } #[derive(Debug)] -struct InnerBundleRoot { +struct SubtreeData { + subtree_id: TreeId, host: HtmlEventTarget, parent: Option, - tree_root_id: TreeId, event_registry: RefCell, } -struct ClosestInstanceSearchResult { - root_or_listener: Element, - responsible_tree_id: TreeId, - did_bubble: bool, -} +/// Bubble events during delegation +static BUBBLE_EVENTS: AtomicBool = AtomicBool::new(true); -/// Deduce the subtree responsible for handling this event. This already -/// partially starts the bubbling process, as long as no listeners are encountered, -/// but stops at subtree roots. -/// Event listeners are installed only on the subtree roots. Still, those roots can -/// nest [1]. This would lead to events getting handled multiple times. We want event -/// handling to start at the most deeply nested subtree. +/// Set, if events should bubble up the DOM tree, calling any matching callbacks. /// -/// # When nesting occurs -/// The nested subtree portals into a element that is controlled by the user and rendered -/// with VNode::VRef. We get the following nesting: -/// AppRoot > .. > UserControlledVRef > .. > NestedTree(PortalExit) > .. -/// -------------- ---------------------------- -/// The underlined parts of the hierarchy are controlled by Yew. -fn find_closest_responsible_instance(event: &Event) -> Option { - let target = event.target()?.dyn_into::().ok()?; - if let Some(cached_id) = event.responsible_tree_id() { - return Some(ClosestInstanceSearchResult { - root_or_listener: target, - responsible_tree_id: cached_id, - did_bubble: false, - }); +/// Bubbling is enabled by default. Disabling bubbling can lead to substantial improvements in event +/// handling performance. +/// +/// Note that yew uses event delegation and implements internal even bubbling for performance +/// reasons. Calling `Event.stopPropagation()` or `Event.stopImmediatePropagation()` in the event +/// handler has no effect. +/// +/// This function should be called before any component is mounted. +pub fn set_event_bubbling(bubble: bool) { + BUBBLE_EVENTS.store(bubble, Ordering::Relaxed); +} + +struct BrandingSearchResult { + branding: TreeId, + closest_branded_ancestor: Element, +} + +/// Deduce the subtree an element is part of. This already partially starts the bubbling +/// process, as long as no listeners are encountered. +/// Subtree roots are always branded with their own subtree id. +fn find_closest_branded_element(mut el: Element, do_bubble: bool) -> Option { + if !do_bubble { + Some(BrandingSearchResult { + branding: el.subtree_id()?, + closest_branded_ancestor: el, + }) + } else { + let responsible_tree_id = loop { + if let Some(tree_id) = el.subtree_id() { + break tree_id; + } + el = el.parent_element()?; + }; + Some(BrandingSearchResult { + branding: responsible_tree_id, + closest_branded_ancestor: el, + }) + } +} + +/// Iterate over all potentially listening elements in bubbling order. +/// If bubbling is turned off, yields at most a single element. +struct BubblingIterator<'tree> { + event: &'tree Event, + subtree: &'tree Rc, + next_el: Option, + should_bubble: bool, +} + +impl<'tree> Iterator for BubblingIterator<'tree> { + type Item = (&'tree Rc, Element); + + fn next(&mut self) -> Option { + let candidate = self.next_el.take()?; + if self.event.cancel_bubble() { + return None; + } + if self.should_bubble { + if let Some((next_subtree, parent)) = candidate + .parent_element() + .and_then(|parent| self.subtree.bubble_to_inner_element(parent, true)) + { + self.subtree = next_subtree; + self.next_el = Some(parent); + } + } + Some((self.subtree, candidate)) + } +} + +impl<'tree> BubblingIterator<'tree> { + fn start_from( + subtree: &'tree Rc, + root_or_listener: Element, + event: &'tree Event, + should_bubble: bool, + ) -> Self { + let start = match subtree.bubble_to_inner_element(root_or_listener, should_bubble) { + Some((subtree, next_el)) => (subtree, Some(next_el)), + None => (subtree, None), + }; + Self { + event, + subtree: start.0, + next_el: start.1, + should_bubble, + } } +} - let mut el = target; - let mut did_bubble = false; - let responsible_tree_id = loop { - if let Some(tree_id) = el.responsible_tree_id() { - break tree_id; +struct SubtreeHierarchyIterator<'tree> { + current: Option<(&'tree Rc, &'tree Element)>, +} + +impl<'tree> Iterator for SubtreeHierarchyIterator<'tree> { + type Item = (&'tree Rc, &'tree Element); + + fn next(&mut self) -> Option { + let next = self.current.take()?; + if let Some(parenting_info) = next.0.parent.as_ref() { + let parent_root = parenting_info + .parent_root + .as_ref() + .expect("Not in SSR, this shouldn't be None"); + self.current = Some((parent_root, &parenting_info.mount_element)); } - el = el.parent_element()?; - did_bubble = true; - }; - event.set_responsible_tree_id(responsible_tree_id); - Some(ClosestInstanceSearchResult { - root_or_listener: el, - responsible_tree_id, - did_bubble, - }) + Some(next) + } } -impl InnerBundleRoot { +impl<'tree> SubtreeHierarchyIterator<'tree> { + fn start_from(subtree: &'tree Rc, el: &'tree Element) -> Self { + Self { + current: Some((subtree, el)), + } + } +} + +impl SubtreeData { + fn new_ref(host_element: &HtmlEventTarget, parent: Option) -> Rc { + let tree_root_id = next_root_id(); + let event_registry = Registry::new(host_element.clone()); + let subtree = Rc::new(SubtreeData { + subtree_id: tree_root_id, + host: host_element.clone(), + parent, + event_registry: RefCell::new(event_registry), + }); + KNOWN_ROOTS.with(|roots| { + roots + .borrow_mut() + .insert(tree_root_id, Rc::downgrade(&subtree)) + }); + subtree + } + fn event_registry(&self) -> &RefCell { &self.event_registry } - /// Handle a global event firing - fn handle(self: &Rc, desc: EventDescriptor, event: Event) { - let closest_instance = match find_closest_responsible_instance(&event) { - Some(closest_instance) if closest_instance.responsible_tree_id == self.tree_root_id => { - closest_instance - } - _ => return, // Don't handle this event - }; - test_log!("Running handler on subtree {}", self.tree_root_id); - if self.host.eq(&closest_instance.root_or_listener) { - let (self_, target) = match self.bubble_at_root() { - Some(bubbled_target) => bubbled_target, - None => return, // No relevant listener + + fn find_by_id(tree_id: TreeId) -> Option> { + KNOWN_ROOTS.with(|roots| { + let mut roots = roots.borrow_mut(); + let subtree = match roots.entry(tree_id) { + Entry::Occupied(subtree) => subtree, + _ => return None, }; - self_.run_handlers(desc, event, target, true); - } else { - let target = closest_instance.root_or_listener; - let did_bubble = closest_instance.did_bubble; - self.run_handlers(desc, event, target, did_bubble); - } + match subtree.get().upgrade() { + Some(subtree) => Some(subtree), + None => { + // Remove stale entry + subtree.remove(); + None + } + } + }) } + // Bubble a potential parent until it reaches an internal element #[allow(clippy::needless_lifetimes)] // I don't see a way to omit the lifetimes here - fn bubble_at_root<'s>(self: &'s Rc) -> Option<(&'s Rc, Element)> { - // we've reached the physical host, delegate to a parent if one exists - let parent = self.parent.as_ref()?; - let parent_root = parent - .parent_root - .as_ref() - .expect("Can't access listeners in SSR"); - Some((parent_root, parent.mount_element.clone())) + fn bubble_to_inner_element<'s>( + self: &'s Rc, + parent_el: Element, + should_bubble: bool, + ) -> Option<(&'s Rc, Element)> { + let mut next_subtree = self; + let mut next_el = parent_el; + if !should_bubble && next_subtree.host.eq(&next_el) { + return None; + } + while next_subtree.host.eq(&next_el) { + // we've reached the host, delegate to a parent if one exists + let parent = next_subtree.parent.as_ref()?; + let parent_root = parent + .parent_root + .as_ref() + .expect("Not in SSR, this shouldn't be None"); + next_subtree = parent_root; + next_el = parent.mount_element.clone(); + } + Some((next_subtree, next_el)) } #[allow(clippy::needless_lifetimes)] // I don't see a way to omit the lifetimes here - fn bubble<'s>(self: &'s Rc, el: Element) -> Option<(&'s Rc, Element)> { - let parent = el.parent_element()?; - if self.host.eq(&parent) { - self.bubble_at_root() + fn start_bubbling_if_responsible<'s>( + self: &'s Rc, + event: &'s Event, + desc: &'s EventDescriptor, + ) -> Option> { + // Note: the event is not necessarily indentically the same object for all installed handlers + // hence this cache can be unreliable. + let self_is_responsible = match event.subtree_id() { + Some(responsible_tree_id) if responsible_tree_id == self.subtree_id => true, + None => false, + // some other handler has determined (via this function, but other `self`) a subtree that is + // responsible for handling this event, and it's not this subtree. + Some(_) => return None, + }; + // We're tasked with finding the subtree that is reponsible with handling the event, and/or + // run the handling if that's `self`. The process is very similar + let target = event.target()?.dyn_into::().ok()?; + let should_bubble = BUBBLE_EVENTS.load(Ordering::Relaxed); + let BrandingSearchResult { + branding, + closest_branded_ancestor, + } = find_closest_branded_element(target.clone(), should_bubble)?; + // The branded element can be in a subtree that has no handler installed for the event. + // We say that the most deeply nested subtree that does have a handler installed is "responsible" + // for handling the event. + let (responsible_tree_id, bubble_start) = if branding == self.subtree_id { + // since we're currently in this handler, `self` has a handler installed and is the most + // deeply nested one. This usual case saves a look-up in the global KNOWN_ROOTS. + if self.host.eq(&target) { + // One more special case: don't handle events that get fired directly on a subtree host + // but we still want to cache this fact + (NONE_TREE_ID, closest_branded_ancestor) + } else { + (self.subtree_id, closest_branded_ancestor) + } } else { - Some((self, parent)) - } + // bubble through subtrees until we find one that has a handler installed for the event descriptor + let target_subtree = Self::find_by_id(branding) + .expect("incorrectly branded element: subtree already removed"); + if target_subtree.host.eq(&target) { + (NONE_TREE_ID, closest_branded_ancestor) + } else { + let responsible_tree = SubtreeHierarchyIterator::start_from( + &target_subtree, + &closest_branded_ancestor, + ) + .find(|(candidate, _)| { + if candidate.subtree_id == self.subtree_id { + true + } else if !self_is_responsible { + // only do this check if we aren't sure which subtree is responsible for handling + candidate.event_registry().borrow().has_any_listeners(desc) + } else { + false + } + }) + .expect("nesting error: current subtree should show up in hierarchy"); + (responsible_tree.0.subtree_id, responsible_tree.1.clone()) + } + }; + event.set_subtree_id(responsible_tree_id); // cache it for other event handlers + (responsible_tree_id == self.subtree_id) + .then(|| BubblingIterator::start_from(self, bubble_start, event, should_bubble)) + // # More details: When nesting occurs + // + // Event listeners are installed only on the subtree roots. Still, those roots can + // nest. This could lead to events getting handled multiple times. We want event handling to start + // at the most deeply nested subtree. + // + // A nested subtree portals into an element that is controlled by the user and rendered + // with VNode::VRef. We get the following dom nesting: + // + // AppRoot > .. > UserControlledVRef > .. > NestedTree(PortalExit) > .. + // -------------- ---------------------------- + // The underlined parts of the hierarchy are controlled by Yew. + // + // from the following virtual_dom + // + // {VNode::VRef(
)} + // {create_portal(, #portal_target)} + // } - - fn run_handlers( - self: &Rc, - desc: EventDescriptor, - event: Event, - closest_target: Element, - did_bubble: bool, // did bubble to find the closest target? - ) { + /// Handle a global event firing + fn handle(self: &Rc, desc: EventDescriptor, event: Event) { let run_handler = |root: &Rc, el: &Element| { let handler = Registry::get_handler(root.event_registry(), el, &desc); if let Some(handler) = handler { handler(&event) } }; - - let should_bubble = BUBBLE_EVENTS.load(Ordering::Relaxed); - - // If we bubbled to find closest_target, respect BUBBLE_EVENTS setting - if should_bubble || !did_bubble { - run_handler(self, &closest_target); - } - - let mut current_root = self; - if should_bubble { - let mut el = closest_target; - while !event.cancel_bubble() { - let next = match current_root.bubble(el) { - Some(next) => next, - None => break, - }; - // Destructuring assignments are unstable - current_root = next.0; - el = next.1; - - run_handler(self, &el); + if let Some(bubbling_it) = self.start_bubbling_if_responsible(&event, &desc) { + test_log!("Running handler on subtree {}", self.subtree_id); + for (subtree, el) in bubbling_it { + run_handler(subtree, &el); } } } @@ -246,13 +389,8 @@ impl BSubtree { host_element: &HtmlEventTarget, parent: Option, ) -> Self { - let event_registry = Registry::new(host_element.clone()); - let root = BSubtree(Some(Rc::new(InnerBundleRoot { - host: host_element.clone(), - parent, - tree_root_id: next_root_id(), - event_registry: RefCell::new(event_registry), - }))); + let shared_inner = SubtreeData::new_ref(host_element, parent); + let root = BSubtree(Some(shared_inner)); root.brand_element(host_element); root } @@ -291,6 +429,6 @@ impl BSubtree { pub fn brand_element(&self, el: &dyn EventGrating) { let inner = self.0.as_deref().expect("Can't access listeners in SSR"); - el.set_responsible_tree_id(inner.tree_root_id); + el.set_subtree_id(inner.subtree_id); } }