slider_rs/
yew.rs

1#![doc = include_str!("../YEW.md")]
2
3use crate::common::{Color, Cursor, Height, Orientation, Size, Width};
4use uuid::Uuid;
5use web_sys::{FocusEvent, HtmlInputElement, InputEvent, KeyboardEvent};
6use yew::prelude::*;
7
8#[derive(Properties, PartialEq)]
9pub struct LabelProps {
10    #[prop_or_default]
11    pub label: &'static str,
12    #[prop_or("font-size: 14px; margin-bottom: 8px; text-align: center;")]
13    pub label_style: &'static str,
14    #[prop_or("slider-label")]
15    pub label_class: &'static str,
16}
17
18#[function_component(Label)]
19fn slider_label(props: &LabelProps) -> Html {
20    html! { <label class={props.label_class} style={props.label_style}>{ props.label }</label> }
21}
22
23#[derive(Properties, PartialEq)]
24pub struct StepsProps {
25    #[prop_or(0.0)]
26    pub min: f64,
27    #[prop_or(10.0)]
28    pub max: f64,
29    #[prop_or(1.0)]
30    pub step: f64,
31    #[prop_or(
32        "width: 100%; display: flex; justify-content: space-between; margin-top: 8px; font-size: 10px;"
33    )]
34    pub steps_style: &'static str,
35    #[prop_or_default]
36    pub orientation: Orientation,
37}
38
39#[function_component(Steps)]
40fn slider_steps(props: &StepsProps) -> Html {
41    let count = ((props.max - props.min) / props.step).floor() as usize;
42
43    let steps = (0..=count)
44        .map(|i| {
45            let val = props.min + (i as f64 * props.step);
46            html! {
47                <span
48                    style={if props.orientation.is_vertical() {
49                    "margin: 4px 0; writing-mode: vertical-rl; text-align: center;"
50                } else {
51                    "text-align: center;"
52                }}
53                >
54                    { format!("{:.0}", val) }
55                </span>
56            }
57        })
58        .collect::<Html>();
59
60    let style = if props.orientation.is_vertical() {
61        "display: flex; flex-direction: column; align-items: center; height: 100%; font-size: 10px;"
62    } else {
63        props.steps_style
64    };
65
66    html! { <div style={style}>{ steps }</div> }
67}
68
69#[derive(Properties, PartialEq)]
70pub struct OutputProps {
71    #[prop_or_default]
72    pub value_display: String,
73    #[prop_or("font-size: 12px; margin-top: 8px; text-align: center;")]
74    pub output_style: &'static str,
75    #[prop_or("slider-output")]
76    pub output_class: &'static str,
77    #[prop_or(
78        "background-color: #333; color: #fff; padding: 4px 8px; border-radius: 4px; font-size: 12px; position: absolute; transform: translate(-50%, -120%); display: block; pointer-events: none;"
79    )]
80    pub tooltip_style: &'static str,
81    #[prop_or(false)]
82    pub show_tooltip: bool,
83    #[prop_or_default]
84    pub tooltip_left: String,
85}
86
87#[function_component(Output)]
88fn slider_output(props: &OutputProps) -> Html {
89    html! {
90        <>
91            <output class={props.output_class} style={props.output_style} aria-live="polite">
92                { &props.value_display }
93            </output>
94            { if props.show_tooltip {
95                    let mut style = props.tooltip_style.to_string();
96                    style.push_str(&format!(" left: {}; ", props.tooltip_left));
97                    html! {
98                        <div class={props.output_class} style={style}>
99                            { &props.value_display }
100                        </div>
101                    }
102                } else {
103                    html! {}
104                } }
105        </>
106    }
107}
108
109#[derive(Properties, PartialEq)]
110pub struct TicksProps {
111    #[prop_or_default]
112    pub id: String,
113    #[prop_or(0.0)]
114    pub min: f64,
115    #[prop_or(10.0)]
116    pub max: f64,
117    #[prop_or(1.0)]
118    pub step: f64,
119}
120
121#[function_component(Ticks)]
122fn slider_ticks(props: &TicksProps) -> Html {
123    let mut current = props.min;
124    let mut children = vec![];
125
126    while current <= props.max {
127        children.push(html! { <option value={current.to_string()} /> });
128        current += props.step;
129    }
130
131    html! { <datalist id={props.id.clone()}>{ for children }</datalist> }
132}
133
134#[derive(Properties, PartialEq)]
135pub struct InputProps {
136    #[prop_or_default]
137    pub input_ref: NodeRef,
138    #[prop_or(0.0)]
139    pub min: f64,
140    #[prop_or(10.0)]
141    pub max: f64,
142    #[prop_or(1.0)]
143    pub step: f64,
144    #[prop_or(0.0)]
145    pub value: f64,
146    #[prop_or_default]
147    pub orientation: Orientation,
148    #[prop_or_default]
149    pub size: Size,
150    #[prop_or_default]
151    pub width: Width,
152    #[prop_or_default]
153    pub height: Height,
154    #[prop_or_default]
155    pub color: Color,
156    #[prop_or_default]
157    pub cursor_style: Cursor,
158    #[prop_or(false)]
159    pub disabled: bool,
160    #[prop_or_default]
161    pub on_input: Callback<InputEvent>,
162    #[prop_or_default]
163    pub on_focus: Callback<FocusEvent>,
164    #[prop_or_default]
165    pub on_blur: Callback<FocusEvent>,
166    #[prop_or_default]
167    pub aria_label: Option<&'static str>,
168    #[prop_or_default]
169    pub aria_describedby: Option<&'static str>,
170    #[prop_or_default]
171    pub datalist_id: Option<String>,
172    #[prop_or("slider-input")]
173    pub input_class: &'static str,
174    #[prop_or("border-radius: 8px; appearance: none; outline: none;")]
175    pub input_style: &'static str,
176    #[prop_or(true)]
177    pub use_gradient: bool,
178    #[prop_or_default]
179    pub custom_thumb_css: Option<&'static str>,
180    #[prop_or_default]
181    pub custom_thumb_html: Option<Html>,
182    #[prop_or(1.0)]
183    pub keyboard_step: f64,
184    #[prop_or(false)]
185    pub rtl_fill: bool,
186}
187
188#[function_component(Input)]
189fn slider_input(props: &InputProps) -> Html {
190    let value_percent = ((props.value - props.min) / (props.max - props.min)) * 100.0;
191    let fill_color = props.color.to_color_code();
192    let gradient = if props.use_gradient {
193        if props.orientation.is_vertical() {
194            if props.rtl_fill {
195                format!(
196                    "background: linear-gradient(to top, {} 0%, {} {:.2}%, #ccc {:.2}%, #ccc 100%);",
197                    fill_color, fill_color, value_percent, value_percent
198                )
199            } else {
200                format!(
201                    "background: linear-gradient(to bottom, {} 0%, {} {:.2}%, #ccc {:.2}%, #ccc 100%);",
202                    fill_color, fill_color, value_percent, value_percent
203                )
204            }
205        } else if props.rtl_fill {
206            format!(
207                "background: linear-gradient(to left, {} 0%, {} {:.2}%, #ccc {:.2}%, #ccc 100%);",
208                fill_color, fill_color, value_percent, value_percent
209            )
210        } else {
211            format!(
212                "background: linear-gradient(to right, {} 0%, {} {:.2}%, #ccc {:.2}%, #ccc 100%);",
213                fill_color, fill_color, value_percent, value_percent
214            )
215        }
216    } else {
217        format!("background: {};", fill_color)
218    };
219
220    let base_style = format!(
221        "cursor: pointer; transition: background 0.3s; {} {} {} {} {} {}",
222        props.input_style,
223        props.width.to_style(),
224        props.height.to_style(),
225        gradient,
226        props.orientation.to_style(),
227        props.size.to_style(),
228    );
229
230    let thumb_style = props.custom_thumb_css.unwrap_or("");
231
232    let on_key_down = {
233        let on_input = props.on_input.clone();
234        let input_ref = props.input_ref.clone();
235        let keyboard_step = props.keyboard_step;
236        let min = props.min;
237        let max = props.max;
238        Callback::from(move |e: KeyboardEvent| {
239            if let Some(input) = input_ref.cast::<HtmlInputElement>() {
240                let current = input.value().parse::<f64>().unwrap_or(0.0);
241                let new_val = match e.key().as_str() {
242                    "ArrowLeft" | "ArrowDown" => current - keyboard_step,
243                    "ArrowRight" | "ArrowUp" => current + keyboard_step,
244                    _ => current,
245                }
246                .clamp(min, max);
247                input.set_value(&new_val.to_string());
248
249                on_input.emit(InputEvent::new("input").unwrap());
250            }
251        })
252    };
253
254    html! {
255        <>
256            <input
257                ref={props.input_ref.clone()}
258                type="range"
259                class={props.input_class}
260                min={props.min.to_string()}
261                max={props.max.to_string()}
262                step={if props.step == 0.0 { "any".to_string() } else { props.step.to_string() }}
263                value={props.value.to_string()}
264                list={props.datalist_id.clone()}
265                oninput={props.on_input.clone()}
266                onfocus={props.on_focus.clone()}
267                onblur={props.on_blur.clone()}
268                onkeydown={on_key_down}
269                style={format!("{} {}", base_style, thumb_style)}
270                orient={props.orientation.to_orient()}
271                disabled={props.disabled}
272                aria-valuemin={props.min.to_string()}
273                aria-valuemax={props.max.to_string()}
274                aria-valuenow={props.value.to_string()}
275                aria-orientation={if props.orientation.is_vertical() { "vertical" } else { "horizontal" }}
276                aria-disabled={props.disabled.to_string()}
277                aria-label={props.aria_label.unwrap_or_default()}
278                aria-describedby={props.aria_describedby.unwrap_or_default()}
279            />
280            { props.custom_thumb_html.clone().unwrap_or(html! {}) }
281        </>
282    }
283}
284
285/// Props for the `Slider` component.
286///
287/// These props configure the behavior, appearance, and accessibility of a slider input component
288/// supporting both single and double (range) sliders.
289///
290/// # Features
291/// - Single or double slider modes.
292/// - Vertical or horizontal orientation.
293/// - Customizable appearance with CSS styles and classes.
294/// - Tooltip, value display, and step indicators.
295/// - Full accessibility support (ARIA attributes, keyboard steps).
296#[derive(Properties, PartialEq, Clone)]
297pub struct Props {
298    /// Label text displayed above the slider.
299    #[prop_or_default]
300    pub label: &'static str,
301
302    /// Minimum value for the slider.
303    #[prop_or(0.0)]
304    pub min: f64,
305
306    /// Maximum value for the slider.
307    #[prop_or(10.0)]
308    pub max: f64,
309
310    /// Increment step size.
311    #[prop_or(1.0)]
312    pub step: f64,
313
314    /// Current value for a single slider mode.
315    #[prop_or_default]
316    pub value: Option<f64>,
317
318    /// Current range values (start, end) for double slider mode.
319    #[prop_or_default]
320    pub range: Option<(f64, f64)>,
321
322    /// Whether to enable double slider mode (range selector).
323    #[prop_or(false)]
324    pub double: bool,
325
326    /// Slider orientation: horizontal or vertical.
327    #[prop_or_default]
328    pub orientation: Orientation,
329
330    /// Size variant for styling the slider track and thumb.
331    #[prop_or_default]
332    pub size: Size,
333
334    /// Color variant for styling the slider.
335    #[prop_or_default]
336    pub color: Color,
337
338    /// Cursor style when hovering over the slider.
339    #[prop_or_default]
340    pub cursor_style: Cursor,
341
342    /// Whether to show the current value as an output.
343    #[prop_or(false)]
344    pub show_value: bool,
345
346    /// Whether to show step ticks along the slider track.
347    #[prop_or(false)]
348    pub show_steps: bool,
349
350    /// Whether to show tooltip above the thumb on hover.
351    #[prop_or(false)]
352    pub show_tooltip: bool,
353
354    /// Whether to disable interaction with the slider.
355    #[prop_or(false)]
356    pub disabled: bool,
357
358    /// Callback triggered when the slider value changes.
359    #[prop_or_default]
360    pub on_change: Callback<f64>,
361
362    /// Callback triggered when the slider range changes (double mode).
363    #[prop_or_default]
364    pub on_change_range: Callback<(f64, f64)>,
365
366    /// Callback triggered when the slider gains focus.
367    #[prop_or_default]
368    pub on_focus: Callback<()>,
369
370    /// Callback triggered when the slider loses focus.
371    #[prop_or_default]
372    pub on_blur: Callback<()>,
373
374    /// ARIA label for accessibility.
375    #[prop_or_default]
376    pub aria_label: Option<&'static str>,
377
378    /// ARIA describedby attribute for accessibility.
379    #[prop_or_default]
380    pub aria_describedby: Option<&'static str>,
381
382    /// CSS class for the container wrapping the slider.
383    #[prop_or("slider-container")]
384    pub container_class: &'static str,
385
386    /// Inline style for the container wrapping the slider.
387    #[prop_or(
388        "display: flex; flex-direction: column; align-items: center; margin: 20px; position: relative;"
389    )]
390    pub container_style: &'static str,
391
392    /// CSS class for the slider label.
393    #[prop_or("slider-label")]
394    pub label_class: &'static str,
395
396    /// Inline style for the slider label.
397    #[prop_or("font-size: 14px; margin-bottom: 8px;")]
398    pub label_style: &'static str,
399
400    /// CSS class for the slider input element.
401    #[prop_or("slider-input")]
402    pub input_class: &'static str,
403
404    /// Inline style for the slider input element.
405    #[prop_or("border-radius: 8px; appearance: none; outline: none;")]
406    pub input_style: &'static str,
407
408    /// CSS class for the value/output display.
409    #[prop_or("slider-output")]
410    pub output_class: &'static str,
411
412    /// Inline style for the value/output display.
413    #[prop_or("font-size: 12px; margin-top: 8px;")]
414    pub output_style: &'static str,
415
416    /// Inline style for the tooltip element.
417    #[prop_or(
418        "background-color: #333; color: #fff; padding: 4px 8px; border-radius: 4px; font-size: 12px; display: none;"
419    )]
420    pub tooltip_style: &'static str,
421
422    /// Inline style for the steps indicator below the slider track.
423    #[prop_or(
424        "width: 100%; display: flex; justify-content: space-between; margin-top: 8px; font-size: 10px;"
425    )]
426    pub steps_style: &'static str,
427
428    /// Custom width for the slider track.
429    #[prop_or_default]
430    pub slider_width: Width,
431
432    /// Custom height for the slider track.
433    #[prop_or_default]
434    pub slider_height: Height,
435
436    /// Optional custom CSS for the slider thumb.
437    #[prop_or_default]
438    pub custom_thumb_css: Option<&'static str>,
439
440    /// Optional custom HTML content for the slider thumb.
441    #[prop_or_default]
442    pub custom_thumb_html: Option<Html>,
443
444    /// Keyboard step increment for arrow key adjustments.
445    #[prop_or(1.0)]
446    pub keyboard_step: f64,
447
448    /// Optional icon element displayed before the slider.
449    #[prop_or_default]
450    pub icon_start: Option<Html>,
451
452    /// Optional icon element displayed after the slider.
453    #[prop_or_default]
454    pub icon_end: Option<Html>,
455}
456
457/// Slider Component
458///
459/// A Yew slider (range input) component, supporting both single and double handle sliders.
460///
461/// # Features
462/// - Single value or double range mode (TODO).
463/// - Horizontal and vertical orientations.
464/// - Customizable track, thumb, tooltip, and step indicators.
465/// - Full ARIA support for accessibility.
466/// - Keyboard controls with adjustable increments.
467///
468/// # Examples
469///
470/// ## Single Value Slider
471/// ```rust
472/// use yew::prelude::*;
473/// use slider_rs::yew::{Slider};
474///
475/// #[function_component]
476/// fn App() -> Html {
477///     html! {
478///         <Slider
479///             label="Volume"
480///             min={0.0}
481///             max={100.0}
482///             step={1.0}
483///             value={Some(50.0)}
484///             show_value={true}
485///             on_change={Callback::from(|v| log::info!("Value: {}", v))}
486///         />
487///     }
488/// }
489/// ```
490///
491/// ## Double Range Slider
492/// ```rust
493/// use yew::prelude::*;
494/// use slider_rs::yew::{Slider};
495///
496/// #[function_component]
497/// fn App() -> Html {
498///     html! {
499///         <Slider
500///             label="Range"
501///             min={0.0}
502///             max={100.0}
503///             step={1.0}
504///             range={Some((20.0, 80.0))}
505///             double={true}
506///             show_value={true}
507///             on_change_range={Callback::from(|(start, end)| log::info!("Range: {} - {}", start, end))}
508///         />
509///     }
510/// }
511/// ```
512///
513/// # Notes
514/// - Subcomponents include: `Label`, `Input`, `Ticks`, `Output`, `Steps`.
515/// - Double slider mode renders two overlapping input elements.
516/// - Styling is fully customizable via `*_style` and `*_class` props.
517///
518/// # Accessibility
519/// - Supports ARIA roles, orientation, and description attributes.
520/// - Keyboard navigation with configurable step increments.
521///
522/// # See Also
523/// - [MDN <input type="range"> Element](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/range)
524#[function_component(Slider)]
525pub fn slider(props: &Props) -> Html {
526    let input_ref1 = use_node_ref();
527    let input_ref2 = use_node_ref();
528    let (initial_val1, initial_val2) = props.range.unwrap_or((props.min, props.max));
529    let val1 = use_state(|| initial_val1);
530    let val2 = use_state(|| initial_val2);
531
532    let list_id = format!("slider-list-{}", Uuid::new_v4());
533
534    let update_range = {
535        let val1 = val1.clone();
536        let val2 = val2.clone();
537        let on_change_range = props.on_change_range.clone();
538        let on_change = props.on_change.clone();
539        Callback::from(move |_| {
540            on_change_range.emit((*val1, *val2));
541            on_change.emit(*val1);
542        })
543    };
544
545    let on_input1 = {
546        let val1 = val1.clone();
547        let update_range = update_range.clone();
548        let on_change = props.on_change.clone();
549        Callback::from(move |e: InputEvent| {
550            if let Some(input) = e.target_dyn_into::<HtmlInputElement>() {
551                if let Ok(v) = input.value().parse::<f64>() {
552                    val1.set(v);
553                    update_range.emit(());
554                    on_change.emit(v);
555                }
556            }
557        })
558    };
559
560    let on_input2 = {
561        let val2 = val2.clone();
562        let update_range = update_range.clone();
563        let on_change = props.on_change.clone();
564        Callback::from(move |e: InputEvent| {
565            if let Some(input) = e.target_dyn_into::<HtmlInputElement>() {
566                if let Ok(v) = input.value().parse::<f64>() {
567                    val2.set(v);
568                    update_range.emit(());
569                    on_change.emit(v);
570                }
571            }
572        })
573    };
574
575    let on_focus_cb = {
576        let cb = props.on_focus.clone();
577        Callback::from(move |_| cb.emit(()))
578    };
579
580    let on_blur_cb = {
581        let cb = props.on_blur.clone();
582        Callback::from(move |_| cb.emit(()))
583    };
584
585    let (input_style1, input_style2): (&'static str, &'static str) = if props.double {
586        let flipped_style = Box::leak(Box::new(format!(
587            "{}; transform: rotate(0deg); direction: rtl; z-index: 3; position: relative; flex: 1;",
588            props.input_style
589        )));
590        let normal_style = Box::leak(Box::new(format!(
591            "{}; z-index: 2; position: relative; flex: 1;",
592            props.input_style
593        )));
594        (flipped_style, normal_style)
595    } else {
596        (props.input_style, props.input_style)
597    };
598
599    html! {
600        <div
601            class={props.container_class}
602            style={props.container_style}
603            role="group"
604            aria-orientation={if props.orientation.is_vertical() { "vertical" } else { "horizontal" }}
605            aria-disabled={props.disabled.to_string()}
606        >
607            <Label
608                label={props.label}
609                label_class={props.label_class}
610                label_style={props.label_style}
611            />
612            { if props.orientation.is_vertical() {
613                    html! {
614                        <div style="display: flex; flex-direction: row; align-items: flex-start;">
615                            { props.icon_start.clone().unwrap_or_default() }
616                            <Input
617                                input_ref={input_ref1}
618                                min={props.min}
619                                max={props.max}
620                                step={props.step}
621                                value={*val1}
622                                orientation={props.orientation.clone()}
623                                disabled={props.disabled}
624                                size={props.size.clone()}
625                                color={props.color.clone()}
626                                cursor_style={props.cursor_style.clone()}
627                                input_class={props.input_class}
628                                on_input={on_input1}
629                                on_focus={on_focus_cb.clone()}
630                                on_blur={on_blur_cb.clone()}
631                                datalist_id={Some(list_id.clone())}
632                                aria_label={props.aria_label}
633                                aria_describedby={props.aria_describedby}
634                                width={props.slider_width.clone()}
635                                height={props.slider_height.clone()}
636                                input_style={input_style1}
637                                custom_thumb_css={props.custom_thumb_css}
638                                custom_thumb_html={props.custom_thumb_html.clone()}
639                                keyboard_step={props.keyboard_step}
640                            />
641                            {
642                                if props.double {
643                                    html! {
644                                        <Input
645                                            input_ref={input_ref2}
646                                            min={props.min}
647                                            max={props.max}
648                                            step={props.step}
649                                            value={*val2}
650                                            orientation={props.orientation.clone()}
651                                            disabled={props.disabled}
652                                            size={props.size.clone()}
653                                            color={props.color.clone()}
654                                            cursor_style={props.cursor_style.clone()}
655                                            input_class={props.input_class}
656                                            on_input={on_input2}
657                                            on_focus={on_focus_cb}
658                                            on_blur={on_blur_cb}
659                                            datalist_id={Some(list_id.clone())}
660                                            aria_label={props.aria_label}
661                                            aria_describedby={props.aria_describedby}
662                                            width={props.slider_width.clone()}
663                                            height={props.slider_height.clone()}
664                                            input_style={input_style2}
665                                            custom_thumb_css={props.custom_thumb_css}
666                                            custom_thumb_html={props.custom_thumb_html.clone()}
667                                            keyboard_step={props.keyboard_step}
668                                        />
669                                    }
670                                } else {
671                                    html! {}
672                                }
673                            }
674                            { props.icon_end.clone().unwrap_or_default() }
675                            {if props.show_steps {
676                                html! {
677                                    <>
678                                        <Ticks id={list_id.clone()} min={props.min} max={props.max} step={props.step} />
679                                        <Steps
680                                            min={props.min}
681                                            max={props.max}
682                                            step={props.step}
683                                            steps_style={props.steps_style}
684                                            orientation={props.orientation.clone()}
685                                        />
686                                    </>
687                                }
688                            }
689                            else {
690                                html!{}
691                            }
692                        }
693                        </div>
694                    }
695                } else if props.double {
696                    html! {
697                        <div style="position: relative; width: 100%; display: flex; align-items: center;">
698                            { props.icon_start.clone().unwrap_or_default() }
699                            <Input
700                                input_ref={input_ref1}
701                                min={props.min}
702                                max={props.max}
703                                step={props.step}
704                                value={*val1}
705                                rtl_fill={true}
706                                orientation={props.orientation.clone()}
707                                disabled={props.disabled}
708                                size={props.size.clone()}
709                                color={props.color.clone()}
710                                cursor_style={props.cursor_style.clone()}
711                                input_class={props.input_class}
712                                on_input={on_input1}
713                                on_focus={on_focus_cb.clone()}
714                                on_blur={on_blur_cb.clone()}
715                                datalist_id={Some(list_id.clone())}
716                                aria_label={props.aria_label}
717                                aria_describedby={props.aria_describedby}
718                                width={props.slider_width.clone()}
719                                height={props.slider_height.clone()}
720                                input_style={input_style1}
721                                custom_thumb_css={props.custom_thumb_css}
722                                custom_thumb_html={props.custom_thumb_html.clone()}
723                                keyboard_step={props.keyboard_step}
724                            />
725                            <Input
726                                input_ref={input_ref2}
727                                min={props.min}
728                                max={props.max}
729                                step={props.step}
730                                value={*val2}
731                                orientation={props.orientation.clone()}
732                                disabled={props.disabled}
733                                size={props.size.clone()}
734                                color={props.color.clone()}
735                                cursor_style={props.cursor_style.clone()}
736                                input_class={props.input_class}
737                                on_input={on_input2}
738                                on_focus={on_focus_cb}
739                                on_blur={on_blur_cb}
740                                datalist_id={Some(list_id.clone())}
741                                aria_label={props.aria_label}
742                                aria_describedby={props.aria_describedby}
743                                width={props.slider_width.clone()}
744                                height={props.slider_height.clone()}
745                                input_style={input_style2}
746                                custom_thumb_css={props.custom_thumb_css}
747                                custom_thumb_html={props.custom_thumb_html.clone()}
748                                keyboard_step={props.keyboard_step}
749                            />
750                            { props.icon_end.clone().unwrap_or_default() }
751                        </div>
752                    }
753                } else {
754                    html! {
755                        <div style="display: flex; align-items: center; width: 100%;">
756                            { props.icon_start.clone().unwrap_or_default() }
757                            <Input
758                                input_ref={input_ref1}
759                                min={props.min}
760                                max={props.max}
761                                step={props.step}
762                                value={*val1}
763                                orientation={props.orientation.clone()}
764                                disabled={props.disabled}
765                                size={props.size.clone()}
766                                color={props.color.clone()}
767                                cursor_style={props.cursor_style.clone()}
768                                input_class={props.input_class}
769                                on_input={on_input1}
770                                on_focus={on_focus_cb.clone()}
771                                on_blur={on_blur_cb.clone()}
772                                datalist_id={Some(list_id.clone())}
773                                aria_label={props.aria_label}
774                                aria_describedby={props.aria_describedby}
775                                width={props.slider_width.clone()}
776                                height={props.slider_height.clone()}
777                                input_style={input_style1}
778                                custom_thumb_css={props.custom_thumb_css}
779                                custom_thumb_html={props.custom_thumb_html.clone()}
780                                keyboard_step={props.keyboard_step}
781                            />
782                            { props.icon_end.clone().unwrap_or_default() }
783                        </div>
784                    }
785                } }
786            <Ticks id={list_id.clone()} min={props.min} max={props.max} step={props.step} />
787            { if props.show_value {
788                html! {
789                    <Output
790                        value_display={format!("{:.1}", *val1)}
791                        output_class={props.output_class}
792                        output_style={props.output_style}
793                        tooltip_style={props.tooltip_style}
794                        show_tooltip={props.show_tooltip}
795                        tooltip_left={format!("{:.2}%", ((*val1 - props.min) / (props.max - props.min)) * 100.0)}
796                    />
797                }
798            } else {
799                html! {}
800            } }
801            { if props.show_steps && !props.orientation.is_vertical() {
802                html! {
803                    <Steps
804                        min={props.min}
805                        max={props.max}
806                        step={props.step}
807                        steps_style={props.steps_style}
808                        orientation={props.orientation.clone()}
809                    />
810                }
811            } else {
812                html! {}
813            } }
814        </div>
815    }
816}