next_rs/
link.rs

1use crate::prelude::*;
2use crate::router::*;
3use serde_json::Value;
4use web_sys::window;
5use web_sys::{ScrollBehavior, ScrollToOptions};
6
7/// Properties for the Link component.
8#[derive(Properties, Clone, PartialEq)]
9pub struct LinkProps {
10    /// The target URL for the link.
11    #[prop_or_default]
12    pub to: &'static str,
13
14    /// The CSS class for styling the link.
15    #[prop_or_default]
16    pub class: &'static str,
17
18    /// The target attribute for the link.
19    #[prop_or("_blank")]
20    pub target: &'static str,
21
22    /// The "rel" attribute for the link.
23    #[prop_or("noreferrer")]
24    pub rel: &'static str,
25
26    /// Route query data
27    #[prop_or_default]
28    pub query: Value,
29
30    /// Route state data
31    #[prop_or_default]
32    pub state: &'static str,
33
34    /// The content to be displayed within the link.
35    #[prop_or_default]
36    pub children: Html,
37
38    /// Enable scrolling behavior when clicking the link.
39    #[prop_or_default]
40    pub scroll: bool,
41
42    /// Enable automatic prefetch of components before clicking the link.
43    #[prop_or_default]
44    pub prefetch: bool,
45
46    /// Offset for the scrolling behavior, specifying how far from the top the scroll should stop.
47    #[prop_or_default]
48    pub scroll_offset: f64,
49
50    /// Scroll behavior when clicking the link. Valid values: "auto", "instant", "smooth".
51    #[prop_or("auto")]
52    pub scroll_behavior: &'static str,
53
54    /// Indicates the current state of the link in a navigation menu. Valid values: "page", "step", "location", "date", "time", "true", "false".
55    #[prop_or_default]
56    pub aria_current: &'static str,
57
58    /// Describes the link using the ID of the element that provides a description.
59    #[prop_or_default]
60    pub aria_describedby: &'static str,
61
62    /// Indicates whether the content associated with the link is currently expanded or collapsed. Valid values: "true", "false".
63    #[prop_or_default]
64    pub aria_expanded: &'static str,
65
66    /// Indicates whether the link is currently hidden from the user. Valid values: "true", "false".
67    #[prop_or_default]
68    pub aria_hidden: &'static str,
69
70    /// Indicates whether the content associated with the link is live and dynamic. Valid values: "off", "assertive", "polite".
71    #[prop_or_default]
72    pub aria_live: &'static str,
73
74    /// Indicates whether the link is currently pressed or selected. Valid values: "true", "false", "mixed", "undefined".
75    #[prop_or_default]
76    pub aria_pressed: &'static str,
77
78    /// ID of the element that the link controls or owns.
79    #[prop_or_default]
80    pub aria_controls: &'static str,
81
82    /// ID of the element that labels the link.
83    #[prop_or_default]
84    pub aria_labelledby: &'static str,
85}
86
87/// The Link component is used for creating accessible links with additional features.
88///
89/// # Arguments
90/// * `props` - The properties of the component.
91///
92/// # Returns
93/// (Html): An HTML representation of the link component.
94///
95/// # Examples
96/// ```
97/// use next_rs::prelude::*;
98/// use next_rs::Link;
99///
100/// #[func]
101/// pub fn MyComponent() -> Html {
102///
103///     rsx! {
104///         <Link
105///             scroll_offset=300.0
106///             scroll_behavior="smooth"
107///             to={"#about"}
108///             scroll=true
109///         >{ "Go Home" }</Link>
110///     }
111/// }
112/// ```
113#[func]
114pub fn Link(props: &LinkProps) -> Html {
115    let props = props.clone();
116    let to = props.to;
117    #[allow(unused_variables)]
118    let state = props.state;
119    #[allow(unused_variables)]
120    let query = props.query;
121    let router = use_router();
122    let router_clone = router.clone();
123    let (target, href) = if props.to.starts_with("/#") {
124        // local anchor
125        ("_self", &props.to[1..])
126    } else if props.to.starts_with('#') {
127        // also local anchor
128        ("_self", props.to)
129    } else {
130        // external
131        (props.target, props.to)
132    };
133    let onclick = Callback::from(move |event: MouseEvent| {
134        let mut router = router.clone();
135        let query = query.clone();
136        match (props.state, query) {
137            ("", Value::Null) => {
138                // Don't push the url twice onto the stack
139                if target != "_blank" {
140                    router.push(to);
141                }
142            }
143            (state, Value::Null) => {
144                event.prevent_default();
145                router.push_with_state(to, state);
146            }
147            ("", query) => {
148                event.prevent_default();
149                router
150                    .push_with_query(to, &query)
151                    .expect("failed push history with query");
152            }
153            (state, query) => {
154                event.prevent_default();
155                router
156                    .push_with_query_and_state(to, &query, state)
157                    .expect("failed push history with query and state");
158            }
159        }
160        if props.scroll {
161            let scroll_behavior = match props.scroll_behavior {
162                "auto" => ScrollBehavior::Auto,
163                "instant" => ScrollBehavior::Instant,
164                "smooth" => ScrollBehavior::Smooth,
165                _ => ScrollBehavior::Auto,
166            };
167
168            if props.to.starts_with('#') || props.to.starts_with("/#") {
169                // Prevent default navigation behavior("instant")
170                event.prevent_default();
171                // Local anchor link
172                if let Some(element) = window()
173                    .and_then(|win| win.document())
174                    .and_then(|doc| doc.get_element_by_id(&href[1..]))
175                {
176                    let offset_top = element.get_bounding_client_rect().y();
177                    window()
178                        .map(|win| {
179                            win.scroll_to_with_scroll_to_options(
180                                ScrollToOptions::new()
181                                    .top(offset_top)
182                                    .behavior(scroll_behavior),
183                            )
184                        })
185                        .expect("Failed to scroll to local anchor link");
186                } else {
187                    // Fallback to prop offset if element is not found
188                    window()
189                        .map(|win| {
190                            win.scroll_to_with_scroll_to_options(
191                                ScrollToOptions::new()
192                                    .top(props.scroll_offset)
193                                    .behavior(scroll_behavior),
194                            )
195                        })
196                        .expect("Failed to scroll to fallback offset");
197                }
198            } else {
199                // External link
200                window()
201                    .map(|win| {
202                        win.scroll_to_with_scroll_to_options(
203                            ScrollToOptions::new()
204                                .top(props.scroll_offset)
205                                .behavior(scroll_behavior),
206                        )
207                    })
208                    .expect("Failed to scroll to external link");
209            }
210        }
211    });
212    use_effect_with((), move |_| {
213        if props.prefetch {
214            let mut router = router_clone.clone();
215            router.prefetch(href);
216        }
217    });
218    let aria_label = "Link to ".to_string() + href;
219
220    let tabindex = if props.scroll { "0" } else { "-1" };
221
222    rsx! {
223        <a
224            href={href}
225            target={target}
226            rel={props.rel}
227            class={props.class}
228            onclick={onclick}
229            role="link"
230            tabindex={tabindex}
231            aria-label={aria_label.clone()}
232            title={aria_label.clone()}
233            aria-haspopup="true"
234            aria-current={props.aria_current}
235            aria-describedby={props.aria_describedby}
236            aria-expanded={props.aria_expanded}
237            aria-hidden={props.aria_hidden}
238            aria-live={props.aria_live}
239            aria-pressed={props.aria_pressed}
240            aria-controls={props.aria_controls}
241            aria-labelledby={props.aria_labelledby}
242        >{ props.children.clone() }</a>
243    }
244}