reactor: radio

This commit is contained in:
luoxiao 2024-05-23 14:48:06 +08:00
parent 97263ec80d
commit ae4a69fd15
7 changed files with 128 additions and 112 deletions

View file

@ -1,26 +1,12 @@
# Radio # Radio
```rust demo ```rust demo
let value = create_rw_signal(false); let value = RwSignal::new(None);
view! {
<Radio value>"Click"</Radio>
}
```
### Group
```rust demo
let value = create_rw_signal(None);
view! { view! {
<RadioGroup value> <RadioGroup value>
<RadioItem key="a"> <Radio value="a" label="Apple"/>
"Apple" <Radio value="o" label="Orange"/>
</RadioItem>
<RadioItem key="o">
"Orange"
</RadioItem>
</RadioGroup> </RadioGroup>
<div style="margin-top: 1rem"> <div style="margin-top: 1rem">
"value: " {move || format!("{:?}", value.get())} "value: " {move || format!("{:?}", value.get())}

View file

@ -25,7 +25,7 @@ pub fn Checkbox(
}; };
view! { view! {
<div <span
class=class_list![ class=class_list![
"thaw-checkbox", ("thaw-checkbox--checked", move || checked.get()), class.map(| c | "thaw-checkbox", ("thaw-checkbox--checked", move || checked.get()), class.map(| c |
move || c.get()) move || c.get())
@ -39,7 +39,7 @@ pub fn Checkbox(
ref=input_ref ref=input_ref
on:change=on_change on:change=on_change
/> />
<div class="thaw-checkbox__indicator"> <div aria-hidden="true" class="thaw-checkbox__indicator">
{ {
move || if checked.get() { move || if checked.get() {
view! { view! {
@ -55,6 +55,6 @@ pub fn Checkbox(
<OptionComp value=children let:children> <OptionComp value=children let:children>
<label class="thaw-checkbox__label" for=id>{children()}</label> <label class="thaw-checkbox__label" for=id>{children()}</label>
</OptionComp> </OptionComp>
</div> </span>
} }
} }

View file

@ -1,45 +1,61 @@
mod radio_group; mod radio_group;
mod radio_item;
pub use radio_group::RadioGroup; pub use radio_group::RadioGroup;
pub use radio_item::RadioItem;
use crate::{theme::use_theme, Theme};
use leptos::*; use leptos::*;
use thaw_utils::{class_list, mount_style, Model, OptionalProp}; use radio_group::RadioGroupInjection;
use thaw_utils::{class_list, mount_style, OptionalProp};
#[component] #[component]
pub fn Radio( pub fn Radio(
#[prop(optional, into)] value: Model<bool>,
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>, #[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
#[prop(optional)] children: Option<Children>, #[prop(optional, into)] value: String,
#[prop(optional, into)] label: MaybeProp<String>,
) -> impl IntoView { ) -> impl IntoView {
let theme = use_theme(Theme::light);
mount_style("radio", include_str!("./radio.css")); mount_style("radio", include_str!("./radio.css"));
let css_vars = create_memo(move |_| { let id = uuid::Uuid::new_v4().to_string();
let mut css_vars = String::new(); let group = RadioGroupInjection::use_();
theme.with(|theme| { let item_value = StoredValue::new(value);
let bg_color = theme.common.color_primary.clone();
css_vars.push_str(&format!("--thaw-background-color-checked: {bg_color};")); let checked = Memo::new({
let group = group.clone();
move |_| {
item_value.with_value(|value| {
group
.0
.with(|group_value| group_value.as_ref() == Some(value))
})
}
}); });
css_vars let on_change = move |_| {
}); group.0.set(Some(item_value.get_value()));
};
view! { view! {
<div <span
class=class_list![ class=class_list![
"thaw-radio", ("thaw-radio--checked", move || value.get()), class.map(| c | move || "thaw-radio", class.map(| c | move || c.get())
c.get())
] ]
style=move || css_vars.get()
on:click=move |_| value.set(!value.get_untracked())
> >
<input class="thaw-radio__input" type="radio" prop:value=move || value.get()/> <input
<div class="thaw-radio__dot"></div> class="thaw-radio__input"
<div class="thaw-radio__label">{children.map(|children| children())}</div> type="radio"
</div> id=id.clone()
prop:checked=move || checked.get()
on:change=on_change
/>
<div aria-hidden="true" class="thaw-radio__indicator"></div>
{
move || if let Some(label) = label.get() {
view! {
<label class="thaw-radio__label" for=id.clone()>{label}</label>
}.into()
} else {
None
}
}
</span>
} }
} }

View file

@ -1,42 +1,81 @@
.thaw-radio { .thaw-radio {
display: inline-flex; display: inline-flex;
align-items: center; position: relative;
cursor: pointer;
} }
.thaw-radio__input { .thaw-radio__input {
width: 0; position: absolute;
height: 0; left: 0px;
top: 0px;
width: calc(16px + 2 * var(--spacingHorizontalS));
height: 100%;
box-sizing: border-box;
margin: 0px;
opacity: 0; opacity: 0;
} }
.thaw-radio__dot { .thaw-radio__input:enabled {
display: inline-block; cursor: pointer;
}
.thaw-radio__indicator {
position: relative; position: relative;
width: 14px; width: 16px;
height: 14px; height: 16px;
border: 1px solid #ddd; font-size: 12px;
border-radius: 50%; box-sizing: border-box;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
border: var(--strokeWidthThin) solid;
border-radius: var(--borderRadiusCircular);
margin: var(--spacingVerticalS) var(--spacingHorizontalS);
fill: currentcolor;
pointer-events: none;
} }
.thaw-radio:hover .thaw-radio__dot, .thaw-radio__input:enabled:not(:checked) ~ .thaw-radio__indicator {
.thaw-radio--checked .thaw-radio__dot { border-color: var(--colorNeutralStrokeAccessible);
border-color: var(--thaw-background-color-checked);
} }
.thaw-radio--checked .thaw-radio__dot::before { .thaw-radio__input:checked ~ .thaw-radio__indicator {
content: ""; border-color: var(--colorCompoundBrandStroke);
color: var(--colorCompoundBrandForeground1);
}
.thaw-radio__indicator::after {
position: absolute; position: absolute;
top: 3px; width: 16px;
bottom: 3px; height: 16px;
left: 3px; border-radius: var(--borderRadiusCircular);
right: 3px; transform: scale(0.625);
background-color: var(--thaw-background-color-checked); background-color: currentcolor;
border-radius: 50%; }
.thaw-radio__input:checked ~ .thaw-radio__indicator::after {
content: "";
} }
.thaw-radio__label { .thaw-radio__label {
display: inline-block; margin-bottom: calc((16px - var(--lineHeightBase300)) / 2);
padding: 0 6px; margin-top: calc((16px - var(--lineHeightBase300)) / 2);
user-select: none; align-self: center;
padding-bottom: var(--spacingVerticalS);
padding-top: var(--spacingVerticalS);
padding-left: var(--spacingHorizontalXS);
padding-right: var(--spacingHorizontalS);
line-height: var(--lineHeightBase300);
font-size: var(--fontSizeBase300);
font-family: var(--fontFamilyBase);
color: var(--colorNeutralForeground1);
}
.thaw-radio__input:enabled:not(:checked) ~ .thaw-radio__label {
color: var(--colorNeutralForeground3);
}
.thaw-radio__input:enabled ~ .thaw-radio__label {
cursor: pointer;
} }

View file

@ -6,12 +6,20 @@ pub fn RadioGroup(
#[prop(optional, into)] value: Model<Option<String>>, #[prop(optional, into)] value: Model<Option<String>>,
children: Children, children: Children,
) -> impl IntoView { ) -> impl IntoView {
view! { <Provider value=RadioGroupInjection(value) children/> } view! {
<Provider value=RadioGroupInjection(value)>
<div class="thaw-radio-group" role="radiogroup" style="display: flex;align-items: flex-start">
{children()}
</div>
</Provider>
}
} }
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct RadioGroupInjection(pub Model<Option<String>>); pub(crate) struct RadioGroupInjection(pub Model<Option<String>>);
pub(crate) fn use_radio_group() -> RadioGroupInjection { impl RadioGroupInjection {
pub fn use_() -> RadioGroupInjection {
expect_context() expect_context()
} }
}

View file

@ -1,39 +0,0 @@
use crate::radio::{radio_group::use_radio_group, Radio};
use leptos::*;
use thaw_utils::OptionalProp;
#[component]
pub fn RadioItem(
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
#[prop(into)] key: String,
#[prop(optional)] children: Option<Children>,
) -> impl IntoView {
let radio_group_value = use_radio_group().0;
let checked = RwSignal::new(false);
let item_key = store_value(key);
let is_checked = Memo::new(move |_| {
radio_group_value.with(|value| item_key.with_value(|key| value.as_ref() == Some(key)))
});
Effect::new(move |prev| {
let checked = checked.get();
if prev.is_some() {
if !is_checked.get_untracked() {
radio_group_value.set(Some(item_key.get_value()))
}
}
checked
});
if let Some(children) = children {
view! {
<Radio class value=(is_checked, checked.write_only())>
{children()}
</Radio>
}
} else {
view! { <Radio class value=(is_checked, checked.write_only())/> }
}
}

View file

@ -35,6 +35,8 @@ pub struct ColorTheme {
pub color_neutral_stroke_accessible_hover: String, pub color_neutral_stroke_accessible_hover: String,
pub color_neutral_stroke_accessible_pressed: String, pub color_neutral_stroke_accessible_pressed: String,
pub color_compound_brand_foreground_1: String,
pub color_compound_brand_background: String, pub color_compound_brand_background: String,
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,
@ -90,6 +92,8 @@ impl ColorTheme {
color_neutral_stroke_accessible_hover: "#575757".into(), color_neutral_stroke_accessible_hover: "#575757".into(),
color_neutral_stroke_accessible_pressed: "#4d4d4d".into(), color_neutral_stroke_accessible_pressed: "#4d4d4d".into(),
color_compound_brand_foreground_1: "#0f6cbd".into(),
color_compound_brand_background: "#0f6cbd".into(), color_compound_brand_background: "#0f6cbd".into(),
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(),
@ -145,6 +149,8 @@ impl ColorTheme {
color_neutral_stroke_accessible_hover: "#bdbdbd".into(), color_neutral_stroke_accessible_hover: "#bdbdbd".into(),
color_neutral_stroke_accessible_pressed: "#b3b3b3".into(), color_neutral_stroke_accessible_pressed: "#b3b3b3".into(),
color_compound_brand_foreground_1: "#479ef5".into(),
color_compound_brand_background: "#479ef5".into(), color_compound_brand_background: "#479ef5".into(),
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(),