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 @@ +

+ + Rust Radix Logo + +

+ +

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::*;