feat: add Portal component (#1820)

This commit is contained in:
Marc-Stefan Cassola 2023-10-09 21:18:52 +01:00 committed by GitHub
parent c080c2cbca
commit 4251f6c0f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 226 additions and 6 deletions

View file

@ -6,7 +6,7 @@ The examples in this directory are all built and tested against the current `mai
To the extent that new features have been released or breaking changes have been made since the previous release, the examples are compatible with the `main` branch but not the current release.
To see the examples as they were at the time of the `0.4.9` release, [click here](https://github.com/leptos-rs/leptos/tree/v0.4.9/examples).
To see the examples as they were at the time of the `0.5.0` release, [click here](https://github.com/leptos-rs/leptos/tree/v0.5.0/examples).
## Cargo Make

View file

@ -0,0 +1,15 @@
[package]
name = "portal"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
log = "0.4"
console_log = "1"
console_error_panic_hook = "0.1.7"
[dev-dependencies]
wasm-bindgen-test = "0.3.0"
wasm-bindgen = "0.2"
web-sys = "0.3"

View file

@ -0,0 +1,5 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/wasm-test.toml" },
{ path = "../cargo-make/trunk_server.toml" },
]

View file

@ -0,0 +1,7 @@
# Leptos Portal Example
This example showcases a basic leptos app with a portal.
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.

View file

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z" data-weak-refs/>
</head>
<body>
<div id="app"></div>
</body>
</html>

View file

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View file

@ -0,0 +1,34 @@
use leptos::*;
#[component]
pub fn App() -> impl IntoView {
let (show_overlay, set_show_overlay) = create_signal(false);
let (show_inside_overlay, set_show_inside_overlay) = create_signal(false);
view! {
<div>
<button id="btn-show" on:click=move |_| set_show_overlay(true)>
Show Overlay
</button>
<Show when=show_overlay fallback=|| ()>
<div>Show</div>
<Portal mount=document().get_element_by_id("app").unwrap()>
<div style="position: fixed; z-index: 10; width: 100vw; height: 100vh; top: 0; left: 0; background: rgba(0, 0, 0, 0.8); color: white;">
<p>This is in the body element</p>
<button id="btn-hide" on:click=move |_| set_show_overlay(false)>
Close Overlay
</button>
<button id="btn-toggle" on:click=move |_| set_show_inside_overlay(!show_inside_overlay())>
Toggle inner
</button>
<Show when=show_inside_overlay fallback=|| view! { "Hidden" }>
Visible
</Show>
</div>
</Portal>
</Show>
</div>
}
}

View file

@ -0,0 +1,15 @@
use leptos::*;
use portal::App;
use wasm_bindgen::JsCast;
fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to(
leptos::document()
.get_element_by_id("app")
.unwrap()
.unchecked_into(),
|| view! { <App/> },
)
}

View file

@ -0,0 +1,60 @@
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
use leptos::*;
use portal::App;
use web_sys::HtmlButtonElement;
#[wasm_bindgen_test]
fn portal() {
let document = leptos::document();
let body = document.body().unwrap();
let div = document.create_element("div").unwrap();
div.set_id("app");
let _ = body.append_child(&div);
mount_to(div.clone().unchecked_into(), || view! { <App/> });
let show_button = document
.get_element_by_id("btn-show")
.unwrap()
.unchecked_into::<HtmlButtonElement>();
show_button.click();
// next_tick().await;
// check HTML
assert_eq!(
div.inner_html(),
"<!-- <App> --><div><button id=\"btn-show\">\n Show Overlay\n </button><!-- <Show> --><!-- <DynChild> --><!-- <> --><div>Show</div><!-- <Portal> --><!-- <() /> --><!-- </Portal> --><!-- </> --><!-- </DynChild> --><!-- </Show> --></div><!-- </App> --><div><!-- <> --><div style=\"position: fixed; z-index: 10; width: 100vw; height: 100vh; top: 0; left: 0; background: rgba(0, 0, 0, 0.8); color: white;\"><p>This is in the body element</p><button id=\"btn-hide\">\n Close Overlay\n </button><button id=\"btn-toggle\">\n Toggle inner\n </button><!-- <Show> --><!-- <DynChild> -->Hidden<!-- </DynChild> --><!-- </Show> --></div><!-- </> --></div>"
);
let toggle_button = document
.get_element_by_id("btn-toggle")
.unwrap()
.unchecked_into::<HtmlButtonElement>();
toggle_button.click();
assert_eq!(
div.inner_html(),
"<!-- <App> --><div><button id=\"btn-show\">\n Show Overlay\n </button><!-- <Show> --><!-- <DynChild> --><!-- <> --><div>Show</div><!-- <Portal> --><!-- <() /> --><!-- </Portal> --><!-- </> --><!-- </DynChild> --><!-- </Show> --></div><!-- </App> --><div><!-- <> --><div style=\"position: fixed; z-index: 10; width: 100vw; height: 100vh; top: 0; left: 0; background: rgba(0, 0, 0, 0.8); color: white;\"><p>This is in the body element</p><button id=\"btn-hide\">\n Close Overlay\n </button><button id=\"btn-toggle\">\n Toggle inner\n </button><!-- <Show> --><!-- <DynChild> --><!-- <> -->\n Visible\n <!-- </> --><!-- </DynChild> --><!-- </Show> --></div><!-- </> --></div>"
);
let hide_button = document
.get_element_by_id("btn-hide")
.unwrap()
.unchecked_into::<HtmlButtonElement>();
hide_button.click();
assert_eq!(
div.inner_html(),
"<!-- <App> --><div><button id=\"btn-show\">\n Show \
Overlay\n </button><!-- <Show> --><!-- <DynChild> --><!-- \
<() /> --><!-- </DynChild> --><!-- </Show> --></div><!-- </App> -->"
);
}

View file

@ -21,19 +21,18 @@ typed-builder-macro = "0.16"
serde = { version = "1", optional = true }
serde_json = { version = "1", optional = true }
server_fn = { workspace = true }
web-sys = { version = "0.3.63", optional = true }
web-sys = { version = "0.3.63", features = ["ShadowRoot", "ShadowRootInit", "ShadowRootMode"] }
wasm-bindgen = { version = "0.2", optional = true }
[features]
default = ["serde"]
template_macro = ["leptos_dom/web", "dep:web-sys", "dep:wasm-bindgen"]
template_macro = ["leptos_dom/web", "dep:wasm-bindgen"]
csr = [
"leptos_dom/csr",
"leptos_macro/csr",
"leptos_reactive/csr",
"leptos_server/csr",
"dep:wasm-bindgen",
"dep:web-sys",
]
hydrate = [
"leptos_dom/hydrate",
@ -41,7 +40,6 @@ hydrate = [
"leptos_reactive/hydrate",
"leptos_server/hydrate",
"dep:wasm-bindgen",
"dep:web-sys",
]
default-tls = ["leptos_server/default-tls", "server_fn/default-tls"]
rustls = ["leptos_server/rustls", "server_fn/rustls"]
@ -79,7 +77,6 @@ denylist = [
"template_macro",
"rustls",
"default-tls",
"web-sys",
"wasm-bindgen",
]
skip_feature_sets = [

View file

@ -232,8 +232,10 @@ pub use wasm_bindgen; // used in islands
pub use web_sys; // used in islands
mod children;
mod portal;
mod view_fn;
pub use children::*;
pub use portal::*;
pub use view_fn::*;
extern crate self as leptos;

74
leptos/src/portal.rs Normal file
View file

@ -0,0 +1,74 @@
use crate::ChildrenFn;
use cfg_if::cfg_if;
use leptos_dom::IntoView;
use leptos_macro::component;
/// Renders components somewhere else in the DOM.
///
/// Useful for inserting modals and tooltips outside of a cropping layout.
/// If no mount point is given, the portal is inserted in `document.body`;
/// it is wrapped in a `<div>` unless `is_svg` is `true` in which case it's wrappend in a `<g>`.
/// Setting `use_shadow` to `true` places the element in a shadow root to isolate styles.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "info", skip_all)
)]
#[component]
pub fn Portal(
/// Target element where the children will be appended
#[prop(into, optional)]
mount: Option<web_sys::Element>,
/// Whether to use a shadow DOM inside `mount`. Defaults to `false`.
#[prop(optional)]
use_shadow: bool,
/// When using SVG this has to be set to `true`. Defaults to `false`.
#[prop(optional)]
is_svg: bool,
/// The children to teleport into the `mount` element
children: ChildrenFn,
) -> impl IntoView {
cfg_if! { if #[cfg(all(target_arch = "wasm32", any(feature = "hydrate", feature = "csr")))] {
use leptos_dom::{document, Mountable};
use leptos_reactive::{create_render_effect, on_cleanup};
use wasm_bindgen::JsCast;
let mount = mount
.unwrap_or_else(|| document().body().expect("body to exist").unchecked_into());
create_render_effect(move |_| {
let tag = if is_svg { "g" } else { "div" };
let container = document()
.create_element(tag)
.expect("element creation to work");
let render_root = if use_shadow {
container
.attach_shadow(&web_sys::ShadowRootInit::new(
web_sys::ShadowRootMode::Open,
))
.map(|root| root.unchecked_into())
.unwrap_or(container.clone())
} else {
container.clone()
};
let _ = render_root.append_child(&children().into_view().get_mountable_node());
let _ = mount.append_child(&container);
on_cleanup({
let mount = mount.clone();
move || {
let _ = mount.remove_child(&container);
}
})
});
} else {
let _ = mount;
let _ = use_shadow;
let _ = is_svg;
let _ = children;
}}
}