freya_components/link.rs
1use std::borrow::Cow;
2
3use dioxus::prelude::*;
4use dioxus_router::prelude::{
5 navigator,
6 NavigationTarget,
7};
8use freya_core::platform::MouseButton;
9use freya_elements::{
10 self as dioxus_elements,
11 events::MouseEvent,
12};
13use freya_hooks::{
14 use_applied_theme,
15 LinkThemeWith,
16};
17
18use crate::{
19 Tooltip,
20 TooltipContainer,
21};
22
23/// Tooltip configuration for the [`Link()`] component.
24#[derive(Clone, PartialEq)]
25pub enum LinkTooltip {
26 /// No tooltip at all.
27 None,
28 /// Default tooltip.
29 ///
30 /// - For a route, this is the same as [`None`](crate::LinkTooltip::None).
31 /// - For a URL, this is the value of that URL.
32 Default,
33 /// Custom tooltip to always show.
34 Custom(String),
35}
36
37/// Similar to [`Link`](dioxus_router::components::Link()), but you can use it in Freya.
38/// Both internal routes (dioxus-router) and external links are supported. When using internal routes
39/// make sure the Link is descendant of a [`Router`](dioxus_router::components::Router) component.
40///
41/// # Styling
42///
43/// Inherits the [`LinkTheme`](freya_hooks::LinkTheme) theme.
44///
45/// # Example
46///
47/// With Dioxus Router:
48///
49/// ```rust
50/// # use dioxus::prelude::*;
51/// # use dioxus_router::prelude::*;
52/// # use freya_elements as dioxus_elements;
53/// # use freya_components::Link;
54/// # #[derive(Routable, Clone)]
55/// # #[rustfmt::skip]
56/// # enum AppRouter {
57/// # #[route("/")]
58/// # Settings,
59/// # #[route("/..routes")]
60/// # NotFound
61/// # }
62/// # #[component]
63/// # fn Settings() -> Element { rsx!(rect { })}
64/// # #[component]
65/// # fn NotFound() -> Element { rsx!(rect { })}
66/// # fn link_example_good() -> Element {
67/// rsx! {
68/// Link {
69/// to: AppRouter::Settings,
70/// label { "App Settings" }
71/// }
72/// }
73/// # }
74/// ```
75///
76/// With external routes:
77///
78/// ```rust
79/// # use dioxus::prelude::*;
80/// # use freya_elements as dioxus_elements;
81/// # use freya_components::Link;
82/// # fn link_example_good() -> Element {
83/// rsx! {
84/// Link {
85/// to: "https://crates.io/crates/freya",
86/// label { "Freya crates.io" }
87/// }
88/// }
89/// # }
90/// ```
91#[allow(non_snake_case)]
92#[component]
93pub fn Link(
94 /// Theme override.
95 #[props(optional)]
96 theme: Option<LinkThemeWith>,
97 /// The route or external URL string to navigate to.
98 #[props(into)]
99 to: NavigationTarget,
100 /// Inner children for the Link.
101 children: Element,
102 /// This event will be fired if opening an external link fails.
103 #[props(optional)]
104 onerror: Option<EventHandler<()>>,
105 /// A little text hint to show when hovering over the anchor.
106 ///
107 /// Setting this to [`None`] is the same as [`LinkTooltip::Default`].
108 /// To remove the tooltip, set this to [`LinkTooltip::None`].
109 #[props(optional)]
110 tooltip: Option<LinkTooltip>,
111) -> Element {
112 let theme = use_applied_theme!(&theme, link);
113 let mut is_hovering = use_signal(|| false);
114
115 let url = if let NavigationTarget::External(ref url) = to {
116 Some(url.clone())
117 } else {
118 None
119 };
120
121 let onmouseenter = move |_: MouseEvent| {
122 is_hovering.set(true);
123 };
124
125 let onmouseleave = move |_: MouseEvent| {
126 is_hovering.set(false);
127 };
128
129 let onclick = {
130 to_owned![url, to];
131 move |event: MouseEvent| {
132 if !matches!(event.trigger_button, Some(MouseButton::Left)) {
133 return;
134 }
135
136 // Open the url if there is any
137 // otherwise change the dioxus router route
138 if let Some(url) = &url {
139 let res = open::that(url);
140
141 if let (Err(_), Some(onerror)) = (res, onerror.as_ref()) {
142 onerror.call(());
143 }
144
145 // TODO(marc2332): Log unhandled errors
146 } else {
147 let router = navigator();
148 router.push(to.clone());
149 }
150 }
151 };
152
153 let color = if *is_hovering.read() {
154 theme.highlight_color
155 } else {
156 Cow::Borrowed("inherit")
157 };
158
159 let tooltip = match tooltip {
160 None | Some(LinkTooltip::Default) => url.clone(),
161 Some(LinkTooltip::None) => None,
162 Some(LinkTooltip::Custom(str)) => Some(str),
163 };
164
165 let link = rsx! {
166 rect {
167 onmouseenter,
168 onmouseleave,
169 onclick,
170 color: "{color}",
171 {children}
172 }
173 };
174
175 if let Some(tooltip) = tooltip {
176 rsx!(
177 TooltipContainer {
178 tooltip: rsx!(
179 Tooltip {
180 text: tooltip
181 }
182 ),
183 {link}
184 }
185 )
186 } else {
187 link
188 }
189}
190
191#[cfg(test)]
192mod test {
193 use dioxus_router::prelude::{
194 Outlet,
195 Routable,
196 Router,
197 };
198 use freya::prelude::*;
199 use freya_testing::prelude::*;
200
201 #[tokio::test]
202 pub async fn link() {
203 #[derive(Routable, Clone)]
204 #[rustfmt::skip]
205 enum Route {
206 #[layout(Layout)]
207 #[route("/")]
208 Home,
209 #[route("/somewhere")]
210 Somewhere,
211 #[route("/..routes")]
212 NotFound
213 }
214
215 #[allow(non_snake_case)]
216 #[component]
217 fn NotFound() -> Element {
218 rsx! {
219 label {
220 "Not found"
221 }
222 }
223 }
224
225 #[allow(non_snake_case)]
226 #[component]
227 fn Home() -> Element {
228 rsx! {
229 label {
230 "Home"
231 }
232 }
233 }
234
235 #[allow(non_snake_case)]
236 #[component]
237 fn Somewhere() -> Element {
238 rsx! {
239 label {
240 "Somewhere"
241 }
242 }
243 }
244
245 #[allow(non_snake_case)]
246 #[component]
247 fn Layout() -> Element {
248 rsx!(
249 Link {
250 to: Route::Home,
251 Button {
252 label { "Home" }
253 }
254 }
255 Link {
256 to: Route::Somewhere,
257 Button {
258 label { "Somewhere" }
259 }
260 }
261 Outlet::<Route> {}
262 )
263 }
264
265 fn link_app() -> Element {
266 rsx!(Router::<Route> {})
267 }
268
269 let mut utils = launch_test(link_app);
270
271 // Check route is Home
272 assert_eq!(utils.root().get(2).get(0).text(), Some("Home"));
273
274 // Go to the "Somewhere" route
275 utils.click_cursor((10., 55.)).await;
276
277 // Check route is Somewhere
278 assert_eq!(utils.root().get(2).get(0).text(), Some("Somewhere"));
279
280 // Go to the "Home" route again
281 utils.click_cursor((10., 10.)).await;
282
283 // Check route is Home
284 assert_eq!(utils.root().get(2).get(0).text(), Some("Home"));
285 }
286}