orbital_base_components/form/numeric_stepper/
base.rs1use leptos::{ev, prelude::*};
2
3use crate::form::bind::FormBind;
4use crate::form::field_injection::FieldInjection;
5
6#[component(transparent)]
8pub fn BaseNumericStepper(
9 #[prop(optional, into)] class: MaybeProp<String>,
10 #[prop(optional, into)] id: MaybeProp<String>,
11 #[prop(optional, into)] name: MaybeProp<String>,
12 #[prop(optional, into)] value: FormBind<i32>,
13 #[prop(default = i32::MIN.into(), into)] min: Signal<i32>,
14 #[prop(default = i32::MAX.into(), into)] max: Signal<i32>,
15 #[prop(default = 1.into(), into)] step: Signal<i32>,
16 #[prop(optional, into)] placeholder: MaybeProp<String>,
17 #[prop(optional, into)] disabled: Signal<bool>,
18) -> impl IntoView {
19 let (id, name) = FieldInjection::use_id_and_name(id, name);
20 let value = StoredValue::new(value);
21
22 let increment_disabled =
23 Memo::new(move |_| disabled.get() || value.get_value().get() >= max.get());
24 let decrement_disabled =
25 Memo::new(move |_| disabled.get() || value.get_value().get() <= min.get());
26
27 let on_change = move |e: ev::Event| {
28 if let Ok(parsed) = event_target_value(&e).parse::<i32>() {
29 let clamped = parsed.clamp(min.get_untracked(), max.get_untracked());
30 value.with_value(|v| v.set(clamped));
31 } else {
32 value.with_value(|v| v.update(|_| {}));
33 }
34 };
35
36 view! {
37 <span class=move || {
38 let mut parts = vec!["orbital-numeric-stepper".to_string()];
39 if disabled.get() {
40 parts.push("orbital-numeric-stepper--disabled".to_string());
41 }
42 if let Some(extra) = class.get() {
43 if !extra.is_empty() {
44 parts.push(extra);
45 }
46 }
47 parts.join(" ")
48 }>
49 <input
50 autocomplete="off"
51 role="spinbutton"
52 aria-valuenow=move || value.get_value().get().to_string()
53 type="text"
54 disabled=move || disabled.get().then_some("")
55 placeholder=move || placeholder.get()
56 prop:value=move || value.get_value().get().to_string()
57 class="orbital-numeric-stepper__input"
58 id=id
59 name=name
60 on:change=on_change
61 />
62 <button
63 tabindex="-1"
64 aria-label="Increment value"
65 type="button"
66 class="orbital-numeric-stepper__increment-button"
67 class=("orbital-numeric-stepper__increment-button--disabled", move || increment_disabled.get())
68 disabled=move || disabled.get().then_some("")
69 on:click=move |_| {
70 if !increment_disabled.get_untracked() {
71 let next = value.get_value().get_untracked() + step.get_untracked();
72 let clamped = next.clamp(min.get_untracked(), max.get_untracked());
73 value.with_value(|v| v.set(clamped));
74 }
75 }
76 >
77 +
78 </button>
79 <button
80 tabindex="-1"
81 aria-label="Decrement value"
82 type="button"
83 class="orbital-numeric-stepper__decrement-button"
84 class=("orbital-numeric-stepper__decrement-button--disabled", move || decrement_disabled.get())
85 disabled=move || disabled.get().then_some("")
86 on:click=move |_| {
87 if !decrement_disabled.get_untracked() {
88 let next = value.get_value().get_untracked() - step.get_untracked();
89 let clamped = next.clamp(min.get_untracked(), max.get_untracked());
90 value.with_value(|v| v.set(clamped));
91 }
92 }
93 >
94 -
95 </button>
96 </span>
97 }
98}