mirror of
https://github.com/adoyle0/thaw.git
synced 2025-01-22 22:09:22 -05:00
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
This commit is contained in:
parent
1ab9dd4c9f
commit
583b03100e
22 changed files with 1121 additions and 64 deletions
|
@ -107,9 +107,11 @@ fn TheRouter() -> impl IntoView {
|
||||||
<Route path=path!("/switch") view=SwitchMdPage/>
|
<Route path=path!("/switch") view=SwitchMdPage/>
|
||||||
<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/>
|
|
||||||
}.into_inner()}
|
}.into_inner()}
|
||||||
{view!{
|
{view!{
|
||||||
|
<Route path=path!("/tag") view=TagMdPage/>
|
||||||
|
<Route path=path!("/tag-group") view=TagGroupMdPage/>
|
||||||
|
<Route path=path!("/tag-picker") view=TagPickerMdPage/>
|
||||||
<Route path=path!("/text") view=TextMdPage/>
|
<Route path=path!("/text") view=TextMdPage/>
|
||||||
<Route path=path!("/textarea") view=TextareaMdPage/>
|
<Route path=path!("/textarea") view=TextareaMdPage/>
|
||||||
<Route path=path!("/time-picker") view=TimePickerMdPage/>
|
<Route path=path!("/time-picker") view=TimePickerMdPage/>
|
||||||
|
|
|
@ -196,7 +196,7 @@ pub fn SiteHeader() -> impl IntoView {
|
||||||
</Caption1Strong>
|
</Caption1Strong>
|
||||||
{
|
{
|
||||||
children.into_iter().map(|item| {
|
children.into_iter().map(|item| {
|
||||||
let NavItemOption { label, value } = item;
|
let NavItemOption { label, value, .. } = item;
|
||||||
view! {
|
view! {
|
||||||
<MenuItem value=value>{label}</MenuItem>
|
<MenuItem value=value>{label}</MenuItem>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::vec;
|
||||||
|
|
||||||
use crate::components::SiteHeader;
|
use crate::components::SiteHeader;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos_meta::Style;
|
use leptos_meta::Style;
|
||||||
|
@ -5,6 +7,7 @@ use leptos_router::{
|
||||||
components::Outlet,
|
components::Outlet,
|
||||||
hooks::{use_location, use_navigate},
|
hooks::{use_location, use_navigate},
|
||||||
};
|
};
|
||||||
|
use tachys::view::any_view::AnyView;
|
||||||
use thaw::*;
|
use thaw::*;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
|
@ -66,14 +69,7 @@ pub fn ComponentsPage() -> impl IntoView {
|
||||||
<Caption1Strong style="margin-inline-start: 10px; margin-top: 10px; display: inline-block">
|
<Caption1Strong style="margin-inline-start: 10px; margin-top: 10px; display: inline-block">
|
||||||
{label}
|
{label}
|
||||||
</Caption1Strong>
|
</Caption1Strong>
|
||||||
{
|
{VecIntoView::into_view(children)}
|
||||||
children.into_iter().map(|item| {
|
|
||||||
let NavItemOption { label, value } = item;
|
|
||||||
view! {
|
|
||||||
<NavItem value>{label}</NavItem>
|
|
||||||
}
|
|
||||||
}).collect_view()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}).collect_view()
|
}).collect_view()
|
||||||
}
|
}
|
||||||
|
@ -93,20 +89,77 @@ pub(crate) struct NavGroupOption {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct NavItemOption {
|
pub(crate) struct NavItemOption {
|
||||||
|
pub group: Option<&'static str>,
|
||||||
pub label: &'static str,
|
pub label: &'static str,
|
||||||
pub value: &'static str,
|
pub value: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trait VecIntoView {
|
||||||
|
fn into_view(self) -> Vec<AnyView<Dom>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VecIntoView for Vec<NavItemOption> {
|
||||||
|
fn into_view(self) -> Vec<AnyView<Dom>> {
|
||||||
|
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! {
|
||||||
|
<NavSubItem value=value>
|
||||||
|
{label}
|
||||||
|
</NavSubItem>
|
||||||
|
});
|
||||||
|
}
|
||||||
|
views.push(
|
||||||
|
view! {
|
||||||
|
<NavCategory value=group>
|
||||||
|
<NavCategoryItem slot>
|
||||||
|
{group}
|
||||||
|
</NavCategoryItem>
|
||||||
|
<NavSubItem value=value>
|
||||||
|
{label}
|
||||||
|
</NavSubItem>
|
||||||
|
{sub_views}
|
||||||
|
</NavCategory>
|
||||||
|
}
|
||||||
|
.into_any(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
views.push(
|
||||||
|
view! {
|
||||||
|
<NavItem value>{label}</NavItem>
|
||||||
|
}
|
||||||
|
.into_any(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
views
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn gen_nav_data() -> Vec<NavGroupOption> {
|
pub(crate) fn gen_nav_data() -> Vec<NavGroupOption> {
|
||||||
vec![
|
vec![
|
||||||
NavGroupOption {
|
NavGroupOption {
|
||||||
label: "Getting Started",
|
label: "Getting Started",
|
||||||
children: vec![
|
children: vec![
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/guide/installation",
|
value: "/guide/installation",
|
||||||
label: "Installation",
|
label: "Installation",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/guide/server-sider-rendering",
|
value: "/guide/server-sider-rendering",
|
||||||
label: "Server Sider Rendering",
|
label: "Server Sider Rendering",
|
||||||
},
|
},
|
||||||
|
@ -125,202 +178,262 @@ pub(crate) fn gen_nav_data() -> Vec<NavGroupOption> {
|
||||||
label: "Components",
|
label: "Components",
|
||||||
children: vec![
|
children: vec![
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/accordion",
|
value: "/components/accordion",
|
||||||
label: "Accordion",
|
label: "Accordion",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/anchor",
|
value: "/components/anchor",
|
||||||
label: "Anchor",
|
label: "Anchor",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/auto-complete",
|
value: "/components/auto-complete",
|
||||||
label: "Auto Complete",
|
label: "Auto Complete",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/avatar",
|
value: "/components/avatar",
|
||||||
label: "Avatar",
|
label: "Avatar",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/back-top",
|
value: "/components/back-top",
|
||||||
label: "Back Top",
|
label: "Back Top",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/badge",
|
value: "/components/badge",
|
||||||
label: "Badge",
|
label: "Badge",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/breadcrumb",
|
value: "/components/breadcrumb",
|
||||||
label: "Breadcrumb",
|
label: "Breadcrumb",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/button",
|
value: "/components/button",
|
||||||
label: "Button",
|
label: "Button",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/calendar",
|
value: "/components/calendar",
|
||||||
label: "Calendar",
|
label: "Calendar",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/card",
|
value: "/components/card",
|
||||||
label: "Card",
|
label: "Card",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/checkbox",
|
value: "/components/checkbox",
|
||||||
label: "Checkbox",
|
label: "Checkbox",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/color-picker",
|
value: "/components/color-picker",
|
||||||
label: "Color Picker",
|
label: "Color Picker",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/combobox",
|
value: "/components/combobox",
|
||||||
label: "Combobox",
|
label: "Combobox",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/config-provider",
|
value: "/components/config-provider",
|
||||||
label: "Config Provider",
|
label: "Config Provider",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/date-picker",
|
value: "/components/date-picker",
|
||||||
label: "Date Picker",
|
label: "Date Picker",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/dialog",
|
value: "/components/dialog",
|
||||||
label: "Dialog",
|
label: "Dialog",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/divider",
|
value: "/components/divider",
|
||||||
label: "Divider",
|
label: "Divider",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/drawer",
|
value: "/components/drawer",
|
||||||
label: "Drawer",
|
label: "Drawer",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/field",
|
value: "/components/field",
|
||||||
label: "Field",
|
label: "Field",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/grid",
|
value: "/components/grid",
|
||||||
label: "Grid",
|
label: "Grid",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/icon",
|
value: "/components/icon",
|
||||||
label: "Icon",
|
label: "Icon",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/image",
|
value: "/components/image",
|
||||||
label: "Image",
|
label: "Image",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/input",
|
value: "/components/input",
|
||||||
label: "Input",
|
label: "Input",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/layout",
|
value: "/components/layout",
|
||||||
label: "Layout",
|
label: "Layout",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/link",
|
value: "/components/link",
|
||||||
label: "Link",
|
label: "Link",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/loading-bar",
|
value: "/components/loading-bar",
|
||||||
label: "Loading Bar",
|
label: "Loading Bar",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/menu",
|
value: "/components/menu",
|
||||||
label: "Menu",
|
label: "Menu",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/message-bar",
|
value: "/components/message-bar",
|
||||||
label: "Message Bar",
|
label: "Message Bar",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/nav",
|
value: "/components/nav",
|
||||||
label: "Nav",
|
label: "Nav",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/pagination",
|
value: "/components/pagination",
|
||||||
label: "Pagination",
|
label: "Pagination",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/popover",
|
value: "/components/popover",
|
||||||
label: "Popover",
|
label: "Popover",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/progress-bar",
|
value: "/components/progress-bar",
|
||||||
label: "ProgressBar",
|
label: "ProgressBar",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/radio",
|
value: "/components/radio",
|
||||||
label: "Radio",
|
label: "Radio",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/scrollbar",
|
value: "/components/scrollbar",
|
||||||
label: "Scrollbar",
|
label: "Scrollbar",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/select",
|
value: "/components/select",
|
||||||
label: "Select",
|
label: "Select",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/skeleton",
|
value: "/components/skeleton",
|
||||||
label: "Skeleton",
|
label: "Skeleton",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/slider",
|
value: "/components/slider",
|
||||||
label: "Slider",
|
label: "Slider",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/space",
|
value: "/components/space",
|
||||||
label: "Space",
|
label: "Space",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/spin-button",
|
value: "/components/spin-button",
|
||||||
label: "Spin Button",
|
label: "Spin Button",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/spinner",
|
value: "/components/spinner",
|
||||||
label: "Spinner",
|
label: "Spinner",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/switch",
|
value: "/components/switch",
|
||||||
label: "Switch",
|
label: "Switch",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/tab-list",
|
value: "/components/tab-list",
|
||||||
label: "Tab List",
|
label: "Tab List",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/table",
|
value: "/components/table",
|
||||||
label: "Table",
|
label: "Table",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: Some("Tag"),
|
||||||
value: "/components/tag",
|
value: "/components/tag",
|
||||||
label: "Tag",
|
label: "Tag",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
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",
|
value: "/components/text",
|
||||||
label: "Text",
|
label: "Text",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/textarea",
|
value: "/components/textarea",
|
||||||
label: "Textarea",
|
label: "Textarea",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/time-picker",
|
value: "/components/time-picker",
|
||||||
label: "Time Picker",
|
label: "Time Picker",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/toast",
|
value: "/components/toast",
|
||||||
label: "Toast",
|
label: "Toast",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/tooltip",
|
value: "/components/tooltip",
|
||||||
label: "Tooltip",
|
label: "Tooltip",
|
||||||
},
|
},
|
||||||
NavItemOption {
|
NavItemOption {
|
||||||
|
group: None,
|
||||||
value: "/components/upload",
|
value: "/components/upload",
|
||||||
label: "Upload",
|
label: "Upload",
|
||||||
},
|
},
|
||||||
|
|
|
@ -67,6 +67,8 @@ pub fn include_md(_token_stream: proc_macro::TokenStream) -> proc_macro::TokenSt
|
||||||
"TabListMdPage" => "../../thaw/src/tab_list/docs/mod.md",
|
"TabListMdPage" => "../../thaw/src/tab_list/docs/mod.md",
|
||||||
"TableMdPage" => "../../thaw/src/table/docs/mod.md",
|
"TableMdPage" => "../../thaw/src/table/docs/mod.md",
|
||||||
"TagMdPage" => "../../thaw/src/tag/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",
|
"TextMdPage" => "../../thaw/src/text/docs/mod.md",
|
||||||
"TextareaMdPage" => "../../thaw/src/textarea/docs/mod.md",
|
"TextareaMdPage" => "../../thaw/src/textarea/docs/mod.md",
|
||||||
"TimePickerMdPage" => "../../thaw/src/time_picker/docs/mod.md",
|
"TimePickerMdPage" => "../../thaw/src/time_picker/docs/mod.md",
|
||||||
|
|
|
@ -3,7 +3,7 @@ use crate::{ConfigInjection, _aria::ActiveDescendantController};
|
||||||
use leptos::{context::Provider, ev, html, prelude::*};
|
use leptos::{context::Provider, ev, html, prelude::*};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use thaw_components::CSSTransition;
|
use thaw_components::CSSTransition;
|
||||||
use thaw_utils::mount_style;
|
use thaw_utils::{mount_style, BoxCallback};
|
||||||
use web_sys::{HtmlElement, Node};
|
use web_sys::{HtmlElement, Node};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
|
@ -12,6 +12,7 @@ pub fn Listbox(
|
||||||
class: &'static str,
|
class: &'static str,
|
||||||
set_listbox: Arc<dyn Fn(Node) + Send + Sync>,
|
set_listbox: Arc<dyn Fn(Node) + Send + Sync>,
|
||||||
listbox_ref: NodeRef<html::Div>,
|
listbox_ref: NodeRef<html::Div>,
|
||||||
|
#[prop(optional)] on_hidden: StoredValue<Vec<BoxCallback>>,
|
||||||
children: Children,
|
children: Children,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
mount_style("listbox", include_str!("./listbox.css"));
|
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::<Vec<BoxCallback>>())
|
||||||
|
{
|
||||||
|
list.into_iter().for_each(|f| f());
|
||||||
|
}
|
||||||
|
};
|
||||||
on_cleanup(move || {
|
on_cleanup(move || {
|
||||||
drop(effect);
|
drop(effect);
|
||||||
});
|
});
|
||||||
|
@ -39,6 +47,7 @@ pub fn Listbox(
|
||||||
appear=open.get_untracked()
|
appear=open.get_untracked()
|
||||||
show=open
|
show=open
|
||||||
let:display
|
let:display
|
||||||
|
on_after_leave
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class=format!("thaw-config-provider thaw-listbox {class}")
|
class=format!("thaw-config-provider thaw-listbox {class}")
|
||||||
|
|
|
@ -44,6 +44,7 @@ mod switch;
|
||||||
mod tab_list;
|
mod tab_list;
|
||||||
mod table;
|
mod table;
|
||||||
mod tag;
|
mod tag;
|
||||||
|
mod tag_picker;
|
||||||
mod text;
|
mod text;
|
||||||
mod textarea;
|
mod textarea;
|
||||||
mod theme;
|
mod theme;
|
||||||
|
@ -97,6 +98,7 @@ pub use switch::*;
|
||||||
pub use tab_list::*;
|
pub use tab_list::*;
|
||||||
pub use table::*;
|
pub use table::*;
|
||||||
pub use tag::*;
|
pub use tag::*;
|
||||||
|
pub use tag_picker::*;
|
||||||
pub use text::*;
|
pub use text::*;
|
||||||
pub use textarea::*;
|
pub use textarea::*;
|
||||||
pub use thaw_utils::{ComponentRef, SignalWatch};
|
pub use thaw_utils::{ComponentRef, SignalWatch};
|
||||||
|
|
|
@ -2,32 +2,79 @@
|
||||||
|
|
||||||
```rust demo
|
```rust demo
|
||||||
view! {
|
view! {
|
||||||
|
<Space>
|
||||||
<Tag>"default"</Tag>
|
<Tag>"default"</Tag>
|
||||||
|
<InteractionTag>
|
||||||
|
<InteractionTagPrimary>
|
||||||
|
"Interaction Tag"
|
||||||
|
</InteractionTagPrimary>
|
||||||
|
</InteractionTag>
|
||||||
|
</Space>
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Closable
|
### Size
|
||||||
|
|
||||||
```rust demo
|
```rust demo
|
||||||
// let message = use_message();
|
view! {
|
||||||
let success = move |_: ev::MouseEvent| {
|
<Space vertical=true>
|
||||||
// message.create(
|
<Space>
|
||||||
// "tag close".into(),
|
<Tag >"Medium"</Tag>
|
||||||
// MessageVariant::Success,
|
<Tag size=TagSize::Small>"Small"</Tag>
|
||||||
// Default::default(),
|
<Tag size=TagSize::ExtraSmall>"Extra small"</Tag>
|
||||||
// );
|
</Space>
|
||||||
|
<Space>
|
||||||
|
<Tag dismissible=true>"Medium"</Tag>
|
||||||
|
<Tag dismissible=true size=TagSize::Small>"Small"</Tag>
|
||||||
|
<Tag dismissible=true size=TagSize::ExtraSmall>"Extra small"</Tag>
|
||||||
|
</Space>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dismiss
|
||||||
|
|
||||||
|
```rust demo
|
||||||
|
let toaster = ToasterInjection::expect_context();
|
||||||
|
|
||||||
|
let on_dismiss = move |_| {
|
||||||
|
toaster.dispatch_toast(view! {
|
||||||
|
<Toast>
|
||||||
|
<ToastTitle>"Tag"</ToastTitle>
|
||||||
|
<ToastBody>
|
||||||
|
"Tag dismiss"
|
||||||
|
</ToastBody>
|
||||||
|
</Toast>
|
||||||
|
}.into_any(), Default::default());
|
||||||
};
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<Tag closable=true on_close=success>"Default"</Tag>
|
<Tag dismissible=true on_dismiss=on_dismiss>"Default"</Tag>
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Tag Props
|
### Tag Props
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
| -------- | ---------------------------------------- | -------------------- | ------------------------------------- |
|
| --- | --- | --- | --- |
|
||||||
|
| class | `MaybeProp<String>` | `Default::default()` | |
|
||||||
|
| size | `Option<MaybeSignal<TagSize>>` | `None` | Size of the tag. |
|
||||||
|
| dismissible | `MaybeSignal<bool>` | `false` | A Tag can be dismissible. |
|
||||||
|
| on_dismiss | `Option<ArcOneCallback<ev::MouseEvent>>` | `None` | Callback for when a tag is dismissed. |
|
||||||
|
| value | `Option<String>` | `None` | Unique value identifying the tag within a TagGroup. |
|
||||||
|
| children | `Children` | | |
|
||||||
|
|
||||||
|
### InteractionTag Props
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
| -------- | ------------------------------ | -------------------- | ---------------- |
|
||||||
|
| class | `MaybeProp<String>` | `Default::default()` | |
|
||||||
|
| size | `Option<MaybeSignal<TagSize>>` | `None` | Size of the tag. |
|
||||||
|
| children | `Children` | | |
|
||||||
|
|
||||||
|
### InteractionTagPrimary Props
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
| -------- | ------------------- | -------------------- | ----------- |
|
||||||
| class | `MaybeProp<String>` | `Default::default()` | |
|
| class | `MaybeProp<String>` | `Default::default()` | |
|
||||||
| closable | `MaybeSignal<bool>` | `false` | Whether the tag shows a close button. |
|
|
||||||
| on_close | `Option<ArcOneCallback<ev::MouseEvent>>` | `None` | Close clicked callback. |
|
|
||||||
| children | `Children` | | |
|
| children | `Children` | | |
|
||||||
|
|
57
thaw/src/tag/docs/tag_group/mod.md
Normal file
57
thaw/src/tag/docs/tag_group/mod.md
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
# Tag Group
|
||||||
|
|
||||||
|
```rust demo
|
||||||
|
view! {
|
||||||
|
<Space vertical=true>
|
||||||
|
<TagGroup attr:role="list">
|
||||||
|
<Tag attr:role="listitem">"Tag 1"</Tag>
|
||||||
|
<Tag attr:role="listitem">"Tag 2"</Tag>
|
||||||
|
<Tag attr:role="listitem">"Tag 3"</Tag>
|
||||||
|
</TagGroup>
|
||||||
|
<TagGroup>
|
||||||
|
<InteractionTag>
|
||||||
|
<InteractionTagPrimary>"Tag 1"</InteractionTagPrimary>
|
||||||
|
</InteractionTag>
|
||||||
|
<InteractionTag>
|
||||||
|
<InteractionTagPrimary>"Tag 2"</InteractionTagPrimary>
|
||||||
|
</InteractionTag>
|
||||||
|
<InteractionTag>
|
||||||
|
<InteractionTagPrimary>"Tag 3"</InteractionTagPrimary>
|
||||||
|
</InteractionTag>
|
||||||
|
</TagGroup>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sizes
|
||||||
|
|
||||||
|
```rust demo
|
||||||
|
view! {
|
||||||
|
<Space vertical=true>
|
||||||
|
<TagGroup size=TagSize::Small>
|
||||||
|
<Tag >"Tag 1"</Tag>
|
||||||
|
<Tag dismissible=true>"Tag 1"</Tag>
|
||||||
|
<InteractionTag>
|
||||||
|
<InteractionTagPrimary>"Tag 1"</InteractionTagPrimary>
|
||||||
|
</InteractionTag>
|
||||||
|
</TagGroup>
|
||||||
|
<TagGroup size=TagSize::ExtraSmall>
|
||||||
|
<Tag >"Tag 1"</Tag>
|
||||||
|
<Tag dismissible=true>"Tag 1"</Tag>
|
||||||
|
<InteractionTag>
|
||||||
|
<InteractionTagPrimary>"Tag 1"</InteractionTagPrimary>
|
||||||
|
</InteractionTag>
|
||||||
|
</TagGroup>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### TagGroup Props
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
| ----------- | -------------------------------- | -------------------- | ------------------------------------- |
|
||||||
|
| class | `MaybeProp<String>` | `Default::default()` | |
|
||||||
|
| size | `MaybeSignal<TagSize>` | `TagSize::Medium` | Size of the tag. |
|
||||||
|
| dismissible | `MaybeSignal<bool>` | `false` | A Tag can be dismissible. |
|
||||||
|
| on_dismiss | `Option<ArcOneCallback<String>>` | `None` | Callback for when a tag is dismissed. |
|
||||||
|
| children | `Children` | | |
|
72
thaw/src/tag/interaction-tag.css
Normal file
72
thaw/src/tag/interaction-tag.css
Normal file
|
@ -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);
|
||||||
|
}
|
44
thaw/src/tag/interaction_tag.rs
Normal file
44
thaw/src/tag/interaction_tag.rs
Normal file
|
@ -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<String>,
|
||||||
|
/// Size of the tag.
|
||||||
|
#[prop(optional, into)]
|
||||||
|
size: Option<MaybeSignal<TagSize>>,
|
||||||
|
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! {
|
||||||
|
<div class=class_list![
|
||||||
|
"thaw-interaction-tag",
|
||||||
|
size_class.map(|size| move || format!("thaw-interaction-tag--{}", size.get().as_str())),
|
||||||
|
class
|
||||||
|
]>{children()}</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn InteractionTagPrimary(
|
||||||
|
#[prop(optional, into)] class: MaybeProp<String>,
|
||||||
|
children: Children,
|
||||||
|
) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<button class=class_list!["thaw-interaction-tag-primary", class]>
|
||||||
|
<span class="thaw-interaction-tag-primary__primary-text">{children()}</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 leptos::{either::Either, ev, prelude::*};
|
||||||
use thaw_utils::{class_list, mount_style, ArcOneCallback};
|
use thaw_utils::{class_list, mount_style, ArcOneCallback};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Tag(
|
pub fn Tag(
|
||||||
#[prop(optional, into)] class: MaybeProp<String>,
|
#[prop(optional, into)] class: MaybeProp<String>,
|
||||||
/// Whether the tag shows a close button.
|
/// Size of the tag.
|
||||||
#[prop(optional, into)]
|
#[prop(optional, into)]
|
||||||
closable: MaybeSignal<bool>,
|
size: Option<MaybeSignal<TagSize>>,
|
||||||
/// Close clicked callback.
|
/// A Tag can be dismissible.
|
||||||
#[prop(optional, into)]
|
#[prop(optional, into)]
|
||||||
on_close: Option<ArcOneCallback<ev::MouseEvent>>,
|
dismissible: MaybeSignal<bool>,
|
||||||
|
/// Callback for when a tag is dismissed.
|
||||||
|
#[prop(optional, into)]
|
||||||
|
on_dismiss: Option<ArcOneCallback<ev::MouseEvent>>,
|
||||||
|
/// Unique value identifying the tag within a TagGroup.
|
||||||
|
#[prop(optional, into)]
|
||||||
|
value: Option<String>,
|
||||||
children: Children,
|
children: Children,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
mount_style("tag", include_str!("./tag.css"));
|
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! {
|
view! {
|
||||||
<span class=class_list!["thaw-tag", ("thaw-tag--closable", move || closable.get()), class]>
|
<span class=class_list![
|
||||||
|
"thaw-tag",
|
||||||
|
("thaw-tag--dismissible", move || group_dismissible.map_or_else(|| dismissible.get(), |d| d.get())),
|
||||||
|
size_class.map(|size| move || format!("thaw-tag--{}", size.get().as_str())),
|
||||||
|
class
|
||||||
|
]>
|
||||||
|
|
||||||
<span class="thaw-tag__primary-text">{children()}</span>
|
<span class="thaw-tag__primary-text">{children()}</span>
|
||||||
|
|
||||||
{move || {
|
{move || {
|
||||||
let on_close = on_close.clone();
|
if group_dismissible.map_or_else(|| dismissible.get(), |d| d.get()) {
|
||||||
let on_close = move |event| {
|
let on_dismiss = on_dismiss.clone();
|
||||||
let Some(on_close) = on_close.as_ref() else {
|
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;
|
return;
|
||||||
};
|
};
|
||||||
on_close(event);
|
on_dismiss(event);
|
||||||
};
|
};
|
||||||
if closable.get() {
|
|
||||||
Either::Left(
|
Either::Left(
|
||||||
view! {
|
view! {
|
||||||
<button class="thaw-tag__close" on:click=on_close>
|
<button class="thaw-tag__dismiss" on:click=on_dismiss>
|
||||||
<svg
|
<svg
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
@ -54,3 +102,21 @@ pub fn Tag(
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy)]
|
||||||
|
pub enum TagSize {
|
||||||
|
#[default]
|
||||||
|
Medium,
|
||||||
|
Small,
|
||||||
|
ExtraSmall,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TagSize {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Medium => "medium",
|
||||||
|
Self::Small => "small",
|
||||||
|
Self::ExtraSmall => "extra-small",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
4
thaw/src/tag/tag-group.css
Normal file
4
thaw/src/tag/tag-group.css
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.thaw-tag-group {
|
||||||
|
display: inline-flex;
|
||||||
|
column-gap: var(--spacingHorizontalS);
|
||||||
|
}
|
|
@ -17,7 +17,17 @@
|
||||||
border-radius: var(--borderRadiusMedium);
|
border-radius: var(--borderRadiusMedium);
|
||||||
}
|
}
|
||||||
|
|
||||||
.thaw-tag--closable {
|
.thaw-tag--small {
|
||||||
|
height: 24px;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-tag--extra-small {
|
||||||
|
height: 20px;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-tag--dismissible {
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +44,13 @@
|
||||||
color: var(--colorNeutralForeground2);
|
color: var(--colorNeutralForeground2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.thaw-tag__close {
|
.thaw-tag--small .thaw-tag__primary-text,
|
||||||
|
.thaw-tag--extra-small .thaw-tag__primary-text {
|
||||||
|
font-size: var(--fontSizeBase200);
|
||||||
|
line-height: var(--lineHeightBase200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-tag__dismiss {
|
||||||
grid-area: dismissIcon;
|
grid-area: dismissIcon;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -46,15 +62,27 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thaw-tag__close > svg {
|
.thaw-tag--small .thaw-tag__dismiss {
|
||||||
|
padding-left: var(--spacingHorizontalXXS);
|
||||||
|
font-size: 16px;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-tag--extra-small .thaw-tag__dismiss {
|
||||||
|
padding-left: var(--spacingHorizontalXXS);
|
||||||
|
font-size: 12px;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-tag__dismiss > svg {
|
||||||
display: inline;
|
display: inline;
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thaw-tag__close:hover {
|
.thaw-tag__dismiss:hover {
|
||||||
color: var(--colorCompoundBrandForeground1Hover);
|
color: var(--colorCompoundBrandForeground1Hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.thaw-tag__close:active {
|
.thaw-tag__dismiss:active {
|
||||||
color: var(--colorCompoundBrandForeground1Pressed);
|
color: var(--colorCompoundBrandForeground1Pressed);
|
||||||
}
|
}
|
||||||
|
|
43
thaw/src/tag/tag_group.rs
Normal file
43
thaw/src/tag/tag_group.rs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
use super::TagSize;
|
||||||
|
use leptos::{context::Provider, prelude::*};
|
||||||
|
use thaw_utils::{class_list, mount_style, ArcOneCallback};
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn TagGroup(
|
||||||
|
#[prop(optional, into)] class: MaybeProp<String>,
|
||||||
|
/// Size of the tag.
|
||||||
|
#[prop(optional, into)]
|
||||||
|
size: MaybeSignal<TagSize>,
|
||||||
|
/// Callback for when a tag is dismissed.
|
||||||
|
#[prop(optional, into)]
|
||||||
|
on_dismiss: Option<ArcOneCallback<String>>,
|
||||||
|
/// A Tag can be dismissible.
|
||||||
|
#[prop(optional, into)]
|
||||||
|
dismissible: MaybeSignal<bool>,
|
||||||
|
children: Children,
|
||||||
|
) -> impl IntoView {
|
||||||
|
mount_style("tag-group", include_str!("./tag-group.css"));
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class=class_list!["thaw-tag-group", class]>
|
||||||
|
<Provider value=TagGroupInjection {
|
||||||
|
size,
|
||||||
|
on_dismiss,
|
||||||
|
dismissible,
|
||||||
|
}>{children()}</Provider>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct TagGroupInjection {
|
||||||
|
pub size: MaybeSignal<TagSize>,
|
||||||
|
pub on_dismiss: Option<ArcOneCallback<String>>,
|
||||||
|
pub dismissible: MaybeSignal<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TagGroupInjection {
|
||||||
|
pub fn use_context() -> Option<Self> {
|
||||||
|
use_context()
|
||||||
|
}
|
||||||
|
}
|
70
thaw/src/tag_picker/docs/mod.md
Normal file
70
thaw/src/tag_picker/docs/mod.md
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
# Tag Picker
|
||||||
|
|
||||||
|
```rust demo
|
||||||
|
let selected_options = RwSignal::new(vec![]);
|
||||||
|
let options = vec!["Cat", "Dog"];
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<TagPicker selected_options>
|
||||||
|
<TagPickerControl slot>
|
||||||
|
<TagPickerGroup>
|
||||||
|
{move || {
|
||||||
|
selected_options.get().into_iter().map(|option| view!{
|
||||||
|
<Tag value=option.clone()>
|
||||||
|
{option}
|
||||||
|
</Tag>
|
||||||
|
}).collect_view()
|
||||||
|
}}
|
||||||
|
</TagPickerGroup>
|
||||||
|
<TagPickerInput />
|
||||||
|
</TagPickerControl>
|
||||||
|
{
|
||||||
|
move || {
|
||||||
|
selected_options.with(|selected_options| {
|
||||||
|
options.iter().filter_map(|option| {
|
||||||
|
if selected_options.iter().any(|o| o == option) {
|
||||||
|
return None
|
||||||
|
} else {
|
||||||
|
Some(view! {
|
||||||
|
<TagPickerOption value=option.clone() text=option.clone() />
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}).collect_view()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</TagPicker>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### TagPicker Props
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
| ------------------ | ----------------------- | -------------------- | --------------------------------- |
|
||||||
|
| class | `MaybeProp<String>` | `Default::default()` | |
|
||||||
|
| selected_option | `Model<Vec<String>>` | `Default::default()` | An array of selected option keys. |
|
||||||
|
| tag_picker_control | slot `TagPickerControl` | | |
|
||||||
|
| children | `Children` | | |
|
||||||
|
|
||||||
|
### TagPickerGroup Props
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
| -------- | ------------------- | -------------------- | ----------- |
|
||||||
|
| class | `MaybeProp<String>` | `Default::default()` | |
|
||||||
|
| children | `Children` | | |
|
||||||
|
|
||||||
|
### TagPickerInput Props
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
| ----- | ------------------- | -------------------- | ----------- |
|
||||||
|
| class | `MaybeProp<String>` | `Default::default()` | |
|
||||||
|
|
||||||
|
### TagPickerOption Props
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| class | `MaybeProp<String>` | `Default::default()` | |
|
||||||
|
| disable | `MaybeProp<bool>` | `false` | Sets an option to the disabled state. |
|
||||||
|
| value | `String` | | Defines a unique identifier for the option. |
|
||||||
|
| text | `String` | | An optional override the string value of the Option's display text, defaulting to the Option's child content. |
|
||||||
|
| children | `Option<Children>` | `None` | |
|
9
thaw/src/tag_picker/mod.rs
Normal file
9
thaw/src/tag_picker/mod.rs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
mod tag_picker;
|
||||||
|
mod tag_picker_input;
|
||||||
|
mod tag_picker_option;
|
||||||
|
mod tag_picker_group;
|
||||||
|
|
||||||
|
pub use tag_picker::*;
|
||||||
|
pub use tag_picker_input::*;
|
||||||
|
pub use tag_picker_option::*;
|
||||||
|
pub use tag_picker_group::*;
|
156
thaw/src/tag_picker/tag-picker.css
Normal file
156
thaw/src/tag_picker/tag-picker.css
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
.thaw-tag-picker-control {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: var(--spacingHorizontalXXS);
|
||||||
|
min-width: 250px;
|
||||||
|
min-height: 32px;
|
||||||
|
padding-right: calc(var(--spacingHorizontalM) + 18px);
|
||||||
|
padding-left: var(--spacingHorizontalM);
|
||||||
|
background-color: var(--colorNeutralBackground1);
|
||||||
|
border-radius: var(--borderRadiusMedium);
|
||||||
|
border: var(--strokeWidthThin) solid var(--colorNeutralStroke1);
|
||||||
|
border-bottom-color: var(--colorNeutralStrokeAccessible);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-tag-picker-control::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: -1px;
|
||||||
|
right: -1px;
|
||||||
|
left: -1px;
|
||||||
|
height: max(2px, var(--borderRadiusMedium));
|
||||||
|
clip-path: inset(calc(100% - 2px) 0px 0px);
|
||||||
|
border-bottom: var(--strokeWidthThick) solid var(--colorCompoundBrandStroke);
|
||||||
|
border-bottom-right-radius: var(--borderRadiusMedium);
|
||||||
|
border-bottom-left-radius: var(--borderRadiusMedium);
|
||||||
|
transition-delay: var(--curveAccelerateMid);
|
||||||
|
transition-duration: var(--durationUltraFast);
|
||||||
|
transition-property: transform;
|
||||||
|
transform: scaleX(0);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-tag-picker-control:focus-within::after {
|
||||||
|
transition-delay: var(--curveDecelerateMid);
|
||||||
|
transition-duration: var(--durationNormal);
|
||||||
|
transition-property: transform;
|
||||||
|
transform: scaleX(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-tag-picker-control__aside {
|
||||||
|
position: absolute;
|
||||||
|
right: var(--spacingHorizontalM);
|
||||||
|
top: 0px;
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
grid-template-rows: minmax(32px, auto) 1fr;
|
||||||
|
grid-template-columns: repeat(2, auto);
|
||||||
|
min-height: 32px;
|
||||||
|
height: 100%;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-tag-picker-control__expand-icon {
|
||||||
|
display: block;
|
||||||
|
margin-left: var(--spacingHorizontalXXS);
|
||||||
|
color: var(--colorNeutralStrokeAccessible);
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-tag-picker-input {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 0;
|
||||||
|
min-width: 24px;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: var(--spacingVerticalSNudge) 0 var(--spacingVerticalSNudge) 0;
|
||||||
|
background-color: var(--colorTransparentBackground);
|
||||||
|
color: var(--colorNeutralForeground1);
|
||||||
|
line-height: var(--lineHeightBase300);
|
||||||
|
font-weight: var(--fontWeightRegular);
|
||||||
|
font-size: var(--fontSizeBase300);
|
||||||
|
font-family: var(--fontFamilyBase);
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-tag-picker-input:focus {
|
||||||
|
outline-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-tag-picker__listbox {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: var(--spacingHorizontalXXS);
|
||||||
|
overflow-y: auto;
|
||||||
|
min-width: 160px;
|
||||||
|
max-height: 80vh;
|
||||||
|
background-color: var(--colorNeutralBackground1);
|
||||||
|
padding: var(--spacingHorizontalXS);
|
||||||
|
outline: 1px solid var(--colorTransparentStroke);
|
||||||
|
border-radius: var(--borderRadiusMedium);
|
||||||
|
box-shadow: var(--shadow16);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-tag-picker-option {
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
column-gap: var(--spacingHorizontalXS);
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--spacingVerticalSNudge) var(--spacingHorizontalS);
|
||||||
|
line-height: var(--lineHeightBase300);
|
||||||
|
font-size: var(--fontSizeBase300);
|
||||||
|
font-family: var(--fontFamilyBase);
|
||||||
|
color: var(--colorNeutralForeground1);
|
||||||
|
border-radius: var(--borderRadiusMedium);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-tag-picker-option[data-activedescendant-focusvisible]::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
right: -2px;
|
||||||
|
left: -2px;
|
||||||
|
bottom: -2px;
|
||||||
|
top: -2px;
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
border-radius: var(--borderRadiusMedium);
|
||||||
|
border: 2px solid var(--colorStrokeFocus2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-tag-picker-option:hover {
|
||||||
|
color: var(--colorNeutralForeground1Hover);
|
||||||
|
background-color: var(--colorNeutralBackground1Hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-tag-picker-option:active {
|
||||||
|
color: var(--colorNeutralForeground1Pressed);
|
||||||
|
background-color: var(--colorNeutralBackground1Pressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-tag-picker-option.thaw-tag-picker-option--disabled {
|
||||||
|
color: var(--colorNeutralForegroundDisabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-tag-picker-option--disabled:active,
|
||||||
|
.thaw-tag-picker-option--disabled:hover {
|
||||||
|
background-color: var(--colorTransparentBackground);
|
||||||
|
color: var(--colorNeutralForegroundDisabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-tag-picker-group {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: var(--spacingHorizontalXS);
|
||||||
|
column-gap: var(--spacingHorizontalXS);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: var(--spacingVerticalSNudge) 0 var(--spacingVerticalSNudge) 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
cursor: text;
|
||||||
|
}
|
199
thaw/src/tag_picker/tag_picker.rs
Normal file
199
thaw/src/tag_picker/tag_picker.rs
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
use crate::{
|
||||||
|
_aria::{use_active_descendant, ActiveDescendantController},
|
||||||
|
icon::ChevronDownRegularIcon,
|
||||||
|
listbox::{listbox_keyboard_event, Listbox},
|
||||||
|
};
|
||||||
|
use leptos::{context::Provider, ev, html, prelude::*};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use thaw_components::{Binder, Follower, FollowerPlacement, FollowerWidth};
|
||||||
|
use thaw_utils::{call_on_click_outside, class_list, mount_style, BoxCallback, Model};
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn TagPicker(
|
||||||
|
#[prop(optional, into)] class: MaybeProp<String>,
|
||||||
|
/// An array of selected option keys.
|
||||||
|
#[prop(optional, into)]
|
||||||
|
selected_options: Model<Vec<String>>,
|
||||||
|
tag_picker_control: TagPickerControl,
|
||||||
|
children: Children,
|
||||||
|
) -> impl IntoView {
|
||||||
|
mount_style("tag-picker", include_str!("./tag-picker.css"));
|
||||||
|
let TagPickerControl {
|
||||||
|
children: control_children,
|
||||||
|
} = tag_picker_control;
|
||||||
|
let is_show_listbox = RwSignal::new(false);
|
||||||
|
let trigger_ref = NodeRef::<html::Div>::new();
|
||||||
|
let input_ref = NodeRef::<html::Input>::new();
|
||||||
|
let listbox_ref = NodeRef::<html::Div>::new();
|
||||||
|
let listbox_hidden_callback = StoredValue::new(vec![]);
|
||||||
|
let options = StoredValue::new(HashMap::<String, (String, String, MaybeSignal<bool>)>::new());
|
||||||
|
let (set_listbox, active_descendant_controller) =
|
||||||
|
use_active_descendant(move |el| el.class_list().contains("thaw-tag-picker-option"));
|
||||||
|
|
||||||
|
let tag_picker_control_injection =
|
||||||
|
TagPickerControlInjection(active_descendant_controller.clone());
|
||||||
|
let tag_picker_injection = TagPickerInjection {
|
||||||
|
selected_options,
|
||||||
|
input_ref,
|
||||||
|
options,
|
||||||
|
is_show_listbox,
|
||||||
|
listbox_hidden_callback,
|
||||||
|
};
|
||||||
|
let on_click = move |e: ev::MouseEvent| {
|
||||||
|
if e.default_prevented() {
|
||||||
|
if is_show_listbox.get() {
|
||||||
|
is_show_listbox.set(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(el) = input_ref.get_untracked() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if document().active_element().as_ref() != Some(&**el) {
|
||||||
|
let _ = el.focus();
|
||||||
|
}
|
||||||
|
is_show_listbox.update(|show| *show = !*show);
|
||||||
|
};
|
||||||
|
call_on_click_outside(
|
||||||
|
trigger_ref,
|
||||||
|
{
|
||||||
|
move || {
|
||||||
|
is_show_listbox.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let on_keydown = move |e| {
|
||||||
|
listbox_keyboard_event(
|
||||||
|
e,
|
||||||
|
is_show_listbox,
|
||||||
|
true,
|
||||||
|
&active_descendant_controller,
|
||||||
|
move |option| {
|
||||||
|
tag_picker_injection.options.with_value(|options| {
|
||||||
|
if let Some((value, _text, disabled)) = options.get(&option.id()) {
|
||||||
|
if disabled.get_untracked() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tag_picker_injection.select_option(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
view! {
|
||||||
|
<Binder target_ref=trigger_ref>
|
||||||
|
<div
|
||||||
|
class=class_list!["thaw-tag-picker-control", class]
|
||||||
|
node_ref=trigger_ref
|
||||||
|
on:keydown=on_keydown
|
||||||
|
on:click=on_click
|
||||||
|
>
|
||||||
|
<Provider value=tag_picker_injection>
|
||||||
|
<Provider value=tag_picker_control_injection>{control_children()}</Provider>
|
||||||
|
</Provider>
|
||||||
|
<span class="thaw-tag-picker-control__aside">
|
||||||
|
<span class="thaw-tag-picker-control__expand-icon">
|
||||||
|
<ChevronDownRegularIcon />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Follower
|
||||||
|
slot
|
||||||
|
show=is_show_listbox
|
||||||
|
placement=FollowerPlacement::BottomStart
|
||||||
|
width=FollowerWidth::MinTarget
|
||||||
|
>
|
||||||
|
<Provider value=tag_picker_injection>
|
||||||
|
<Listbox
|
||||||
|
open=is_show_listbox.read_only()
|
||||||
|
set_listbox
|
||||||
|
listbox_ref
|
||||||
|
on_hidden=listbox_hidden_callback
|
||||||
|
class="thaw-tag-picker__listbox"
|
||||||
|
>
|
||||||
|
{children()}
|
||||||
|
</Listbox>
|
||||||
|
</Provider>
|
||||||
|
</Follower>
|
||||||
|
</Binder>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[slot]
|
||||||
|
pub struct TagPickerControl {
|
||||||
|
children: Children,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct TagPickerControlInjection(pub ActiveDescendantController);
|
||||||
|
|
||||||
|
impl TagPickerControlInjection {
|
||||||
|
pub fn expect_context() -> Self {
|
||||||
|
expect_context()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub(crate) struct TagPickerInjection {
|
||||||
|
pub input_ref: NodeRef<html::Input>,
|
||||||
|
selected_options: Model<Vec<String>>,
|
||||||
|
pub options: StoredValue<HashMap<String, (String, String, MaybeSignal<bool>)>>,
|
||||||
|
is_show_listbox: RwSignal<bool>,
|
||||||
|
listbox_hidden_callback: StoredValue<Vec<BoxCallback>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TagPickerInjection {
|
||||||
|
pub fn expect_context() -> Self {
|
||||||
|
expect_context()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// value: (value, text, disabled)
|
||||||
|
pub fn insert_option(&self, id: String, value: (String, String, MaybeSignal<bool>)) {
|
||||||
|
self.options
|
||||||
|
.update_value(|options| options.insert(id, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_option(&self, id: &String) {
|
||||||
|
self.options.update_value(|options| options.remove(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_selected(&self, value: &String) -> bool {
|
||||||
|
self.selected_options
|
||||||
|
.with(|options| options.contains(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_option(&self, value: &String) {
|
||||||
|
self.selected_options.update(|options| {
|
||||||
|
if let Some(index) = options.iter().position(|v| v == value) {
|
||||||
|
options.remove(index);
|
||||||
|
} else {
|
||||||
|
options.push(value.clone());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.is_show_listbox.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_selected_option(&self, value: String) {
|
||||||
|
if self.is_show_listbox.get_untracked() {
|
||||||
|
let selected_options = self.selected_options;
|
||||||
|
self.listbox_hidden_callback.update_value(|list| {
|
||||||
|
list.push(BoxCallback::new(move || {
|
||||||
|
selected_options.try_update(|options| {
|
||||||
|
if let Some(index) = options.iter().position(|v| v == &value) {
|
||||||
|
options.remove(index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.selected_options.update(|options| {
|
||||||
|
if let Some(index) = options.iter().position(|v| v == &value) {
|
||||||
|
options.remove(index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
thaw/src/tag_picker/tag_picker_group.rs
Normal file
26
thaw/src/tag_picker/tag_picker_group.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
use super::TagPickerInjection;
|
||||||
|
use crate::{TagGroup, TagSize};
|
||||||
|
use leptos::prelude::*;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn TagPickerGroup(
|
||||||
|
#[prop(optional, into)] class: MaybeProp<String>,
|
||||||
|
children: Children,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let tag_picker = TagPickerInjection::expect_context();
|
||||||
|
let class = MaybeProp::derive(move || {
|
||||||
|
Some(format!(
|
||||||
|
"thaw-tag-picker-group {}",
|
||||||
|
class.get().unwrap_or_default()
|
||||||
|
))
|
||||||
|
});
|
||||||
|
let on_dismiss = move |value| {
|
||||||
|
tag_picker.remove_selected_option(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<TagGroup attr:role="listbox" class size=TagSize::ExtraSmall dismissible=true on_dismiss>
|
||||||
|
{children()}
|
||||||
|
</TagGroup>
|
||||||
|
}
|
||||||
|
}
|
55
thaw/src/tag_picker/tag_picker_input.rs
Normal file
55
thaw/src/tag_picker/tag_picker_input.rs
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
use super::{TagPickerControlInjection, TagPickerInjection};
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use thaw_utils::class_list;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn TagPickerInput(#[prop(optional, into)] class: MaybeProp<String>) -> impl IntoView {
|
||||||
|
let TagPickerInjection {
|
||||||
|
input_ref, options, ..
|
||||||
|
} = TagPickerInjection::expect_context();
|
||||||
|
let TagPickerControlInjection(active_descendant_controller) =
|
||||||
|
TagPickerControlInjection::expect_context();
|
||||||
|
let value_trigger = ArcTrigger::new();
|
||||||
|
let on_blur = {
|
||||||
|
let value_trigger = value_trigger.clone();
|
||||||
|
move |_| {
|
||||||
|
value_trigger.track();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let on_input = move |ev| {
|
||||||
|
let value = event_target_value(&ev);
|
||||||
|
let value = value.trim().to_ascii_lowercase();
|
||||||
|
if value.is_empty() {
|
||||||
|
active_descendant_controller.blur();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if active_descendant_controller
|
||||||
|
.find(|id| {
|
||||||
|
options.with_value(|options| {
|
||||||
|
let Some((_, text, _)) = options.get(&id) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
text.to_ascii_lowercase().contains(&value)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
active_descendant_controller.blur();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<input
|
||||||
|
node_ref=input_ref
|
||||||
|
type="text"
|
||||||
|
role="combobox"
|
||||||
|
class=class_list!["thaw-tag-picker-input", class]
|
||||||
|
on:blur=on_blur
|
||||||
|
on:input=on_input
|
||||||
|
prop:value=move || {
|
||||||
|
value_trigger.trigger();
|
||||||
|
""
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
66
thaw/src/tag_picker/tag_picker_option.rs
Normal file
66
thaw/src/tag_picker/tag_picker_option.rs
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
use super::TagPickerInjection;
|
||||||
|
use crate::listbox::ListboxInjection;
|
||||||
|
use leptos::{either::Either, ev, prelude::*};
|
||||||
|
use thaw_utils::class_list;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn TagPickerOption(
|
||||||
|
#[prop(optional, into)] class: MaybeProp<String>,
|
||||||
|
/// Sets an option to the disabled state.
|
||||||
|
#[prop(optional, into)]
|
||||||
|
disabled: MaybeSignal<bool>,
|
||||||
|
/// Defines a unique identifier for the option.
|
||||||
|
#[prop(into)]
|
||||||
|
value: String,
|
||||||
|
/// An optional override the string value of the Option's display text, defaulting to the Option's child content.
|
||||||
|
#[prop(into)]
|
||||||
|
text: String,
|
||||||
|
#[prop(optional)] children: Option<Children>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let tag_picker = TagPickerInjection::expect_context();
|
||||||
|
let listbox = ListboxInjection::expect_context();
|
||||||
|
let value = StoredValue::new(value);
|
||||||
|
let text = StoredValue::new(text);
|
||||||
|
let is_selected = Memo::new(move |_| value.with_value(|value| tag_picker.is_selected(&value)));
|
||||||
|
let id = uuid::Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
let on_click = move |e: ev::MouseEvent| {
|
||||||
|
if disabled.get_untracked() {
|
||||||
|
e.stop_propagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
value.with_value(|value| {
|
||||||
|
tag_picker.select_option(value);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
{
|
||||||
|
tag_picker.insert_option(id.clone(), (value.get_value(), text.get_value(), disabled));
|
||||||
|
let id = id.clone();
|
||||||
|
listbox.trigger();
|
||||||
|
on_cleanup(move || {
|
||||||
|
tag_picker.remove_option(&id);
|
||||||
|
listbox.trigger();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
view! {
|
||||||
|
<div
|
||||||
|
role="option"
|
||||||
|
aria-disabled=move || if disabled.get() { "true" } else { "" }
|
||||||
|
aria-selected=move || is_selected.get().to_string()
|
||||||
|
id=id
|
||||||
|
class=class_list![
|
||||||
|
"thaw-tag-picker-option",
|
||||||
|
("thaw-tag-picker-option--selected", move || is_selected.get()),
|
||||||
|
("thaw-tag-picker-option--disabled", move || disabled.get()),
|
||||||
|
class
|
||||||
|
]
|
||||||
|
on:click=on_click
|
||||||
|
>
|
||||||
|
{if let Some(children) = children {
|
||||||
|
Either::Left(children())
|
||||||
|
} else {
|
||||||
|
Either::Right(text.get_value())
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,28 +1,15 @@
|
||||||
use crate::BoxCallback;
|
use crate::BoxCallback;
|
||||||
use leptos::{html::Div, prelude::*};
|
use leptos::{html::Div, prelude::*};
|
||||||
use tachys::reactive_graph::node_ref::NodeRef;
|
|
||||||
|
|
||||||
pub fn call_on_click_outside(element: NodeRef<Div>, on_click: BoxCallback) {
|
pub fn call_on_click_outside(element: NodeRef<Div>, on_click: BoxCallback) {
|
||||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||||
{
|
{
|
||||||
use leptos::ev;
|
let handle = window_event_listener(::leptos::ev::click, move |ev| {
|
||||||
let handle = window_event_listener(ev::click, move |ev| {
|
|
||||||
use leptos::wasm_bindgen::__rt::IntoJsResult;
|
|
||||||
let el = ev.target();
|
|
||||||
let mut el: Option<web_sys::Element> =
|
|
||||||
el.into_js_result().map_or(None, |el| Some(el.into()));
|
|
||||||
let body = document().body().unwrap();
|
|
||||||
while let Some(current_el) = el {
|
|
||||||
if current_el == *body {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
let Some(displayed_el) = element.get_untracked() else {
|
let Some(displayed_el) = element.get_untracked() else {
|
||||||
break;
|
|
||||||
};
|
|
||||||
if current_el == **displayed_el {
|
|
||||||
return;
|
return;
|
||||||
}
|
};
|
||||||
el = current_el.parent_element();
|
if ev.composed_path().includes(&displayed_el, 0) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
on_click();
|
on_click();
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue