diff --git a/Cargo.lock b/Cargo.lock
index ffaa9721..f4ec5c58 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3126,6 +3126,14 @@ dependencies = [
"yew",
]
+[[package]]
+name = "radix-yew-dismissable-layer"
+version = "0.0.2"
+dependencies = [
+ "web-sys",
+ "yew",
+]
+
[[package]]
name = "radix-yew-focus-guards"
version = "0.0.2"
diff --git a/packages/primitives/yew/dismissable-layer/Cargo.toml b/packages/primitives/yew/dismissable-layer/Cargo.toml
new file mode 100644
index 00000000..4aa1b4d9
--- /dev/null
+++ b/packages/primitives/yew/dismissable-layer/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "radix-yew-dismissable-layer"
+description = "Yew port of Radix Dismissable Layer."
+
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+version.workspace = true
+
+[dependencies]
+web-sys = { workspace = true, features = ["CustomEvent"] }
+yew.workspace = true
diff --git a/packages/primitives/yew/dismissable-layer/README.md b/packages/primitives/yew/dismissable-layer/README.md
new file mode 100644
index 00000000..1e6fe245
--- /dev/null
+++ b/packages/primitives/yew/dismissable-layer/README.md
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+radix-yew-dismissable-layer
+
+This is an internal utility, not intended for public usage.
+
+[Rust Radix](https://github.com/RustForWeb/radix) is a Rust port of [Radix](https://www.radix-ui.com/primitives).
+
+## Documentation
+
+See [the Rust Radix book](https://radix.rustforweb.org/) for documentation.
+
+## Rust For Web
+
+The Rust Radix project is part of the [Rust For Web](https://github.com/RustForWeb).
+
+[Rust For Web](https://github.com/RustForWeb) creates and ports web UI libraries for Rust. All projects are free and open source.
diff --git a/packages/primitives/yew/dismissable-layer/src/dismissable_layer.rs b/packages/primitives/yew/dismissable-layer/src/dismissable_layer.rs
new file mode 100644
index 00000000..478da4ed
--- /dev/null
+++ b/packages/primitives/yew/dismissable-layer/src/dismissable_layer.rs
@@ -0,0 +1,241 @@
+use std::{cell::RefCell, rc::Rc};
+
+use web_sys::CustomEvent;
+use yew::prelude::*;
+
+#[derive(Clone, PartialEq)]
+struct DismissableLayerContextValue {
+ layers: Rc>>,
+ layers_with_outside_pointer_events_disabled: Rc>>,
+ branches: Rc>>,
+}
+
+#[derive(PartialEq, Properties)]
+pub struct DismissableLayerProps {
+ /// When `true`, hover/focus/click interactions will be disabled on elements outside
+ /// the `DismissableLayer`. Users will need to click twice on outside elements to
+ /// interact with them: once to close the `DismissableLayer`, and again to trigger the element.
+ #[prop_or(false)]
+ pub disable_outside_pointer_events: bool,
+ /// Event handler called when the escape key is down.
+ /// Can be prevented.
+ #[prop_or_default]
+ pub on_escape_key_down: Callback,
+ /// Event handler called when the a `pointerdown` event happens outside of the `DismissableLayer`.
+ /// Can be prevented.
+ #[prop_or_default]
+ pub on_pointer_down_outside: Callback,
+ /// Event handler called when the focus moves outside of the `DismissableLayer`.
+ /// Can be prevented.
+ #[prop_or_default]
+ pub on_focus_outside: Callback,
+ /// Event handler called when an interaction happens outside the `DismissableLayer`.
+ /// Specifically, when a `pointerdown` event happens outside or focus moves outside of it.
+ /// Can be prevented.
+ #[prop_or_default]
+ pub on_interact_outside: Callback,
+ /// Handler called when the `DismissableLayer` should be dismissed.
+ #[prop_or_default]
+ pub on_dismiss: Callback<()>,
+ #[prop_or_default]
+ pub node_ref: NodeRef,
+ #[prop_or_default]
+ pub id: Option,
+ #[prop_or_default]
+ pub class: Option,
+ #[prop_or_default]
+ pub style: Option,
+ #[prop_or_default]
+ pub as_child: Option>,
+ #[prop_or_default]
+ pub children: Html,
+}
+
+#[derive(Clone, Default, PartialEq)]
+pub struct DismissableLayerChildProps {
+ pub node_ref: NodeRef,
+ pub id: Option,
+ pub class: Option,
+ pub style: String,
+}
+
+impl DismissableLayerChildProps {
+ pub fn render(self, children: Html) -> Html {
+ html! {
+
+ {children}
+
+ }
+ }
+}
+
+#[function_component]
+pub fn DismissableLayer(props: &DismissableLayerProps) -> Html {
+ let layers = use_mut_ref(Vec::new);
+ let layers_with_outside_pointer_events_disabled = use_mut_ref(Vec::new);
+ let branches = use_mut_ref(Vec::new);
+
+ let context_value = use_memo((), |_| DismissableLayerContextValue {
+ layers,
+ layers_with_outside_pointer_events_disabled,
+ branches,
+ });
+
+ html! {
+ // Unlike React, Yew's `use_context` does not provide a default value without a `ContextProvider`.
+ context={(*context_value).clone()}>
+
+ {props.children.clone()}
+
+ >
+ }
+}
+
+#[function_component]
+pub fn DismissableLayerImpl(props: &DismissableLayerProps) -> Html {
+ let context = use_context::()
+ .expect("Dismissable layer context is required.");
+ let node_ref = use_node_ref();
+ // TODO: owner_document, force?
+ let composed_refs = use_composed_ref(&[props.node_ref.clone(), node_ref.clone()]);
+ // TODO
+ let is_body_pointer_events_disabled = !context
+ .layers_with_outside_pointer_events_disabled
+ .borrow()
+ .is_empty();
+ // let is_pointer_events_enabled =
+
+ let child_props = DismissableLayerChildProps {
+ node_ref: composed_refs,
+ id: props.id.clone(),
+ class: props.class.clone(),
+ style: format!(
+ "{}{}",
+ is_body_pointer_events_disabled
+ .then_some("".to_string())
+ .unwrap_or_default(),
+ props.style.clone().unwrap_or_default()
+ ),
+ };
+
+ if let Some(as_child) = props.as_child.as_ref() {
+ as_child.emit(child_props)
+ } else {
+ child_props.render(props.children.clone())
+ }
+}
+
+#[derive(PartialEq, Properties)]
+pub struct DismissableLayerBranchProps {
+ #[prop_or_default]
+ pub node_ref: NodeRef,
+ #[prop_or_default]
+ pub id: Option,
+ #[prop_or_default]
+ pub class: Option,
+ #[prop_or_default]
+ pub style: Option,
+ #[prop_or_default]
+ pub as_child: Option>,
+ #[prop_or_default]
+ pub children: Html,
+}
+
+#[derive(Clone, Default, PartialEq)]
+pub struct DismissableLayerBranchChildProps {
+ pub node_ref: NodeRef,
+ pub id: Option,
+ pub class: Option,
+ pub style: Option,
+}
+
+impl DismissableLayerBranchChildProps {
+ pub fn render(self, children: Html) -> Html {
+ html! {
+
+ {children}
+
+ }
+ }
+}
+
+#[function_component]
+pub fn DismissableLayerBranch(props: &DismissableLayerBranchProps) -> Html {
+ let context = use_context::()
+ .expect("Dismissable layer context is required.");
+ let node_ref = use_node_ref();
+ let composed_refs = use_composed_ref(&[props.node_ref.clone(), node_ref.clone()]);
+
+ use_effect_with(node_ref, |node_ref| {
+ let mut cleanup: Option> = None;
+
+ if let Some(node) = node_ref.cast::() {
+ context.branches.borrow_mut().push(node.clone());
+
+ cleanup = Some(Box::new(move || {
+ context
+ .branches
+ .borrow_mut()
+ .retain(|branch| *branch != node);
+ }));
+ }
+
+ move || {
+ if let Some(cleanup) = cleanup {
+ cleanup();
+ }
+ }
+ });
+
+ let child_props = DismissableLayerBranchChildProps {
+ node_ref: composed_refs,
+ id: props.id.clone(),
+ class: props.class.clone(),
+ style: props.style.clone(),
+ };
+
+ if let Some(as_child) = props.as_child.as_ref() {
+ as_child.emit(child_props)
+ } else {
+ child_props.render(props.children.clone())
+ }
+}
+
+pub struct PointerDownOutsideEventDetail {
+ pub original_event: PointerEvent,
+}
+
+pub type PointerDownOutsideEvent = CustomEvent;
+
+pub struct FocusOutsideEventDetail {
+ pub original_event: FocusEvent,
+}
+
+pub type FocusOutsideEvent = CustomEvent;
+
+pub enum InteractOutsideEvent {
+ PointerDownOutside(PointerDownOutsideEvent),
+ FocusOutside(FocusOutsideEvent),
+}
diff --git a/packages/primitives/yew/dismissable-layer/src/lib.rs b/packages/primitives/yew/dismissable-layer/src/lib.rs
new file mode 100644
index 00000000..0641a562
--- /dev/null
+++ b/packages/primitives/yew/dismissable-layer/src/lib.rs
@@ -0,0 +1,9 @@
+//! Yew port of [Radix Dismissable Layer](https://www.radix-ui.com/primitives).
+//!
+//! This is an internal utility, not intended for public usage.
+//!
+//! See [`@radix-ui/react-dismissable-layer`](https://www.npmjs.com/package/@radix-ui/react-dismissable-layer) for the original package.
+
+mod dismissable_layer;
+
+pub use dismissable_layer::*;