Skip to content
This repository was archived by the owner on Feb 2, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ members = [
"packages/primitives/leptos/focus-guards",
"packages/primitives/leptos/id",
"packages/primitives/leptos/label",
"packages/primitives/leptos/portal",
"packages/primitives/leptos/primitive",
"packages/primitives/leptos/separator",
"packages/primitives/leptos/use-controllable-state",
Expand Down Expand Up @@ -68,6 +69,7 @@ radix-leptos-icons = { path = "./packages/icons/leptos", version = "0.0.2" }
radix-leptos-id = { path = "./packages/primitives/leptos/id", version = "0.0.2" }
radix-leptos-label = { path = "./packages/primitives/leptos/label", version = "0.0.2" }
# radix-leptos-popper = { path = "./packages/primitives/leptos/popper", version = "0.0.2" }
radix-leptos-portal = { path = "./packages/primitives/leptos/portal", version = "0.0.2" }
# radix-leptos-presence = { path = "./packages/primitives/leptos/presence", version = "0.0.2" }
radix-leptos-primitive = { path = "./packages/primitives/leptos/primitive", version = "0.0.2" }
# radix-leptos-roving-focus = { path = "./packages/primitives/leptos/roving-focus", version = "0.0.2" }
Expand Down
19 changes: 6 additions & 13 deletions book/src/primitives/utilities/portal.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,7 @@ Install the component from your command line.
{{#tab name="Leptos" }}

```shell
# CSR
cargo add radix-leptos-portal --features csr

# Hydrate
cargo add radix-leptos-portal --features hydrate

# SSR
cargo add radix-leptos-portal --features ssr
cargo add radix-leptos-portal
```

- [View on crates.io](https://crates.io/crates/radix-leptos-portal)
Expand Down Expand Up @@ -89,11 +82,11 @@ Anything you put inside this component will be rendered in a separate `<div>` el
{{#tabs global="framework" }}
{{#tab name="Leptos" }}

| Prop | Type | Default |
| --------------- | ----------------------------- | ------- |
| `as_child` | `MaybeProp<bool>` | `false` |
| `container` | `MaybeProp<web_sys::Element>` | - |
| `container_ref` | `NodeRef<AnyElement>` | - |
| Prop | Type | Default |
| --------------- | ------------------------------------------ | ------- |
| `as_child` | `MaybeProp<bool>` | `false` |
| `container` | `MaybeProp<SendWrapper<web_sys::Element>>` | - |
| `container_ref` | `NodeRef<AnyElement>` | - |

{{#endtab }}
{{#tab name="Yew" }}
Expand Down
16 changes: 9 additions & 7 deletions packages/primitives/leptos/portal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ version.workspace = true
cfg-if = "1.0.0"
leptos.workspace = true
leptos_dom.workspace = true
tracing = "0.1"
leptos-node-ref.workspace = true
radix-leptos-primitive.workspace = true
send_wrapper.workspace = true
web-sys.workspace = true

[features]
csr = ["leptos_dom/csr"]
hydrate = ["leptos_dom/hydrate"]
ssr = ["leptos_dom/ssr"]
# [features]
# csr = ["leptos_dom/csr"]
# hydrate = ["leptos_dom/hydrate"]
# ssr = ["leptos_dom/ssr"]

[package.metadata.cargo-all-features]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
# [package.metadata.cargo-all-features]
# skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
139 changes: 63 additions & 76 deletions packages/primitives/leptos/portal/src/portal.rs
Original file line number Diff line number Diff line change
@@ -1,122 +1,109 @@
use leptos::{html::AnyElement, *};
use leptos::{html, prelude::*};
use leptos_node_ref::AnyNodeRef;
use leptos_portal::LeptosPortal;
use radix_leptos_primitive::Primitive;
use send_wrapper::SendWrapper;

#[component]
pub fn Portal(
#[prop(into, optional)] container: MaybeProp<web_sys::Element>,
#[prop(optional)] container_ref: NodeRef<AnyElement>,
#[prop(into, optional)] container: MaybeProp<SendWrapper<web_sys::Element>>,
#[prop(optional)] container_ref: AnyNodeRef,
#[prop(into, optional)] as_child: MaybeProp<bool>,
#[prop(optional)] node_ref: NodeRef<AnyElement>,
#[prop(attrs)] attrs: Vec<(&'static str, Attribute)>,
#[prop(optional)] node_ref: AnyNodeRef,
children: ChildrenFn,
) -> impl IntoView {
let attrs = StoredValue::new(attrs);
let children = StoredValue::new(children);

// TODO: pass attrs to primitive
view! {
<LeptosPortal mount=container mount_ref=container_ref>
<Primitive
element=html::div
as_child=as_child
node_ref=node_ref
attrs=attrs.get_value()
>
{children.with_value(|children| children())}
</Primitive>
</LeptosPortal>
// <AttributeInterceptor let:attrs>
<LeptosPortal mount=container mount_ref=container_ref>
<Primitive
element=html::div
as_child=as_child
node_ref={node_ref}
// {..attrs}
>
{children.with_value(|children| children())}
</Primitive>
</LeptosPortal>
// </AttributeInterceptor>
}
}

/// Based on [`leptos::Portal`].
mod leptos_portal {
use cfg_if::cfg_if;
use leptos::{component, html::AnyElement, ChildrenFn, MaybeProp, NodeRef};
use leptos_dom::IntoView;
use std::sync::Arc;

use leptos::prelude::{
Effect, Get, IntoView, MaybeProp, Owner, RwSignal, Set, Signal, TypedChildrenFn, component,
mount_to, untrack,
};
use leptos_dom::helpers::document;
use leptos_node_ref::AnyNodeRef;
use send_wrapper::SendWrapper;

/// 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`;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all)
)]
/// If no mount point is given, the portal is inserted in `document.body`.
#[component]
pub fn LeptosPortal(
pub fn LeptosPortal<V>(
/// Target element where the children will be appended
#[prop(into, optional)]
mount: MaybeProp<web_sys::Element>,
#[prop(optional)] mount_ref: NodeRef<AnyElement>,
mount: MaybeProp<SendWrapper<web_sys::Element>>,
#[prop(optional)] mount_ref: AnyNodeRef,
/// 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::{on_cleanup, Effect, RwSignal, Signal, SignalGet, SignalSet, StoredValue};
use leptos_dom::{document, Mountable};
children: TypedChildrenFn<V>,
) -> impl IntoView
where
V: IntoView + 'static,
{
if cfg!(target_arch = "wasm32")
&& Owner::current_shared_context()
.map(|sc| sc.is_browser())
.unwrap_or(true)
{
use web_sys::wasm_bindgen::JsCast;

let children = StoredValue::new(children);

let mount = Signal::derive(move || {
mount_ref
.get()
.map(|mount| {
let element: &web_sys::HtmlElement = &mount;
element.clone().unchecked_into::<web_sys::Element>()
})
.map(|mount| SendWrapper::new(mount.unchecked_into::<web_sys::Element>()))
.or_else(|| mount.get())
.unwrap_or_else(|| document().body().expect("body to exist").into())
.unwrap_or_else(|| {
SendWrapper::new(document().body().expect("body to exist").into())
})
});
let children = children.into_inner();

let current_mount: RwSignal<Option<web_sys::Element>> = RwSignal::new(None);
let current_nodes: RwSignal<Option<Vec<web_sys::Node>>> = RwSignal::new(None);

let remove_nodes = move |current_mount: &web_sys::Element | {
if let Some(current_nodes) = current_nodes.get() {
for current_node in current_nodes {
current_mount.remove_child(&current_node).expect("child to be removed");
}
}
};
let current_mount: RwSignal<Option<SendWrapper<web_sys::Element>>> =
RwSignal::new(None);

Effect::new(move |_| {
let mount = mount.get();
if current_mount.get().as_ref() != Some(&mount) {
if let Some(current_mount) = current_mount.get() {
remove_nodes(&current_mount);
}

if current_mount.get().as_deref() != Some(&*mount) {
current_mount.set(Some(mount));
}
});

Effect::new(move |_| {
if let Some(current_mount) = current_mount.get() {
remove_nodes(&current_mount);
let handle =
SendWrapper::new(mount_to((*current_mount).clone().unchecked_into(), {
let children = Arc::clone(&children);
move || untrack(|| children())
}));

let node = children.with_value(|children| children().into_view().get_mountable_node());
if let Some(fragment) = node.dyn_ref::<web_sys::DocumentFragment>() {
let mut nodes: Vec<web_sys::Node> = vec![];
for index in 0..fragment.children().length() {
nodes.push(fragment.children().item(index).expect("child to exist").into());
Owner::on_cleanup({
move || {
let handle = handle.take();
drop(handle);
}

current_mount.append_child(&node).expect("child to be appended");
current_nodes.set(Some(nodes));
} else {
current_nodes.set(Some(vec![current_mount.append_child(&node).expect("child to be appended")]));
}
}
});

on_cleanup(move || {
if let Some(current_mount) = current_mount.get() {
remove_nodes(&current_mount);
})
}
});
} else {
let _ = mount;
let _ = mount_ref;
let _ = children;
}}
}
}
}
8 changes: 4 additions & 4 deletions packages/primitives/leptos/primitive/src/primitive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ where
original.run(event.clone());
}

if !check_default_prevented || !event.clone().into().default_prevented() {
if let Some(our) = &our_handler {
our.run(event);
}
if (!check_default_prevented || !event.clone().into().default_prevented())
&& let Some(our) = &our_handler
{
our.run(event);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ pub fn use_controllable_state<T: Clone + PartialEq + Send + Sync>(

let set_value = Callback::new(move |next_value| {
if is_controlled.get() {
if next_value != prop.get() {
if let Some(on_change) = on_change {
on_change.run(next_value);
}
if next_value != prop.get()
&& let Some(on_change) = on_change
{
on_change.run(next_value);
}
} else {
set_uncontrolled_prop.set(next_value);
Expand All @@ -57,11 +57,11 @@ fn use_uncontrolled_state<T: Clone + PartialEq + Send + Sync>(

Effect::new(move |_| {
let value = value.get();
if prev_value.get() != value {
if let Some(on_change) = on_change {
on_change.run(value.clone());
prev_value.set(value);
}
if prev_value.get() != value
&& let Some(on_change) = on_change
{
on_change.run(value.clone());
prev_value.set(value);
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ pub fn use_escape_keydown(
type HandleKeyDown = dyn Fn(KeyboardEvent);
let handle_key_down: Arc<SendWrapper<Closure<HandleKeyDown>>> = Arc::new(SendWrapper::new(
Closure::new(move |event: KeyboardEvent| {
if event.key() == "Escape" {
if let Some(on_escape_key_down) = on_escape_key_down {
on_escape_key_down.run(event);
}
if event.key() == "Escape"
&& let Some(on_escape_key_down) = on_escape_key_down
{
on_escape_key_down.run(event);
}
}),
));
Expand Down
38 changes: 19 additions & 19 deletions packages/primitives/yew/checkbox/src/checkbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,25 +391,25 @@ fn BubbleInput(props: &BubbleInputProps) -> Html {
use_effect_with(
(node_ref.clone(), prev_checked, props.checked, props.bubbles),
|(node_ref, prev_checked, checked, bubbles)| {
if let Some(input) = node_ref.cast::<web_sys::HtmlInputElement>() {
if **prev_checked != *checked {
let init = web_sys::EventInit::new();
init.set_bubbles(*bubbles);

let event = web_sys::Event::new_with_event_init_dict("click", &init)
.expect("Click event should be instantiated.");

input.set_indeterminate(is_indeterminiate(*checked));
input.set_checked(match checked {
CheckedState::False => false,
CheckedState::True => true,
CheckedState::Indeterminate => false,
});

input
.dispatch_event(&event)
.expect("Click event should be dispatched.");
}
if let Some(input) = node_ref.cast::<web_sys::HtmlInputElement>()
&& **prev_checked != *checked
{
let init = web_sys::EventInit::new();
init.set_bubbles(*bubbles);

let event = web_sys::Event::new_with_event_init_dict("click", &init)
.expect("Click event should be instantiated.");

input.set_indeterminate(is_indeterminiate(*checked));
input.set_checked(match checked {
CheckedState::False => false,
CheckedState::True => true,
CheckedState::Indeterminate => false,
});

input
.dispatch_event(&event)
.expect("Click event should be dispatched.");
}
},
);
Expand Down
Loading
Loading