Feat/select component (#245)

* feat: adds Select component

* fix: Select component initial value
This commit is contained in:
luoxiaozero 2024-09-01 13:37:45 +08:00 committed by GitHub
parent 98270c17ce
commit ec056a6dec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 361 additions and 2 deletions

View file

@ -98,6 +98,7 @@ fn TheRouter() -> impl IntoView {
<Route path=path!("/progress-bar") view=ProgressBarMdPage/>
<Route path=path!("/radio") view=RadioMdPage/>
<Route path=path!("/scrollbar") view=ScrollbarMdPage/>
<Route path=path!("/select") view=SelectMdPage/>
<Route path=path!("/skeleton") view=SkeletonMdPage/>
<Route path=path!("/slider") view=SliderMdPage/>
<Route path=path!("/space") view=SpaceMdPage/>
@ -107,10 +108,10 @@ fn TheRouter() -> impl IntoView {
<Route path=path!("/tab-list") view=TabListMdPage/>
<Route path=path!("/table") view=TableMdPage/>
<Route path=path!("/tag") view=TagMdPage/>
<Route path=path!("/text") view=TextMdPage/>
<Route path=path!("/textarea") view=TextareaMdPage/>
}.into_inner()}
{view!{
<Route path=path!("/text") view=TextMdPage/>
<Route path=path!("/textarea") view=TextareaMdPage/>
<Route path=path!("/time-picker") view=TimePickerMdPage/>
<Route path=path!("/toast") view=ToastMdPage />
<Route path=path!("/tooltip") view=TooltipMdPage />

View file

@ -260,6 +260,10 @@ pub(crate) fn gen_nav_data() -> Vec<NavGroupOption> {
value: "/components/scrollbar",
label: "Scrollbar",
},
NavItemOption {
value: "/components/select",
label: "Select",
},
NavItemOption {
value: "/components/skeleton",
label: "Skeleton",

View file

@ -57,6 +57,7 @@ pub fn include_md(_token_stream: proc_macro::TokenStream) -> proc_macro::TokenSt
"ProgressBarMdPage" => "../../thaw/src/progress_bar/docs/mod.md",
"RadioMdPage" => "../../thaw/src/radio/docs/mod.md",
"ScrollbarMdPage" => "../../thaw/src/scrollbar/docs/mod.md",
"SelectMdPage" => "../../thaw/src/select/docs/mod.md",
"SkeletonMdPage" => "../../thaw/src/skeleton/docs/mod.md",
"SliderMdPage" => "../../thaw/src/slider/docs/mod.md",
"SpaceMdPage" => "../../thaw/src/space/docs/mod.md",

View file

@ -53,6 +53,13 @@ view! {
<ComboboxOption value="dog" text="Dog" />
</Combobox>
</Field>
<Field label="Select" name="select">
<Select rules=vec![SelectRule::required(true.into())] >
<option>"Red"</option>
<option>"Green"</option>
<option>"Blue"</option>
</Select>
</Field>
<Field label="SpinButton" name="spinbutton">
<SpinButton
step_page=1

View file

@ -0,0 +1,19 @@
use leptos::prelude::*;
#[component]
pub fn ChevronDownRegularIcon() -> impl IntoView {
view! {
<svg
fill="currentColor"
aria-hidden="true"
width="1em"
height="1em"
viewBox="0 0 20 20"
>
<path
d="M15.85 7.65c.2.2.2.5 0 .7l-5.46 5.49a.55.55 0 0 1-.78 0L4.15 8.35a.5.5 0 1 1 .7-.7L10 12.8l5.15-5.16c.2-.2.5-.2.7 0Z"
fill="currentColor"
></path>
</svg>
}
}

View file

@ -1,5 +1,9 @@
// copy https://github.com/Carlosted/leptos-icons
// leptos updated version causes leptos_icons error
mod icons;
pub(crate) use icons::*;
use leptos::{ev, prelude::*};
use thaw_utils::{class_list, mount_style, ArcOneCallback};

View file

@ -34,6 +34,7 @@ mod popover;
mod progress_bar;
mod radio;
mod scrollbar;
mod select;
mod skeleton;
mod slider;
mod space;
@ -86,6 +87,7 @@ pub use popover::*;
pub use progress_bar::*;
pub use radio::*;
pub use scrollbar::*;
pub use select::*;
pub use skeleton::*;
pub use slider::*;
pub use space::*;

View file

@ -0,0 +1,65 @@
# Select
```rust demo
view! {
<Select>
<option>"Red"</option>
<option>"Green"</option>
<option>"Blue"</option>
</Select>
}
```
### Controlled
```rust demo
let value = RwSignal::new("Red".to_string());
view! {
<Select value>
<option>"Red"</option>
<option>"Green"</option>
<option>"Blue"</option>
</Select>
<Button on_click=move |_| value.set("Blue".to_string())>
"Select Blue"
</Button>
}
```
### Disabled
```rust demo
view! {
<Select disabled=true>
<option>"Red"</option>
<option>"Green"</option>
<option>"Blue"</option>
</Select>
}
```
### Initial Value
```rust demo
view! {
<Select default_value="Green">
<option>"Red"</option>
<option>"Green"</option>
<option>"Blue"</option>
</Select>
}
```
### Select Props
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| class | `MaybeProp<String>` | `Default::default()` | |
| id | `MaybeProp<String>` | `Default::default()` | |
| name | `MaybeProp<String>` | `Default::default()` | 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. |
| rules | `Vec<SelectRule>` | `vec![]` | The rules to validate Field. |
| value | `Model<String>` | `Default::default()` | Set the select value. |
| default_value | `Option<String>` | `None` | |
| disabled | `MaybeSignal<bool>` | `false` | Whether the select is disabled. |
| children | `Children` | | |

3
thaw/src/select/mod.rs Normal file
View file

@ -0,0 +1,3 @@
mod select;
pub use select::*;

114
thaw/src/select/select.css Normal file
View file

@ -0,0 +1,114 @@
.thaw-select {
position: relative;
display: flex;
flex-wrap: nowrap;
align-items: center;
box-sizing: border-box;
font-family: var(--fontFamilyBase);
}
.thaw-select::after {
content: "";
position: absolute;
right: 0px;
left: 0px;
bottom: 0px;
height: var(--borderRadiusMedium);
background-image: linear-gradient(
0deg,
var(--colorCompoundBrandStroke) 0%,
var(--colorCompoundBrandStroke) 50%,
transparent 50%,
transparent 100%
);
transition-delay: var(--curveAccelerateMid);
transition-duration: var(--durationUltraFast);
transition-property: transform;
transform: scaleX(0);
box-sizing: border-box;
border-radius: 0 0 var(--borderRadiusMedium) var(--borderRadiusMedium);
}
.thaw-select:focus-within::after {
transition-delay: var(--curveDecelerateMid);
transition-duration: var(--durationNormal);
transition-property: transform;
transform: scaleX(1);
}
.thaw-select__select {
flex-grow: 1;
padding-right: calc(
var(--spacingHorizontalMNudge) + 20px + var(--spacingHorizontalXXS) +
var(--spacingHorizontalXXS)
);
padding-left: calc(
var(--spacingHorizontalMNudge) + var(--spacingHorizontalXXS)
);
padding-top: 0px;
padding-bottom: 0px;
max-width: 100%;
height: 32px;
background-color: var(--colorNeutralBackground1);
color: var(--colorNeutralForeground1);
line-height: var(--lineHeightBase300);
font-weight: var(--fontWeightRegular);
font-size: var(--fontSizeBase300);
font-family: var(--fontFamilyBase);
border-radius: var(--borderRadiusMedium);
border: 1px solid var(--colorNeutralStroke1);
border-bottom-color: var(--colorNeutralStrokeAccessible);
box-shadow: none;
appearance: none;
box-sizing: border-box;
cursor: pointer;
}
.thaw-select__select:focus {
outline-color: transparent;
outline-style: solid;
outline-width: 2px;
}
.thaw-select:hover {
border-bottom-color: var(--colorNeutralStrokeAccessible);
border-left-color: var(--colorNeutralStroke1Hover);
border-right-color: var(--colorNeutralStroke1Hover);
border-top-color: var(--colorNeutralStroke1Hover);
}
.thaw-select:active {
border-bottom-color: var(--colorNeutralStrokeAccessible);
border-left-color: var(--colorNeutralStroke1Pressed);
border-right-color: var(--colorNeutralStroke1Pressed);
border-top-color: var(--colorNeutralStroke1Pressed);
}
.thaw-select__icon {
position: absolute;
width: 20px;
height: 20px;
right: var(--spacingHorizontalMNudge);
display: block;
pointer-events: none;
color: var(--colorNeutralStrokeAccessible);
box-sizing: border-box;
font-size: 20px;
}
.thaw-select__icon svg {
display: block;
line-height: 0;
}
.thaw-select--disabled > .thaw-select__select {
border-color: var(--colorNeutralStrokeDisabled);
border-bottom-color: var(--colorNeutralStrokeDisabled);
background-color: var(--colorTransparentBackground);
color: var(--colorNeutralForegroundDisabled);
cursor: not-allowed;
}
.thaw-select--disabled > .thaw-select__icon {
color: var(--colorNeutralForegroundDisabled);
}

139
thaw/src/select/select.rs Normal file
View file

@ -0,0 +1,139 @@
use crate::{icon::ChevronDownRegularIcon, FieldInjection, FieldValidationState, Rule};
use leptos::{html, prelude::*};
use std::ops::Deref;
use thaw_utils::{class_list, mount_style, Model};
#[component]
pub fn Select(
#[prop(optional, into)] class: MaybeProp<String>,
#[prop(optional, into)] id: MaybeProp<String>,
#[prop(optional, into)] rules: Vec<SelectRule>,
/// 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<String>,
#[prop(optional, into)] value: Model<String>,
#[prop(optional, into)] default_value: Option<String>,
/// Whether the select is disabled.
#[prop(optional, into)]
disabled: MaybeSignal<bool>,
children: Children,
) -> impl IntoView {
mount_style("select", include_str!("./select.css"));
let (id, name) = FieldInjection::use_id_and_name(id, name);
let validate = Rule::validate(rules, value, name);
let select_ref = NodeRef::<html::Select>::new();
Effect::new(move |prev: Option<bool>| {
let Some(el) = select_ref.get() else {
return false;
};
let el_value = el.value();
if !prev.unwrap_or_default() {
if let Some(default_value) = default_value.as_ref() {
el.set_value(default_value);
value.set(default_value.clone());
} else {
value.set(el_value);
}
value.with(|_| {});
return true;
}
value.with(|value| {
if value != &el_value {
el.set_value(value);
}
});
true
});
let on_change = move |_| {
let Some(el) = select_ref.get() else {
return;
};
value.set(el.value());
validate.run(Some(SelectRuleTrigger::Change));
};
view! {
<span
class=class_list![
"thaw-select",
("thaw-select--disabled", move || disabled.get()),
class
]
>
<select
class="thaw-select__select"
id=id
name=name
node_ref=select_ref
disabled=disabled
on:change=on_change
>
{children()}
</select>
<span class="thaw-select__icon">
<ChevronDownRegularIcon />
</span>
</span>
}
}
#[derive(Debug, Default, PartialEq, Clone, Copy)]
pub enum SelectRuleTrigger {
#[default]
Change,
}
pub struct SelectRule(Rule<String, SelectRuleTrigger>);
impl SelectRule {
pub fn required(required: MaybeSignal<bool>) -> Self {
Self::validator(move |value, name| {
if required.get_untracked() && value.is_empty() {
let message = name.get_untracked().map_or_else(
|| String::from("Please select!"),
|name| format!("Please select {name}!"),
);
Err(FieldValidationState::Error(message))
} else {
Ok(())
}
})
}
pub fn required_with_message(
required: MaybeSignal<bool>,
message: MaybeSignal<String>,
) -> Self {
Self::validator(move |value, _| {
if required.get_untracked() && value.is_empty() {
Err(FieldValidationState::Error(message.get_untracked()))
} else {
Ok(())
}
})
}
pub fn validator(
f: impl Fn(&String, Signal<Option<String>>) -> Result<(), FieldValidationState>
+ Send
+ Sync
+ 'static,
) -> Self {
Self(Rule::validator(f))
}
pub fn with_trigger(self, trigger: SelectRuleTrigger) -> Self {
Self(Rule::with_trigger(self.0, trigger))
}
}
impl Deref for SelectRule {
type Target = Rule<String, SelectRuleTrigger>;
fn deref(&self) -> &Self::Target {
&self.0
}
}