-
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a1efcef
commit c14e0d4
Showing
5 changed files
with
292 additions
and
0 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
<p align="center"> | ||
<a href="../../../../logo.svg"> | ||
<img src="../../../../logo.svg" width="300" height="200" alt="Rust Radix Logo"> | ||
</a> | ||
</p> | ||
|
||
<h1 align="center">radix-yew-dismissable-layer</h1> | ||
|
||
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. |
241 changes: 241 additions & 0 deletions
241
packages/primitives/yew/dismissable-layer/src/dismissable_layer.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,241 @@ | ||
use std::{cell::RefCell, rc::Rc}; | ||
|
||
use web_sys::CustomEvent; | ||
use yew::prelude::*; | ||
|
||
#[derive(Clone, PartialEq)] | ||
struct DismissableLayerContextValue { | ||
layers: Rc<RefCell<Vec<web_sys::Element>>>, | ||
layers_with_outside_pointer_events_disabled: Rc<RefCell<Vec<web_sys::Element>>>, | ||
branches: Rc<RefCell<Vec<web_sys::Element>>>, | ||
} | ||
|
||
#[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<KeyboardEvent>, | ||
/// 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<PointerDownOutsideEvent>, | ||
/// Event handler called when the focus moves outside of the `DismissableLayer`. | ||
/// Can be prevented. | ||
#[prop_or_default] | ||
pub on_focus_outside: Callback<FocusOutsideEvent>, | ||
/// 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<InteractOutsideEvent>, | ||
/// 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<String>, | ||
#[prop_or_default] | ||
pub class: Option<String>, | ||
#[prop_or_default] | ||
pub style: Option<String>, | ||
#[prop_or_default] | ||
pub as_child: Option<Callback<DismissableLayerChildProps, Html>>, | ||
#[prop_or_default] | ||
pub children: Html, | ||
} | ||
|
||
#[derive(Clone, Default, PartialEq)] | ||
pub struct DismissableLayerChildProps { | ||
pub node_ref: NodeRef, | ||
pub id: Option<String>, | ||
pub class: Option<String>, | ||
pub style: String, | ||
} | ||
|
||
impl DismissableLayerChildProps { | ||
pub fn render(self, children: Html) -> Html { | ||
html! { | ||
<div | ||
ref={self.node_ref} | ||
id={self.id} | ||
class={self.class} | ||
style={self.style} | ||
> | ||
{children} | ||
</div> | ||
} | ||
} | ||
} | ||
|
||
#[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`. | ||
<ContextProvider<DismissableLayerContextValue> context={(*context_value).clone()}> | ||
<DismissableLayerImpl | ||
disable_outside_pointer_events={props.disable_outside_pointer_events} | ||
on_escape_key_down={props.on_escape_key_down.clone()} | ||
on_pointer_down_outside={props.on_pointer_down_outside.clone()} | ||
on_focus_outside={props.on_focus_outside.clone()} | ||
on_interact_outside={props.on_interact_outside.clone()} | ||
on_dismiss={props.on_dismiss.clone()} | ||
node_ref={props.node_ref.clone()} | ||
id={props.id.clone()} | ||
class={props.class.clone()} | ||
style={props.style.clone()} | ||
as_child={props.as_child.clone()} | ||
> | ||
{props.children.clone()} | ||
</DismissableLayerImpl> | ||
</ContextProvider<DismissableLayerContextValue>> | ||
} | ||
} | ||
|
||
#[function_component] | ||
pub fn DismissableLayerImpl(props: &DismissableLayerProps) -> Html { | ||
let context = use_context::<DismissableLayerContextValue>() | ||
.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<String>, | ||
#[prop_or_default] | ||
pub class: Option<String>, | ||
#[prop_or_default] | ||
pub style: Option<String>, | ||
#[prop_or_default] | ||
pub as_child: Option<Callback<DismissableLayerBranchChildProps, Html>>, | ||
#[prop_or_default] | ||
pub children: Html, | ||
} | ||
|
||
#[derive(Clone, Default, PartialEq)] | ||
pub struct DismissableLayerBranchChildProps { | ||
pub node_ref: NodeRef, | ||
pub id: Option<String>, | ||
pub class: Option<String>, | ||
pub style: Option<String>, | ||
} | ||
|
||
impl DismissableLayerBranchChildProps { | ||
pub fn render(self, children: Html) -> Html { | ||
html! { | ||
<div | ||
ref={self.node_ref} | ||
id={self.id} | ||
class={self.class} | ||
style={self.style} | ||
> | ||
{children} | ||
</div> | ||
} | ||
} | ||
} | ||
|
||
#[function_component] | ||
pub fn DismissableLayerBranch(props: &DismissableLayerBranchProps) -> Html { | ||
let context = use_context::<DismissableLayerContextValue>() | ||
.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<Box<dyn Fn()>> = None; | ||
|
||
if let Some(node) = node_ref.cast::<web_sys::Element>() { | ||
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), | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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::*; |