feat: Combobox adds clearable prop

This commit is contained in:
luoxiao 2024-06-27 17:31:52 +08:00
parent c7be391441
commit f6d534220e
3 changed files with 101 additions and 8 deletions

View file

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

View file

@ -78,6 +78,7 @@
outline-style: none; outline-style: none;
} }
.thaw-combobox__clear-icon,
.thaw-combobox__expand-icon { .thaw-combobox__expand-icon {
display: block; display: block;
margin-left: var(--spacingHorizontalXXS); margin-left: var(--spacingHorizontalXXS);
@ -152,7 +153,6 @@
.thaw-combobox-option__check-icon { .thaw-combobox-option__check-icon {
visibility: hidden; visibility: hidden;
margin-left: calc(var(--spacingHorizontalXXS) * -1); margin-left: calc(var(--spacingHorizontalXXS) * -1);
margin-right: var(--spacingHorizontalXXS); margin-right: var(--spacingHorizontalXXS);
font-size: var(--fontSizeBase400); font-size: var(--fontSizeBase400);
@ -162,6 +162,8 @@
visibility: visible; visibility: visible;
} }
.thaw-combobox__clear-icon > svg,
.thaw-combobox__expand-icon > svg,
.thaw-combobox-option__check-icon--multiselect > svg, .thaw-combobox-option__check-icon--multiselect > svg,
.thaw-combobox-option__check-icon > svg { .thaw-combobox-option__check-icon > svg {
display: block; display: block;

View file

@ -1,12 +1,13 @@
use crate::ConfigInjection; use crate::ConfigInjection;
use leptos::*; use leptos::*;
use thaw_components::{Binder, CSSTransition, Follower, FollowerPlacement, FollowerWidth}; use thaw_components::{Binder, CSSTransition, Follower, FollowerPlacement, FollowerWidth};
use thaw_utils::{mount_style, Model}; use thaw_utils::{add_event_listener, mount_style, Model};
#[component] #[component]
pub fn Combobox( pub fn Combobox(
#[prop(optional, into)] value: Model<Vec<String>>, #[prop(optional, into)] value: Model<Vec<String>>,
#[prop(optional)] multiselect: bool, #[prop(optional)] multiselect: bool,
#[prop(optional)] clearable: bool,
children: Children, children: Children,
) -> impl IntoView { ) -> impl IntoView {
mount_style("combobox", include_str!("./combobox.css")); mount_style("combobox", include_str!("./combobox.css"));
@ -15,13 +16,63 @@ pub fn Combobox(
let listbox_ref = NodeRef::<html::Div>::new(); let listbox_ref = NodeRef::<html::Div>::new();
let is_show_listbox = RwSignal::new(false); let is_show_listbox = RwSignal::new(false);
let clear_icon_ref = NodeRef::<html::Span>::new();
let is_show_clear_icon = Memo::new(move |_| {
if clearable {
value.with(|value| !value.is_empty())
} else {
false
}
});
if clearable {
clear_icon_ref.on_load(move |clear_icon_el| {
let handler = add_event_listener(clear_icon_el.into_any(), ev::click, move |e| {
e.stop_propagation();
value.set(vec![]);
});
on_cleanup(move || handler.remove());
});
}
#[cfg(any(feature = "csr", feature = "hydrate"))]
{
let handle = window_event_listener(ev::click, move |ev| {
use leptos::wasm_bindgen::__rt::IntoJsResult;
if !is_show_listbox.get_untracked() {
return;
}
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();
if let Some(listbox_el) = listbox_ref.get_untracked() {
if let Some(trigger_el) = trigger_ref.get_untracked() {
while let Some(current_el) = el {
if current_el == *body {
break;
};
if current_el == ***listbox_el {
return;
}
if current_el == ***trigger_el {
return;
}
el = current_el.parent_element();
}
}
}
is_show_listbox.set(false);
});
on_cleanup(move || handle.remove());
}
view! { view! {
<Binder target_ref=trigger_ref> <Binder target_ref=trigger_ref>
<div <div
class="thaw-combobox" class="thaw-combobox"
ref=trigger_ref ref=trigger_ref
on:click=move |_| { on:click=move |_| {
is_show_listbox.set(true); is_show_listbox.update(|show| *show = !*show);
} }
> >
<input <input
@ -29,21 +80,42 @@ pub fn Combobox(
aria-expanded="true" aria-expanded="true"
role="combobox" role="combobox"
class="thaw-combobox__input" class="thaw-combobox__input"
// #TODO
readonly=true
prop:value=move || { prop:value=move || {
value.with(|value| { value.with(|value| {
if multiselect { if multiselect {
None String::new()
} else { } else {
value.first().cloned() value.first().cloned().unwrap_or_default()
} }
}) })
} }
/> />
{
if clearable {
view! {
<span
aria-hidden="true"
class="thaw-combobox__clear-icon"
style=move || (!is_show_clear_icon.get()).then(|| "display: none")
ref=clear_icon_ref
>
<svg fill="currentColor" aria-hidden="true" width="1em" height="1em" viewBox="0 0 20 20">
<path d="m4.09 4.22.06-.07a.5.5 0 0 1 .63-.06l.07.06L10 9.29l5.15-5.14a.5.5 0 0 1 .63-.06l.07.06c.18.17.2.44.06.63l-.06.07L10.71 10l5.14 5.15c.18.17.2.44.06.63l-.06.07a.5.5 0 0 1-.63.06l-.07-.06L10 10.71l-5.15 5.14a.5.5 0 0 1-.63.06l-.07-.06a.5.5 0 0 1-.06-.63l.06-.07L9.29 10 4.15 4.85a.5.5 0 0 1-.06-.63l.06-.07-.06.07Z" fill="currentColor"></path>
</svg>
</span>
}.into()
} else {
None
}
}
<span <span
aria-expanded="true" aria-expanded="true"
role="button" role="button"
aria-label="Open" aria-label="Open"
class="thaw-combobox__expand-icon" class="thaw-combobox__expand-icon"
style=move || is_show_clear_icon.get().then(|| "display: none")
> >
<svg fill="currentColor" aria-hidden="true" width="1em" height="1em" viewBox="0 0 20 20"> <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 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">
@ -57,7 +129,7 @@ pub fn Combobox(
placement=FollowerPlacement::BottomStart placement=FollowerPlacement::BottomStart
width=FollowerWidth::MinTarget width=FollowerWidth::MinTarget
> >
<Provider value=ComboboxInjection{value, multiselect}> <Provider value=ComboboxInjection{value, multiselect, is_show_listbox}>
<CSSTransition <CSSTransition
node_ref=listbox_ref node_ref=listbox_ref
name="fade-in-scale-up-transition" name="fade-in-scale-up-transition"
@ -84,6 +156,7 @@ pub fn Combobox(
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub(crate) struct ComboboxInjection { pub(crate) struct ComboboxInjection {
value: Model<Vec<String>>, value: Model<Vec<String>>,
is_show_listbox: RwSignal<bool>,
pub multiselect: bool, pub multiselect: bool,
} }
@ -103,10 +176,11 @@ impl ComboboxInjection {
value.remove(index); value.remove(index);
return; return;
} }
value.push(key.clone());
} else { } else {
value.clear(); *value = vec![key.clone()];
self.is_show_listbox.set(false);
} }
value.push(key.clone());
}); });
} }
} }