feat: adds Combobox component

This commit is contained in:
luoxiao 2024-06-27 14:51:36 +08:00
parent 25b5485395
commit c7be391441
9 changed files with 399 additions and 0 deletions

View file

@ -59,6 +59,7 @@ fn TheRouter(is_routing: RwSignal<bool>) -> impl IntoView {
<Route path="/card" view=CardMdPage/> <Route path="/card" view=CardMdPage/>
<Route path="/checkbox" view=CheckboxMdPage/> <Route path="/checkbox" view=CheckboxMdPage/>
<Route path="/color-picker" view=ColorPickerMdPage/> <Route path="/color-picker" view=ColorPickerMdPage/>
<Route path="/combobox" view=ComboboxMdPage/>
<Route path="/config-provider" view=ConfigProviderMdPage/> <Route path="/config-provider" view=ConfigProviderMdPage/>
<Route path="/date-picker" view=DatePickerMdPage/> <Route path="/date-picker" view=DatePickerMdPage/>
<Route path="/divider" view=DividerMdPage/> <Route path="/divider" view=DividerMdPage/>

View file

@ -187,6 +187,10 @@ pub(crate) fn gen_menu_data() -> Vec<MenuGroupOption> {
value: "/components/color-picker".into(), value: "/components/color-picker".into(),
label: "Color Picker".into(), label: "Color Picker".into(),
}, },
MenuItemOption {
value: "/components/combobox".into(),
label: "Combobox".into(),
},
MenuItemOption { MenuItemOption {
value: "/components/config-provider".into(), value: "/components/config-provider".into(),
label: "Config Provider".into(), label: "Config Provider".into(),

View file

@ -0,0 +1,33 @@
# Combobox
```rust demo
let value = RwSignal::new(vec![]);
view! {
<Combobox value>
<ComboboxOption key="cat">
"Cat"
</ComboboxOption>
<ComboboxOption key="dog">
"Dog"
</ComboboxOption>
</Combobox>
}
```
### Multiselect
```rust demo
let value = RwSignal::new(vec![]);
view! {
<Combobox value multiselect=true>
<ComboboxOption key="cat">
"Cat"
</ComboboxOption>
<ComboboxOption key="dog">
"Dog"
</ComboboxOption>
</Combobox>
}
```

View file

@ -41,6 +41,7 @@ pub fn include_md(_token_stream: proc_macro::TokenStream) -> proc_macro::TokenSt
"CardMdPage" => "../docs/card/mod.md", "CardMdPage" => "../docs/card/mod.md",
"CheckboxMdPage" => "../docs/checkbox/mod.md", "CheckboxMdPage" => "../docs/checkbox/mod.md",
"ColorPickerMdPage" => "../docs/color_picker/mod.md", "ColorPickerMdPage" => "../docs/color_picker/mod.md",
"ComboboxMdPage" => "../docs/combobox/mod.md",
"ConfigProviderMdPage" => "../docs/config_provider/mod.md", "ConfigProviderMdPage" => "../docs/config_provider/mod.md",
"DatePickerMdPage" => "../docs/date_picker/mod.md", "DatePickerMdPage" => "../docs/date_picker/mod.md",
"DividerMdPage" => "../docs/divider/mod.md", "DividerMdPage" => "../docs/divider/mod.md",

View file

@ -0,0 +1,192 @@
.thaw-combobox {
position: relative;
display: inline-grid;
justify-content: space-between;
align-items: center;
grid-template-columns: 1fr auto;
column-gap: var(--spacingHorizontalXXS);
min-width: 250px;
height: 32px;
padding-right: var(--spacingHorizontalMNudge);
background-color: var(--colorNeutralBackground1);
border-radius: var(--borderRadiusMedium);
border: var(--strokeWidthThin) solid var(--colorNeutralStroke1);
border-bottom-color: var(--colorNeutralStrokeAccessible);
box-sizing: border-box;
}
.thaw-combobox:hover {
border-color: var(--colorNeutralStroke1Hover);
border-bottom-color: var(--colorNeutralStrokeAccessible);
}
.thaw-combobox:active {
border-color: var(--colorNeutralStroke1Pressed);
border-bottom-color: var(--colorNeutralStrokeAccessible);
}
.thaw-combobox:focus-within {
outline-color: transparent;
outline-style: solid;
outline-width: 2px;
}
.thaw-combobox::after {
content: "";
position: absolute;
bottom: -1px;
right: -1px;
left: -1px;
height: max(2px, var(--borderRadiusMedium));
border-bottom-left-radius: var(--borderRadiusMedium);
border-bottom-right-radius: var(--borderRadiusMedium);
border-bottom: var(--strokeWidthThick) solid var(--colorCompoundBrandStroke);
transition-delay: var(--curveAccelerateMid);
transition-duration: var(--durationUltraFast);
transition-property: transform;
transform: scaleX(0);
clip-path: inset(calc(100% - 2px) 0px 0px);
box-sizing: border-box;
}
.thaw-combobox:focus-within::after {
transition-delay: var(--curveDecelerateMid);
transition-duration: var(--durationNormal);
transition-property: transform;
transform: scaleX(1);
}
.thaw-combobox:focus-within:active::after {
border-bottom-color: var(--colorCompoundBrandStrokePressed);
}
.thaw-combobox__input {
align-self: stretch;
background-color: var(--colorTransparentBackground);
line-height: var(--lineHeightBase300);
font-weight: var(--fontWeightRegular);
font-size: var(--fontSizeBase300);
font-family: var(--fontFamilyBase);
color: var(--colorNeutralForeground1);
padding: 0 0 0
calc(var(--spacingHorizontalMNudge) + var(--spacingHorizontalXXS));
border: none;
}
.thaw-combobox__input:focus {
outline-style: none;
}
.thaw-combobox__expand-icon {
display: block;
margin-left: var(--spacingHorizontalXXS);
color: var(--colorNeutralStrokeAccessible);
box-sizing: border-box;
cursor: pointer;
font-size: 20px;
}
.thaw-combobox__listbox {
row-gap: var(--spacingHorizontalXXS);
display: flex;
flex-direction: column;
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;
overflow-y: auto;
}
.thaw-combobox__listbox.fade-in-scale-up-transition-leave-active {
transform-origin: inherit;
transition: opacity 0.2s cubic-bezier(0.4, 0, 1, 1),
transform 0.2s cubic-bezier(0.4, 0, 1, 1);
}
.thaw-combobox__listbox.fade-in-scale-up-transition-enter-active {
transform-origin: inherit;
transition: opacity 0.2s cubic-bezier(0, 0, 0.2, 1),
transform 0.2s cubic-bezier(0, 0, 0.2, 1);
}
.thaw-combobox__listbox.fade-in-scale-up-transition-enter-from,
.thaw-combobox__listbox.fade-in-scale-up-transition-leave-to {
opacity: 0;
transform: scale(0.9);
}
.thaw-combobox__listbox.fade-in-scale-up-transition-leave-from,
.thaw-combobox__listbox.fade-in-scale-up-transition-enter-to {
opacity: 1;
transform: scale(1);
}
.thaw-combobox-option {
column-gap: var(--spacingHorizontalXS);
position: relative;
cursor: pointer;
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);
}
.thaw-combobox-option:hover {
color: var(--colorNeutralForeground1Hover);
background-color: var(--colorNeutralBackground1Hover);
}
.thaw-combobox-option:active {
color: var(--colorNeutralForeground1Pressed);
background-color: var(--colorNeutralBackground1Pressed);
}
.thaw-combobox-option__check-icon {
visibility: hidden;
margin-left: calc(var(--spacingHorizontalXXS) * -1);
margin-right: var(--spacingHorizontalXXS);
font-size: var(--fontSizeBase400);
}
.thaw-combobox-option--selected > .thaw-combobox-option__check-icon {
visibility: visible;
}
.thaw-combobox-option__check-icon--multiselect > svg,
.thaw-combobox-option__check-icon > svg {
display: block;
line-height: 0;
}
.thaw-combobox-option__check-icon--multiselect {
display: flex;
align-items: center;
justify-content: center;
visibility: visible;
margin-left: calc(var(--spacingHorizontalXXS) * -1);
margin-right: var(--spacingHorizontalXXS);
width: 16px;
height: 16px;
font-size: 12px;
border-radius: var(--borderRadiusSmall);
border: var(--strokeWidthThin) solid var(--colorNeutralStrokeAccessible);
box-sizing: border-box;
fill: currentcolor;
}
.thaw-combobox-option--selected
> .thaw-combobox-option__check-icon--multiselect {
border-color: var(--colorCompoundBrandBackground);
color: var(--colorNeutralForegroundInverted);
background-color: var(--colorCompoundBrandBackground);
}

View file

@ -0,0 +1,112 @@
use crate::ConfigInjection;
use leptos::*;
use thaw_components::{Binder, CSSTransition, Follower, FollowerPlacement, FollowerWidth};
use thaw_utils::{mount_style, Model};
#[component]
pub fn Combobox(
#[prop(optional, into)] value: Model<Vec<String>>,
#[prop(optional)] multiselect: bool,
children: Children,
) -> impl IntoView {
mount_style("combobox", include_str!("./combobox.css"));
let config_provider = ConfigInjection::use_();
let trigger_ref = NodeRef::<html::Div>::new();
let listbox_ref = NodeRef::<html::Div>::new();
let is_show_listbox = RwSignal::new(false);
view! {
<Binder target_ref=trigger_ref>
<div
class="thaw-combobox"
ref=trigger_ref
on:click=move |_| {
is_show_listbox.set(true);
}
>
<input
type="text"
aria-expanded="true"
role="combobox"
class="thaw-combobox__input"
prop:value=move || {
value.with(|value| {
if multiselect {
None
} else {
value.first().cloned()
}
})
}
/>
<span
aria-expanded="true"
role="button"
aria-label="Open"
class="thaw-combobox__expand-icon"
>
<svg fill="currentColor" aria-hidden="true" width="1em" height="1em" viewBox="0 0 20 20">
<path d="M15.85 7.65c.2.2.2.5 0 .7l-5.46 5.49a.55.55 0 0 1-.78 0L4.15 8.35a.5.5 0 1 1 .7-.7L10 12.8l5.15-5.16c.2-.2.5-.2.7 0Z" fill="currentColor">
</path>
</svg>
</span>
</div>
<Follower
slot
show=is_show_listbox
placement=FollowerPlacement::BottomStart
width=FollowerWidth::MinTarget
>
<Provider value=ComboboxInjection{value, multiselect}>
<CSSTransition
node_ref=listbox_ref
name="fade-in-scale-up-transition"
appear=is_show_listbox.get_untracked()
show=is_show_listbox
let:display
>
<div
class="thaw-config-provider thaw-combobox__listbox"
style=move || display.get()
data-thaw-id=config_provider.id().clone()
ref=listbox_ref
role="listbox"
>
{children()}
</div>
</CSSTransition>
</Provider>
</Follower>
</Binder>
}
}
#[derive(Clone, Copy)]
pub(crate) struct ComboboxInjection {
value: Model<Vec<String>>,
pub multiselect: bool,
}
impl ComboboxInjection {
pub fn use_() -> Self {
expect_context()
}
pub fn is_selected(&self, key: &String) -> bool {
self.value.with(|value| value.contains(key))
}
pub fn on_option_select(&self, key: &String) {
self.value.update(|value| {
if self.multiselect {
if let Some(index) = value.iter().position(|k| k == key) {
value.remove(index);
return;
}
} else {
value.clear();
}
value.push(key.clone());
});
}
}

View file

@ -0,0 +1,49 @@
use crate::ComboboxInjection;
use leptos::*;
use thaw_components::{If, Then};
use thaw_utils::class_list;
#[component]
pub fn ComboboxOption(#[prop(into)] key: String, children: Children) -> impl IntoView {
let combobox = ComboboxInjection::use_();
let key = StoredValue::new(key);
view! {
<div
role="option"
aria-selected="true"
class=class_list![
"thaw-combobox-option",
("thaw-combobox-option--selected", move || key.with_value(|key| combobox.is_selected(&key)))
]
on:click=move |_| {
key.with_value(|key| combobox.on_option_select(key));
}
>
{
if combobox.multiselect {
view! {
<span aria-hidden="true" class="thaw-combobox-option__check-icon--multiselect">
<If cond=Signal::derive(move || key.with_value(|key| combobox.is_selected(&key)))>
<Then slot>
<svg fill="currentColor" aria-hidden="true" width="12" height="12" viewBox="0 0 12 12">
<path d="M9.76 3.2c.3.29.32.76.04 1.06l-4.25 4.5a.75.75 0 0 1-1.08.02L2.22 6.53a.75.75 0 0 1 1.06-1.06l1.7 1.7L8.7 3.24a.75.75 0 0 1 1.06-.04Z" fill="currentColor"></path>
</svg>
</Then>
</If>
</span>
}
} else {
view! {
<span aria-hidden="true" class="thaw-combobox-option__check-icon">
<svg fill="currentColor" aria-hidden="true" width="1em" height="1em" viewBox="0 0 20 20">
<path d="M7.03 13.9 3.56 10a.75.75 0 0 0-1.12 1l4 4.5c.29.32.79.34 1.09.03l10.5-10.5a.75.75 0 0 0-1.06-1.06l-9.94 9.94Z" fill="currentColor"></path>
</svg>
</span>
}
}
}
{children()}
</div>
}
}

5
thaw/src/combobox/mod.rs Normal file
View file

@ -0,0 +1,5 @@
mod combobox;
mod combobox_option;
pub use combobox::*;
pub use combobox_option::*;

View file

@ -12,6 +12,7 @@ mod card;
mod checkbox; mod checkbox;
mod code; mod code;
mod color_picker; mod color_picker;
mod combobox;
mod config_provider; mod config_provider;
mod date_picker; mod date_picker;
mod divider; mod divider;
@ -59,6 +60,7 @@ pub use card::*;
pub use checkbox::*; pub use checkbox::*;
pub use code::*; pub use code::*;
pub use color_picker::*; pub use color_picker::*;
pub use combobox::*;
pub use config_provider::*; pub use config_provider::*;
pub use date_picker::*; pub use date_picker::*;
pub use divider::*; pub use divider::*;