Skip to main content

dioxus_native_dom/
dioxus_document.rs

1//! Integration between Dioxus and Blitz
2use crate::NodeId;
3use crate::events::{
4    BlitzKeyboardData, NativeConverter, NativeFocusData, NativeFormData, NativePointerData,
5    NativeScrollData, NativeTouchData, NativeWheelData, NodeHandle,
6};
7use crate::mutation_writer::DioxusState;
8use crate::qual_name;
9use blitz_dom::{
10    Attribute, BaseDocument, DocGuard, DocGuardMut, Document, DocumentConfig, EventDriver,
11    EventHandler, Node,
12};
13use blitz_traits::events::{DomEvent, DomEventData, EventState, UiEvent};
14use dioxus_core::{ElementId, Event, VirtualDom};
15use dioxus_html::{PlatformEventData, set_event_converter};
16use futures_util::task::noop_waker;
17use std::cell::RefCell;
18use std::future::Future;
19use std::mem;
20use std::pin::pin;
21use std::sync::LazyLock;
22use std::task::{Context as TaskContext, Waker};
23use std::{any::Any, rc::Rc};
24
25fn wrap_event_data<T: Any>(value: T) -> Rc<dyn Any> {
26    Rc::new(PlatformEventData::new(Box::new(value)))
27}
28
29/// Get the value of the "dioxus-data-id" attribute parsed aa usize
30fn get_dioxus_id(node: &Node) -> Option<ElementId> {
31    node.element_data()?
32        .attrs
33        .iter()
34        .find(|attr| *attr.name.local == *"data-dioxus-id")
35        .and_then(|attr| attr.value.parse::<usize>().ok())
36        .map(ElementId::from_raw)
37}
38
39/// Integrates [`BaseDocument`] from  [`blitz-dom`](blitz_dom)  with [`VirtualDom`] from [`dioxus-core`](dioxus_core)
40///
41/// ### Example
42///
43/// ```rust
44/// use blitz_traits::shell::{Viewport, ColorScheme};
45/// use dioxus_native_dom::{DioxusDocument, DocumentConfig};
46/// use dioxus::prelude::*;
47///
48/// // Example Dioxus app
49/// fn app() -> Element {
50///     rsx! {
51///         div { "Hello, world!" }
52///     }
53/// }
54///
55/// fn main() {
56///    let vdom = VirtualDom::new(app);
57///    let mut doc = DioxusDocument::new(vdom, DocumentConfig {
58///         viewport: Some(Viewport::new(800, 600, 1.0, ColorScheme::Light)),
59///         ..Default::default()
60///    });
61///    doc.initial_build();
62/// }
63/// ```
64///
65/// You can just push events into the [`DioxusDocument`] with [`doc.handle_ui_event(..)`](Self::handle_ui_event)
66/// and then flush the changes with [`doc.poll(..)`](Self::poll)
67pub struct DioxusDocument {
68    pub inner: Rc<RefCell<BaseDocument>>,
69    pub vdom: VirtualDom,
70    pub vdom_state: DioxusState,
71
72    #[allow(unused)]
73    pub(crate) html_element_id: NodeId,
74    #[allow(unused)]
75    pub(crate) head_element_id: NodeId,
76    #[allow(unused)]
77    pub(crate) body_element_id: NodeId,
78    #[allow(unused)]
79    pub(crate) main_element_id: NodeId,
80}
81
82impl DioxusDocument {
83    /// Create a new [`DioxusDocument`] from a [`VirtualDom`].
84    pub fn new(vdom: VirtualDom, mut config: DocumentConfig) -> Self {
85        // Only really needs to happen once
86        set_event_converter(Box::new(NativeConverter {}));
87
88        config.base_url = Some(
89            config
90                .base_url
91                .unwrap_or_else(|| String::from("dioxus://index.html")),
92        );
93        let mut doc = BaseDocument::new(config);
94
95        // Create some minimal HTML to render the app into.
96        // HTML is equivalent to:
97        //
98        // <html>
99        // <head></head>
100        // <body>
101        //    <div id="main"></div>
102        // </body>
103        // </html>
104        //
105        // TODO: Support arbitrary "index.html" templates
106
107        // Create the html element
108        let mut mutr = doc.mutate();
109        let html_element_id = mutr.create_element(qual_name("html", None), vec![]);
110        mutr.append_children(mutr.doc.root_node().id, &[html_element_id]);
111
112        // Create the body element
113        let head_element_id = mutr.create_element(qual_name("head", None), vec![]);
114        mutr.append_children(html_element_id, &[head_element_id]);
115
116        // Create the body element
117        let body_element_id = mutr.create_element(qual_name("body", None), vec![]);
118        mutr.append_children(html_element_id, &[body_element_id]);
119
120        // Create another virtual element to hold the root <div id="main"></div> under the html element
121        let main_attr = blitz_dom::Attribute {
122            name: qual_name("id", None),
123            value: "main".to_string(),
124        };
125        let main_element_id = mutr.create_element(qual_name("main", None), vec![main_attr]);
126        mutr.append_children(body_element_id, &[main_element_id]);
127
128        drop(mutr);
129
130        let vdom_state = DioxusState::create(main_element_id);
131        Self {
132            vdom,
133            vdom_state,
134            inner: Rc::new(RefCell::new(doc)),
135            html_element_id,
136            head_element_id,
137            body_element_id,
138            main_element_id,
139        }
140    }
141
142    /// Run an initial build of the Dioxus vdom
143    pub fn initial_build(&mut self) {
144        // Mutations are buffered and only applied once the vdom has finished running. This means
145        // that the document is not borrowed while user code (which may access it) is running.
146        self.vdom.rebuild(&mut self.vdom_state.buffered_mutations);
147        self.vdom_state
148            .apply_buffered_mutations(&mut self.inner.borrow_mut());
149        self.flush_queued_mounted_events();
150    }
151
152    /// Used to respond to a `CreateHeadElement` event generated by Dioxus. These
153    /// events allow Dioxus to create elements in the `<head>` of the document.
154    #[doc(hidden)]
155    pub fn create_head_element(
156        &mut self,
157        name: &str,
158        attributes: &[(String, String)],
159        contents: &Option<String>,
160    ) {
161        let mut inner = self.inner.borrow_mut();
162        let mut mutr = inner.mutate();
163
164        let attributes = attributes
165            .iter()
166            .map(|(name, value)| Attribute {
167                name: qual_name(name, None),
168                value: value.clone(),
169            })
170            .collect();
171
172        let new_elem_id = mutr.create_element(qual_name(name, None), attributes);
173        mutr.append_children(self.head_element_id, &[new_elem_id]);
174        if let Some(contents) = contents {
175            let text_node_id = mutr.create_text_node(contents);
176            mutr.append_children(new_elem_id, &[text_node_id]);
177        }
178    }
179
180    pub(crate) fn flush_queued_mounted_events(&mut self) {
181        let mut queued_mounted_events = mem::take(&mut self.vdom_state.queued_mounted_events);
182        for element_id in queued_mounted_events.drain(..) {
183            let node_id = self.vdom_state.element_to_node_id(element_id);
184
185            if self.inner.borrow().get_node(node_id).is_some() {
186                let event = Event::new(
187                    Rc::new(PlatformEventData::new(Box::new(NodeHandle {
188                        doc: Rc::clone(&self.inner),
189                        node_id,
190                    }))) as Rc<dyn Any>,
191                    false,
192                );
193                self.vdom
194                    .runtime()
195                    .handle_event("mounted", event, element_id);
196            }
197        }
198
199        self.vdom_state.queued_mounted_events = queued_mounted_events;
200    }
201}
202
203// Implement DocumentLike and required traits for DioxusDocument
204impl Document for DioxusDocument {
205    fn id(&self) -> usize {
206        self.inner.borrow().id()
207    }
208
209    fn inner(&self) -> DocGuard<'_> {
210        DocGuard::RefCell(self.inner.borrow())
211    }
212
213    fn inner_mut(&mut self) -> DocGuardMut<'_> {
214        DocGuardMut::RefCell(self.inner.borrow_mut())
215    }
216
217    fn poll(&mut self, cx: Option<TaskContext>) -> bool {
218        static NOOP_WAKER: LazyLock<Waker> = LazyLock::new(noop_waker);
219        let waker = cx
220            .as_ref()
221            .map(|cx| cx.waker().clone())
222            .unwrap_or_else(|| NOOP_WAKER.clone());
223
224        // Poll any sub-documents, which may have pending async operations of
225        // their own (e.g. JavaScript timers)
226        let subdoc_changes = self.inner.borrow_mut().poll_subdocuments(Some(&waker));
227
228        {
229            let fut = self.vdom.wait_for_work();
230            let mut pinned_fut = pin!(fut);
231
232            let mut cx = TaskContext::from_waker(&waker);
233            match pinned_fut.as_mut().poll(&mut cx) {
234                std::task::Poll::Ready(_) => {}
235                std::task::Poll::Pending => return subdoc_changes,
236            }
237        }
238
239        // Mutations are buffered and only applied once the vdom has finished running. This means
240        // that the document is not borrowed while user code (which may access it) is running.
241        self.vdom
242            .render_immediate(&mut self.vdom_state.buffered_mutations);
243        self.vdom_state
244            .apply_buffered_mutations(&mut self.inner.borrow_mut());
245        self.flush_queued_mounted_events();
246
247        true
248    }
249
250    fn handle_ui_event(&mut self, event: UiEvent) {
251        let handler = DioxusEventHandler {
252            vdom: &mut self.vdom,
253            vdom_state: &mut self.vdom_state,
254        };
255        let mut driver = EventDriver::new(&mut self.inner, handler);
256        driver.handle_ui_event(event);
257    }
258}
259
260pub struct DioxusEventHandler<'v> {
261    vdom: &'v mut VirtualDom,
262    vdom_state: &'v mut DioxusState,
263}
264
265impl EventHandler for DioxusEventHandler<'_> {
266    fn handle_event(
267        &mut self,
268        chain: &[NodeId],
269        event: &mut DomEvent,
270        doc: &mut dyn Document,
271        event_state: &mut EventState,
272    ) {
273        // As an optimisation we maintain a count of the total number event handlers of a given type
274        // If this count is zero then we can skip handling that kind of event entirely.
275        let event_kind_idx = event.data.discriminant() as usize;
276        let event_kind_count = self.vdom_state.event_handler_counts[event_kind_idx];
277        if event_kind_count == 0 {
278            return;
279        }
280
281        let event_data = match &event.data {
282            DomEventData::PointerMove(mevent)
283            | DomEventData::PointerDown(mevent)
284            | DomEventData::PointerUp(mevent)
285            | DomEventData::PointerCancel(mevent)
286            | DomEventData::PointerLeave(mevent)
287            | DomEventData::PointerEnter(mevent)
288            | DomEventData::PointerOver(mevent)
289            | DomEventData::PointerOut(mevent)
290            | DomEventData::MouseMove(mevent)
291            | DomEventData::MouseDown(mevent)
292            | DomEventData::MouseUp(mevent)
293            | DomEventData::MouseLeave(mevent)
294            | DomEventData::MouseEnter(mevent)
295            | DomEventData::MouseOver(mevent)
296            | DomEventData::MouseOut(mevent)
297            | DomEventData::Click(mevent)
298            | DomEventData::ContextMenu(mevent)
299            | DomEventData::DoubleClick(mevent) => {
300                Some(wrap_event_data(NativePointerData(mevent.clone())))
301            }
302
303            DomEventData::TouchStart(tevent)
304            | DomEventData::TouchMove(tevent)
305            | DomEventData::TouchEnd(tevent)
306            | DomEventData::TouchCancel(tevent) => {
307                Some(wrap_event_data(NativeTouchData(tevent.clone())))
308            }
309
310            DomEventData::Scroll(sevent) => Some(wrap_event_data(NativeScrollData(sevent.clone()))),
311            DomEventData::Wheel(wevent) => Some(wrap_event_data(NativeWheelData(wevent.clone()))),
312
313            DomEventData::Focus(_) => Some(wrap_event_data(NativeFocusData)),
314            DomEventData::Blur(_) => Some(wrap_event_data(NativeFocusData)),
315            DomEventData::FocusIn(_) => Some(wrap_event_data(NativeFocusData)),
316            DomEventData::FocusOut(_) => Some(wrap_event_data(NativeFocusData)),
317
318            DomEventData::KeyDown(kevent)
319            | DomEventData::KeyUp(kevent)
320            | DomEventData::KeyPress(kevent) => {
321                Some(wrap_event_data(BlitzKeyboardData(kevent.clone())))
322            }
323
324            DomEventData::Input(data) => Some(wrap_event_data(NativeFormData {
325                value: data.value.clone(),
326                values: vec![],
327            })),
328
329            // TODO: Implement IME handling
330            DomEventData::Ime(_) => None,
331
332            // AppleStandardKeybinding events are not exposed to script
333            DomEventData::AppleStandardKeybinding(_) => None,
334        };
335
336        let Some(event_data) = event_data else {
337            return;
338        };
339
340        // Get dioxus vdom id for node
341        let dioxus_id = chain
342            .iter()
343            .find_map(|node_id| doc.inner().get_node(*node_id).and_then(get_dioxus_id));
344        let Some(id) = dioxus_id else {
345            return;
346        };
347
348        // Handle event in vdom
349        let dx_event = Event::new(event_data.clone(), event.bubbles);
350        self.vdom
351            .runtime()
352            .handle_event(event.name(), dx_event.clone(), id);
353
354        // Update event state
355        if !dx_event.default_action_enabled() {
356            event_state.prevent_default();
357        }
358        if !dx_event.propagates() {
359            event_state.stop_propagation();
360        }
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367    use blitz_dom::DocumentConfig;
368    use dioxus::prelude::*;
369    use dioxus_core::ScopeId;
370    use std::cell::{Cell, RefCell};
371    use std::collections::HashMap;
372
373    #[test]
374    // Regression test for a panic. The keyed `div`s are re-ordered as the (unordered) `HashMap` grows,
375    // which previously caused a crash when moving keyed nodes within their parent.
376    fn keyed_nodes_do_not_crash() {
377        type SharedData = Rc<RefCell<HashMap<usize, usize>>>;
378        let data: SharedData = Rc::new(RefCell::new(HashMap::new()));
379
380        fn app(data: SharedData) -> Element {
381            let entries: Vec<usize> = data.borrow().keys().copied().collect();
382            rsx!(
383                for id in entries {
384                    div {
385                        key: "item_{id}",
386                        "{id}"
387                    }
388                }
389            )
390        }
391
392        let vdom = VirtualDom::new_with_props(app, Rc::clone(&data));
393        let mut doc = DioxusDocument::new(vdom, DocumentConfig::default());
394        doc.initial_build();
395
396        // Mirror `examples/crash.rs`: incrementally insert 100 items, flushing
397        // the resulting mutations after each insert.
398        for i in 0..100 {
399            data.borrow_mut().insert(i, i);
400            doc.vdom.mark_dirty(ScopeId::APP);
401            doc.poll(None);
402        }
403
404        // The `<main>` element should end up with exactly one keyed `div` per
405        // inserted item, and applying the mutations must not have panicked.
406        let inner = doc.inner.borrow();
407        let main = inner.get_node(doc.main_element_id).unwrap();
408        assert_eq!(main.children.len(), 100);
409    }
410
411    fn node_count(doc: &DioxusDocument) -> usize {
412        doc.inner.borrow().tree().len()
413    }
414
415    fn rerender(doc: &mut DioxusDocument) {
416        doc.vdom.mark_dirty(ScopeId::APP);
417        poll_until_idle(doc);
418    }
419
420    fn poll_until_idle(doc: &mut DioxusDocument) {
421        for _ in 0..8 {
422            if !doc.poll(None) {
423                return;
424            }
425        }
426        panic!("document did not become idle");
427    }
428
429    fn main_text(doc: &DioxusDocument) -> String {
430        let inner = doc.inner.borrow();
431        let mut text = String::new();
432        let mut stack = vec![doc.main_element_id];
433        while let Some(id) = stack.pop() {
434            let node = inner.get_node(id).unwrap();
435            if let Some(data) = node.text_data() {
436                text.push_str(&data.content);
437            }
438            stack.extend(node.children.iter().rev().copied());
439        }
440        text
441    }
442
443    #[test]
444    fn removed_nodes_are_dropped() {
445        fn app() -> Element {
446            let item_count = consume_context::<Rc<Cell<usize>>>();
447            rsx!(
448                for id in 0..item_count.get() {
449                    div { key: "{id}", span { "{id}" } }
450                }
451            )
452        }
453
454        let item_count = Rc::new(Cell::new(0usize));
455        let vdom = VirtualDom::new(app).with_root_context(Rc::clone(&item_count));
456        let mut doc = DioxusDocument::new(vdom, DocumentConfig::default());
457        doc.initial_build();
458
459        let cycle = |doc: &mut DioxusDocument| {
460            item_count.set(50);
461            rerender(doc);
462            let main_id = doc.main_element_id;
463            assert_eq!(
464                doc.inner.borrow().get_node(main_id).unwrap().children.len(),
465                50
466            );
467            item_count.set(0);
468            rerender(doc);
469            node_count(doc)
470        };
471
472        // The first cycle creates the (retained) template prototype nodes.
473        let baseline = cycle(&mut doc);
474        for _ in 0..5 {
475            assert_eq!(cycle(&mut doc), baseline);
476        }
477    }
478
479    #[test]
480    fn suspended_nodes_are_dropped_and_recreated() {
481        #[derive(Default)]
482        struct State {
483            suspended: Cell<bool>,
484            generation: Cell<usize>,
485            waker: Cell<Option<std::task::Waker>>,
486        }
487
488        // `generation` only exists to defeat memoization so that the component re-renders
489        #[component]
490        fn Child(generation: usize) -> Element {
491            let state = consume_context::<Rc<State>>();
492            let mut resolved = use_signal(|| false);
493            if state.suspended.get() {
494                resolved.set(false);
495                let task = spawn(async move {
496                    std::future::poll_fn(|cx| {
497                        if state.suspended.get() {
498                            state.waker.set(Some(cx.waker().clone()));
499                            std::task::Poll::Pending
500                        } else {
501                            std::task::Poll::Ready(())
502                        }
503                    })
504                    .await;
505                    resolved.set(true);
506                });
507                suspend(task)?;
508            }
509            rsx!( div { span { "content" } } )
510        }
511
512        fn app() -> Element {
513            let state = consume_context::<Rc<State>>();
514            rsx!(
515                SuspenseBoundary {
516                    fallback: |_| rsx!( p { "loading" } ),
517                    Child { generation: state.generation.get() }
518                }
519            )
520        }
521
522        let state = Rc::new(State::default());
523        let vdom = VirtualDom::new(app).with_root_context(Rc::clone(&state));
524        let mut doc = DioxusDocument::new(vdom, DocumentConfig::default());
525        doc.initial_build();
526        assert_eq!(main_text(&doc), "content");
527
528        let cycle = |doc: &mut DioxusDocument| {
529            state.suspended.set(true);
530            state.generation.set(state.generation.get() + 1);
531            rerender(doc);
532            assert_eq!(main_text(doc), "loading");
533            state.suspended.set(false);
534            state.waker.take().unwrap().wake();
535            poll_until_idle(doc);
536            assert_eq!(main_text(doc), "content");
537            node_count(doc)
538        };
539
540        let baseline = cycle(&mut doc);
541        for _ in 0..5 {
542            assert_eq!(cycle(&mut doc), baseline);
543        }
544    }
545
546    #[test]
547    fn todo_like_stress() {
548        use std::cell::RefCell;
549        #[derive(Clone, PartialEq)]
550        struct Item {
551            id: u32,
552            checked: bool,
553            editing: bool,
554        }
555        #[derive(Default)]
556        struct State {
557            items: RefCell<Vec<Item>>,
558            filter: Cell<u8>,
559        }
560
561        #[component]
562        fn Entry(item: Item) -> Element {
563            rsx! {
564                li { class: if item.checked { "completed" },
565                    div { class: "view",
566                        input { r#type: "checkbox", id: "cbg-{item.id}" }
567                        label { "todo {item.id}" }
568                        button { class: "destroy" }
569                    }
570                    if item.editing {
571                        input { class: "edit", value: "todo {item.id}" }
572                    }
573                }
574            }
575        }
576
577        fn app() -> Element {
578            let state = consume_context::<Rc<State>>();
579            let items = state.items.borrow().clone();
580            let filter = state.filter.get();
581            let active = items.iter().filter(|i| !i.checked).count();
582            let visible: Vec<Item> = items
583                .iter()
584                .filter(|i| match filter {
585                    1 => !i.checked,
586                    2 => i.checked,
587                    _ => true,
588                })
589                .cloned()
590                .collect();
591            rsx! {
592                section {
593                    if !items.is_empty() {
594                        input { id: "toggle-all", r#type: "checkbox" }
595                        label { r#for: "toggle-all" }
596                    }
597                    ul {
598                        for item in visible {
599                            Entry { key: "{item.id}", item }
600                        }
601                    }
602                    if !items.is_empty() {
603                        footer {
604                            span { strong { "{active} " } span { match active { 1 => "item", _ => "items" } " left" } }
605                            if items.iter().any(|i| i.checked) {
606                                button { "Clear completed" }
607                            }
608                        }
609                    }
610                }
611            }
612        }
613
614        let state = Rc::new(State::default());
615        let vdom = VirtualDom::new(app).with_root_context(Rc::clone(&state));
616        let mut doc = DioxusDocument::new(vdom, DocumentConfig::default());
617        doc.initial_build();
618
619        let mut seed = 0x2545F4914F6CDD1Du64;
620        let mut rng = move || {
621            seed ^= seed << 13;
622            seed ^= seed >> 7;
623            seed ^= seed << 17;
624            seed
625        };
626        let mut next_id = 0;
627        let mut counts = Vec::new();
628        for step in 1..=3000 {
629            let r = rng();
630            {
631                let mut items = state.items.borrow_mut();
632                let len = items.len();
633                match r % 8 {
634                    0 | 1 => {
635                        items.push(Item {
636                            id: next_id,
637                            checked: false,
638                            editing: false,
639                        });
640                        next_id += 1;
641                    }
642                    2 if len > 0 => {
643                        items.remove((r >> 8) as usize % len);
644                    }
645                    3 if len > 0 => {
646                        let i = (r >> 8) as usize % len;
647                        items[i].checked ^= true;
648                    }
649                    4 if len > 0 => {
650                        let i = (r >> 8) as usize % len;
651                        items[i].editing ^= true;
652                    }
653                    5 => state.filter.set(((r >> 8) % 3) as u8),
654                    6 => items.retain(|i| !i.checked),
655                    7 if len > 12 => items.clear(),
656                    _ => {}
657                }
658            }
659            rerender(&mut doc);
660            if step % 1000 == 0 {
661                state.items.borrow_mut().clear();
662                rerender(&mut doc);
663                counts.push(node_count(&doc));
664            }
665        }
666        // The first round creates the (retained) template prototype nodes.
667        assert_eq!(counts[1], counts[2]);
668    }
669
670    #[test]
671    // Regression test for a panic. Tasks are polled while the vdom renders, at which point
672    // the document must not be borrowed.
673    fn task_polled_during_render_can_access_document() {
674        #[derive(Default)]
675        struct State {
676            focus: Cell<bool>,
677            mounted: RefCell<Option<Rc<MountedData>>>,
678        }
679
680        fn app() -> Element {
681            let state = consume_context::<Rc<State>>();
682            if state.focus.get() {
683                let mounted = state.mounted.borrow().clone().unwrap();
684                spawn(async move {
685                    mounted.set_focus(true).await.unwrap();
686                    mounted.get_client_rect().await.unwrap();
687                });
688            }
689            let focus = state.focus.get();
690            rsx!(input {
691                "data-focus": "{focus}",
692                onmounted: move |e| *state.mounted.borrow_mut() = Some(e.data()),
693            })
694        }
695
696        let state = Rc::new(State::default());
697        let vdom = VirtualDom::new(app).with_root_context(Rc::clone(&state));
698        let mut doc = DioxusDocument::new(vdom, DocumentConfig::default());
699        doc.initial_build();
700        state.focus.set(true);
701        rerender(&mut doc);
702        let inner = doc.inner.borrow();
703        let focussed = inner.get_node(inner.get_focussed_node_id().unwrap());
704        assert_eq!(
705            focussed.unwrap().attr(crate::LocalName::from("data-focus")),
706            Some("true")
707        );
708    }
709
710    #[test]
711    fn mounted_data_of_removed_element_reports_error() {
712        #[derive(Default)]
713        struct State {
714            show: Cell<bool>,
715            mounted: RefCell<Option<Rc<MountedData>>>,
716        }
717
718        fn app() -> Element {
719            let state = consume_context::<Rc<State>>();
720            rsx!(if state.show.get() {
721                div {
722                    onmounted: move |e| *state.mounted.borrow_mut() = Some(e.data()),
723                }
724            })
725        }
726
727        let state = Rc::new(State::default());
728        state.show.set(true);
729        let vdom = VirtualDom::new(app).with_root_context(Rc::clone(&state));
730        let mut doc = DioxusDocument::new(vdom, DocumentConfig::default());
731        doc.initial_build();
732        let mounted = state.mounted.take().unwrap();
733        let focus = |focus: bool| {
734            let mut cx = std::task::Context::from_waker(std::task::Waker::noop());
735            let fut = std::pin::pin!(mounted.set_focus(focus));
736            matches!(fut.poll(&mut cx), std::task::Poll::Ready(Ok(())))
737        };
738        assert!(focus(true));
739        let focussed = |doc: &DioxusDocument| doc.inner.borrow().get_focussed_node_id();
740        let mounted_id = focussed(&doc).unwrap();
741        state.show.set(false);
742        rerender(&mut doc);
743        assert!(doc.inner.borrow().get_node(mounted_id).is_none());
744        assert_ne!(focussed(&doc), Some(mounted_id));
745
746        // Focussing or blurring the removed element is a no-op.
747        let focussed_after_removal = focussed(&doc);
748        assert!(focus(true));
749        assert!(focus(false));
750        assert_eq!(focussed(&doc), focussed_after_removal);
751
752        fn is_err<T>(fut: impl Future<Output = Result<T, MountedError>>) -> bool {
753            let mut cx = std::task::Context::from_waker(std::task::Waker::noop());
754            let fut = std::pin::pin!(fut);
755            matches!(fut.poll(&mut cx), std::task::Poll::Ready(Err(_)))
756        }
757        assert!(is_err(mounted.get_scroll_offset()));
758        assert!(is_err(mounted.get_scroll_size()));
759        assert!(is_err(mounted.get_client_rect()));
760        assert!(is_err(mounted.scroll_to(ScrollBehavior::Instant)));
761        assert!(is_err(
762            mounted.scroll(Default::default(), ScrollBehavior::Instant)
763        ));
764    }
765}