diff --git a/thaw/src/slider/docs/mod.md b/thaw/src/slider/docs/mod.md index 9dd664cd..ece72a05 100644 --- a/thaw/src/slider/docs/mod.md +++ b/thaw/src/slider/docs/mod.md @@ -18,6 +18,16 @@ view! { } ``` +### Vertical + +```rust demo +let value = RwSignal::new(6.0); + +view! { + +} +``` + ## Slider Label ```rust demo @@ -50,6 +60,7 @@ view! { | min | `Signal` | `0` | Min value of the slider. | | max | `Signal` | `100` | Max value of the slider. | | step | `Signal` | `0` | The step in which value is incremented. | +| vertical | `Signal` | `false` | Render the Slider in a vertical orientation, smallest value on the bottom. | | children | `Option` | `None` | | ### SliderLabel props diff --git a/thaw/src/slider/docs/range-slider.md b/thaw/src/slider/docs/range-slider.md index c395326e..7c1164d0 100644 --- a/thaw/src/slider/docs/range-slider.md +++ b/thaw/src/slider/docs/range-slider.md @@ -21,6 +21,16 @@ view! { } ``` +### Vertical + +```rust demo +let value = RwSignal::new((6.0, 8.0)); + +view! { + +} +``` + ### SliderLabel ```rust demo @@ -43,11 +53,12 @@ view! { ### RangeSlider Props -| Name | Type | Default | Description | -| ----- | ------------------- | -------------------- | ------------------------------------------- | -| class | `MaybeProp` | `Default::default()` | | -| style | `MaybeProp` | `Default::default()` | | -| value | `Model<(f64, f64)>` | `(0.0, 0.0)` | The current value of the controlled Slider. | -| min | `Signal` | `0` | Min value of the slider. | -| max | `Signal` | `100` | Max value of the slider. | -| step | `Signal` | `0` | The step in which value is incremented. | +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| class | `MaybeProp` | `Default::default()` | | +| style | `MaybeProp` | `Default::default()` | | +| value | `Model<(f64, f64)>` | `(0.0, 0.0)` | The current value of the controlled Slider. | +| min | `Signal` | `0` | Min value of the slider. | +| max | `Signal` | `100` | Max value of the slider. | +| step | `Signal` | `0` | The step in which value is incremented. | +| vertical | `Signal` | `false` | Render the Slider in a vertical orientation, smallest value on the bottom. | diff --git a/thaw/src/slider/mod.rs b/thaw/src/slider/mod.rs index 9a2057f5..314ef569 100644 --- a/thaw/src/slider/mod.rs +++ b/thaw/src/slider/mod.rs @@ -1,156 +1,7 @@ mod range_slider; +mod slider; mod slider_label; pub use range_slider::*; +pub use slider::*; pub use slider_label::SliderLabel; - -use crate::{FieldInjection, FieldValidationState, Rule}; -use leptos::{context::Provider, ev, prelude::*}; -use std::ops::Deref; -use thaw_components::OptionComp; -use thaw_utils::{class_list, mount_style, Model}; - -#[component] -pub fn Slider( - #[prop(optional, into)] class: MaybeProp, - #[prop(optional, into)] id: MaybeProp, - /// A string specifying a name for the input control. - /// This name is submitted along with the control's value when the form data is submitted. - #[prop(optional, into)] - name: MaybeProp, - /// The rules to validate Field. - #[prop(optional, into)] - rules: Vec, - /// The current value of the controlled Slider. - #[prop(optional, into)] - value: Model, - /// Min value of the slider. - #[prop(default = 0f64.into(), into)] - min: Signal, - /// Max value of the slider. - #[prop(default = 100f64.into(), into)] - max: Signal, - /// The step in which value is incremented. - #[prop(optional, into)] - step: MaybeProp, - #[prop(optional)] children: Option, -) -> impl IntoView { - mount_style("slider", include_str!("./slider.css")); - let (id, name) = FieldInjection::use_id_and_name(id, name); - let validate = Rule::validate(rules, value, name); - let is_chldren = children.is_some(); - let current_value = Memo::new(move |_| { - let max = max.get(); - let min = min.get(); - let v = value.get(); - if v > max { - max - } else if v < min { - min - } else { - v - } - }); - - let on_input = move |e: ev::Event| { - if let Ok(range_value) = event_target_value(&e).parse::() { - value.set(range_value); - validate.run(Some(SliderRuleTrigger::Input)); - } - }; - - let css_vars = move || { - let max = max.get(); - let min = min.get(); - let mut css_vars = format!( - "--thaw-slider--direction: 90deg;--thaw-slider--progress: {:.2}%;", - if max == min { - 0.0 - } else { - (current_value.get() - min) / (max - min) * 100.0 - } - ); - - if is_chldren { - css_vars.push_str(&format!("--thaw-slider--max: {:.2};", max)); - css_vars.push_str(&format!("--thaw-slider--min: {:.2};", min)); - } - - if let Some(step) = step.get() { - if step > 0.0 { - css_vars.push_str(&format!( - "--thaw-slider--steps-percent: {:.2}%", - step * 100.0 / (max - min) - )); - } - } - css_vars - }; - - view! { - -
- -
-
- -
{children()}
-
-
-
- } -} - -#[derive(Clone)] -pub(crate) struct SliderInjection { - pub max: Signal, - pub min: Signal, -} - -impl SliderInjection { - pub fn expect_context() -> Self { - expect_context() - } -} - -#[derive(Debug, Default, PartialEq, Clone, Copy)] -pub enum SliderRuleTrigger { - #[default] - Input, -} - -pub struct SliderRule(Rule); - -impl SliderRule { - pub fn validator( - f: impl Fn(&f64, Signal>) -> Result<(), FieldValidationState> - + Send - + Sync - + 'static, - ) -> Self { - Self(Rule::validator(f)) - } - - pub fn with_trigger(self, trigger: SliderRuleTrigger) -> Self { - Self(Rule::with_trigger(self.0, trigger)) - } -} - -impl Deref for SliderRule { - type Target = Rule; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} diff --git a/thaw/src/slider/range_slider/mod.rs b/thaw/src/slider/range_slider/mod.rs index cb3b0abe..d5684e3b 100644 --- a/thaw/src/slider/range_slider/mod.rs +++ b/thaw/src/slider/range_slider/mod.rs @@ -1,5 +1,5 @@ use super::super::SliderInjection; -use leptos::{context::Provider, ev, html, logging, prelude::*}; +use leptos::{context::Provider, ev, html, prelude::*}; use thaw_components::OptionComp; use thaw_utils::{class_list, mount_style, Model}; @@ -17,6 +17,9 @@ pub fn RangeSlider( /// The step in which value is incremented. #[prop(optional, into)] step: MaybeProp, + /// Render the Slider in a vertical orientation, smallest value on the bottom. + #[prop(optional, into)] + vertical: Signal, #[prop(optional)] children: Option, ) -> impl IntoView { mount_style("range-slider", include_str!("./range-slider.css")); @@ -35,8 +38,6 @@ pub fn RangeSlider( left = closest_multiple(left, step, min, max); right = closest_multiple(right, step, min, max); - logging::log!("l {} r {}", left, right); - (left, right) }); @@ -58,6 +59,13 @@ pub fn RangeSlider( let css_vars = move || { let mut css_vars = style.get().unwrap_or_default(); + + if vertical.get() { + css_vars.push_str(";--thaw-slider--direction: 0deg;"); + } else { + css_vars.push_str(";--thaw-slider--direction: 90deg;"); + } + if let Some(step) = step.get() { if step > 0.0 { let max = max.get(); @@ -96,14 +104,20 @@ pub fn RangeSlider( let on_click = move |e: web_sys::MouseEvent| { if let Some(slider) = slider_ref.get_untracked() { - let rect = slider.get_bounding_client_rect(); - let ev_x = f64::from(e.x()); let min = min.get_untracked(); let max = max.get_untracked(); - let percentage = (ev_x - rect.x()) / rect.width() * (max - min); - let (left, right) = current_value.get(); + let rect = slider.get_bounding_client_rect(); + let percentage = if vertical.get_untracked() { + let ev_y = f64::from(e.y()); + let slider_height = rect.height(); + (slider_height + rect.y() - ev_y) / slider_height * (max - min) + } else { + let ev_x = f64::from(e.x()); + (ev_x - rect.x()) / rect.width() * (max - min) + }; + let (left, right) = current_value.get(); let left_diff = (left - percentage).abs(); let right_diff = (right - percentage).abs(); @@ -132,20 +146,39 @@ pub fn RangeSlider( let on_mousemove = move || { let mousemove = window_event_listener(ev::mousemove, move |e| { if let Some(slider_el) = slider_ref.get_untracked() { + let min = min.get_untracked(); + let max = max.get_untracked(); + let slider_rect = slider_el.get_bounding_client_rect(); - let slider_width = slider_rect.width(); - let slider_x = slider_rect.x(); - let ev_x = f64::from(e.x()); - let length = if ev_x < slider_x { - 0.0 - } else if ev_x > slider_x + slider_width { - slider_width + let percentage = if vertical.get_untracked() { + let ev_y = f64::from(e.y()); + let slider_y = slider_rect.y(); + let slider_height = slider_rect.height(); + + let length = if ev_y < slider_y { + 0.0 + } else if ev_y > slider_y + slider_height { + slider_height + } else { + slider_y + slider_height - ev_y + }; + + length / slider_height * (max - min) } else { - ev_x - slider_x + let ev_x = f64::from(e.x()); + let slider_x = slider_rect.x(); + let slider_width = slider_rect.width(); + + let length = if ev_x < slider_x { + 0.0 + } else if ev_x > slider_x + slider_width { + slider_width + } else { + ev_x - slider_x + }; + + length / slider_width * (max - min) }; - let min = min.get_untracked(); - let max = max.get_untracked(); - let percentage = length / slider_width * (max - min); if left_mousemove.get_value() { update_value(percentage, current_value.get_untracked().1); @@ -182,7 +215,11 @@ pub fn RangeSlider( view! {
- +
{children()}
diff --git a/thaw/src/slider/range_slider/range-slider.css b/thaw/src/slider/range_slider/range-slider.css index 8f43a483..daf84b71 100644 --- a/thaw/src/slider/range_slider/range-slider.css +++ b/thaw/src/slider/range_slider/range-slider.css @@ -1,20 +1,28 @@ .thaw-range-slider { position: relative; - min-width: 120px; min-height: 32px; display: inline-grid; - grid-template-columns: - 1fr calc(100% - 20px) - 1fr; - grid-template-rows: 1fr 20px 1fr; justify-items: center; align-items: center; touch-action: none; cursor: pointer; + --thaw-slider__rail--size: 4px; --thaw-slider__thumb--size: 20px; } +.thaw-range-slider--horizontal { + min-width: 120px; + grid-template-rows: 1fr var(--thaw-slider__thumb--size) 1fr; + grid-template-columns: 1fr calc(100% - var(--thaw-slider__thumb--size)) 1fr; +} + +.thaw-range-slider--vertical { + min-height: 120px; + grid-template-rows: 1fr calc(100% - var(--thaw-slider__thumb--size)) 1fr; + grid-template-columns: 1fr var(--thaw-slider__thumb--size) 1fr; +} + .thaw-range-slider__rail { position: relative; forced-color-adjust: none; @@ -22,10 +30,8 @@ grid-column-start: 2; grid-row-end: 2; grid-row-start: 2; - width: 100%; - height: 4px; background-image: linear-gradient( - 90deg, + var(--thaw-slider--direction), var(--colorNeutralStrokeAccessible) 0% var(--thaw-range-slider--left-progress), var(--colorCompoundBrandBackground) @@ -39,16 +45,22 @@ pointer-events: none; } +.thaw-range-slider--horizontal .thaw-range-slider__rail { + width: 100%; + height: var(--thaw-slider__rail--size); +} + +.thaw-range-slider--vertical .thaw-range-slider__rail { + width: var(--thaw-slider__rail--size); + height: 100%; +} + .thaw-range-slider__rail::before { content: ""; position: absolute; - height: 4px; - right: -1px; - left: -1px; - background-image: repeating-linear-gradient( - 90deg, + var(--thaw-slider--direction), #0000 0%, #0000 calc(var(--thaw-range-slider--steps-percent) - 1px), var(--colorNeutralBackground1) @@ -57,6 +69,18 @@ ); } +.thaw-range-slider--horizontal .thaw-range-slider__rail::before { + height: var(--thaw-slider__rail--size); + right: -1px; + left: -1px; +} + +.thaw-range-slider--vertical .thaw-range-slider__rail::before { + width: var(--thaw-slider__rail--size); + top: -1px; + bottom: -1px; +} + .thaw-range-slider__thumb { position: absolute; forced-color-adjust: none; @@ -64,15 +88,23 @@ grid-column-start: 2; grid-row-end: 2; grid-row-start: 2; - height: 20px; - width: 20px; - left: var(--thaw-range-slider--progress); + height: var(--thaw-slider__thumb--size); + width: var(--thaw-slider__thumb--size); background-color: var(--colorCompoundBrandBackground); outline-style: none; /* pointer-events: none; */ border-radius: var(--borderRadiusCircular); box-shadow: 0 0 0 4px var(--colorNeutralBackground1) inset; +} + +.thaw-range-slider--horizontal .thaw-range-slider__thumb { transform: translateX(-50%); + left: var(--thaw-range-slider--progress); +} + +.thaw-range-slider--vertical .thaw-range-slider__thumb { + transform: translateY(50%); + bottom: var(--thaw-range-slider--progress); } .thaw-range-slider__thumb:hover { @@ -100,6 +132,14 @@ .thaw-range-slider__datalist { display: block; position: absolute; +} + +.thaw-range-slider--horizontal .thaw-range-slider__datalist { width: 100%; - top: 24px; + top: calc(var(--thaw-slider__thumb--size) + 4px); +} + +.thaw-range-slider--vertical .thaw-range-slider__datalist { + height: 100%; + left: calc(var(--thaw-slider__thumb--size) + 4px); } diff --git a/thaw/src/slider/slider/mod.rs b/thaw/src/slider/slider/mod.rs new file mode 100644 index 00000000..137e6714 --- /dev/null +++ b/thaw/src/slider/slider/mod.rs @@ -0,0 +1,130 @@ +mod types; + +pub use types::*; + +use crate::{FieldInjection, Rule}; +use leptos::{context::Provider, ev, prelude::*}; +use thaw_components::OptionComp; +use thaw_utils::{class_list, mount_style, Model}; + +#[component] +pub fn Slider( + #[prop(optional, into)] class: MaybeProp, + #[prop(optional, into)] id: MaybeProp, + /// A string specifying a name for the input control. + /// This name is submitted along with the control's value when the form data is submitted. + #[prop(optional, into)] + name: MaybeProp, + /// The rules to validate Field. + #[prop(optional, into)] + rules: Vec, + /// The current value of the controlled Slider. + #[prop(optional, into)] + value: Model, + /// Min value of the slider. + #[prop(default = 0f64.into(), into)] + min: Signal, + /// Max value of the slider. + #[prop(default = 100f64.into(), into)] + max: Signal, + /// The step in which value is incremented. + #[prop(optional, into)] + step: MaybeProp, + /// Render the Slider in a vertical orientation, smallest value on the bottom. + #[prop(optional, into)] + vertical: Signal, + #[prop(optional)] children: Option, +) -> impl IntoView { + mount_style("slider", include_str!("./slider.css")); + let (id, name) = FieldInjection::use_id_and_name(id, name); + let validate = Rule::validate(rules, value, name); + let is_chldren = children.is_some(); + let current_value = Memo::new(move |_| { + let max = max.get(); + let min = min.get(); + let v = value.get(); + if v > max { + max + } else if v < min { + min + } else { + v + } + }); + + let on_input = move |e: ev::Event| { + if let Ok(range_value) = event_target_value(&e).parse::() { + value.set(range_value); + validate.run(Some(SliderRuleTrigger::Input)); + } + }; + + let css_vars = move || { + let max = max.get(); + let min = min.get(); + let mut css_vars = format!( + "--thaw-slider--progress: {:.2}%;", + if max == min { + 0.0 + } else { + (current_value.get() - min) / (max - min) * 100.0 + } + ); + + if vertical.get() { + css_vars.push_str("--thaw-slider--direction: 0deg;"); + } else { + css_vars.push_str("--thaw-slider--direction: 90deg;"); + } + + if is_chldren { + css_vars.push_str(&format!("--thaw-slider--max: {:.2};", max)); + css_vars.push_str(&format!("--thaw-slider--min: {:.2};", min)); + } + + if let Some(step) = step.get() { + if step > 0.0 { + css_vars.push_str(&format!( + "--thaw-slider--steps-percent: {:.2}%", + step * 100.0 / (max - min) + )); + } + } + css_vars + }; + + view! { +
+ +
+
+ + +
{children()}
+
+
+
+ } +} diff --git a/thaw/src/slider/slider.css b/thaw/src/slider/slider/slider.css similarity index 77% rename from thaw/src/slider/slider.css rename to thaw/src/slider/slider/slider.css index ec06052a..c372e8df 100644 --- a/thaw/src/slider/slider.css +++ b/thaw/src/slider/slider/slider.css @@ -1,12 +1,9 @@ .thaw-slider { - min-width: 120px; min-height: 32px; justify-items: center; touch-action: none; display: inline-grid; - grid-template-columns: 1fr calc(100% - var(--thaw-slider__thumb--size)) 1fr; - grid-template-rows: 1fr var(--thaw-slider__thumb--size) 1fr; position: relative; align-items: center; @@ -18,6 +15,18 @@ --thaw-slider__rail--color: var(--colorNeutralStrokeAccessible); } +.thaw-slider--horizontal { + min-width: 120px; + grid-template-rows: 1fr var(--thaw-slider__thumb--size) 1fr; + grid-template-columns: 1fr calc(100% - var(--thaw-slider__thumb--size)) 1fr; +} + +.thaw-slider--vertical { + min-height: 120px; + grid-template-rows: 1fr calc(100% - var(--thaw-slider__thumb--size)) 1fr; + grid-template-columns: 1fr var(--thaw-slider__thumb--size) 1fr; +} + .thaw-slider:hover { --thaw-slider__progress--color: var(--colorCompoundBrandBackgroundHover); --thaw-slider__thumb--color: var(--colorCompoundBrandBackgroundHover); @@ -55,11 +64,18 @@ margin: 0; padding: 0; opacity: 0; + cursor: pointer; +} +.thaw-slider--horizontal .thaw-slider__input { width: 100%; height: var(--thaw-slider__thumb--size); +} - cursor: pointer; +.thaw-slider--vertical .thaw-slider__input { + width: var(--thaw-slider__thumb--size); + height: 100%; + -webkit-appearance: slider-vertical; } .thaw-slider__rail { @@ -69,8 +85,7 @@ grid-column-start: 2; grid-row-end: 2; grid-row-start: 2; - width: 100%; - height: var(--thaw-slider__rail--size); + background-image: linear-gradient( var(--thaw-slider--direction), var(--thaw-slider__progress--color) 0%, @@ -82,14 +97,20 @@ pointer-events: none; } +.thaw-slider--horizontal .thaw-slider__rail { + width: 100%; + height: var(--thaw-slider__rail--size); +} + +.thaw-slider--vertical .thaw-slider__rail { + width: var(--thaw-slider__rail--size); + height: 100%; +} + .thaw-slider__rail::before { content: ""; position: absolute; - height: var(--thaw-slider__rail--size); - right: -1px; - left: -1px; - background-image: repeating-linear-gradient( var(--thaw-slider--direction), #0000 0%, @@ -100,6 +121,18 @@ ); } +.thaw-slider--horizontal .thaw-slider__rail::before { + height: var(--thaw-slider__rail--size); + right: -1px; + left: -1px; +} + +.thaw-slider--vertical .thaw-slider__rail::before { + width: var(--thaw-slider__rail--size); + top: -1px; + bottom: -1px; +} + .thaw-slider__thumb { position: absolute; @@ -111,7 +144,6 @@ height: var(--thaw-slider__thumb--size); width: var(--thaw-slider__thumb--size); - left: var(--thaw-slider--progress); background-color: var(--thaw-slider__thumb--color); outline-style: none; @@ -119,8 +151,16 @@ border-radius: var(--borderRadiusCircular); box-shadow: 0 0 0 calc(var(--thaw-slider__thumb--size) * 0.2) var(--colorNeutralBackground1) inset; +} +.thaw-slider--horizontal .thaw-slider__thumb { transform: translateX(-50%); + left: var(--thaw-slider--progress); +} + +.thaw-slider--vertical .thaw-slider__thumb { + transform: translateY(50%); + bottom: var(--thaw-slider--progress); } .thaw-slider__thumb::before { @@ -141,6 +181,14 @@ .thaw-slider__datalist { display: block; position: absolute; +} + +.thaw-slider--horizontal .thaw-slider__datalist { width: 100%; top: calc(var(--thaw-slider__thumb--size) + 4px); } + +.thaw-slider--vertical .thaw-slider__datalist { + height: 100%; + left: calc(var(--thaw-slider__thumb--size) + 4px); +} diff --git a/thaw/src/slider/slider/types.rs b/thaw/src/slider/slider/types.rs new file mode 100644 index 00000000..180036fe --- /dev/null +++ b/thaw/src/slider/slider/types.rs @@ -0,0 +1,47 @@ +use crate::{FieldValidationState, Rule}; +use leptos::prelude::*; +use std::ops::Deref; + +#[derive(Clone, Copy)] +pub(crate) struct SliderInjection { + pub max: Signal, + pub min: Signal, + pub vertical: Signal, +} + +impl SliderInjection { + pub fn expect_context() -> Self { + expect_context() + } +} + +#[derive(Debug, Default, PartialEq, Clone, Copy)] +pub enum SliderRuleTrigger { + #[default] + Input, +} + +pub struct SliderRule(Rule); + +impl SliderRule { + pub fn validator( + f: impl Fn(&f64, Signal>) -> Result<(), FieldValidationState> + + Send + + Sync + + 'static, + ) -> Self { + Self(Rule::validator(f)) + } + + pub fn with_trigger(self, trigger: SliderRuleTrigger) -> Self { + Self(Rule::with_trigger(self.0, trigger)) + } +} + +impl Deref for SliderRule { + type Target = Rule; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/thaw/src/slider/slider_label.css b/thaw/src/slider/slider_label.css index 725911cb..8d67e037 100644 --- a/thaw/src/slider/slider_label.css +++ b/thaw/src/slider/slider_label.css @@ -1,5 +1,12 @@ .thaw-slider-label { position: absolute; display: inline-block; +} + +.thaw-slider-label--horizontal { transform: translateX(-50%); } + +.thaw-slider-label--vertical { + transform: translateY(50%); +} diff --git a/thaw/src/slider/slider_label.rs b/thaw/src/slider/slider_label.rs index d8e4cc36..4203e6bb 100644 --- a/thaw/src/slider/slider_label.rs +++ b/thaw/src/slider/slider_label.rs @@ -16,11 +16,23 @@ pub fn SliderLabel( let style = move || { let value = (value.get() - slider.min.get()) / (slider.max.get() - slider.min.get()); - format!("left: calc({} * (100% - var(--thaw-slider__thumb--size)) + var(--thaw-slider__thumb--size) / 2)", value) + + if slider.vertical.get() { + format!("bottom: calc({} * (100% - var(--thaw-slider__thumb--size)) + var(--thaw-slider__thumb--size) / 2)", value) + } else { + format!("left: calc({} * (100% - var(--thaw-slider__thumb--size)) + var(--thaw-slider__thumb--size) / 2)", value) + } }; view! { -
+
{children()}