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="/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/>
|
||||
|
|
|
@ -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 {
|
||||
|
|
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",
|
||||
"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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -58,7 +58,7 @@
|
|||
transition-delay: var(--curveDecelerateMid);
|
||||
}
|
||||
|
||||
.r1qp7m7v:focus-within:active::after {
|
||||
.thaw-input:focus-within:active::after {
|
||||
border-bottom-color: var(--colorCompoundBrandStrokePressed);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
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_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(),
|
||||
|
|
Loading…
Add table
Reference in a new issue