Skip to main content

leptos/
portal.rs

1use crate::{children::TypedChildrenFn, mount, IntoView};
2use leptos_dom::helpers::document;
3use leptos_macro::component;
4use reactive_graph::{effect::Effect, graph::untrack, owner::Owner};
5use std::sync::Arc;
6
7/// Renders components somewhere else in the DOM.
8///
9/// Useful for inserting modals and tooltips outside of a cropping layout.
10/// If no mount point is given, the portal is inserted in `document.body`;
11/// it is wrapped in a `<div>` unless  `is_svg` is `true` in which case it's wrapped in a `<g>`.
12/// Setting `use_shadow` to `true` places the element in a shadow root to isolate styles.
13#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
14#[component]
15pub fn Portal<V>(
16    /// Target element where the children will be appended
17    #[prop(into, optional)]
18    mount: Option<web_sys::Element>,
19    /// Whether to use a shadow DOM inside `mount`. Defaults to `false`.
20    #[prop(optional)]
21    use_shadow: bool,
22    /// When using SVG this has to be set to `true`. Defaults to `false`.
23    #[prop(optional)]
24    is_svg: bool,
25    /// The children to teleport into the `mount` element
26    children: TypedChildrenFn<V>,
27) -> impl IntoView
28where
29    V: IntoView + 'static,
30{
31    if cfg!(target_arch = "wasm32")
32        && Owner::current_shared_context()
33            .map(|sc| sc.is_browser())
34            .unwrap_or(true)
35    {
36        use send_wrapper::SendWrapper;
37        use wasm_bindgen::JsCast;
38
39        let mount = mount.unwrap_or_else(|| {
40            document().body().expect("body to exist").unchecked_into()
41        });
42        let children = children.into_inner();
43
44        Effect::new(move |_| {
45            let container = if is_svg {
46                document()
47                    .create_element_ns(Some("http://www.w3.org/2000/svg"), "g")
48                    .expect("SVG element creation to work")
49            } else {
50                document()
51                    .create_element("div")
52                    .expect("HTML element creation to work")
53            };
54
55            let render_root = if use_shadow {
56                container
57                    .attach_shadow(&web_sys::ShadowRootInit::new(
58                        web_sys::ShadowRootMode::Open,
59                    ))
60                    .map(|root| root.unchecked_into())
61                    .unwrap_or(container.clone())
62            } else {
63                container.clone()
64            };
65
66            let _ = mount.append_child(&container);
67            let handle = SendWrapper::new((
68                mount::mount_to(render_root.unchecked_into(), {
69                    let children = Arc::clone(&children);
70                    move || untrack(|| children())
71                }),
72                mount.clone(),
73                container,
74            ));
75
76            Owner::on_cleanup({
77                move || {
78                    let (handle, mount, container) = handle.take();
79                    drop(handle);
80                    let _ = mount.remove_child(&container);
81                }
82            })
83        });
84    }
85}