1use crate::components::config_provider::use_config;
2use crate::components::form::use_form_item_control;
3#[cfg(target_arch = "wasm32")]
4use crate::components::interaction::as_pointer_event;
5use crate::components::number_utils::{NumberRules, apply_step};
6use dioxus::events::{KeyboardEvent, PointerData};
7use dioxus::prelude::Key;
8use dioxus::prelude::*;
9use serde_json::{Number, Value};
10#[cfg(target_arch = "wasm32")]
11use wasm_bindgen::JsCast;
12
13#[derive(Props, Clone, PartialEq)]
15pub struct RateProps {
16 #[props(optional)]
18 pub value: Option<f64>,
19 #[props(optional)]
21 pub default_value: Option<f64>,
22 #[props(default = 5)]
24 pub count: usize,
25 #[props(default)]
27 pub allow_half: bool,
28 #[props(default = true)]
30 pub allow_clear: bool,
31 #[props(default)]
33 pub disabled: bool,
34 #[props(optional)]
36 pub character: Option<Element>,
37 #[props(optional)]
39 pub tooltips: Option<Vec<String>>,
40 #[props(optional)]
41 pub class: Option<String>,
42 #[props(optional)]
43 pub style: Option<String>,
44 #[props(optional)]
46 pub on_change: Option<EventHandler<Option<f64>>>,
47 #[props(optional)]
49 pub on_hover_change: Option<EventHandler<Option<f64>>>,
50 #[props(optional)]
51 pub on_focus: Option<EventHandler<()>>,
52 #[props(optional)]
53 pub on_blur: Option<EventHandler<()>>,
54}
55
56#[component]
57pub fn Rate(props: RateProps) -> Element {
58 let RateProps {
59 value,
60 default_value,
61 count,
62 allow_half,
63 allow_clear,
64 disabled,
65 character,
66 tooltips,
67 class,
68 style,
69 on_change,
70 on_hover_change,
71 on_focus,
72 on_blur,
73 } = props;
74
75 let config = use_config();
76 let form_control = use_form_item_control();
77 let controlled = value.is_some();
78
79 let rules = NumberRules {
80 min: Some(0.0),
81 max: Some(count as f64),
82 step: Some(if allow_half { 0.5 } else { 1.0 }),
83 precision: Some(if allow_half { 1 } else { 0 }),
84 };
85
86 let inner_value = use_signal(|| default_value);
87
88 {
90 let form_ctx = form_control.clone();
91 let prop_val = value.clone();
92 let mut inner_signal = inner_value.clone();
93 use_effect(move || {
94 let next = resolve_value(prop_val.clone(), &form_ctx, &inner_signal);
95 inner_signal.set(next);
96 });
97 }
98
99 let hover_value = use_signal(|| None::<f64>);
100
101 let is_disabled =
102 disabled || config.disabled || form_control.as_ref().is_some_and(|ctx| ctx.is_disabled());
103
104 let mut class_list = vec!["adui-rate".to_string()];
105 if allow_half {
106 class_list.push("adui-rate-half".into());
107 }
108 if is_disabled {
109 class_list.push("adui-rate-disabled".into());
110 }
111 if let Some(extra) = class {
112 class_list.push(extra);
113 }
114 let class_attr = class_list.join(" ");
115 let style_attr = style.unwrap_or_default();
116
117 let on_hover_cb = on_hover_change;
118 let mut hover_setter = hover_value.clone();
119 let hover_cb = on_hover_cb;
120
121 let current_value = resolve_value(value, &form_control, &inner_value);
122 let display_value = hover_value.read().or(current_value).unwrap_or(0.0);
123
124 let handle_keyboard = {
125 let mut hover_signal = hover_value.clone();
126 let form_for_key = form_control.clone();
127 let mut inner_for_key = inner_value.clone();
128 let on_change_for_key = on_change.clone();
129 move |evt: KeyboardEvent| {
130 if is_disabled {
131 return;
132 }
133 let current = resolve_value(value, &form_for_key, &inner_for_key).unwrap_or(0.0);
134 let next = match evt.key() {
135 Key::ArrowRight | Key::ArrowUp => Some(apply_step(current, 1, &rules)),
136 Key::ArrowLeft | Key::ArrowDown => Some(apply_step(current, -1, &rules)),
137 Key::Home => Some(0.0),
138 Key::End => Some(count as f64),
139 Key::Enter => Some(current),
140 Key::Character(c) if c == " " => Some(current),
141 _ => None,
142 };
143 if let Some(val) = next {
144 apply_rate(
145 Some(val),
146 true,
147 controlled,
148 &mut inner_for_key,
149 &form_for_key,
150 &on_change_for_key,
151 );
152 hover_signal.set(None);
153 }
154 }
155 };
156
157 rsx! {
158 div {
159 class: "{class_attr}",
160 style: "{style_attr}",
161 role: "slider",
162 tabindex: if is_disabled { -1 } else { 0 },
163 aria_valuemin: 0,
164 aria_valuemax: count,
165 aria_valuenow: display_value,
166 onkeydown: handle_keyboard,
167 onfocus: move |_| if let Some(cb) = on_focus.as_ref() { cb.call(()); },
168 onblur: {
169 let mut hover_for_blur = hover_value.clone();
170 move |_| {
171 hover_for_blur.set(None);
172 if let Some(cb) = on_blur.as_ref() { cb.call(()); }
173 }
174 },
175 onpointerleave: move |_| {
176 hover_setter.set(None);
177 if let Some(cb) = hover_cb.as_ref() { cb.call(None); }
178 },
179 {(0..count).map(|idx| {
180 let star_index = idx + 1;
181 let tooltip = tooltips.as_ref().and_then(|t| t.get(idx).cloned());
182 let char_node = character.clone().unwrap_or(rsx! { span { class: "adui-rate-star-default", "★" } });
183 let is_full = display_value + f64::EPSILON >= star_index as f64;
184 let is_half = allow_half && !is_full && display_value + f64::EPSILON >= star_index as f64 - 0.5;
185 let mut star_classes = vec!["adui-rate-star".to_string()];
186 if is_full {
187 star_classes.push("adui-rate-star-full".into());
188 } else if is_half {
189 star_classes.push("adui-rate-star-half".into());
190 }
191
192 let on_pointer_move = {
193 let mut hover_signal = hover_value.clone();
194 move |evt: Event<PointerData>| {
195 if is_disabled {
196 return;
197 }
198 let val = pointer_value(&evt, star_index, allow_half).unwrap_or(star_index as f64);
199 hover_signal.set(Some(val));
200 if let Some(cb) = hover_cb.as_ref() {
201 cb.call(Some(val));
202 }
203 }
204 };
205
206 let on_pointer_down = {
207 let form_for_click = form_control.clone();
208 let mut inner_for_click = inner_value.clone();
209 let on_change_for_click = on_change.clone();
210 move |evt: Event<PointerData>| {
211 if is_disabled {
212 return;
213 }
214 let current = resolve_value(value, &form_for_click, &inner_for_click);
215 let mut val = pointer_value(&evt, star_index, allow_half).unwrap_or(star_index as f64);
216 if allow_clear && current.is_some_and(|v| (v - val).abs() < f64::EPSILON) {
217 val = 0.0;
218 }
219 let next = if val == 0.0 { None } else { Some(val) };
220 apply_rate(
221 next,
222 true,
223 controlled,
224 &mut inner_for_click,
225 &form_for_click,
226 &on_change_for_click,
227 );
228 hover_setter.set(None);
229 if let Some(cb) = hover_cb.as_ref() {
230 cb.call(next);
231 }
232 }
233 };
234
235 rsx! {
236 span {
237 class: "{star_classes.join(\" \")}",
238 title: tooltip.unwrap_or_default(),
239 onpointermove: on_pointer_move,
240 onpointerdown: on_pointer_down,
241 onpointerenter: on_pointer_move,
242 {char_node}
243 }
244 }
245 })}
246 }
247 }
248}
249
250fn resolve_value(
251 value: Option<f64>,
252 form_control: &Option<crate::components::form::FormItemControlContext>,
253 inner: &Signal<Option<f64>>,
254) -> Option<f64> {
255 value
256 .or_else(|| {
257 form_control
258 .as_ref()
259 .and_then(|ctx| value_from_form(ctx.value()))
260 })
261 .or_else(|| *inner.read())
262}
263
264fn value_from_form(val: Option<Value>) -> Option<f64> {
265 match val {
266 Some(Value::Number(n)) => n.as_f64(),
267 Some(Value::String(s)) => s.parse::<f64>().ok(),
268 _ => None,
269 }
270}
271
272fn apply_rate(
273 next: Option<f64>,
274 fire_change: bool,
275 controlled: bool,
276 inner: &mut Signal<Option<f64>>,
277 form_control: &Option<crate::components::form::FormItemControlContext>,
278 on_change: &Option<EventHandler<Option<f64>>>,
279) {
280 if !controlled {
281 inner.set(next);
282 }
283
284 if let Some(ctx) = form_control.as_ref() {
285 let val = match next.and_then(Number::from_f64) {
286 Some(num) => Value::Number(num),
287 None => Value::Null,
288 };
289 ctx.set_value(val);
290 }
291
292 if fire_change {
293 if let Some(cb) = on_change.as_ref() {
294 cb.call(next);
295 }
296 }
297}
298
299#[cfg(target_arch = "wasm32")]
300fn pointer_value(evt: &Event<PointerData>, star_index: usize, allow_half: bool) -> Option<f64> {
301 if let Some(p_evt) = as_pointer_event(evt) {
302 if let Some(target) = p_evt
303 .current_target()
304 .and_then(|t| t.dyn_into::<web_sys::Element>().ok())
305 {
306 let rect = target.get_bounding_client_rect();
307 if allow_half {
308 let mid = rect.width() / 2.0;
309 let left = p_evt.client_x() as f64 - rect.x();
310 if left < mid {
311 return Some(star_index as f64 - 0.5);
312 }
313 }
314 }
315 }
316 Some(star_index as f64)
317}
318
319#[cfg(not(target_arch = "wasm32"))]
320fn pointer_value(_evt: &Event<PointerData>, star_index: usize, _allow_half: bool) -> Option<f64> {
321 Some(star_index as f64)
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327 use serde_json::{Number, Value};
328
329 #[test]
330 fn rate_props_defaults() {
331 }
337
338 #[test]
342 fn value_from_form_number() {
343 let val = Some(Value::Number(Number::from_f64(4.5).unwrap()));
344 assert_eq!(value_from_form(val), Some(4.5));
345 }
346
347 #[test]
348 fn value_from_form_string_valid() {
349 let val = Some(Value::String("3.5".to_string()));
350 assert_eq!(value_from_form(val), Some(3.5));
351 }
352
353 #[test]
354 fn value_from_form_string_invalid() {
355 let val = Some(Value::String("not a number".to_string()));
356 assert_eq!(value_from_form(val), None);
357 }
358
359 #[test]
360 fn value_from_form_other_types() {
361 assert_eq!(value_from_form(Some(Value::Bool(true))), None);
362 assert_eq!(value_from_form(Some(Value::Null)), None);
363 assert_eq!(value_from_form(Some(Value::Array(vec![]))), None);
364 assert_eq!(value_from_form(None), None);
365 }
366
367 #[test]
368 fn value_from_form_number_zero() {
369 let val = Some(Value::Number(Number::from_f64(0.0).unwrap()));
370 assert_eq!(value_from_form(val), Some(0.0));
371 }
372
373 #[test]
374 fn value_from_form_number_negative() {
375 let val = Some(Value::Number(Number::from_f64(-1.5).unwrap()));
376 assert_eq!(value_from_form(val), Some(-1.5));
377 }
378
379 #[test]
380 fn value_from_form_number_integer() {
381 let val = Some(Value::Number(Number::from(5)));
382 assert_eq!(value_from_form(val), Some(5.0));
383 }
384
385 #[test]
386 fn value_from_form_number_large() {
387 let val = Some(Value::Number(Number::from_f64(100.0).unwrap()));
388 assert_eq!(value_from_form(val), Some(100.0));
389 }
390
391 #[test]
392 fn value_from_form_string_zero() {
393 let val = Some(Value::String("0".to_string()));
394 assert_eq!(value_from_form(val), Some(0.0));
395 }
396
397 #[test]
398 fn value_from_form_string_negative() {
399 let val = Some(Value::String("-2.5".to_string()));
400 assert_eq!(value_from_form(val), Some(-2.5));
401 }
402
403 #[test]
404 fn value_from_form_string_integer() {
405 let val = Some(Value::String("10".to_string()));
406 assert_eq!(value_from_form(val), Some(10.0));
407 }
408
409 #[test]
410 fn value_from_form_string_with_whitespace() {
411 let val = Some(Value::String(" 5.5 ".to_string()));
412 assert_eq!(value_from_form(val), None);
414 }
415
416 #[test]
417 fn value_from_form_string_empty() {
418 let val = Some(Value::String(String::new()));
419 assert_eq!(value_from_form(val), None);
420 }
421
422 #[test]
423 fn value_from_form_string_scientific_notation() {
424 let val = Some(Value::String("1e2".to_string()));
425 assert_eq!(value_from_form(val), Some(100.0));
426 }
427
428 #[test]
429 fn value_from_form_string_decimal_point_only() {
430 let val = Some(Value::String(".".to_string()));
431 assert_eq!(value_from_form(val), None);
432 }
433
434 #[test]
435 fn value_from_form_string_multiple_decimal_points() {
436 let val = Some(Value::String("1.2.3".to_string()));
437 assert_eq!(value_from_form(val), None);
438 }
439
440 #[test]
441 fn value_from_form_string_leading_plus() {
442 let val = Some(Value::String("+5.5".to_string()));
443 assert_eq!(value_from_form(val), Some(5.5));
444 }
445
446 #[test]
447 fn value_from_form_string_with_letters() {
448 let val = Some(Value::String("abc123".to_string()));
449 assert_eq!(value_from_form(val), None);
450 }
451
452 #[test]
453 fn value_from_form_string_partial_number() {
454 let val = Some(Value::String("123abc".to_string()));
455 assert_eq!(value_from_form(val), None);
457 }
458
459 #[test]
460 fn value_from_form_number_precision() {
461 let val = Some(Value::Number(Number::from_f64(4.999999).unwrap()));
462 let result = value_from_form(val);
463 assert!(result.is_some());
464 assert!((result.unwrap() - 4.999999).abs() < f64::EPSILON);
465 }
466
467 #[test]
471 fn pointer_value_non_wasm() {
472 }
477}