feat: SpinButton

This commit is contained in:
luoxiao 2024-05-22 17:28:20 +08:00
parent be91dcbf6a
commit 7d99f9bd09
10 changed files with 259 additions and 2 deletions

View file

@ -80,6 +80,7 @@ fn TheRouter(is_routing: RwSignal<bool>) -> impl IntoView {
<Route path="/skeleton" view=SkeletonMdPage/>
<Route path="/slider" view=SliderMdPage/>
<Route path="/space" view=SpaceMdPage/>
<Route path="/spin-button" view=SpinButtonMdPage/>
<Route path="/spinner" view=SpinnerMdPage/>
<Route path="/switch" view=SwitchMdPage/>
<Route path="/table" view=TableMdPage/>

View file

@ -140,6 +140,10 @@ pub(crate) fn gen_menu_data() -> Vec<MenuGroupOption> {
value: "typography".into(),
label: "Typography".into(),
},
MenuItemOption {
value: "spin-button".into(),
label: "Spin Button".into(),
},
],
},
MenuGroupOption {

View 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>
}
```

View file

@ -62,6 +62,7 @@ pub fn include_md(_token_stream: proc_macro::TokenStream) -> proc_macro::TokenSt
"SkeletonMdPage" => "../docs/skeleton/mod.md",
"SliderMdPage" => "../docs/slider/mod.md",
"SpaceMdPage" => "../docs/space/mod.md",
"SpinButtonMdPage" => "../docs/spin_button/mod.md",
"SpinnerMdPage" => "../docs/spinner/mod.md",
"SwitchMdPage" => "../docs/switch/mod.md",
"TableMdPage" => "../docs/table/mod.md",

View file

@ -2,7 +2,7 @@ mod theme;
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 leptos::*;
use std::ops::Deref;

View file

@ -58,7 +58,7 @@
transition-delay: var(--curveDecelerateMid);
}
.r1qp7m7v:focus-within:active::after {
.thaw-input:focus-within:active::after {
border-bottom-color: var(--colorCompoundBrandStrokePressed);
}

View file

@ -36,6 +36,7 @@ mod select;
mod skeleton;
mod slider;
mod space;
mod spin_button;
mod spinner;
mod switch;
mod table;
@ -83,6 +84,7 @@ pub use select::*;
pub use skeleton::*;
pub use slider::*;
pub use space::*;
pub use spin_button::*;
pub use spinner::*;
pub use switch::*;
pub use table::*;

110
thaw/src/spin_button/mod.rs Normal file
View 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>
}
}

View 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);
}

View file

@ -39,6 +39,7 @@ pub struct ColorTheme {
pub color_compound_brand_background_hover: String,
pub color_compound_brand_background_pressed: String,
pub color_compound_brand_stroke: String,
pub color_compound_brand_stroke_pressed: String,
pub color_brand_background: String,
pub color_brand_background_hover: String,
@ -93,6 +94,7 @@ impl ColorTheme {
color_compound_brand_background_hover: "#115ea3".into(),
color_compound_brand_background_pressed: "#0f548c".into(),
color_compound_brand_stroke: "#0f6cbd".into(),
color_compound_brand_stroke_pressed: "#0f548c".into(),
color_brand_background: "#0f6cbd".into(),
color_brand_background_hover: "#115ea3".into(),
@ -147,6 +149,7 @@ impl ColorTheme {
color_compound_brand_background_hover: "#62abf5".into(),
color_compound_brand_background_pressed: "#2886de".into(),
color_compound_brand_stroke: "#479ef5".into(),
color_compound_brand_stroke_pressed: "#2886de".into(),
color_brand_background: "#115ea3".into(),
color_brand_background_hover: "#0f6cbd".into(),