Skip to main content

orbital_base_components/form/slider/
base.rs

1use leptos::{context::Provider, ev, prelude::*};
2
3use crate::form::bind::FormBind;
4use crate::form::field_injection::FieldInjection;
5
6#[derive(Clone, Copy)]
7pub(crate) struct SliderInjection {
8    pub max: Signal<f64>,
9    pub min: Signal<f64>,
10    pub vertical: Signal<bool>,
11}
12
13impl SliderInjection {
14    pub fn expect_context() -> Self {
15        expect_context()
16    }
17}
18
19/// Headless native `<input type="range">` with optional label markers.
20#[component(transparent)]
21pub fn BaseSlider(
22    #[prop(optional, into)] class: MaybeProp<String>,
23    #[prop(optional, into)] style: MaybeProp<String>,
24    #[prop(optional, into)] id: MaybeProp<String>,
25    #[prop(optional, into)] name: MaybeProp<String>,
26    #[prop(optional, into)] value: FormBind<f64>,
27    #[prop(default = 0.0.into(), into)] min: Signal<f64>,
28    #[prop(default = 100.0.into(), into)] max: Signal<f64>,
29    #[prop(optional, into)] step: MaybeProp<f64>,
30    #[prop(default = true.into(), into)] show_stops: Signal<bool>,
31    #[prop(optional, into)] vertical: Signal<bool>,
32    #[prop(optional)] children: Option<Children>,
33) -> impl IntoView {
34    let (id, name) = FieldInjection::use_id_and_name(id, name);
35    let has_labels = children.is_some();
36
37    let current_value_value = value.clone();
38    let current_value = Memo::new(move |_| {
39        let max = max.get();
40        let min = min.get();
41        let v = current_value_value.get();
42        if v > max {
43            max
44        } else if v < min {
45            min
46        } else {
47            v
48        }
49    });
50
51    let on_input_value = value.clone();
52    let on_input = move |e: ev::Event| {
53        if let Ok(range_value) = event_target_value(&e).parse::<f64>() {
54            on_input_value.set(range_value);
55        }
56    };
57
58    let css_vars = move || {
59        let max = max.get();
60        let min = min.get();
61        let mut css_vars = format!(
62            "--orbital-slider--progress: {:.2}%;",
63            if max == min {
64                0.0
65            } else {
66                (current_value.get() - min) / (max - min) * 100.0
67            }
68        );
69
70        if vertical.get() {
71            css_vars.push_str("--orbital-slider--direction: 0deg;");
72        } else {
73            css_vars.push_str("--orbital-slider--direction: 90deg;");
74        }
75
76        if has_labels {
77            css_vars.push_str(&format!("--orbital-slider--max: {:.2};", max));
78            css_vars.push_str(&format!("--orbital-slider--min: {:.2};", min));
79        }
80
81        if let Some(step) = step.get() {
82            if step > 0.0 && show_stops.get() && max != min {
83                css_vars.push_str(&format!(
84                    "--orbital-slider--steps-percent: {:.2}%;",
85                    step * 100.0 / (max - min)
86                ));
87            }
88        }
89
90        if let Some(extra_style) = style.get() {
91            css_vars.push_str(&extra_style);
92        }
93
94        css_vars
95    };
96
97    view! {
98        <div class=move || {
99            let mut parts = vec![
100                "orbital-slider".to_string(),
101                if vertical.get() {
102                    "orbital-slider--vertical".to_string()
103                } else {
104                    "orbital-slider--horizontal".to_string()
105                },
106            ];
107            if let Some(extra) = class.get() {
108                if !extra.is_empty() {
109                    parts.push(extra);
110                }
111            }
112            parts.join(" ")
113        } style=css_vars>
114            <input
115                min=move || min.get()
116                max=move || max.get()
117                step=move || step.get()
118                type="range"
119                class="orbital-slider__input"
120                id=id
121                name=name
122                on:input=on_input
123                value=current_value.get_untracked()
124                prop:value=move || current_value.get()
125            />
126            <div class="orbital-slider__rail"></div>
127            <div class="orbital-slider__thumb"></div>
128            {if let Some(children) = children {
129                let slider = SliderInjection { max, min, vertical };
130                view! {
131                    <Provider value=slider>
132                        <div class="orbital-slider__datalist">{children()}</div>
133                    </Provider>
134                }
135                    .into_any()
136            } else {
137                ().into_any()
138            }}
139        </div>
140    }
141}