From ec056a6decea4b0649cd2eac615e197bd345f597 Mon Sep 17 00:00:00 2001 From: luoxiaozero <48741584+luoxiaozero@users.noreply.github.com> Date: Sun, 1 Sep 2024 13:37:45 +0800 Subject: [PATCH] Feat/select component (#245) * feat: adds Select component * fix: Select component initial value --- demo/src/app.rs | 5 +- demo/src/pages/components.rs | 4 + demo_markdown/src/lib.rs | 1 + thaw/src/field/docs/mod.md | 7 ++ thaw/src/icon/icons/mod.rs | 19 +++++ thaw/src/icon/mod.rs | 4 + thaw/src/lib.rs | 2 + thaw/src/select/docs/mod.md | 65 ++++++++++++++++ thaw/src/select/mod.rs | 3 + thaw/src/select/select.css | 114 ++++++++++++++++++++++++++++ thaw/src/select/select.rs | 139 +++++++++++++++++++++++++++++++++++ 11 files changed, 361 insertions(+), 2 deletions(-) create mode 100644 thaw/src/icon/icons/mod.rs create mode 100644 thaw/src/select/docs/mod.md create mode 100644 thaw/src/select/mod.rs create mode 100644 thaw/src/select/select.css create mode 100644 thaw/src/select/select.rs diff --git a/demo/src/app.rs b/demo/src/app.rs index 1ab9c20..1cd5050 100644 --- a/demo/src/app.rs +++ b/demo/src/app.rs @@ -98,6 +98,7 @@ fn TheRouter() -> impl IntoView { + @@ -107,10 +108,10 @@ fn TheRouter() -> impl IntoView { - - }.into_inner()} {view!{ + + diff --git a/demo/src/pages/components.rs b/demo/src/pages/components.rs index d7567ea..5544929 100644 --- a/demo/src/pages/components.rs +++ b/demo/src/pages/components.rs @@ -260,6 +260,10 @@ pub(crate) fn gen_nav_data() -> Vec { value: "/components/scrollbar", label: "Scrollbar", }, + NavItemOption { + value: "/components/select", + label: "Select", + }, NavItemOption { value: "/components/skeleton", label: "Skeleton", diff --git a/demo_markdown/src/lib.rs b/demo_markdown/src/lib.rs index c83793b..7f26f1e 100644 --- a/demo_markdown/src/lib.rs +++ b/demo_markdown/src/lib.rs @@ -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", diff --git a/thaw/src/field/docs/mod.md b/thaw/src/field/docs/mod.md index 8b07d03..b1070be 100644 --- a/thaw/src/field/docs/mod.md +++ b/thaw/src/field/docs/mod.md @@ -53,6 +53,13 @@ view! { + + + impl IntoView { + view! { + + } +} diff --git a/thaw/src/icon/mod.rs b/thaw/src/icon/mod.rs index 5900085..c29864f 100644 --- a/thaw/src/icon/mod.rs +++ b/thaw/src/icon/mod.rs @@ -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}; diff --git a/thaw/src/lib.rs b/thaw/src/lib.rs index 7c222e5..97d7570 100644 --- a/thaw/src/lib.rs +++ b/thaw/src/lib.rs @@ -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::*; diff --git a/thaw/src/select/docs/mod.md b/thaw/src/select/docs/mod.md new file mode 100644 index 0000000..4d6f9a3 --- /dev/null +++ b/thaw/src/select/docs/mod.md @@ -0,0 +1,65 @@ +# Select + +```rust demo +view! { + +} +``` + +### Controlled + +```rust demo +let value = RwSignal::new("Red".to_string()); + +view! { + + +} +``` + +### Disabled + +```rust demo +view! { + +} +``` + +### Initial Value + +```rust demo +view! { + +} +``` + +### Select Props + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| class | `MaybeProp` | `Default::default()` | | +| id | `MaybeProp` | `Default::default()` | | +| name | `MaybeProp` | `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` | `vec![]` | The rules to validate Field. | +| value | `Model` | `Default::default()` | Set the select value. | +| default_value | `Option` | `None` | | +| disabled | `MaybeSignal` | `false` | Whether the select is disabled. | +| children | `Children` | | | diff --git a/thaw/src/select/mod.rs b/thaw/src/select/mod.rs new file mode 100644 index 0000000..0feca8a --- /dev/null +++ b/thaw/src/select/mod.rs @@ -0,0 +1,3 @@ +mod select; + +pub use select::*; \ No newline at end of file diff --git a/thaw/src/select/select.css b/thaw/src/select/select.css new file mode 100644 index 0000000..26b15b5 --- /dev/null +++ b/thaw/src/select/select.css @@ -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); +} diff --git a/thaw/src/select/select.rs b/thaw/src/select/select.rs new file mode 100644 index 0000000..3bd878f --- /dev/null +++ b/thaw/src/select/select.rs @@ -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, + #[prop(optional, into)] id: MaybeProp, + #[prop(optional, into)] rules: Vec, + /// 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, + #[prop(optional, into)] value: Model, + #[prop(optional, into)] default_value: Option, + /// Whether the select is disabled. + #[prop(optional, into)] + disabled: MaybeSignal, + 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::::new(); + Effect::new(move |prev: Option| { + 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! { + + + + + + + } +} + +#[derive(Debug, Default, PartialEq, Clone, Copy)] +pub enum SelectRuleTrigger { + #[default] + Change, +} + +pub struct SelectRule(Rule); + +impl SelectRule { + pub fn required(required: MaybeSignal) -> 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, + message: MaybeSignal, + ) -> 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>) -> 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; + + fn deref(&self) -> &Self::Target { + &self.0 + } +}