From 583b03100eb5f78f2847389705abe0f822487f49 Mon Sep 17 00:00:00 2001 From: luoxiaozero <48741584+luoxiaozero@users.noreply.github.com> Date: Sun, 8 Sep 2024 22:01:41 +0800 Subject: [PATCH] Feat/tag picker component (#251) * feat: adds TagPicker component * feat: improved TagPicker component * feat: improved TagPicker component * feat: adds TagGroup and InteractionTag * feat: Tag adds size prop * feat: TagGroup adds size prop * feat: change Tag's closable to dismissible * feat: improved TagPicker component * feat: improved TagPicker component * feat: improved TagPicker component --- demo/src/app.rs | 4 +- demo/src/components/site_header.rs | 2 +- demo/src/pages/components.rs | 129 ++++++++++++++- demo_markdown/src/lib.rs | 2 + thaw/src/combobox/listbox.rs | 11 +- thaw/src/lib.rs | 2 + thaw/src/tag/docs/mod.md | 79 +++++++-- thaw/src/tag/docs/tag_group/mod.md | 57 +++++++ thaw/src/tag/interaction-tag.css | 72 ++++++++ thaw/src/tag/interaction_tag.rs | 44 +++++ thaw/src/tag/mod.rs | 92 +++++++++-- thaw/src/tag/tag-group.css | 4 + thaw/src/tag/tag.css | 38 ++++- thaw/src/tag/tag_group.rs | 43 +++++ thaw/src/tag_picker/docs/mod.md | 70 ++++++++ thaw/src/tag_picker/mod.rs | 9 + thaw/src/tag_picker/tag-picker.css | 156 ++++++++++++++++++ thaw/src/tag_picker/tag_picker.rs | 199 +++++++++++++++++++++++ thaw/src/tag_picker/tag_picker_group.rs | 26 +++ thaw/src/tag_picker/tag_picker_input.rs | 55 +++++++ thaw/src/tag_picker/tag_picker_option.rs | 66 ++++++++ thaw_utils/src/on_click_outside.rs | 25 +-- 22 files changed, 1121 insertions(+), 64 deletions(-) create mode 100644 thaw/src/tag/docs/tag_group/mod.md create mode 100644 thaw/src/tag/interaction-tag.css create mode 100644 thaw/src/tag/interaction_tag.rs create mode 100644 thaw/src/tag/tag-group.css create mode 100644 thaw/src/tag/tag_group.rs create mode 100644 thaw/src/tag_picker/docs/mod.md create mode 100644 thaw/src/tag_picker/mod.rs create mode 100644 thaw/src/tag_picker/tag-picker.css create mode 100644 thaw/src/tag_picker/tag_picker.rs create mode 100644 thaw/src/tag_picker/tag_picker_group.rs create mode 100644 thaw/src/tag_picker/tag_picker_input.rs create mode 100644 thaw/src/tag_picker/tag_picker_option.rs diff --git a/demo/src/app.rs b/demo/src/app.rs index 1cd5050..2eb3a5b 100644 --- a/demo/src/app.rs +++ b/demo/src/app.rs @@ -107,9 +107,11 @@ fn TheRouter() -> impl IntoView { - }.into_inner()} {view!{ + + + diff --git a/demo/src/components/site_header.rs b/demo/src/components/site_header.rs index 8951741..4035bc2 100644 --- a/demo/src/components/site_header.rs +++ b/demo/src/components/site_header.rs @@ -196,7 +196,7 @@ pub fn SiteHeader() -> impl IntoView { { children.into_iter().map(|item| { - let NavItemOption { label, value } = item; + let NavItemOption { label, value, .. } = item; view! { {label} } diff --git a/demo/src/pages/components.rs b/demo/src/pages/components.rs index 5544929..11073eb 100644 --- a/demo/src/pages/components.rs +++ b/demo/src/pages/components.rs @@ -1,3 +1,5 @@ +use std::vec; + use crate::components::SiteHeader; use leptos::prelude::*; use leptos_meta::Style; @@ -5,6 +7,7 @@ use leptos_router::{ components::Outlet, hooks::{use_location, use_navigate}, }; +use tachys::view::any_view::AnyView; use thaw::*; #[component] @@ -66,14 +69,7 @@ pub fn ComponentsPage() -> impl IntoView { {label} - { - children.into_iter().map(|item| { - let NavItemOption { label, value } = item; - view! { - {label} - } - }).collect_view() - } + {VecIntoView::into_view(children)} } }).collect_view() } @@ -93,20 +89,77 @@ pub(crate) struct NavGroupOption { } pub(crate) struct NavItemOption { + pub group: Option<&'static str>, pub label: &'static str, pub value: &'static str, } +trait VecIntoView { + fn into_view(self) -> Vec>; +} + +impl VecIntoView for Vec { + fn into_view(self) -> Vec> { + let mut iter = self.into_iter().peekable(); + let mut views = vec![]; + while let Some(item) = iter.next() { + let NavItemOption { + group, + label, + value, + } = item; + if let Some(group) = group { + let mut sub_views = vec![]; + while let Some(item) = iter.peek() { + if item.group != Some(group) { + break; + } + let NavItemOption { label, value, .. } = iter.next().unwrap(); + sub_views.push(view! { + + {label} + + }); + } + views.push( + view! { + + + {group} + + + {label} + + {sub_views} + + } + .into_any(), + ); + } else { + views.push( + view! { + {label} + } + .into_any(), + ); + } + } + views + } +} + pub(crate) fn gen_nav_data() -> Vec { vec![ NavGroupOption { label: "Getting Started", children: vec![ NavItemOption { + group: None, value: "/guide/installation", label: "Installation", }, NavItemOption { + group: None, value: "/guide/server-sider-rendering", label: "Server Sider Rendering", }, @@ -125,202 +178,262 @@ pub(crate) fn gen_nav_data() -> Vec { label: "Components", children: vec![ NavItemOption { + group: None, value: "/components/accordion", label: "Accordion", }, NavItemOption { + group: None, value: "/components/anchor", label: "Anchor", }, NavItemOption { + group: None, value: "/components/auto-complete", label: "Auto Complete", }, NavItemOption { + group: None, value: "/components/avatar", label: "Avatar", }, NavItemOption { + group: None, value: "/components/back-top", label: "Back Top", }, NavItemOption { + group: None, value: "/components/badge", label: "Badge", }, NavItemOption { + group: None, value: "/components/breadcrumb", label: "Breadcrumb", }, NavItemOption { + group: None, value: "/components/button", label: "Button", }, NavItemOption { + group: None, value: "/components/calendar", label: "Calendar", }, NavItemOption { + group: None, value: "/components/card", label: "Card", }, NavItemOption { + group: None, value: "/components/checkbox", label: "Checkbox", }, NavItemOption { + group: None, value: "/components/color-picker", label: "Color Picker", }, NavItemOption { + group: None, value: "/components/combobox", label: "Combobox", }, NavItemOption { + group: None, value: "/components/config-provider", label: "Config Provider", }, NavItemOption { + group: None, value: "/components/date-picker", label: "Date Picker", }, NavItemOption { + group: None, value: "/components/dialog", label: "Dialog", }, NavItemOption { + group: None, value: "/components/divider", label: "Divider", }, NavItemOption { + group: None, value: "/components/drawer", label: "Drawer", }, NavItemOption { + group: None, value: "/components/field", label: "Field", }, NavItemOption { + group: None, value: "/components/grid", label: "Grid", }, NavItemOption { + group: None, value: "/components/icon", label: "Icon", }, NavItemOption { + group: None, value: "/components/image", label: "Image", }, NavItemOption { + group: None, value: "/components/input", label: "Input", }, NavItemOption { + group: None, value: "/components/layout", label: "Layout", }, NavItemOption { + group: None, value: "/components/link", label: "Link", }, NavItemOption { + group: None, value: "/components/loading-bar", label: "Loading Bar", }, NavItemOption { + group: None, value: "/components/menu", label: "Menu", }, NavItemOption { + group: None, value: "/components/message-bar", label: "Message Bar", }, NavItemOption { + group: None, value: "/components/nav", label: "Nav", }, NavItemOption { + group: None, value: "/components/pagination", label: "Pagination", }, NavItemOption { + group: None, value: "/components/popover", label: "Popover", }, NavItemOption { + group: None, value: "/components/progress-bar", label: "ProgressBar", }, NavItemOption { + group: None, value: "/components/radio", label: "Radio", }, NavItemOption { + group: None, value: "/components/scrollbar", label: "Scrollbar", }, NavItemOption { + group: None, value: "/components/select", label: "Select", }, NavItemOption { + group: None, value: "/components/skeleton", label: "Skeleton", }, NavItemOption { + group: None, value: "/components/slider", label: "Slider", }, NavItemOption { + group: None, value: "/components/space", label: "Space", }, NavItemOption { + group: None, value: "/components/spin-button", label: "Spin Button", }, NavItemOption { + group: None, value: "/components/spinner", label: "Spinner", }, NavItemOption { + group: None, value: "/components/switch", label: "Switch", }, NavItemOption { + group: None, value: "/components/tab-list", label: "Tab List", }, NavItemOption { + group: None, value: "/components/table", label: "Table", }, NavItemOption { + group: Some("Tag"), value: "/components/tag", label: "Tag", }, NavItemOption { + group: Some("Tag"), + value: "/components/tag-group", + label: "Tag Group", + }, + NavItemOption { + group: None, + value: "/components/tag-picker", + label: "Tag Picker", + }, + NavItemOption { + group: None, value: "/components/text", label: "Text", }, NavItemOption { + group: None, value: "/components/textarea", label: "Textarea", }, NavItemOption { + group: None, value: "/components/time-picker", label: "Time Picker", }, NavItemOption { + group: None, value: "/components/toast", label: "Toast", }, NavItemOption { + group: None, value: "/components/tooltip", label: "Tooltip", }, NavItemOption { + group: None, value: "/components/upload", label: "Upload", }, diff --git a/demo_markdown/src/lib.rs b/demo_markdown/src/lib.rs index 7f26f1e..ec48ca4 100644 --- a/demo_markdown/src/lib.rs +++ b/demo_markdown/src/lib.rs @@ -67,6 +67,8 @@ pub fn include_md(_token_stream: proc_macro::TokenStream) -> proc_macro::TokenSt "TabListMdPage" => "../../thaw/src/tab_list/docs/mod.md", "TableMdPage" => "../../thaw/src/table/docs/mod.md", "TagMdPage" => "../../thaw/src/tag/docs/mod.md", + "TagGroupMdPage" => "../../thaw/src/tag/docs/tag_group/mod.md", + "TagPickerMdPage" => "../../thaw/src/tag_picker/docs/mod.md", "TextMdPage" => "../../thaw/src/text/docs/mod.md", "TextareaMdPage" => "../../thaw/src/textarea/docs/mod.md", "TimePickerMdPage" => "../../thaw/src/time_picker/docs/mod.md", diff --git a/thaw/src/combobox/listbox.rs b/thaw/src/combobox/listbox.rs index f2cf5a8..8bb0685 100644 --- a/thaw/src/combobox/listbox.rs +++ b/thaw/src/combobox/listbox.rs @@ -3,7 +3,7 @@ use crate::{ConfigInjection, _aria::ActiveDescendantController}; use leptos::{context::Provider, ev, html, prelude::*}; use std::sync::Arc; use thaw_components::CSSTransition; -use thaw_utils::mount_style; +use thaw_utils::{mount_style, BoxCallback}; use web_sys::{HtmlElement, Node}; #[component] @@ -12,6 +12,7 @@ pub fn Listbox( class: &'static str, set_listbox: Arc, listbox_ref: NodeRef, + #[prop(optional)] on_hidden: StoredValue>, children: Children, ) -> impl IntoView { mount_style("listbox", include_str!("./listbox.css")); @@ -27,6 +28,13 @@ pub fn Listbox( } } }); + let on_after_leave = move || { + if let Some(list) = + on_hidden.try_update_value(|list| list.drain(..).collect::>()) + { + list.into_iter().for_each(|f| f()); + } + }; on_cleanup(move || { drop(effect); }); @@ -39,6 +47,7 @@ pub fn Listbox( appear=open.get_untracked() show=open let:display + on_after_leave >
"default" + + "default" + + + "Interaction Tag" + + + } ``` -### Closable +### Size ```rust demo -// let message = use_message(); -let success = move |_: ev::MouseEvent| { - // message.create( - // "tag close".into(), - // MessageVariant::Success, - // Default::default(), - // ); +view! { + + + "Medium" + "Small" + "Extra small" + + + "Medium" + "Small" + "Extra small" + + +} +``` + +### Dismiss + +```rust demo +let toaster = ToasterInjection::expect_context(); + +let on_dismiss = move |_| { + toaster.dispatch_toast(view! { + + "Tag" + + "Tag dismiss" + + + }.into_any(), Default::default()); }; view! { - "Default" + "Default" } ``` ### Tag Props -| Name | Type | Default | Description | -| -------- | ---------------------------------------- | -------------------- | ------------------------------------- | -| class | `MaybeProp` | `Default::default()` | | -| closable | `MaybeSignal` | `false` | Whether the tag shows a close button. | -| on_close | `Option>` | `None` | Close clicked callback. | -| children | `Children` | | | +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| class | `MaybeProp` | `Default::default()` | | +| size | `Option>` | `None` | Size of the tag. | +| dismissible | `MaybeSignal` | `false` | A Tag can be dismissible. | +| on_dismiss | `Option>` | `None` | Callback for when a tag is dismissed. | +| value | `Option` | `None` | Unique value identifying the tag within a TagGroup. | +| children | `Children` | | | + +### InteractionTag Props + +| Name | Type | Default | Description | +| -------- | ------------------------------ | -------------------- | ---------------- | +| class | `MaybeProp` | `Default::default()` | | +| size | `Option>` | `None` | Size of the tag. | +| children | `Children` | | | + +### InteractionTagPrimary Props + +| Name | Type | Default | Description | +| -------- | ------------------- | -------------------- | ----------- | +| class | `MaybeProp` | `Default::default()` | | +| children | `Children` | | | diff --git a/thaw/src/tag/docs/tag_group/mod.md b/thaw/src/tag/docs/tag_group/mod.md new file mode 100644 index 0000000..acdad1b --- /dev/null +++ b/thaw/src/tag/docs/tag_group/mod.md @@ -0,0 +1,57 @@ +# Tag Group + +```rust demo +view! { + + + "Tag 1" + "Tag 2" + "Tag 3" + + + + "Tag 1" + + + "Tag 2" + + + "Tag 3" + + + +} +``` + +### Sizes + +```rust demo +view! { + + + "Tag 1" + "Tag 1" + + "Tag 1" + + + + "Tag 1" + "Tag 1" + + "Tag 1" + + + +} +``` + +### TagGroup Props + +| Name | Type | Default | Description | +| ----------- | -------------------------------- | -------------------- | ------------------------------------- | +| class | `MaybeProp` | `Default::default()` | | +| size | `MaybeSignal` | `TagSize::Medium` | Size of the tag. | +| dismissible | `MaybeSignal` | `false` | A Tag can be dismissible. | +| on_dismiss | `Option>` | `None` | Callback for when a tag is dismissed. | +| children | `Children` | | | diff --git a/thaw/src/tag/interaction-tag.css b/thaw/src/tag/interaction-tag.css new file mode 100644 index 0000000..af9359c --- /dev/null +++ b/thaw/src/tag/interaction-tag.css @@ -0,0 +1,72 @@ +.thaw-interaction-tag { + display: inline-flex; + align-items: center; + box-sizing: border-box; + width: fit-content; + height: 32px; + border-radius: var(--borderRadiusMedium); +} + +.thaw-interaction-tag--small { + height: 24px; +} + +.thaw-interaction-tag--extra-small { + height: 20px; +} + +.thaw-interaction-tag-primary { + display: inline-grid; + height: 100%; + align-items: center; + grid-template-areas: + "media primary" + "media secondary"; + padding: 0 7px; + background-color: var(--colorNeutralBackground3); + color: var(--colorNeutralForeground2); + font-family: inherit; + appearance: button; + text-align: unset; + border: var(--strokeWidthThin) solid var(--colorTransparentStroke); + border-radius: var(--borderRadiusMedium); +} + +.thaw-interaction-tag--small .thaw-interaction-tag-primary { + padding: 0 5px; +} + +.thaw-interaction-tag--extra-small .thaw-interaction-tag-primary { + padding: 0 5px; +} + +.thaw-interaction-tag-primary:hover { + color: var(--colorNeutralForeground2Hover); + background-color: var(--colorNeutralBackground3Hover); + cursor: pointer; +} + +.thaw-interaction-tag-primary:active { + color: var(--colorNeutralForeground2Pressed); + background-color: var(--colorNeutralBackground3Pressed); +} + +.thaw-interaction-tag-primary__primary-text { + grid-row-end: secondary; + grid-row-start: primary; + grid-column-start: primary; + white-space: nowrap; + padding-left: var(--spacingHorizontalXXS); + padding-right: var(--spacingHorizontalXXS); + padding-bottom: var(--spacingHorizontalXXS); + line-height: var(--lineHeightBase300); + font-weight: var(--fontWeightRegular); + font-size: var(--fontSizeBase300); + font-family: var(--fontFamilyBase); +} + +.thaw-interaction-tag--extra-small .thaw-interaction-tag-primary__primary-text, +.thaw-interaction-tag--extra-small .thaw-interaction-tag-primary__primary-text { + font-size: var(--fontSizeBase200); + line-height: var(--lineHeightBase200); +} diff --git a/thaw/src/tag/interaction_tag.rs b/thaw/src/tag/interaction_tag.rs new file mode 100644 index 0000000..6b7bfdb --- /dev/null +++ b/thaw/src/tag/interaction_tag.rs @@ -0,0 +1,44 @@ +use leptos::prelude::*; +use thaw_utils::{class_list, mount_style}; +use super::{TagSize, TagGroupInjection}; + +#[component] +pub fn InteractionTag( + #[prop(optional, into)] class: MaybeProp, + /// Size of the tag. + #[prop(optional, into)] + size: Option>, + children: Children, +) -> impl IntoView { + mount_style("interaction-tag", include_str!("./interaction-tag.css")); + let tag_group = TagGroupInjection::use_context(); + let size_class = { + if let Some(size) = size { + Some(size) + } else if let Some(tag_group) = tag_group { + Some(tag_group.size) + } else { + None + } + }; + + view! { +
{children()}
+ } +} + +#[component] +pub fn InteractionTagPrimary( + #[prop(optional, into)] class: MaybeProp, + children: Children, +) -> impl IntoView { + view! { + + } +} diff --git a/thaw/src/tag/mod.rs b/thaw/src/tag/mod.rs index f0edb14..6cfb9eb 100644 --- a/thaw/src/tag/mod.rs +++ b/thaw/src/tag/mod.rs @@ -1,36 +1,84 @@ +mod interaction_tag; +mod tag_group; + +pub use interaction_tag::*; +pub use tag_group::*; + use leptos::{either::Either, ev, prelude::*}; use thaw_utils::{class_list, mount_style, ArcOneCallback}; #[component] pub fn Tag( #[prop(optional, into)] class: MaybeProp, - /// Whether the tag shows a close button. + /// Size of the tag. #[prop(optional, into)] - closable: MaybeSignal, - /// Close clicked callback. + size: Option>, + /// A Tag can be dismissible. #[prop(optional, into)] - on_close: Option>, + dismissible: MaybeSignal, + /// Callback for when a tag is dismissed. + #[prop(optional, into)] + on_dismiss: Option>, + /// Unique value identifying the tag within a TagGroup. + #[prop(optional, into)] + value: Option, children: Children, ) -> impl IntoView { mount_style("tag", include_str!("./tag.css")); + let (group_size, group_on_dismiss, group_dismissible) = TagGroupInjection::use_context() + .map( + |TagGroupInjection { + size, + on_dismiss, + dismissible, + }| { + if value.is_none() { + (Some(size), None, None) + } else { + (Some(size), on_dismiss, Some(dismissible)) + } + }, + ) + .unwrap_or_default(); + + let size_class = { + if let Some(size) = size { + Some(size) + } else if let Some(group_size) = group_size { + Some(group_size) + } else { + None + } + }; view! { - + {children()} {move || { - let on_close = on_close.clone(); - let on_close = move |event| { - let Some(on_close) = on_close.as_ref() else { - return; + if group_dismissible.map_or_else(|| dismissible.get(), |d| d.get()) { + let on_dismiss = on_dismiss.clone(); + let group_on_dismiss = group_on_dismiss.clone(); + let value = value.clone(); + let on_dismiss = move |event: ev::MouseEvent| { + if let Some(on_dismiss) = group_on_dismiss.as_ref() { + event.prevent_default(); + on_dismiss(value.clone().unwrap()); + } + let Some(on_dismiss) = on_dismiss.as_ref() else { + return; + }; + on_dismiss(event); }; - on_close(event); - }; - if closable.get() { Either::Left( view! { -