1use crate::prelude::*;
2use crate::router::*;
3use serde_json::Value;
4use web_sys::window;
5use web_sys::{ScrollBehavior, ScrollToOptions};
6
7#[derive(Properties, Clone, PartialEq)]
9pub struct LinkProps {
10 #[prop_or_default]
12 pub to: &'static str,
13
14 #[prop_or_default]
16 pub class: &'static str,
17
18 #[prop_or("_blank")]
20 pub target: &'static str,
21
22 #[prop_or("noreferrer")]
24 pub rel: &'static str,
25
26 #[prop_or_default]
28 pub query: Value,
29
30 #[prop_or_default]
32 pub state: &'static str,
33
34 #[prop_or_default]
36 pub children: Html,
37
38 #[prop_or_default]
40 pub scroll: bool,
41
42 #[prop_or_default]
44 pub prefetch: bool,
45
46 #[prop_or_default]
48 pub scroll_offset: f64,
49
50 #[prop_or("auto")]
52 pub scroll_behavior: &'static str,
53
54 #[prop_or_default]
56 pub aria_current: &'static str,
57
58 #[prop_or_default]
60 pub aria_describedby: &'static str,
61
62 #[prop_or_default]
64 pub aria_expanded: &'static str,
65
66 #[prop_or_default]
68 pub aria_hidden: &'static str,
69
70 #[prop_or_default]
72 pub aria_live: &'static str,
73
74 #[prop_or_default]
76 pub aria_pressed: &'static str,
77
78 #[prop_or_default]
80 pub aria_controls: &'static str,
81
82 #[prop_or_default]
84 pub aria_labelledby: &'static str,
85}
86
87#[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 ("_self", &props.to[1..])
126 } else if props.to.starts_with('#') {
127 ("_self", props.to)
129 } else {
130 (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 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 event.prevent_default();
171 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 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 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}