Skip to main content

fc_dioxus/components/
slider.rs

1//! A native range input with a filled track and visible value, from slider.css. The fill
2//! is a port of upstream's `slider.ts`.
3use crate::{
4    core::{
5        assets,
6        attrs::{availability_attributes, component_attributes},
7        measure::{custom_property, CustomProperty},
8    },
9    prelude::*,
10    theme::use_stylesheets,
11};
12const BEM: Bem = Bem::new("fc-slider");
13
14/// The share of the range `value` covers, as upstream's `sliderPercent` computes it: a
15/// degenerate range is drawn empty rather than dividing by zero.
16///
17/// ```
18/// use fc_dioxus::components::slider::slider_percent;
19/// assert_eq!(slider_percent(0.7, 0.0, 2.0).get(), 35.0);
20/// assert_eq!(slider_percent(5.0, 5.0, 5.0).get(), 0.0);
21/// assert_eq!(slider_percent(-1.0, 0.0, 10.0).get(), 0.0);
22/// ```
23#[must_use]
24pub fn slider_percent(value: f64, min: f64, max: f64) -> Percent {
25    let span = max - min;
26    if span > 0.0 {
27        Percent::new((value - min) / span * 100.0)
28    } else {
29        Percent::new(0.0)
30    }
31}
32
33/// A native `<input type="range">`, which already has the whole keyboard contract and
34/// announces its own value. The visible `<output>` is not a live region, so a step is
35/// never announced twice.
36#[component]
37pub fn Slider(
38    label: AccessibleName,
39    #[props(default = 0.0)] min: f64,
40    #[props(default = 100.0)] max: f64,
41    #[props(default = 1.0)] step: f64,
42    #[props(default)] value: Option<ReadSignal<f64>>,
43    #[props(default)] default_value: f64,
44    #[props(default)] oninput: Option<EventHandler<f64>>,
45    /// Show the value beside the track.
46    #[props(default)]
47    show_value: bool,
48    /// Labels under the track, e.g. the ends and the middle. Decorative.
49    #[props(default)]
50    marks: Vec<String>,
51    #[props(default)] invalid: bool,
52    #[props(default)] availability: Availability,
53    #[props(default)] class: Option<String>,
54    #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
55) -> Element {
56    let state = use_controllable(value, || default_value, oninput);
57    let id = use_id();
58    let current = state.get();
59    let attributes = component_attributes(
60        attributes,
61        ClassList::new(BEM.block()).extend(class.as_deref()),
62        &[],
63    );
64    let mut input = availability_attributes(&availability);
65    input.push(custom_property(
66        CustomProperty::SliderPct,
67        format!("{}%", slider_percent(current, min, max)),
68    ));
69    rsx! {
70        if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_SLIDER_CSS } }
71        div { ..attributes,
72            input { class: BEM.element("input"), r#type: "range", id: "{id}", min: "{min}", max: "{max}", step: "{step}",
73                value: "{current}", aria_label: label.to_string(), aria_invalid: invalid.then_some("true"),
74                oninput: move |event| if let Ok(value) = event.value().parse::<f64>() { state.set(value); },
75                ..input,
76            }
77            if show_value { output { class: BEM.element("value"), ..crate::spread![Attribute::new("for", id.to_string(), None, false)], "{current}" } }
78            if !marks.is_empty() {
79                ol { class: BEM.element("marks"), aria_hidden: "true",
80                    for mark in marks { li { "{mark}" } }
81                }
82            }
83        }
84    }
85}