Browse Source

Merge remote-tracking branch 'upstream/master' into clean_up_rsx_imports

Evan Almloff 3 years ago
parent
commit
ef4ece42b3

+ 3 - 0
packages/core/src/events.rs

@@ -63,6 +63,9 @@ pub struct UserEvent {
     /// The event type IE "onclick" or "onmouseover"
     /// The event type IE "onclick" or "onmouseover"
     pub name: &'static str,
     pub name: &'static str,
 
 
+    /// If the event is bubbles up through the vdom
+    pub bubbles: bool,
+
     /// The event data to be passed onto the event handler
     /// The event data to be passed onto the event handler
     pub data: Arc<dyn Any + Send + Sync>,
     pub data: Arc<dyn Any + Send + Sync>,
 }
 }

+ 5 - 1
packages/core/src/scopes.rs

@@ -335,7 +335,7 @@ impl ScopeArena {
                             log::trace!("calling listener {:?}", listener.event);
                             log::trace!("calling listener {:?}", listener.event);
                             if state.canceled.get() {
                             if state.canceled.get() {
                                 // stop bubbling if canceled
                                 // stop bubbling if canceled
-                                break;
+                                return;
                             }
                             }
 
 
                             let mut cb = listener.callback.borrow_mut();
                             let mut cb = listener.callback.borrow_mut();
@@ -349,6 +349,10 @@ impl ScopeArena {
                                     data: event.data.clone(),
                                     data: event.data.clone(),
                                 });
                                 });
                             }
                             }
+
+                            if !event.bubbles {
+                                return;
+                            }
                         }
                         }
                     }
                     }
 
 

+ 3 - 1
packages/desktop/src/events.rs

@@ -4,6 +4,7 @@ use std::any::Any;
 use std::sync::Arc;
 use std::sync::Arc;
 
 
 use dioxus_core::{ElementId, EventPriority, UserEvent};
 use dioxus_core::{ElementId, EventPriority, UserEvent};
+use dioxus_html::event_bubbles;
 use dioxus_html::on::*;
 use dioxus_html::on::*;
 
 
 #[derive(serde::Serialize, serde::Deserialize)]
 #[derive(serde::Serialize, serde::Deserialize)]
@@ -47,8 +48,8 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
     } = serde_json::from_value(val).unwrap();
     } = serde_json::from_value(val).unwrap();
 
 
     let mounted_dom_id = Some(ElementId(mounted_dom_id as usize));
     let mounted_dom_id = Some(ElementId(mounted_dom_id as usize));
-
     let name = event_name_from_type(&event);
     let name = event_name_from_type(&event);
+
     let event = make_synthetic_event(&event, contents);
     let event = make_synthetic_event(&event, contents);
 
 
     UserEvent {
     UserEvent {
@@ -56,6 +57,7 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
         priority: EventPriority::Low,
         priority: EventPriority::Low,
         scope_id: None,
         scope_id: None,
         element: mounted_dom_id,
         element: mounted_dom_id,
+        bubbles: event_bubbles(name),
         data: event,
         data: event,
     }
     }
 }
 }

+ 88 - 0
packages/html/src/events.rs

@@ -1324,3 +1324,91 @@ pub(crate) fn _event_meta(event: &UserEvent) -> (bool, EventPriority) {
         _ => (true, Low),
         _ => (true, Low),
     }
     }
 }
 }
+
+pub fn event_bubbles(evt: &str) -> bool {
+    match evt {
+        "copy" => true,
+        "cut" => true,
+        "paste" => true,
+        "compositionend" => true,
+        "compositionstart" => true,
+        "compositionupdate" => true,
+        "keydown" => true,
+        "keypress" => true,
+        "keyup" => true,
+        "focus" => false,
+        "focusout" => true,
+        "focusin" => true,
+        "blur" => false,
+        "change" => true,
+        "input" => true,
+        "invalid" => true,
+        "reset" => true,
+        "submit" => true,
+        "click" => true,
+        "contextmenu" => true,
+        "doubleclick" => true,
+        "dblclick" => true,
+        "drag" => true,
+        "dragend" => true,
+        "dragenter" => false,
+        "dragexit" => false,
+        "dragleave" => true,
+        "dragover" => true,
+        "dragstart" => true,
+        "drop" => true,
+        "mousedown" => true,
+        "mouseenter" => false,
+        "mouseleave" => false,
+        "mousemove" => true,
+        "mouseout" => true,
+        "scroll" => false,
+        "mouseover" => true,
+        "mouseup" => true,
+        "pointerdown" => true,
+        "pointermove" => true,
+        "pointerup" => true,
+        "pointercancel" => true,
+        "gotpointercapture" => true,
+        "lostpointercapture" => true,
+        "pointerenter" => false,
+        "pointerleave" => false,
+        "pointerover" => true,
+        "pointerout" => true,
+        "select" => true,
+        "touchcancel" => true,
+        "touchend" => true,
+        "touchmove" => true,
+        "touchstart" => true,
+        "wheel" => true,
+        "abort" => false,
+        "canplay" => true,
+        "canplaythrough" => true,
+        "durationchange" => true,
+        "emptied" => true,
+        "encrypted" => true,
+        "ended" => true,
+        "error" => false,
+        "loadeddata" => true,
+        "loadedmetadata" => true,
+        "loadstart" => false,
+        "pause" => true,
+        "play" => true,
+        "playing" => true,
+        "progress" => false,
+        "ratechange" => true,
+        "seeked" => true,
+        "seeking" => true,
+        "stalled" => true,
+        "suspend" => true,
+        "timeupdate" => true,
+        "volumechange" => true,
+        "waiting" => true,
+        "animationstart" => true,
+        "animationend" => true,
+        "animationiteration" => true,
+        "transitionend" => true,
+        "toggle" => true,
+        _ => panic!("unsupported event type {:?}", evt),
+    }
+}

+ 8 - 2
packages/interpreter/src/bindings.rs

@@ -48,10 +48,16 @@ extern "C" {
     pub fn CreatePlaceholder(this: &Interpreter, root: u64);
     pub fn CreatePlaceholder(this: &Interpreter, root: u64);
 
 
     #[wasm_bindgen(method)]
     #[wasm_bindgen(method)]
-    pub fn NewEventListener(this: &Interpreter, name: &str, root: u64, handler: &Function);
+    pub fn NewEventListener(
+        this: &Interpreter,
+        name: &str,
+        root: u64,
+        handler: &Function,
+        bubbles: bool,
+    );
 
 
     #[wasm_bindgen(method)]
     #[wasm_bindgen(method)]
-    pub fn RemoveEventListener(this: &Interpreter, root: u64, name: &str);
+    pub fn RemoveEventListener(this: &Interpreter, root: u64, name: &str, bubbles: bool);
 
 
     #[wasm_bindgen(method)]
     #[wasm_bindgen(method)]
     pub fn SetText(this: &Interpreter, root: u64, text: JsValue);
     pub fn SetText(this: &Interpreter, root: u64, text: JsValue);

+ 239 - 18
packages/interpreter/src/interpreter.js

@@ -5,11 +5,64 @@ export function main() {
     window.ipc.postMessage(serializeIpcMessage("initialize"));
     window.ipc.postMessage(serializeIpcMessage("initialize"));
   }
   }
 }
 }
+
+class ListenerMap {
+  constructor(root) {
+    // bubbling events can listen at the root element
+    this.global = {};
+    // non bubbling events listen at the element the listener was created at
+    this.local = {};
+    this.root = root;
+  }
+
+  createBubbling(event_name, handler) {
+    if (this.global[event_name] === undefined) {
+      this.global[event_name] = {};
+      this.global[event_name].active = 1;
+      this.global[event_name].callback = handler;
+      this.root.addEventListener(event_name, handler);
+    } else {
+      this.global[event_name].active++;
+    }
+  }
+
+  createNonBubbling(event_name, element, handler) {
+    const id = element.getAttribute("data-dioxus-id");
+    if (!this.local[id]) {
+      this.local[id] = {};
+    }
+    this.local[id][event_name] = handler;
+    element.addEventListener(event_name, handler);
+  }
+
+  removeBubbling(event_name) {
+    this.global[event_name].active--;
+    if (this.global[event_name].active === 0) {
+      this.root.removeEventListener(event_name, this.global[event_name].callback);
+      delete this.global[event_name];
+    }
+  }
+
+  removeNonBubbling(element, event_name) {
+    const id = element.getAttribute("data-dioxus-id");
+    delete this.local[id][event_name];
+    if (this.local[id].length === 0) {
+      delete this.local[id];
+    }
+    element.removeEventListener(event_name, handler);
+  }
+
+  removeAllNonBubbling(element) {
+    const id = element.getAttribute("data-dioxus-id");
+    delete this.local[id];
+  }
+}
+
 export class Interpreter {
 export class Interpreter {
   constructor(root) {
   constructor(root) {
     this.root = root;
     this.root = root;
     this.stack = [root];
     this.stack = [root];
-    this.listeners = {};
+    this.listeners = new ListenerMap(root);
     this.handlers = {};
     this.handlers = {};
     this.lastNodeWasText = false;
     this.lastNodeWasText = false;
     this.nodes = [root];
     this.nodes = [root];
@@ -40,6 +93,7 @@ export class Interpreter {
   ReplaceWith(root_id, m) {
   ReplaceWith(root_id, m) {
     let root = this.nodes[root_id];
     let root = this.nodes[root_id];
     let els = this.stack.splice(this.stack.length - m);
     let els = this.stack.splice(this.stack.length - m);
+    this.listeners.removeAllNonBubbling(root);
     root.replaceWith(...els);
     root.replaceWith(...els);
   }
   }
   InsertAfter(root, n) {
   InsertAfter(root, n) {
@@ -54,6 +108,7 @@ export class Interpreter {
   }
   }
   Remove(root) {
   Remove(root) {
     let node = this.nodes[root];
     let node = this.nodes[root];
+    this.listeners.removeAllNonBubbling(node);
     if (node !== undefined) {
     if (node !== undefined) {
       node.remove();
       node.remove();
     }
     }
@@ -79,25 +134,24 @@ export class Interpreter {
     this.stack.push(el);
     this.stack.push(el);
     this.nodes[root] = el;
     this.nodes[root] = el;
   }
   }
-  NewEventListener(event_name, root, handler) {
+  NewEventListener(event_name, root, handler, bubbles) {
     const element = this.nodes[root];
     const element = this.nodes[root];
     element.setAttribute("data-dioxus-id", `${root}`);
     element.setAttribute("data-dioxus-id", `${root}`);
-    if (this.listeners[event_name] === undefined) {
-      this.listeners[event_name] = 1;
-      this.handlers[event_name] = handler;
-      this.root.addEventListener(event_name, handler);
-    } else {
-      this.listeners[event_name]++;
+    if (bubbles) {
+      this.listeners.createBubbling(event_name, handler);
+    }
+    else {
+      this.listeners.createNonBubbling(event_name, element, handler);
     }
     }
   }
   }
-  RemoveEventListener(root, event_name) {
+  RemoveEventListener(root, event_name, bubbles) {
     const element = this.nodes[root];
     const element = this.nodes[root];
     element.removeAttribute(`data-dioxus-id`);
     element.removeAttribute(`data-dioxus-id`);
-    this.listeners[event_name]--;
-    if (this.listeners[event_name] === 0) {
-      this.root.removeEventListener(event_name, this.handlers[event_name]);
-      delete this.listeners[event_name];
-      delete this.handlers[event_name];
+    if (bubbles) {
+      this.listeners.removeBubbling(event_name)
+    }
+    else {
+      this.listeners.removeNonBubbling(element, event_name);
     }
     }
   }
   }
   SetText(root, text) {
   SetText(root, text) {
@@ -198,12 +252,9 @@ export class Interpreter {
         this.RemoveEventListener(edit.root, edit.event_name);
         this.RemoveEventListener(edit.root, edit.event_name);
         break;
         break;
       case "NewEventListener":
       case "NewEventListener":
-        console.log(this.listeners);
-
         // this handler is only provided on desktop implementations since this
         // this handler is only provided on desktop implementations since this
         // method is not used by the web implementation
         // method is not used by the web implementation
         let handler = (event) => {
         let handler = (event) => {
-          console.log(event);
 
 
           let target = event.target;
           let target = event.target;
           if (target != null) {
           if (target != null) {
@@ -292,7 +343,8 @@ export class Interpreter {
             );
             );
           }
           }
         };
         };
-        this.NewEventListener(edit.event_name, edit.root, handler);
+        this.NewEventListener(edit.event_name, edit.root, handler, event_bubbles(edit.event_name));
+
         break;
         break;
       case "SetText":
       case "SetText":
         this.SetText(edit.root, edit.text);
         this.SetText(edit.root, edit.text);
@@ -607,3 +659,172 @@ const bool_attrs = {
   selected: true,
   selected: true,
   truespeed: true,
   truespeed: true,
 };
 };
+
+function event_bubbles(event) {
+  switch (event) {
+    case "copy":
+      return true;
+    case "cut":
+      return true;
+    case "paste":
+      return true;
+    case "compositionend":
+      return true;
+    case "compositionstart":
+      return true;
+    case "compositionupdate":
+      return true;
+    case "keydown":
+      return true;
+    case "keypress":
+      return true;
+    case "keyup":
+      return true;
+    case "focus":
+      return false;
+    case "focusout":
+      return true;
+    case "focusin":
+      return true;
+    case "blur":
+      return false;
+    case "change":
+      return true;
+    case "input":
+      return true;
+    case "invalid":
+      return true;
+    case "reset":
+      return true;
+    case "submit":
+      return true;
+    case "click":
+      return true;
+    case "contextmenu":
+      return true;
+    case "doubleclick":
+      return true;
+    case "dblclick":
+      return true;
+    case "drag":
+      return true;
+    case "dragend":
+      return true;
+    case "dragenter":
+      return false;
+    case "dragexit":
+      return false;
+    case "dragleave":
+      return true;
+    case "dragover":
+      return true;
+    case "dragstart":
+      return true;
+    case "drop":
+      return true;
+    case "mousedown":
+      return true;
+    case "mouseenter":
+      return false;
+    case "mouseleave":
+      return false;
+    case "mousemove":
+      return true;
+    case "mouseout":
+      return true;
+    case "scroll":
+      return false;
+    case "mouseover":
+      return true;
+    case "mouseup":
+      return true;
+    case "pointerdown":
+      return true;
+    case "pointermove":
+      return true;
+    case "pointerup":
+      return true;
+    case "pointercancel":
+      return true;
+    case "gotpointercapture":
+      return true;
+    case "lostpointercapture":
+      return true;
+    case "pointerenter":
+      return false;
+    case "pointerleave":
+      return false;
+    case "pointerover":
+      return true;
+    case "pointerout":
+      return true;
+    case "select":
+      return true;
+    case "touchcancel":
+      return true;
+    case "touchend":
+      return true;
+    case "touchmove":
+      return true;
+    case "touchstart":
+      return true;
+    case "wheel":
+      return true;
+    case "abort":
+      return false;
+    case "canplay":
+      return true;
+    case "canplaythrough":
+      return true;
+    case "durationchange":
+      return true;
+    case "emptied":
+      return true;
+    case "encrypted":
+      return true;
+    case "ended":
+      return true;
+    case "error":
+      return false;
+    case "loadeddata":
+      return true;
+    case "loadedmetadata":
+      return true;
+    case "loadstart":
+      return false;
+    case "pause":
+      return true;
+    case "play":
+      return true;
+    case "playing":
+      return true;
+    case "progress":
+      return false;
+    case "ratechange":
+      return true;
+    case "seeked":
+      return true;
+    case "seeking":
+      return true;
+    case "stalled":
+      return true;
+    case "suspend":
+      return true;
+    case "timeupdate":
+      return true;
+    case "volumechange":
+      return true;
+    case "waiting":
+      return true;
+    case "animationstart":
+      return true;
+    case "animationend":
+      return true;
+    case "animationiteration":
+      return true;
+    case "transitionend":
+      return true;
+    case "toggle":
+      return true;
+  }
+}

+ 2 - 0
packages/liveview/src/events.rs

@@ -6,6 +6,7 @@ use std::any::Any;
 use std::sync::Arc;
 use std::sync::Arc;
 
 
 use dioxus_core::{ElementId, EventPriority, UserEvent};
 use dioxus_core::{ElementId, EventPriority, UserEvent};
+use dioxus_html::event_bubbles;
 use dioxus_html::on::*;
 use dioxus_html::on::*;
 
 
 #[derive(serde::Serialize, serde::Deserialize)]
 #[derive(serde::Serialize, serde::Deserialize)]
@@ -46,6 +47,7 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
         scope_id: None,
         scope_id: None,
         element: mounted_dom_id,
         element: mounted_dom_id,
         data: event,
         data: event,
+        bubbles: event_bubbles(name),
     }
     }
 }
 }
 
 

+ 6 - 1
packages/tui/src/hooks.rs

@@ -9,7 +9,7 @@ use dioxus_html::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, S
 use dioxus_html::input_data::keyboard_types::Modifiers;
 use dioxus_html::input_data::keyboard_types::Modifiers;
 use dioxus_html::input_data::MouseButtonSet as DioxusMouseButtons;
 use dioxus_html::input_data::MouseButtonSet as DioxusMouseButtons;
 use dioxus_html::input_data::{MouseButton as DioxusMouseButton, MouseButtonSet};
 use dioxus_html::input_data::{MouseButton as DioxusMouseButton, MouseButtonSet};
-use dioxus_html::{on::*, KeyCode};
+use dioxus_html::{event_bubbles, on::*, KeyCode};
 use std::{
 use std::{
     any::Any,
     any::Any,
     cell::{RefCell, RefMut},
     cell::{RefCell, RefMut},
@@ -187,6 +187,7 @@ impl InnerInputState {
                     name: "focus",
                     name: "focus",
                     element: Some(id),
                     element: Some(id),
                     data: Arc::new(FocusData {}),
                     data: Arc::new(FocusData {}),
+                    bubbles: event_bubbles("focus"),
                 });
                 });
                 resolved_events.push(UserEvent {
                 resolved_events.push(UserEvent {
                     scope_id: None,
                     scope_id: None,
@@ -194,6 +195,7 @@ impl InnerInputState {
                     name: "focusin",
                     name: "focusin",
                     element: Some(id),
                     element: Some(id),
                     data: Arc::new(FocusData {}),
                     data: Arc::new(FocusData {}),
+                    bubbles: event_bubbles("focusin"),
                 });
                 });
             }
             }
             if let Some(id) = old_focus {
             if let Some(id) = old_focus {
@@ -203,6 +205,7 @@ impl InnerInputState {
                     name: "focusout",
                     name: "focusout",
                     element: Some(id),
                     element: Some(id),
                     data: Arc::new(FocusData {}),
                     data: Arc::new(FocusData {}),
+                    bubbles: event_bubbles("focusout"),
                 });
                 });
             }
             }
         }
         }
@@ -248,6 +251,7 @@ impl InnerInputState {
                     name,
                     name,
                     element: Some(node.id),
                     element: Some(node.id),
                     data,
                     data,
+                    bubbles: event_bubbles(name),
                 })
                 })
             }
             }
         }
         }
@@ -649,6 +653,7 @@ impl RinkInputHandler {
                             name: event,
                             name: event,
                             element: Some(node.id),
                             element: Some(node.id),
                             data: data.clone(),
                             data: data.clone(),
+                            bubbles: event_bubbles(event),
                         });
                         });
                     }
                     }
                 }
                 }

+ 39 - 11
packages/tui/tests/events.rs

@@ -1,8 +1,31 @@
-use std::time::Duration;
-
 use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent};
 use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent};
 use dioxus::prelude::*;
 use dioxus::prelude::*;
 use dioxus_tui::TuiContext;
 use dioxus_tui::TuiContext;
+use std::future::Future;
+use std::pin::Pin;
+use std::task::{Context, Poll};
+
+/// The tui renderer will look for any event that has occured or any future that has resolved in a loop.
+/// It will resolve at most one event per loop.
+/// This future will resolve after a certain number of polls. If the number of polls is greater than the number of events triggered, and the event has not been recieved there is an issue with the event system.
+struct PollN(usize);
+impl PollN {
+    fn new(n: usize) -> Self {
+        PollN(n)
+    }
+}
+impl Future for PollN {
+    type Output = ();
+
+    fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Self::Output> {
+        if self.0 == 0 {
+            Poll::Ready(())
+        } else {
+            self.0 -= 1;
+            Poll::Pending
+        }
+    }
+}
 
 
 #[test]
 #[test]
 fn key_down() {
 fn key_down() {
@@ -13,12 +36,17 @@ fn key_down() {
         let tui_ctx: TuiContext = cx.consume_context().unwrap();
         let tui_ctx: TuiContext = cx.consume_context().unwrap();
         let render_count_handle = render_count.clone();
         let render_count_handle = render_count.clone();
         cx.spawn(async move {
         cx.spawn(async move {
-            tokio::time::sleep(Duration::from_millis(100)).await;
+            PollN::new(3).await;
             render_count_handle.modify(|x| *x + 1);
             render_count_handle.modify(|x| *x + 1);
         });
         });
         if *render_count.get() > 2 {
         if *render_count.get() > 2 {
             panic!("Event was not received");
             panic!("Event was not received");
         }
         }
+        // focus the element
+        tui_ctx.inject_event(Event::Key(KeyEvent {
+            code: KeyCode::Tab,
+            modifiers: KeyModifiers::NONE,
+        }));
         tui_ctx.inject_event(Event::Key(KeyEvent {
         tui_ctx.inject_event(Event::Key(KeyEvent {
             code: KeyCode::Char('a'),
             code: KeyCode::Char('a'),
             modifiers: KeyModifiers::NONE,
             modifiers: KeyModifiers::NONE,
@@ -45,7 +73,7 @@ fn mouse_down() {
         let tui_ctx: TuiContext = cx.consume_context().unwrap();
         let tui_ctx: TuiContext = cx.consume_context().unwrap();
         let render_count_handle = render_count.clone();
         let render_count_handle = render_count.clone();
         cx.spawn(async move {
         cx.spawn(async move {
-            tokio::time::sleep(Duration::from_millis(100)).await;
+            PollN::new(2).await;
             render_count_handle.modify(|x| *x + 1);
             render_count_handle.modify(|x| *x + 1);
         });
         });
         if *render_count.get() > 2 {
         if *render_count.get() > 2 {
@@ -79,7 +107,7 @@ fn mouse_up() {
         let tui_ctx: TuiContext = cx.consume_context().unwrap();
         let tui_ctx: TuiContext = cx.consume_context().unwrap();
         let render_count_handle = render_count.clone();
         let render_count_handle = render_count.clone();
         cx.spawn(async move {
         cx.spawn(async move {
-            tokio::time::sleep(Duration::from_millis(100)).await;
+            PollN::new(3).await;
             render_count_handle.modify(|x| *x + 1);
             render_count_handle.modify(|x| *x + 1);
         });
         });
         if *render_count.get() > 2 {
         if *render_count.get() > 2 {
@@ -118,7 +146,7 @@ fn mouse_enter() {
         let tui_ctx: TuiContext = cx.consume_context().unwrap();
         let tui_ctx: TuiContext = cx.consume_context().unwrap();
         let render_count_handle = render_count.clone();
         let render_count_handle = render_count.clone();
         cx.spawn(async move {
         cx.spawn(async move {
-            tokio::time::sleep(Duration::from_millis(100)).await;
+            PollN::new(3).await;
             render_count_handle.modify(|x| *x + 1);
             render_count_handle.modify(|x| *x + 1);
         });
         });
         if *render_count.get() > 2 {
         if *render_count.get() > 2 {
@@ -157,7 +185,7 @@ fn mouse_exit() {
         let tui_ctx: TuiContext = cx.consume_context().unwrap();
         let tui_ctx: TuiContext = cx.consume_context().unwrap();
         let render_count_handle = render_count.clone();
         let render_count_handle = render_count.clone();
         cx.spawn(async move {
         cx.spawn(async move {
-            tokio::time::sleep(Duration::from_millis(100)).await;
+            PollN::new(3).await;
             render_count_handle.modify(|x| *x + 1);
             render_count_handle.modify(|x| *x + 1);
         });
         });
         if *render_count.get() > 2 {
         if *render_count.get() > 2 {
@@ -196,7 +224,7 @@ fn mouse_move() {
         let tui_ctx: TuiContext = cx.consume_context().unwrap();
         let tui_ctx: TuiContext = cx.consume_context().unwrap();
         let render_count_handle = render_count.clone();
         let render_count_handle = render_count.clone();
         cx.spawn(async move {
         cx.spawn(async move {
-            tokio::time::sleep(Duration::from_millis(100)).await;
+            PollN::new(3).await;
             render_count_handle.modify(|x| *x + 1);
             render_count_handle.modify(|x| *x + 1);
         });
         });
         if *render_count.get() > 2 {
         if *render_count.get() > 2 {
@@ -235,7 +263,7 @@ fn wheel() {
         let tui_ctx: TuiContext = cx.consume_context().unwrap();
         let tui_ctx: TuiContext = cx.consume_context().unwrap();
         let render_count_handle = render_count.clone();
         let render_count_handle = render_count.clone();
         cx.spawn(async move {
         cx.spawn(async move {
-            tokio::time::sleep(Duration::from_millis(100)).await;
+            PollN::new(3).await;
             render_count_handle.modify(|x| *x + 1);
             render_count_handle.modify(|x| *x + 1);
         });
         });
         if *render_count.get() > 2 {
         if *render_count.get() > 2 {
@@ -275,7 +303,7 @@ fn click() {
         let tui_ctx: TuiContext = cx.consume_context().unwrap();
         let tui_ctx: TuiContext = cx.consume_context().unwrap();
         let render_count_handle = render_count.clone();
         let render_count_handle = render_count.clone();
         cx.spawn(async move {
         cx.spawn(async move {
-            tokio::time::sleep(Duration::from_millis(100)).await;
+            PollN::new(3).await;
             render_count_handle.modify(|x| *x + 1);
             render_count_handle.modify(|x| *x + 1);
         });
         });
         if *render_count.get() > 2 {
         if *render_count.get() > 2 {
@@ -314,7 +342,7 @@ fn context_menu() {
         let tui_ctx: TuiContext = cx.consume_context().unwrap();
         let tui_ctx: TuiContext = cx.consume_context().unwrap();
         let render_count_handle = render_count.clone();
         let render_count_handle = render_count.clone();
         cx.spawn(async move {
         cx.spawn(async move {
-            tokio::time::sleep(Duration::from_millis(100)).await;
+            PollN::new(3).await;
             render_count_handle.modify(|x| *x + 1);
             render_count_handle.modify(|x| *x + 1);
         });
         });
         if *render_count.get() > 2 {
         if *render_count.get() > 2 {

+ 12 - 4
packages/web/src/dom.rs

@@ -8,6 +8,7 @@
 //! - Partial delegation?>
 //! - Partial delegation?>
 
 
 use dioxus_core::{DomEdit, ElementId, SchedulerMsg, UserEvent};
 use dioxus_core::{DomEdit, ElementId, SchedulerMsg, UserEvent};
+use dioxus_html::event_bubbles;
 use dioxus_interpreter_js::Interpreter;
 use dioxus_interpreter_js::Interpreter;
 use js_sys::Function;
 use js_sys::Function;
 use std::{any::Any, rc::Rc, sync::Arc};
 use std::{any::Any, rc::Rc, sync::Arc};
@@ -45,6 +46,7 @@ impl WebsysDom {
                             element: Some(ElementId(id)),
                             element: Some(ElementId(id)),
                             scope_id: None,
                             scope_id: None,
                             priority: dioxus_core::EventPriority::Medium,
                             priority: dioxus_core::EventPriority::Medium,
+                            bubbles: event.bubbles(),
                         });
                         });
                     }
                     }
                     Some(Err(e)) => {
                     Some(Err(e)) => {
@@ -64,6 +66,7 @@ impl WebsysDom {
                                 element: None,
                                 element: None,
                                 scope_id: None,
                                 scope_id: None,
                                 priority: dioxus_core::EventPriority::Low,
                                 priority: dioxus_core::EventPriority::Low,
+                                bubbles: event.bubbles(),
                             });
                             });
                         }
                         }
                     }
                     }
@@ -121,12 +124,17 @@ impl WebsysDom {
                     event_name, root, ..
                     event_name, root, ..
                 } => {
                 } => {
                     let handler: &Function = self.handler.as_ref().unchecked_ref();
                     let handler: &Function = self.handler.as_ref().unchecked_ref();
-                    self.interpreter.NewEventListener(event_name, root, handler);
+                    self.interpreter.NewEventListener(
+                        event_name,
+                        root,
+                        handler,
+                        event_bubbles(event_name),
+                    );
                 }
                 }
 
 
-                DomEdit::RemoveEventListener { root, event } => {
-                    self.interpreter.RemoveEventListener(root, event)
-                }
+                DomEdit::RemoveEventListener { root, event } => self
+                    .interpreter
+                    .RemoveEventListener(root, event, event_bubbles(event)),
 
 
                 DomEdit::RemoveAttribute { root, name, ns } => {
                 DomEdit::RemoveAttribute { root, name, ns } => {
                     self.interpreter.RemoveAttribute(root, name, ns)
                     self.interpreter.RemoveAttribute(root, name, ns)

+ 2 - 0
packages/web/src/rehydrate.rs

@@ -1,5 +1,6 @@
 use crate::dom::WebsysDom;
 use crate::dom::WebsysDom;
 use dioxus_core::{VNode, VirtualDom};
 use dioxus_core::{VNode, VirtualDom};
+use dioxus_html::event_bubbles;
 use wasm_bindgen::JsCast;
 use wasm_bindgen::JsCast;
 use web_sys::{Comment, Element, Node, Text};
 use web_sys::{Comment, Element, Node, Text};
 
 
@@ -111,6 +112,7 @@ impl WebsysDom {
                         listener.event,
                         listener.event,
                         listener.mounted_node.get().unwrap().as_u64(),
                         listener.mounted_node.get().unwrap().as_u64(),
                         self.handler.as_ref().unchecked_ref(),
                         self.handler.as_ref().unchecked_ref(),
+                        event_bubbles(listener.event),
                     );
                     );
                 }
                 }