{"This paragraph is colored red, and its style is mounted into "}
{"document.head"}
{" with a portal"}
-
-
{"This paragraph is rendered in a shadow dom and thus not affected by the surrounding styling context"}
- {"Buttons clicked inside the shadow dom work fine."}
-
-
-
{format!("The button has been clicked {} times. This is also reflected in the title of the tab!", self.counter)}
+
+
+
{"This paragraph is rendered in a shadow dom and thus not affected by the surrounding styling context"}
+ {"Buttons clicked inside the shadow dom work fine."}
+
+
+
{format!("The button has been clicked {} times. This is also reflected in the title of the tab!", self.counter)}
+
{title}
>
}
diff --git a/packages/yew/src/dom_bundle/subtree_root.rs b/packages/yew/src/dom_bundle/subtree_root.rs
index 5105b807e38..c0329a72dbe 100644
--- a/packages/yew/src/dom_bundle/subtree_root.rs
+++ b/packages/yew/src/dom_bundle/subtree_root.rs
@@ -7,7 +7,7 @@ use std::cell::RefCell;
use std::collections::HashSet;
use std::hash::{Hash, Hasher};
use std::rc::{Rc, Weak};
-use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
+use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsCast;
use web_sys::{Element, Event, EventTarget as HtmlEventTarget};
@@ -17,16 +17,24 @@ use web_sys::{Element, Event, EventTarget as HtmlEventTarget};
pub trait EventGrating {
fn subtree_id(&self) -> Option;
fn set_subtree_id(&self, tree_id: TreeId);
+ // When caching, we key on the length of the `composed_path`. Important to check
+ // considering event retargeting!
+ fn cache_key(&self) -> Option;
+ fn set_cache_key(&self, key: u32);
}
#[wasm_bindgen]
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)]
+ #[wasm_bindgen(method, getter = __yew_subtree_id, structural)]
fn subtree_id(this: &EventTargetable) -> Option;
- #[wasm_bindgen(method, setter = __yew_subtree_root_id, structural)]
+ #[wasm_bindgen(method, setter = __yew_subtree_id, structural)]
fn set_subtree_id(this: &EventTargetable, id: TreeId);
+ #[wasm_bindgen(method, getter = __yew_subtree_cache_key, structural)]
+ fn cache_key(this: &EventTargetable) -> Option;
+ #[wasm_bindgen(method, setter = __yew_subtree_cache_key, structural)]
+ fn set_cache_key(this: &EventTargetable, key: u32);
}
macro_rules! impl_event_grating {
@@ -40,6 +48,12 @@ macro_rules! impl_event_grating {
self.unchecked_ref::()
.set_subtree_id(tree_id);
}
+ fn cache_key(&self) -> Option {
+ self.unchecked_ref::().cache_key()
+ }
+ fn set_cache_key(&self, key: u32) {
+ self.unchecked_ref::().set_cache_key(key)
+ }
}
)*
}
@@ -53,11 +67,11 @@ impl_event_grating!(
/// 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;
+type TreeId = u32;
/// 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);
+static NEXT_ROOT_ID: AtomicU32 = AtomicU32::new(1);
fn next_root_id() -> TreeId {
NEXT_ROOT_ID.fetch_add(1, Ordering::SeqCst)
@@ -358,9 +372,21 @@ impl SubtreeData {
event: &'s Event,
) -> Option> {
// Note: the event is not necessarily indentically the same object for all installed handlers
- // hence this cache can be unreliable.
- let cached_responsible_tree_id = event.subtree_id();
- if matches!(cached_responsible_tree_id, Some(responsible_tree_id) if responsible_tree_id != self.subtree_id)
+ // hence this cache can be unreliable. Hence the cached repsonsible_tree_id might be missing.
+ // On the other hand, due to event retargeting at shadow roots, the cache might be wrong!
+ // Keep in mind that we handle events in the capture phase, so top-down. When descending and
+ // retargeting into closed shadow-dom, the event might have been handled 'prematurely'.
+ // TODO: figure out how to prevent this and establish correct event handling for closed shadow root.
+ // Note: Other frameworks also get this wrong and dispatch such events multiple times.
+ let event_path = event.composed_path();
+ let derived_cached_key = event_path.length();
+ let cached_branding = if matches!(event.cache_key(), Some(cache_key) if cache_key == derived_cached_key)
+ {
+ event.subtree_id()
+ } else {
+ None
+ };
+ if matches!(cached_branding, Some(responsible_tree_id) if responsible_tree_id != self.subtree_id)
{
// 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.
@@ -368,29 +394,30 @@ impl SubtreeData {
}
// We're tasked with finding the subtree that is reponsible with handling the event, and/or
// run the handling if that's `self`.
- let target = event.composed_path().get(0).dyn_into::().ok()?;
+ let target = event_path.get(0).dyn_into::().ok()?;
let should_bubble = BUBBLE_EVENTS.load(Ordering::Relaxed);
// We say that the most deeply nested subtree is "responsible" for handling the event.
- let (responsible_tree_id, bubbling_start) =
- if let Some(branding) = cached_responsible_tree_id {
- (branding, target)
- } else if let Some(branding) = find_closest_branded_element(target, should_bubble) {
- let BrandingSearchResult {
- branding,
- closest_branded_ancestor,
- } = branding;
- event.set_subtree_id(branding);
- (branding, closest_branded_ancestor)
- } else {
- // Possible only? if bubbling is disabled
- // No tree should handle this event
- event.set_subtree_id(NONE_TREE_ID);
- return None;
- };
+ let (responsible_tree_id, bubbling_start) = if let Some(branding) = cached_branding {
+ (branding, target.clone())
+ } else if let Some(branding) = find_closest_branded_element(target.clone(), should_bubble) {
+ let BrandingSearchResult {
+ branding,
+ closest_branded_ancestor,
+ } = branding;
+ event.set_subtree_id(branding);
+ event.set_cache_key(derived_cached_key);
+ (branding, closest_branded_ancestor)
+ } else {
+ // Possible only? if bubbling is disabled
+ // No tree should handle this event
+ event.set_subtree_id(NONE_TREE_ID);
+ event.set_cache_key(derived_cached_key);
+ return None;
+ };
if self.subtree_id != responsible_tree_id {
return None;
}
- if self.host.eq(&bubbling_start) {
+ if self.host.eq(&target) {
// One more special case: don't handle events that get fired directly on a subtree host
return None;
}
diff --git a/website/docs/advanced-topics/portals.mdx b/website/docs/advanced-topics/portals.mdx
index 8a660015726..2a355f98ad7 100644
--- a/website/docs/advanced-topics/portals.mdx
+++ b/website/docs/advanced-topics/portals.mdx
@@ -54,5 +54,10 @@ in an unrelated location in the actual DOM.
This allows developers to be oblivious of whether a component they consume, is implemented with
or without portals. Events fired on its children will bubble up regardless.
+A known issue is that events from portals into **closed** shadow roots will be dispatched twice,
+once targeting the element inside the shadow root and once targeting the host element itself. Keep
+in mind that **open** shadow roots work fine. If this impacts you, feel free to open a bug report
+about it.
+
## Further reading
- [Portals example](https://github.com/yewstack/yew/tree/master/examples/portals)
diff --git a/website/docs/concepts/html/events.mdx b/website/docs/concepts/html/events.mdx
index 86f51f6bd04..4076102b72d 100644
--- a/website/docs/concepts/html/events.mdx
+++ b/website/docs/concepts/html/events.mdx
@@ -135,6 +135,23 @@ listens for `click` events.
| `ontransitionrun` | [TransitionEvent](https://docs.rs/web-sys/latest/web_sys/struct.TransitionEvent.html) |
| `ontransitionstart` | [TransitionEvent](https://docs.rs/web-sys/latest/web_sys/struct.TransitionEvent.html) |
+## Event bubbling
+
+Events dispatched by Yew follow the virtual DOM hierarchy when bubbling up to listeners. Currently, only the bubbling phase
+is supported for listeners. Note that the virtual DOM hierarchy is most often, but not always, identical to the actual
+DOM hierarchy. The distinction is important when working with [portals](../../advanced-topics/portals.mdx) and other
+more advanced techniques. The intuition for well implemented components should be that events bubble from children
+to parents, so that the hierarchy in your coded `html!` is the one observed by event handlers.
+
+If you are not interested in event bubbling, you can turn it off by calling
+
+```rust
+yew::set_event_bubbling(false);
+```
+
+*before* starting your app. This speeds up event handling, but some components may break from not receiving events they expect.
+Use this with care!
+
## Typed event target
:::caution