mirror of
https://github.com/adoyle0/thaw.git
synced 2025-02-02 08:34:15 -05:00
feat: SpinButton
This commit is contained in:
parent
be91dcbf6a
commit
7d99f9bd09
10 changed files with 259 additions and 2 deletions
|
@ -80,6 +80,7 @@ fn TheRouter(is_routing: RwSignal<bool>) -> impl IntoView {
|
||||||
<Route path="/skeleton" view=SkeletonMdPage/>
|
<Route path="/skeleton" view=SkeletonMdPage/>
|
||||||
<Route path="/slider" view=SliderMdPage/>
|
<Route path="/slider" view=SliderMdPage/>
|
||||||
<Route path="/space" view=SpaceMdPage/>
|
<Route path="/space" view=SpaceMdPage/>
|
||||||
|
<Route path="/spin-button" view=SpinButtonMdPage/>
|
||||||
<Route path="/spinner" view=SpinnerMdPage/>
|
<Route path="/spinner" view=SpinnerMdPage/>
|
||||||
<Route path="/switch" view=SwitchMdPage/>
|
<Route path="/switch" view=SwitchMdPage/>
|
||||||
<Route path="/table" view=TableMdPage/>
|
<Route path="/table" view=TableMdPage/>
|
||||||
|
|
|
@ -140,6 +140,10 @@ pub(crate) fn gen_menu_data() -> Vec<MenuGroupOption> {
|
||||||
value: "typography".into(),
|
value: "typography".into(),
|
||||||
label: "Typography".into(),
|
label: "Typography".into(),
|
||||||
},
|
},
|
||||||
|
MenuItemOption {
|
||||||
|
value: "spin-button".into(),
|
||||||
|
label: "Spin Button".into(),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
MenuGroupOption {
|
MenuGroupOption {
|
||||||
|
|
13
demo_markdown/docs/spin_button/mod.md
Normal file
13
demo_markdown/docs/spin_button/mod.md
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# SpinButton
|
||||||
|
|
||||||
|
```rust demo
|
||||||
|
let value = RwSignal::new(0);
|
||||||
|
let value_f64 = RwSignal::new(0.0);
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<Space vertical=true>
|
||||||
|
<SpinButton value step_page=1/>
|
||||||
|
<SpinButton value=value_f64 step_page=1.0/>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
```
|
|
@ -62,6 +62,7 @@ pub fn include_md(_token_stream: proc_macro::TokenStream) -> proc_macro::TokenSt
|
||||||
"SkeletonMdPage" => "../docs/skeleton/mod.md",
|
"SkeletonMdPage" => "../docs/skeleton/mod.md",
|
||||||
"SliderMdPage" => "../docs/slider/mod.md",
|
"SliderMdPage" => "../docs/slider/mod.md",
|
||||||
"SpaceMdPage" => "../docs/space/mod.md",
|
"SpaceMdPage" => "../docs/space/mod.md",
|
||||||
|
"SpinButtonMdPage" => "../docs/spin_button/mod.md",
|
||||||
"SpinnerMdPage" => "../docs/spinner/mod.md",
|
"SpinnerMdPage" => "../docs/spinner/mod.md",
|
||||||
"SwitchMdPage" => "../docs/switch/mod.md",
|
"SwitchMdPage" => "../docs/switch/mod.md",
|
||||||
"TableMdPage" => "../docs/table/mod.md",
|
"TableMdPage" => "../docs/table/mod.md",
|
||||||
|
|
|
@ -2,7 +2,7 @@ mod theme;
|
||||||
|
|
||||||
pub use theme::CalendarTheme;
|
pub use theme::CalendarTheme;
|
||||||
|
|
||||||
use crate::{use_theme, Button, ButtonGroup, ButtonAppearance, Theme};
|
use crate::{use_theme, Button, ButtonGroup, Theme};
|
||||||
use chrono::{Datelike, Days, Local, Month, Months, NaiveDate};
|
use chrono::{Datelike, Days, Local, Month, Months, NaiveDate};
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
|
@ -58,7 +58,7 @@
|
||||||
transition-delay: var(--curveDecelerateMid);
|
transition-delay: var(--curveDecelerateMid);
|
||||||
}
|
}
|
||||||
|
|
||||||
.r1qp7m7v:focus-within:active::after {
|
.thaw-input:focus-within:active::after {
|
||||||
border-bottom-color: var(--colorCompoundBrandStrokePressed);
|
border-bottom-color: var(--colorCompoundBrandStrokePressed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@ mod select;
|
||||||
mod skeleton;
|
mod skeleton;
|
||||||
mod slider;
|
mod slider;
|
||||||
mod space;
|
mod space;
|
||||||
|
mod spin_button;
|
||||||
mod spinner;
|
mod spinner;
|
||||||
mod switch;
|
mod switch;
|
||||||
mod table;
|
mod table;
|
||||||
|
@ -83,6 +84,7 @@ pub use select::*;
|
||||||
pub use skeleton::*;
|
pub use skeleton::*;
|
||||||
pub use slider::*;
|
pub use slider::*;
|
||||||
pub use space::*;
|
pub use space::*;
|
||||||
|
pub use spin_button::*;
|
||||||
pub use spinner::*;
|
pub use spinner::*;
|
||||||
pub use switch::*;
|
pub use switch::*;
|
||||||
pub use table::*;
|
pub use table::*;
|
||||||
|
|
110
thaw/src/spin_button/mod.rs
Normal file
110
thaw/src/spin_button/mod.rs
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
use leptos::*;
|
||||||
|
use num_traits::Bounded;
|
||||||
|
use std::ops::{Add, Sub};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use thaw_utils::{mount_style, Model, StoredMaybeSignal};
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn SpinButton<T>(
|
||||||
|
#[prop(optional, into)] value: Model<T>,
|
||||||
|
#[prop(into)] step_page: MaybeSignal<T>,
|
||||||
|
#[prop(default = T::min_value().into(), into)] min: MaybeSignal<T>,
|
||||||
|
#[prop(default = T::max_value().into(), into)] max: MaybeSignal<T>,
|
||||||
|
#[prop(optional, into)] disabled: MaybeSignal<bool>,
|
||||||
|
) -> impl IntoView
|
||||||
|
where
|
||||||
|
T: Add<Output = T> + Sub<Output = T> + PartialOrd + Bounded,
|
||||||
|
T: Default + Clone + FromStr + ToString + 'static,
|
||||||
|
{
|
||||||
|
mount_style("spin-button", include_str!("./spin-button.css"));
|
||||||
|
|
||||||
|
let initialization_value = value.get_untracked().to_string();
|
||||||
|
let step_page: StoredMaybeSignal<_> = step_page.into();
|
||||||
|
let min: StoredMaybeSignal<_> = min.into();
|
||||||
|
let max: StoredMaybeSignal<_> = max.into();
|
||||||
|
let input_value = RwSignal::new(String::new());
|
||||||
|
|
||||||
|
Effect::new_isomorphic(move |prev| {
|
||||||
|
value.with(|value| {
|
||||||
|
if let Some(prev) = prev {
|
||||||
|
if value == &prev {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input_value.set(value.to_string());
|
||||||
|
value.clone()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let update_value = move |new_value| {
|
||||||
|
let min = min.get_untracked();
|
||||||
|
let max = max.get_untracked();
|
||||||
|
if new_value < min {
|
||||||
|
value.set(min);
|
||||||
|
} else if new_value > max {
|
||||||
|
value.set(max);
|
||||||
|
} else if with!(|value| value != &new_value) {
|
||||||
|
value.set(new_value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let increment_disabled = Memo::new(move |_| disabled.get() || value.get() <= min.get());
|
||||||
|
let decrement_disabled = Memo::new(move |_| disabled.get() || value.get() >= max.get());
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<span
|
||||||
|
class="thaw-spin-button"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
autocomplete="off"
|
||||||
|
role="spinbutton"
|
||||||
|
aria-valuenow=move || value.get().to_string()
|
||||||
|
type="text"
|
||||||
|
disabled=move || disabled.get()
|
||||||
|
value=initialization_value
|
||||||
|
prop:value=move || input_value.get()
|
||||||
|
class="thaw-spin-button__input"
|
||||||
|
on:change=move |e| {
|
||||||
|
let target_value = event_target_value(&e);
|
||||||
|
let Ok(v) = target_value.parse::<T>() else {
|
||||||
|
input_value.update(|_| {});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
update_value(v);
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
tabindex="-1"
|
||||||
|
aria-label="Increment value"
|
||||||
|
type="button"
|
||||||
|
class="thaw-spin-button__increment-button"
|
||||||
|
class=("thaw-spin-button__increment-button--disabled", move || increment_disabled.get())
|
||||||
|
on:click=move |_| {
|
||||||
|
if !increment_disabled.get_untracked() {
|
||||||
|
update_value(value.get_untracked() + step_page.get_untracked());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<svg fill="currentColor" aria-hidden="true" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path d="M3.15 10.35c.2.2.5.2.7 0L8 6.21l4.15 4.14a.5.5 0 0 0 .7-.7l-4.5-4.5a.5.5 0 0 0-.7 0l-4.5 4.5a.5.5 0 0 0 0 .7Z" fill="currentColor"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
tabindex="-1"
|
||||||
|
aria-label="Decrement value"
|
||||||
|
type="button"
|
||||||
|
class="thaw-spin-button__decrement-button"
|
||||||
|
class=("thaw-spin-button__decrement-button--disabled", move || decrement_disabled.get())
|
||||||
|
on:click=move |_| {
|
||||||
|
if !decrement_disabled.get_untracked() {
|
||||||
|
update_value(value.get_untracked() - step_page.get_untracked());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<svg fill="currentColor" aria-hidden="true" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path d="M3.15 5.65c.2-.2.5-.2.7 0L8 9.79l4.15-4.14a.5.5 0 0 1 .7.7l-4.5 4.5a.5.5 0 0 1-.7 0l-4.5-4.5a.5.5 0 0 1 0-.7Z" fill="currentColor"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
}
|
123
thaw/src/spin_button/spin-button.css
Normal file
123
thaw/src/spin_button/spin-button.css
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
.thaw-spin-button {
|
||||||
|
display: inline-grid;
|
||||||
|
grid-template-columns: 1fr 24px;
|
||||||
|
grid-template-rows: 1fr 1fr;
|
||||||
|
column-gap: var(--spacingHorizontalXS);
|
||||||
|
row-gap: 0px;
|
||||||
|
position: relative;
|
||||||
|
isolation: isolate;
|
||||||
|
background-color: var(--colorNeutralBackground1);
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 0 0 0 var(--spacingHorizontalMNudge);
|
||||||
|
|
||||||
|
border: 1px solid var(--colorNeutralStroke1);
|
||||||
|
border-bottom-color: var(--colorNeutralStrokeAccessible);
|
||||||
|
border-radius: var(--borderRadiusMedium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-spin-button:hover {
|
||||||
|
border-color: var(--colorNeutralStroke1Hover);
|
||||||
|
border-bottom-color: var(--colorNeutralStrokeAccessibleHover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-spin-button:focus-within {
|
||||||
|
outline: transparent solid 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-spin-button:active,
|
||||||
|
.thaw-spin-button:focus-within {
|
||||||
|
border-color: var(--colorNeutralStroke1Pressed);
|
||||||
|
border-bottom-color: var(--colorNeutralStrokeAccessiblePressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-spin-button::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: -1px;
|
||||||
|
bottom: -1px;
|
||||||
|
right: -1px;
|
||||||
|
height: max(2px, var(--borderRadiusMedium));
|
||||||
|
border-bottom-left-radius: var(--borderRadiusMedium);
|
||||||
|
border-bottom-right-radius: var(--borderRadiusMedium);
|
||||||
|
border-bottom: 2px solid var(--colorCompoundBrandStroke);
|
||||||
|
clip-path: inset(calc(100% - 2px) 0px 0px);
|
||||||
|
transform: scaleX(0);
|
||||||
|
transition-property: transform;
|
||||||
|
transition-duration: var(--durationUltraFast);
|
||||||
|
transition-delay: var(--curveAccelerateMid);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-spin-button:focus-within::after {
|
||||||
|
transform: scaleX(1);
|
||||||
|
transition-property: transform;
|
||||||
|
transition-duration: var(--durationNormal);
|
||||||
|
transition-delay: var(--curveDecelerateMid);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-spin-button:focus-within:active::after {
|
||||||
|
border-bottom-color: var(--colorCompoundBrandStrokePressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-spin-button__input {
|
||||||
|
grid-area: 1 / 1 / 3 / 2;
|
||||||
|
outline-style: none;
|
||||||
|
border: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
color: var(--colorNeutralForeground1);
|
||||||
|
background-color: transparent;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-spin-button__increment-button,
|
||||||
|
.thaw-spin-button__decrement-button {
|
||||||
|
display: inline-flex;
|
||||||
|
width: 24px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 0px;
|
||||||
|
position: absolute;
|
||||||
|
outline-style: none;
|
||||||
|
height: 16px;
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--colorNeutralForeground3);
|
||||||
|
grid-column-start: 2;
|
||||||
|
border-radius: 0px;
|
||||||
|
padding: 0px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-spin-button__increment-button:enabled:hover,
|
||||||
|
.thaw-spin-button__decrement-button:enabled:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--colorNeutralForeground3Hover);
|
||||||
|
background-color: var(--colorSubtleBackgroundHover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-spin-button__increment-button:enabled:active,
|
||||||
|
.thaw-spin-button__decrement-button:enabled:active {
|
||||||
|
color: var(--colorNeutralForeground3Pressed);
|
||||||
|
background-color: var(--colorSubtleBackgroundPressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-spin-button__increment-button:active,
|
||||||
|
.thaw-spin-button__decrement-button:active {
|
||||||
|
outline-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-spin-button__increment-button {
|
||||||
|
grid-row-start: 1;
|
||||||
|
padding-top: 4px;
|
||||||
|
padding-bottom: 1px;
|
||||||
|
border-top-right-radius: var(--borderRadiusMedium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-spin-button__decrement-button {
|
||||||
|
padding-bottom: 4px;
|
||||||
|
padding-top: 1px;
|
||||||
|
grid-row-start: 2;
|
||||||
|
border-bottom-right-radius: var(--borderRadiusMedium);
|
||||||
|
}
|
|
@ -39,6 +39,7 @@ pub struct ColorTheme {
|
||||||
pub color_compound_brand_background_hover: String,
|
pub color_compound_brand_background_hover: String,
|
||||||
pub color_compound_brand_background_pressed: String,
|
pub color_compound_brand_background_pressed: String,
|
||||||
pub color_compound_brand_stroke: String,
|
pub color_compound_brand_stroke: String,
|
||||||
|
pub color_compound_brand_stroke_pressed: String,
|
||||||
|
|
||||||
pub color_brand_background: String,
|
pub color_brand_background: String,
|
||||||
pub color_brand_background_hover: String,
|
pub color_brand_background_hover: String,
|
||||||
|
@ -93,6 +94,7 @@ impl ColorTheme {
|
||||||
color_compound_brand_background_hover: "#115ea3".into(),
|
color_compound_brand_background_hover: "#115ea3".into(),
|
||||||
color_compound_brand_background_pressed: "#0f548c".into(),
|
color_compound_brand_background_pressed: "#0f548c".into(),
|
||||||
color_compound_brand_stroke: "#0f6cbd".into(),
|
color_compound_brand_stroke: "#0f6cbd".into(),
|
||||||
|
color_compound_brand_stroke_pressed: "#0f548c".into(),
|
||||||
|
|
||||||
color_brand_background: "#0f6cbd".into(),
|
color_brand_background: "#0f6cbd".into(),
|
||||||
color_brand_background_hover: "#115ea3".into(),
|
color_brand_background_hover: "#115ea3".into(),
|
||||||
|
@ -147,6 +149,7 @@ impl ColorTheme {
|
||||||
color_compound_brand_background_hover: "#62abf5".into(),
|
color_compound_brand_background_hover: "#62abf5".into(),
|
||||||
color_compound_brand_background_pressed: "#2886de".into(),
|
color_compound_brand_background_pressed: "#2886de".into(),
|
||||||
color_compound_brand_stroke: "#479ef5".into(),
|
color_compound_brand_stroke: "#479ef5".into(),
|
||||||
|
color_compound_brand_stroke_pressed: "#2886de".into(),
|
||||||
|
|
||||||
color_brand_background: "#115ea3".into(),
|
color_brand_background: "#115ea3".into(),
|
||||||
color_brand_background_hover: "#0f6cbd".into(),
|
color_brand_background_hover: "#0f6cbd".into(),
|
||||||
|
|
Loading…
Add table
Reference in a new issue