Skip to content

Commit

Permalink
Add start of Yew dismissable layer
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielleHuisman committed Oct 14, 2024
1 parent a1efcef commit c14e0d4
Show file tree
Hide file tree
Showing 5 changed files with 292 additions and 0 deletions.
8 changes: 8 additions & 0 deletions Cargo.lock

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

13 changes: 13 additions & 0 deletions packages/primitives/yew/dismissable-layer/Cargo.toml
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
21 changes: 21 additions & 0 deletions packages/primitives/yew/dismissable-layer/README.md
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 packages/primitives/yew/dismissable-layer/src/dismissable_layer.rs
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),
}
9 changes: 9 additions & 0 deletions packages/primitives/yew/dismissable-layer/src/lib.rs
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::*;

0 comments on commit c14e0d4

Please sign in to comment.