Skip to main content

orbital_base_components/form/numeric_stepper/
base.rs

1use leptos::{ev, prelude::*};
2
3use crate::form::bind::FormBind;
4use crate::form::field_injection::FieldInjection;
5
6/// Headless numeric stepper with native text input and inc/dec controls.
7#[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}