dioxus_tw_components/components/molecules/hovercard/
props.rs

1use crate::attributes::*;
2use chrono::{DateTime, Local, TimeDelta};
3use dioxus::prelude::*;
4use dioxus_core::AttributeValue;
5use dioxus_tw_components_macro::UiComp;
6
7#[cfg(target_arch = "wasm32")]
8use gloo_timers::future::TimeoutFuture;
9
10#[derive(Clone, Debug)]
11pub struct HoverState {
12    is_active: bool,
13    is_hovered: bool,
14    last_hover: DateTime<Local>,
15    closing_delay_ms: TimeDelta,
16}
17
18impl HoverState {
19    fn new(closing_delay_ms: u32) -> Self {
20        Self {
21            is_active: false,
22            closing_delay_ms: TimeDelta::milliseconds(closing_delay_ms as i64),
23            is_hovered: false,
24            last_hover: DateTime::default(),
25        }
26    }
27
28    fn toggle(&mut self) {
29        self.is_active = !self.is_active;
30    }
31
32    fn open(&mut self) {
33        self.is_active = true;
34    }
35
36    fn close(&mut self) {
37        self.is_active = false;
38    }
39
40    fn set_is_hovered(&mut self, is_hovered: bool) {
41        self.is_hovered = is_hovered;
42    }
43
44    fn get_is_hovered(&self) -> bool {
45        self.is_hovered
46    }
47
48    fn set_last_hover(&mut self, last_hover: DateTime<Local>) {
49        self.last_hover = last_hover;
50    }
51
52    fn get_last_hover(&self) -> DateTime<Local> {
53        self.last_hover
54    }
55
56    fn get_closing_delay(&self) -> TimeDelta {
57        self.closing_delay_ms
58    }
59}
60
61impl IntoAttributeValue for HoverState {
62    fn into_value(self) -> AttributeValue {
63        match self.is_active {
64            true => AttributeValue::Text("active".to_string()),
65            false => AttributeValue::Text("inactive".to_string()),
66        }
67    }
68}
69
70#[derive(Clone, PartialEq, Props, UiComp)]
71pub struct HoverCardProps {
72    /// Corresponds to the time in ms it takes for the toggle to close itself if not hovered
73    #[props(default = 500)]
74    closing_delay_ms: u32,
75
76    #[props(extends = div, extends = GlobalAttributes)]
77    attributes: Vec<Attribute>,
78
79    children: Element,
80}
81
82impl std::default::Default for HoverCardProps {
83    fn default() -> Self {
84        Self {
85            closing_delay_ms: 500,
86            attributes: Vec::<Attribute>::default(),
87            children: rsx! {},
88        }
89    }
90}
91
92#[component]
93pub fn HoverCard(mut props: HoverCardProps) -> Element {
94    let mut state = use_context_provider(|| Signal::new(HoverState::new(props.closing_delay_ms)));
95
96    props.update_class_attribute();
97
98    let onmouseenter = move |_event| {
99        state.write().set_is_hovered(true);
100        state.write().open();
101    };
102
103    let onmouseleave = move |_| {
104        state.write().set_last_hover(Local::now());
105        state.write().set_is_hovered(false);
106
107        let closing_delay_ms = state.read().closing_delay_ms;
108
109        spawn(async move {
110            #[cfg(target_arch = "wasm32")]
111            {
112                TimeoutFuture::new(
113                    closing_delay_ms
114                        .num_milliseconds()
115                        .try_into()
116                        .unwrap_or_default(),
117                )
118                .await;
119            }
120            #[cfg(not(target_arch = "wasm32"))]
121            {
122                let _ = tokio::time::sleep(std::time::Duration::from_millis(
123                    closing_delay_ms
124                        .num_milliseconds()
125                        .try_into()
126                        .unwrap_or_default(),
127                ))
128                .await;
129            }
130
131            let is_hovered = state.read().get_is_hovered();
132
133            let last_hover = state.read().get_last_hover();
134            let now = Local::now();
135            let dt = state.read().get_closing_delay();
136
137            if !is_hovered && now - last_hover >= dt {
138                state.write().close();
139            }
140        });
141    };
142
143    rsx! {
144        div {
145            "data-state": state.into_value(),
146            onmouseenter,
147            onmouseleave,
148            ..props.attributes,
149            {props.children}
150        }
151    }
152}
153
154#[derive(Clone, PartialEq, Props, UiComp)]
155pub struct HoverCardTriggerProps {
156    #[props(extends = div, extends = GlobalAttributes)]
157    attributes: Vec<Attribute>,
158
159    #[props(optional, default)]
160    onclick: EventHandler<MouseEvent>,
161
162    children: Element,
163}
164
165impl std::default::Default for HoverCardTriggerProps {
166    fn default() -> Self {
167        Self {
168            attributes: Vec::<Attribute>::default(),
169            onclick: EventHandler::<MouseEvent>::default(),
170            children: rsx! {},
171        }
172    }
173}
174
175#[component]
176pub fn HoverCardTrigger(mut props: HoverCardTriggerProps) -> Element {
177    let mut state = use_context::<Signal<HoverState>>();
178
179    props.update_class_attribute();
180
181    // We need this event here to not close the hover card when clicking its content
182    let onclick = move |event| {
183        state.write().toggle();
184        props.onclick.call(event);
185    };
186
187    rsx! {
188        div {
189            role: "button",
190            "data-state": state.into_value(),
191            onclick,
192            ..props.attributes,
193            {props.children}
194        }
195    }
196}
197
198#[derive(Clone, PartialEq, Props, UiComp)]
199pub struct HoverCardContentProps {
200    #[props(extends = div, extends = GlobalAttributes)]
201    attributes: Vec<Attribute>,
202
203    #[props(optional, default)]
204    pub animation: ReadOnlySignal<Animation>,
205
206    children: Element,
207}
208
209impl std::default::Default for HoverCardContentProps {
210    fn default() -> Self {
211        Self {
212            attributes: Vec::<Attribute>::default(),
213            animation: ReadOnlySignal::<Animation>::default(),
214            children: rsx! {},
215        }
216    }
217}
218
219#[component]
220pub fn HoverCardContent(mut props: HoverCardContentProps) -> Element {
221    let state = use_context::<Signal<HoverState>>();
222
223    props.update_class_attribute();
224
225    rsx! {
226        div { "data-state": state.into_value(), ..props.attributes, {props.children} }
227    }
228}