1use crate::components::config_provider::use_config;
2use crate::components::control::{ControlStatus, push_status_class};
3use crate::components::form::{FormItemControlContext, use_form_item_control};
4use crate::components::number_utils::{
5 NumberRules, apply_step, parse_and_normalize, round_with_precision,
6};
7use dioxus::events::KeyboardEvent;
8use dioxus::prelude::{Key, *};
9use serde_json::{Number, Value};
10#[derive(Props, Clone, PartialEq)]
12pub struct InputNumberProps {
13 #[props(optional)]
15 pub value: Option<f64>,
16 #[props(optional)]
18 pub default_value: Option<f64>,
19 #[props(optional)]
20 pub min: Option<f64>,
21 #[props(optional)]
22 pub max: Option<f64>,
23 #[props(optional)]
24 pub step: Option<f64>,
25 #[props(optional)]
26 pub precision: Option<u32>,
27 #[props(default = true)]
28 pub controls: bool,
29 #[props(default)]
30 pub disabled: bool,
31 #[props(optional)]
32 pub status: Option<ControlStatus>,
33 #[props(optional)]
34 pub prefix: Option<Element>,
35 #[props(optional)]
36 pub suffix: Option<Element>,
37 #[props(default)]
38 pub class: Option<String>,
39 #[props(optional)]
40 pub style: Option<String>,
41 #[props(optional)]
43 pub on_change: Option<EventHandler<Option<f64>>>,
44 #[props(optional)]
46 pub on_change_complete: Option<EventHandler<Option<f64>>>,
47}
48
49#[component]
50pub fn InputNumber(props: InputNumberProps) -> Element {
51 let InputNumberProps {
52 value,
53 default_value,
54 min,
55 max,
56 step,
57 precision,
58 controls,
59 disabled,
60 status,
61 prefix,
62 suffix,
63 class,
64 style,
65 on_change,
66 on_change_complete,
67 } = props;
68
69 let config = use_config();
70 let form_control = use_form_item_control();
71 let controlled_by_prop = value.is_some();
72
73 let rules = NumberRules {
74 min,
75 max,
76 step,
77 precision,
78 };
79
80 let inner_value = use_signal(|| default_value);
82 let draft = {
83 let initial = format_value(
84 resolve_current_value(value, &form_control, inner_value.clone()),
85 precision,
86 );
87 use_signal(|| initial)
88 };
89
90 {
92 let mut draft_signal = draft.clone();
93 let inner_value_signal = inner_value.clone();
94 let form_control_ctx = form_control.clone();
95 use_effect(move || {
96 let current =
97 resolve_current_value(value, &form_control_ctx, inner_value_signal.clone());
98 draft_signal.set(format_value(current, precision));
99 });
100 }
101
102 let is_disabled =
103 disabled || config.disabled || form_control.as_ref().is_some_and(|ctx| ctx.is_disabled());
104
105 let mut classes = vec!["adui-input-number".to_string()];
106 if is_disabled {
107 classes.push("adui-input-number-disabled".into());
108 }
109 push_status_class(&mut classes, status);
110 if let Some(extra) = class {
111 classes.push(extra);
112 }
113 let class_attr = classes.join(" ");
114 let style_attr = style.unwrap_or_default();
115
116 let mut draft_for_input = draft.clone();
117 let draft_for_blur = draft.clone();
118 let draft_for_keys = draft.clone();
119 let on_change_complete_cb = on_change_complete.clone();
120
121 let form_for_input = form_control.clone();
122 let inner_for_input = inner_value.clone();
123 let on_change_for_input = on_change.clone();
124
125 let form_for_keys = form_control.clone();
126 let inner_for_keys = inner_value.clone();
127 let on_change_for_keys = on_change.clone();
128
129 let form_for_blur = form_control.clone();
130 let inner_for_blur = inner_value.clone();
131 let on_change_for_blur = on_change.clone();
132
133 let form_for_up = form_control.clone();
134 let inner_for_up = inner_value.clone();
135 let on_change_for_up = on_change.clone();
136 let draft_for_up = draft.clone();
137
138 let form_for_down = form_control.clone();
139 let inner_for_down = inner_value.clone();
140 let on_change_for_down = on_change.clone();
141 let draft_for_down = draft.clone();
142
143 let input_value = draft.read().clone();
144
145 rsx! {
146 div { class: "{class_attr}", style: "{style_attr}",
147 if let Some(icon) = prefix {
148 span { class: "adui-input-number-prefix", {icon} }
149 }
150 input {
151 class: "adui-input-number-input",
152 r#type: "text",
153 inputmode: "decimal",
154 disabled: is_disabled,
155 value: "{input_value}",
156 oninput: move |evt| {
157 let text = evt.value();
158 *draft_for_input.write() = text.clone();
159
160 let trimmed = text.trim();
161 if trimmed.is_empty() {
162 apply_value(
163 None,
164 false,
165 controlled_by_prop,
166 inner_for_input.clone(),
167 &form_for_input,
168 precision,
169 draft_for_input.clone(),
170 &on_change_for_input,
171 );
172 return;
173 }
174
175 if let Some(parsed) = parse_and_normalize(trimmed, &rules) {
176 apply_value(
177 Some(parsed),
178 false,
179 controlled_by_prop,
180 inner_for_input.clone(),
181 &form_for_input,
182 precision,
183 draft_for_input.clone(),
184 &on_change_for_input,
185 );
186 }
187 },
188 onkeydown: move |evt: KeyboardEvent| {
189 match evt.key() {
190 Key::ArrowUp => {
191 let base = resolve_current_value(value, &form_for_keys, inner_for_keys.clone())
192 .or(rules.min)
193 .unwrap_or(0.0);
194 let next = apply_step(base, 1, &rules);
195 apply_value(
196 Some(next),
197 true,
198 controlled_by_prop,
199 inner_for_keys.clone(),
200 &form_for_keys,
201 precision,
202 draft_for_keys.clone(),
203 &on_change_for_keys,
204 );
205 }
206 Key::ArrowDown => {
207 let base = resolve_current_value(value, &form_for_keys, inner_for_keys.clone())
208 .or(rules.min)
209 .unwrap_or(0.0);
210 let next = apply_step(base, -1, &rules);
211 apply_value(
212 Some(next),
213 true,
214 controlled_by_prop,
215 inner_for_keys.clone(),
216 &form_for_keys,
217 precision,
218 draft_for_keys.clone(),
219 &on_change_for_keys,
220 );
221 }
222 Key::Enter => {
223 let current_text = draft.read().clone();
224 let normalized = if current_text.trim().is_empty() {
225 None
226 } else {
227 parse_and_normalize(¤t_text, &rules)
228 };
229 apply_value(
230 normalized,
231 true,
232 controlled_by_prop,
233 inner_for_keys.clone(),
234 &form_for_keys,
235 precision,
236 draft_for_keys.clone(),
237 &on_change_for_keys,
238 );
239 if let Some(cb) = on_change_complete_cb.as_ref() {
240 cb.call(normalized);
241 }
242 }
243 _ => {}
244 }
245 },
246 onblur: move |_| {
247 let current_text = draft_for_blur.read().clone();
248 let normalized = if current_text.trim().is_empty() {
249 None
250 } else {
251 parse_and_normalize(¤t_text, &rules)
252 };
253 apply_value(
254 normalized,
255 true,
256 controlled_by_prop,
257 inner_for_blur.clone(),
258 &form_for_blur,
259 precision,
260 draft_for_blur.clone(),
261 &on_change_for_blur,
262 );
263 if let Some(cb) = on_change_complete_cb.as_ref() {
264 cb.call(normalized);
265 }
266 },
267 }
268 if let Some(icon) = suffix {
269 span { class: "adui-input-number-suffix", {icon} }
270 }
271 if controls {
272 div { class: "adui-input-number-handlers",
273 button {
274 class: "adui-input-number-handler adui-input-number-handler-up",
275 disabled: is_disabled,
276 onclick: move |_| {
277 let base = resolve_current_value(value, &form_for_up, inner_for_up.clone())
278 .or(rules.min)
279 .unwrap_or(0.0);
280 let next = apply_step(base, 1, &rules);
281 apply_value(
282 Some(next),
283 true,
284 controlled_by_prop,
285 inner_for_up.clone(),
286 &form_for_up,
287 precision,
288 draft_for_up.clone(),
289 &on_change_for_up,
290 );
291 },
292 "▲"
293 }
294 button {
295 class: "adui-input-number-handler adui-input-number-handler-down",
296 disabled: is_disabled,
297 onclick: move |_| {
298 let base = resolve_current_value(value, &form_for_down, inner_for_down.clone())
299 .or(rules.min)
300 .unwrap_or(0.0);
301 let next = apply_step(base, -1, &rules);
302 apply_value(
303 Some(next),
304 true,
305 controlled_by_prop,
306 inner_for_down.clone(),
307 &form_for_down,
308 precision,
309 draft_for_down.clone(),
310 &on_change_for_down,
311 );
312 },
313 "▼"
314 }
315 }
316 }
317 }
318 }
319}
320
321#[allow(clippy::too_many_arguments)]
322fn apply_value(
323 next: Option<f64>,
324 normalize_display: bool,
325 controlled_by_prop: bool,
326 inner: Signal<Option<f64>>,
327 form_control: &Option<FormItemControlContext>,
328 precision: Option<u32>,
329 draft: Signal<String>,
330 on_change: &Option<EventHandler<Option<f64>>>,
331) {
332 if let Some(ctx) = form_control.as_ref() {
333 let value_to_set = match next.and_then(Number::from_f64) {
334 Some(num) => Value::Number(num),
335 None => Value::Null,
336 };
337 ctx.set_value(value_to_set);
338 } else if !controlled_by_prop {
339 let mut state = inner;
340 state.set(next);
341 }
342
343 if let Some(cb) = on_change.as_ref() {
344 cb.call(next);
345 }
346
347 if normalize_display {
348 let mut d = draft;
349 d.set(format_value(next, precision));
350 }
351}
352
353fn format_value(value: Option<f64>, precision: Option<u32>) -> String {
354 match value {
355 None => String::new(),
356 Some(v) => {
357 if let Some(p) = precision {
358 format!("{:.*}", p as usize, round_with_precision(v, Some(p)))
359 } else {
360 let mut text = v.to_string();
361 if text.contains('.') {
362 while text.ends_with('0') {
363 text.pop();
364 }
365 if text.ends_with('.') {
366 text.pop();
367 }
368 }
369 text
370 }
371 }
372 }
373}
374
375fn resolve_current_value(
376 value: Option<f64>,
377 form_control: &Option<crate::components::form::FormItemControlContext>,
378 inner: Signal<Option<f64>>,
379) -> Option<f64> {
380 value
381 .or_else(|| {
382 form_control
383 .as_ref()
384 .and_then(|ctx| value_from_form(ctx.value()))
385 })
386 .or_else(|| *inner.read())
387}
388
389fn value_from_form(val: Option<Value>) -> Option<f64> {
390 match val {
391 Some(Value::Number(n)) => n.as_f64(),
392 Some(Value::String(s)) => s.parse::<f64>().ok(),
393 Some(Value::Bool(b)) => Some(if b { 1.0 } else { 0.0 }),
394 _ => None,
395 }
396}
397
398#[cfg(test)]
399mod input_number_tests {
400 use super::*;
401 use serde_json::Number;
402
403 #[test]
404 fn input_number_props_defaults() {
405 assert_eq!(
407 InputNumberProps {
408 value: None,
409 default_value: None,
410 min: None,
411 max: None,
412 step: None,
413 precision: None,
414 controls: true,
415 disabled: false,
416 status: None,
417 prefix: None,
418 suffix: None,
419 class: None,
420 style: None,
421 on_change: None,
422 on_change_complete: None,
423 }
424 .controls,
425 true
426 );
427 }
428
429 #[test]
430 fn format_value_none() {
431 assert_eq!(format_value(None, None), "");
432 }
433
434 #[test]
435 fn format_value_with_precision() {
436 let result = format_value(Some(3.14159), Some(2));
437 assert_eq!(result, "3.14");
438 }
439
440 #[test]
441 fn format_value_without_precision() {
442 let result = format_value(Some(3.0), None);
443 assert_eq!(result, "3");
444 }
445
446 #[test]
447 fn value_from_form_number() {
448 let num = Number::from_f64(42.5).unwrap();
449 assert_eq!(value_from_form(Some(Value::Number(num))), Some(42.5));
450 }
451
452 #[test]
453 fn value_from_form_string() {
454 assert_eq!(
455 value_from_form(Some(Value::String("42.5".to_string()))),
456 Some(42.5)
457 );
458 }
459
460 #[test]
461 fn value_from_form_bool() {
462 assert_eq!(value_from_form(Some(Value::Bool(true))), Some(1.0));
463 assert_eq!(value_from_form(Some(Value::Bool(false))), Some(0.0));
464 }
465
466 #[test]
467 fn value_from_form_none() {
468 assert_eq!(value_from_form(None), None);
469 assert_eq!(value_from_form(Some(Value::Null)), None);
470 }
471}