mirror of
https://github.com/adoyle0/thaw.git
synced 2025-01-23 06:19:22 -05:00
Feat/select component (#245)
* feat: adds Select component * fix: Select component initial value
This commit is contained in:
parent
98270c17ce
commit
ec056a6dec
11 changed files with 361 additions and 2 deletions
|
@ -98,6 +98,7 @@ fn TheRouter() -> impl IntoView {
|
||||||
<Route path=path!("/progress-bar") view=ProgressBarMdPage/>
|
<Route path=path!("/progress-bar") view=ProgressBarMdPage/>
|
||||||
<Route path=path!("/radio") view=RadioMdPage/>
|
<Route path=path!("/radio") view=RadioMdPage/>
|
||||||
<Route path=path!("/scrollbar") view=ScrollbarMdPage/>
|
<Route path=path!("/scrollbar") view=ScrollbarMdPage/>
|
||||||
|
<Route path=path!("/select") view=SelectMdPage/>
|
||||||
<Route path=path!("/skeleton") view=SkeletonMdPage/>
|
<Route path=path!("/skeleton") view=SkeletonMdPage/>
|
||||||
<Route path=path!("/slider") view=SliderMdPage/>
|
<Route path=path!("/slider") view=SliderMdPage/>
|
||||||
<Route path=path!("/space") view=SpaceMdPage/>
|
<Route path=path!("/space") view=SpaceMdPage/>
|
||||||
|
@ -107,10 +108,10 @@ fn TheRouter() -> impl IntoView {
|
||||||
<Route path=path!("/tab-list") view=TabListMdPage/>
|
<Route path=path!("/tab-list") view=TabListMdPage/>
|
||||||
<Route path=path!("/table") view=TableMdPage/>
|
<Route path=path!("/table") view=TableMdPage/>
|
||||||
<Route path=path!("/tag") view=TagMdPage/>
|
<Route path=path!("/tag") view=TagMdPage/>
|
||||||
<Route path=path!("/text") view=TextMdPage/>
|
|
||||||
<Route path=path!("/textarea") view=TextareaMdPage/>
|
|
||||||
}.into_inner()}
|
}.into_inner()}
|
||||||
{view!{
|
{view!{
|
||||||
|
<Route path=path!("/text") view=TextMdPage/>
|
||||||
|
<Route path=path!("/textarea") view=TextareaMdPage/>
|
||||||
<Route path=path!("/time-picker") view=TimePickerMdPage/>
|
<Route path=path!("/time-picker") view=TimePickerMdPage/>
|
||||||
<Route path=path!("/toast") view=ToastMdPage />
|
<Route path=path!("/toast") view=ToastMdPage />
|
||||||
<Route path=path!("/tooltip") view=TooltipMdPage />
|
<Route path=path!("/tooltip") view=TooltipMdPage />
|
||||||
|
|
|
@ -260,6 +260,10 @@ pub(crate) fn gen_nav_data() -> Vec<NavGroupOption> {
|
||||||
value: "/components/scrollbar",
|
value: "/components/scrollbar",
|
||||||
label: "Scrollbar",
|
label: "Scrollbar",
|
||||||
},
|
},
|
||||||
|
NavItemOption {
|
||||||
|
value: "/components/select",
|
||||||
|
label: "Select",
|
||||||
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
value: "/components/skeleton",
|
value: "/components/skeleton",
|
||||||
label: "Skeleton",
|
label: "Skeleton",
|
||||||
|
|
|
@ -57,6 +57,7 @@ pub fn include_md(_token_stream: proc_macro::TokenStream) -> proc_macro::TokenSt
|
||||||
"ProgressBarMdPage" => "../../thaw/src/progress_bar/docs/mod.md",
|
"ProgressBarMdPage" => "../../thaw/src/progress_bar/docs/mod.md",
|
||||||
"RadioMdPage" => "../../thaw/src/radio/docs/mod.md",
|
"RadioMdPage" => "../../thaw/src/radio/docs/mod.md",
|
||||||
"ScrollbarMdPage" => "../../thaw/src/scrollbar/docs/mod.md",
|
"ScrollbarMdPage" => "../../thaw/src/scrollbar/docs/mod.md",
|
||||||
|
"SelectMdPage" => "../../thaw/src/select/docs/mod.md",
|
||||||
"SkeletonMdPage" => "../../thaw/src/skeleton/docs/mod.md",
|
"SkeletonMdPage" => "../../thaw/src/skeleton/docs/mod.md",
|
||||||
"SliderMdPage" => "../../thaw/src/slider/docs/mod.md",
|
"SliderMdPage" => "../../thaw/src/slider/docs/mod.md",
|
||||||
"SpaceMdPage" => "../../thaw/src/space/docs/mod.md",
|
"SpaceMdPage" => "../../thaw/src/space/docs/mod.md",
|
||||||
|
|
|
@ -53,6 +53,13 @@ view! {
|
||||||
<ComboboxOption value="dog" text="Dog" />
|
<ComboboxOption value="dog" text="Dog" />
|
||||||
</Combobox>
|
</Combobox>
|
||||||
</Field>
|
</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">
|
<Field label="SpinButton" name="spinbutton">
|
||||||
<SpinButton
|
<SpinButton
|
||||||
step_page=1
|
step_page=1
|
||||||
|
|
19
thaw/src/icon/icons/mod.rs
Normal file
19
thaw/src/icon/icons/mod.rs
Normal 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>
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,9 @@
|
||||||
// copy https://github.com/Carlosted/leptos-icons
|
// copy https://github.com/Carlosted/leptos-icons
|
||||||
// leptos updated version causes leptos_icons error
|
// leptos updated version causes leptos_icons error
|
||||||
|
mod icons;
|
||||||
|
|
||||||
|
pub(crate) use icons::*;
|
||||||
|
|
||||||
use leptos::{ev, prelude::*};
|
use leptos::{ev, prelude::*};
|
||||||
use thaw_utils::{class_list, mount_style, ArcOneCallback};
|
use thaw_utils::{class_list, mount_style, ArcOneCallback};
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ mod popover;
|
||||||
mod progress_bar;
|
mod progress_bar;
|
||||||
mod radio;
|
mod radio;
|
||||||
mod scrollbar;
|
mod scrollbar;
|
||||||
|
mod select;
|
||||||
mod skeleton;
|
mod skeleton;
|
||||||
mod slider;
|
mod slider;
|
||||||
mod space;
|
mod space;
|
||||||
|
@ -86,6 +87,7 @@ pub use popover::*;
|
||||||
pub use progress_bar::*;
|
pub use progress_bar::*;
|
||||||
pub use radio::*;
|
pub use radio::*;
|
||||||
pub use scrollbar::*;
|
pub use scrollbar::*;
|
||||||
|
pub use select::*;
|
||||||
pub use skeleton::*;
|
pub use skeleton::*;
|
||||||
pub use slider::*;
|
pub use slider::*;
|
||||||
pub use space::*;
|
pub use space::*;
|
||||||
|
|
65
thaw/src/select/docs/mod.md
Normal file
65
thaw/src/select/docs/mod.md
Normal 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
3
thaw/src/select/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
mod select;
|
||||||
|
|
||||||
|
pub use select::*;
|
114
thaw/src/select/select.css
Normal file
114
thaw/src/select/select.css
Normal 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
139
thaw/src/select/select.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue