diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index ce95f74204a..575845f7c49 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -76,6 +76,14 @@ wasm-bindgen-futures = "0.4" rustversion = "1" trybuild = "1" +[dev-dependencies.web-sys] +version = "0.3" +features = [ + "ShadowRoot", + "ShadowRootInit", + "ShadowRootMode", +] + [features] ssr = ["futures", "html-escape"] csr = [] diff --git a/packages/yew/src/dom_bundle/btag/listeners.rs b/packages/yew/src/dom_bundle/btag/listeners.rs index 9ec034febfb..d5676316d86 100644 --- a/packages/yew/src/dom_bundle/btag/listeners.rs +++ b/packages/yew/src/dom_bundle/btag/listeners.rs @@ -199,12 +199,12 @@ mod tests { use std::marker::PhantomData; use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; - use web_sys::{Event, EventInit, MouseEvent}; + use web_sys::{Event, EventInit, HtmlElement, MouseEvent}; wasm_bindgen_test_configure!(run_in_browser); use crate::{ create_portal, html, html::TargetCast, scheduler, virtual_dom::VNode, AppHandle, Component, - Context, Html, Properties, + Context, Html, NodeRef, Properties, }; use gloo_utils::document; use wasm_bindgen::JsCast; @@ -224,10 +224,16 @@ mod tests { text: String, } - trait Mixin { + #[derive(Default, PartialEq, Properties)] + struct MixinProps { + state_ref: NodeRef, + wrapped: M, + } + + trait Mixin: Properties + Sized { fn view(ctx: &Context, state: &State) -> Html where - C: Component; + C: Component>; } struct Comp @@ -243,7 +249,7 @@ mod tests { M: Mixin + Properties + 'static, { type Message = Message; - type Properties = M; + type Properties = MixinProps; fn create(_: &Context) -> Self { Comp { @@ -273,34 +279,48 @@ mod tests { } #[track_caller] - fn assert_count(el: &web_sys::HtmlElement, count: isize) { - assert_eq!(el.text_content(), Some(count.to_string())) + fn assert_count(el: &NodeRef, count: isize) { + let text = el + .get() + .expect("State ref not bound in the test case?") + .text_content(); + assert_eq!(text, Some(count.to_string())) + } + + #[track_caller] + fn click(el: &NodeRef) { + el.get().unwrap().dyn_into::().unwrap().click(); + scheduler::start_now(); } - fn get_el_by_tag(tag: &str) -> web_sys::HtmlElement { + fn get_el_by_selector(selector: &str) -> web_sys::HtmlElement { document() - .query_selector(tag) + .query_selector(selector) .unwrap() .unwrap() .dyn_into::() .unwrap() } - fn init(tag: &str) -> (AppHandle>, web_sys::HtmlElement) + fn init() -> (AppHandle>, NodeRef) where M: Mixin + Properties + Default, { // Remove any existing elements - if let Some(el) = document().query_selector(tag).unwrap() { - el.parent_element().unwrap().remove(); + let body = document().body().unwrap(); + while let Some(child) = body.query_selector("div#testroot").unwrap() { + body.remove_child(&child).unwrap(); } let root = document().create_element("div").unwrap(); - document().body().unwrap().append_child(&root).unwrap(); - let app = crate::Renderer::>::with_root(root).render(); + root.set_id("testroot"); + body.append_child(&root).unwrap(); + let props = as Component>::Properties::default(); + let el_ref = props.state_ref.clone(); + let app = crate::Renderer::>::with_root_and_props(root, props).render(); scheduler::start_now(); - (app, get_el_by_tag(tag)) + (app, el_ref) } #[test] @@ -311,21 +331,17 @@ mod tests { impl Mixin for Synchronous { fn view(ctx: &Context, state: &State) -> Html where - C: Component, + C: Component>, { - let link = ctx.link().clone(); - let onclick = Callback::from(move |_| { - link.send_message(Message::Action); - scheduler::start_now(); - }); + let onclick = ctx.link().callback(|_| Message::Action); if state.stop_listening { html! { - {state.action} + {state.action} } } else { html! { - + {state.action} } @@ -333,20 +349,20 @@ mod tests { } } - let (link, el) = init::("a"); + let (link, el) = init::(); assert_count(&el, 0); - el.click(); + click(&el); assert_count(&el, 1); - el.click(); + click(&el); assert_count(&el, 2); link.send_message(Message::StopListening); scheduler::start_now(); - el.click(); + click(&el); assert_count(&el, 2); } @@ -358,7 +374,7 @@ mod tests { impl Mixin for NonBubbling { fn view(ctx: &Context, state: &State) -> Html where - C: Component, + C: Component>, { let link = ctx.link().clone(); let onblur = Callback::from(move |_| { @@ -367,7 +383,7 @@ mod tests { }); html! {
- + {state.action} @@ -376,7 +392,7 @@ mod tests { } } - let (_, el) = init::("a"); + let (_, el) = init::(); assert_count(&el, 0); @@ -404,25 +420,21 @@ mod tests { impl Mixin for Bubbling { fn view(ctx: &Context, state: &State) -> Html where - C: Component, + C: Component>, { if state.stop_listening { html! { } } else { - let link = ctx.link().clone(); - let cb = Callback::from(move |_| { - link.send_message(Message::Action); - scheduler::start_now(); - }); + let cb = ctx.link().callback(|_| Message::Action); html! { @@ -431,19 +443,17 @@ mod tests { } } - let (link, el) = init::("a"); + let (link, el) = init::(); assert_count(&el, 0); - - el.click(); + click(&el); assert_count(&el, 2); - - el.click(); + click(&el); assert_count(&el, 4); link.send_message(Message::StopListening); scheduler::start_now(); - el.click(); + click(&el); assert_count(&el, 4); } @@ -455,24 +465,17 @@ mod tests { impl Mixin for CancelBubbling { fn view(ctx: &Context, state: &State) -> Html where - C: Component, + C: Component>, { - let link = ctx.link().clone(); - let onclick = Callback::from(move |_| { - link.send_message(Message::Action); - scheduler::start_now(); - }); - - let link = ctx.link().clone(); - let onclick2 = Callback::from(move |e: MouseEvent| { + let onclick = ctx.link().callback(|_| Message::Action); + let onclick2 = ctx.link().callback(|e: MouseEvent| { e.stop_propagation(); - link.send_message(Message::Action); - scheduler::start_now(); + Message::Action }); html! { @@ -480,14 +483,12 @@ mod tests { } } - let (_, el) = init::("a"); + let (_, el) = init::(); assert_count(&el, 0); - - el.click(); + click(&el); assert_count(&el, 1); - - el.click(); + click(&el); assert_count(&el, 2); } @@ -502,24 +503,17 @@ mod tests { impl Mixin for CancelBubbling { fn view(ctx: &Context, state: &State) -> Html where - C: Component, + C: Component>, { - let link = ctx.link().clone(); - let onclick = Callback::from(move |_| { - link.send_message(Message::Action); - scheduler::start_now(); - }); - - let link = ctx.link().clone(); - let onclick2 = Callback::from(move |e: MouseEvent| { + let onclick = ctx.link().callback(|_| Message::Action); + let onclick2 = ctx.link().callback(|e: MouseEvent| { e.stop_propagation(); - link.send_message(Message::Action); - scheduler::start_now(); + Message::Action }); html! {
@@ -528,22 +522,20 @@ mod tests { } } - let (_, el) = init::("a"); + let (_, el) = init::(); assert_count(&el, 0); - - el.click(); + click(&el); assert_count(&el, 1); - - el.click(); + click(&el); assert_count(&el, 2); } + /// 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. #[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, @@ -558,29 +550,18 @@ mod tests { impl Mixin for PortalBubbling { fn view(ctx: &Context, state: &State) -> Html where - C: Component, + 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(), - ); - + let portal_target = ctx.props().wrapped.host.clone(); + let onclick = ctx.link().callback(|_| Message::Action); html! { <>
- {portal} + {create_portal(html! { + + {state.action} + + }, portal_target.clone())}
{VNode::VRef(portal_target.into())} @@ -588,20 +569,64 @@ mod tests { } } - let (_, el) = init::("a"); + let (_, el) = init::(); assert_count(&el, 0); - - el.click(); + click(&el); assert_count(&el, 1); + } - el.click(); - assert_count(&el, 2); + /// Here an event is being from inside a shadow root. It should only be caught exactly once on each handler + #[test] + fn open_shadow_dom_bubbling() { + use web_sys::{ShadowRootInit, ShadowRootMode}; + #[derive(PartialEq, Properties)] + struct OpenShadowDom { + host: web_sys::Element, + inner_root: web_sys::Element, + } + impl Default for OpenShadowDom { + fn default() -> Self { + let host = document().create_element("div").unwrap(); + let inner_root = document().create_element("div").unwrap(); + let shadow = host + .attach_shadow(&ShadowRootInit::new(ShadowRootMode::Open)) + .unwrap(); + shadow.append_child(&inner_root).unwrap(); + OpenShadowDom { host, inner_root } + } + } + impl Mixin for OpenShadowDom { + fn view(ctx: &Context, state: &State) -> Html + where + C: Component>, + { + let onclick = ctx.link().callback(|_| Message::Action); + let mixin = &ctx.props().wrapped; + html! { +
+
+ {create_portal(html! { + + {state.action} + + }, mixin.inner_root.clone())} +
+ {VNode::VRef(mixin.host.clone().into())} +
+ } + } + } + let (_, el) = init::(); + + assert_count(&el, 0); + click(&el); + assert_count(&el, 2); // Once caught per handler } fn test_input_listener(make_event: impl Fn() -> E) where - E: JsCast + std::fmt::Debug, + E: Into + std::fmt::Debug, { #[derive(Default, PartialEq, Properties)] struct Input; @@ -609,45 +634,41 @@ mod tests { impl Mixin for Input { fn view(ctx: &Context, state: &State) -> Html where - C: Component, + C: Component>, { if state.stop_listening { html! {
-

{state.text.clone()}

+

{state.text.clone()}

} } else { - let link = ctx.link().clone(); - let onchange = Callback::from(move |e: web_sys::Event| { + let onchange = ctx.link().callback(|e: web_sys::Event| { let el: web_sys::HtmlInputElement = e.target_unchecked_into(); - link.send_message(Message::SetText(el.value())); - scheduler::start_now(); + Message::SetText(el.value()) }); - - let link = ctx.link().clone(); - let oninput = Callback::from(move |e: web_sys::InputEvent| { + let oninput = ctx.link().callback(|e: web_sys::InputEvent| { let el: web_sys::HtmlInputElement = e.target_unchecked_into(); - link.send_message(Message::SetText(el.value())); - scheduler::start_now(); + Message::SetText(el.value()) }); html! {
-

{state.text.clone()}

+

{state.text.clone()}

} } } } - let (link, input_el) = init::("input"); - let input_el = input_el.dyn_into::().unwrap(); - let p_el = get_el_by_tag("p"); + let (link, state_ref) = init::(); + let input_el = get_el_by_selector("input") + .dyn_into::() + .unwrap(); - assert_eq!(&p_el.text_content().unwrap(), ""); + assert_eq!(&state_ref.get().unwrap().text_content().unwrap(), ""); for mut s in ["foo", "bar", "baz"].iter() { input_el.set_value(s); if s == &"baz" { @@ -656,12 +677,9 @@ mod tests { s = &"bar"; } - input_el - .dyn_ref::() - .unwrap() - .dispatch_event(&make_event().dyn_into().unwrap()) - .unwrap(); - assert_eq!(&p_el.text_content().unwrap(), s); + input_el.dispatch_event(&make_event().into()).unwrap(); + scheduler::start_now(); + assert_eq!(&state_ref.get().unwrap().text_content().unwrap(), s); } } diff --git a/packages/yew/tests/failed_tests/base_component_impl-fail.stderr b/packages/yew/tests/failed_tests/base_component_impl-fail.stderr index a7e774e1dc7..563ac8e341e 100644 --- a/packages/yew/tests/failed_tests/base_component_impl-fail.stderr +++ b/packages/yew/tests/failed_tests/base_component_impl-fail.stderr @@ -1,12 +1,12 @@ error[E0277]: the trait bound `Comp: yew::Component` is not satisfied - --> tests/failed_tests/base_component_impl-fail.rs:6:6 - | -6 | impl BaseComponent for Comp { - | ^^^^^^^^^^^^^ the trait `yew::Component` is not implemented for `Comp` - | - = note: required because of the requirements on the impl of `html::component::sealed::SealedBaseComponent` for `Comp` + --> tests/failed_tests/base_component_impl-fail.rs:6:6 + | +6 | impl BaseComponent for Comp { + | ^^^^^^^^^^^^^ the trait `yew::Component` is not implemented for `Comp` + | + = note: required because of the requirements on the impl of `html::component::sealed::SealedBaseComponent` for `Comp` note: required by a bound in `BaseComponent` - --> src/html/component/mod.rs - | - | pub trait BaseComponent: sealed::SealedBaseComponent + Sized + 'static { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `BaseComponent` + --> src/html/component/mod.rs + | + | pub trait BaseComponent: sealed::SealedBaseComponent + Sized + 'static { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `BaseComponent`