feat: ComboboxOption adds disabled prop

This commit is contained in:
luoxiao 2024-08-06 21:56:18 +08:00
parent 25d0b81c07
commit 1c243922bd
4 changed files with 97 additions and 23 deletions

View file

@ -78,6 +78,11 @@
outline-style: none; outline-style: none;
} }
.thaw-combobox__input::placeholder {
color: var(--colorNeutralForeground4);
opacity: 1;
}
.thaw-combobox__clear-icon, .thaw-combobox__clear-icon,
.thaw-combobox__expand-icon { .thaw-combobox__expand-icon {
display: block; display: block;
@ -88,6 +93,28 @@
font-size: 20px; font-size: 20px;
} }
.thaw-combobox.thaw-combobox--disabled {
border-color: var(--colorNeutralStrokeDisabled);
border-bottom-color: var(--colorNeutralStrokeDisabled);
background-color: var(--colorTransparentBackground);
cursor: not-allowed;
}
.thaw-combobox--disabled > .thaw-combobox__input {
background-color: var(--colorTransparentBackground);
color: var(--colorNeutralForegroundDisabled);
cursor: not-allowed;
}
.thaw-combobox--disabled > .thaw-combobox__input::placeholder {
color: var(--colorNeutralForegroundDisabled);
}
.thaw-combobox--disabled > .thaw-combobox__clear-icon,
.thaw-combobox--disabled > .thaw-combobox__expand-icon {
cursor: not-allowed;
}
.thaw-combobox__listbox { .thaw-combobox__listbox {
row-gap: var(--spacingHorizontalXXS); row-gap: var(--spacingHorizontalXXS);
display: flex; display: flex;
@ -181,3 +208,13 @@
color: var(--colorNeutralForegroundInverted); color: var(--colorNeutralForegroundInverted);
background-color: var(--colorCompoundBrandBackground); background-color: var(--colorCompoundBrandBackground);
} }
.thaw-combobox-option--disabled {
color: var(--colorNeutralForegroundDisabled);
}
.thaw-combobox-option--disabled:active,
.thaw-combobox-option--disabled:hover {
background-color: var(--colorTransparentBackground);
color: var(--colorNeutralForegroundDisabled);
}

View file

@ -10,8 +10,11 @@ pub fn Combobox(
#[prop(optional, into)] class: MaybeProp<String>, #[prop(optional, into)] class: MaybeProp<String>,
#[prop(optional, into)] value: Model<String>, #[prop(optional, into)] value: Model<String>,
#[prop(optional, into)] selected_options: VecModel<String>, #[prop(optional, into)] selected_options: VecModel<String>,
#[prop(optional, into)] disabled: MaybeSignal<bool>,
#[prop(optional, into)] placeholder: MaybeProp<String>,
/// If set, the combobox will show an icon to clear the current value. /// If set, the combobox will show an icon to clear the current value.
#[prop(optional)] clearable: 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"));
@ -19,7 +22,7 @@ pub fn Combobox(
let input_ref = NodeRef::<html::Input>::new(); let input_ref = NodeRef::<html::Input>::new();
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 options = StoredValue::new(HashMap::<String, (String, String)>::new()); let options = StoredValue::new(HashMap::<String, (String, String, MaybeSignal<bool>)>::new());
let clear_icon_ref = NodeRef::<html::Span>::new(); let clear_icon_ref = NodeRef::<html::Span>::new();
let is_show_clear_icon = Memo::new(move |_| { let is_show_clear_icon = Memo::new(move |_| {
@ -40,6 +43,9 @@ pub fn Combobox(
return; return;
}; };
let handler = add_event_listener(clear_icon_el.into(), ev::click, move |e| { let handler = add_event_listener(clear_icon_el.into(), ev::click, move |e| {
if disabled.get_untracked() {
return;
}
e.stop_propagation(); e.stop_propagation();
selected_options.set(vec![]); selected_options.set(vec![]);
}); });
@ -138,7 +144,10 @@ pub fn Combobox(
&active_descendant_controller, &active_descendant_controller,
move |option| { move |option| {
combobox_injection.options.with_value(|options| { combobox_injection.options.with_value(|options| {
if let Some((value, text)) = options.get(&option.id()) { if let Some((value, text, disabled)) = options.get(&option.id()) {
if disabled.get_untracked() {
return;
}
combobox_injection.select_option(value, text); combobox_injection.select_option(value, text);
} }
}); });
@ -149,7 +158,11 @@ pub fn Combobox(
view! { view! {
<Binder target_ref=trigger_ref> <Binder target_ref=trigger_ref>
<div <div
class=class_list!["thaw-combobox", class] class=class_list![
"thaw-combobox",
("thaw-combobox--disabled", move || disabled.get()),
class
]
node_ref=trigger_ref node_ref=trigger_ref
> >
<input <input
@ -160,6 +173,8 @@ pub fn Combobox(
prop:value=move || { prop:value=move || {
value.get() value.get()
} }
placeholder=move || placeholder.get()
disabled=move || disabled.get()
node_ref=input_ref node_ref=input_ref
on:input=on_input on:input=on_input
on:blur=on_blur on:blur=on_blur
@ -187,12 +202,16 @@ pub fn Combobox(
} }
} }
<span <span
aria-expanded="true" aria-disabled=move || if disabled.get() { "true" } else { "" }
aria-expanded=move || is_show_listbox.get().to_string()
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").unwrap_or_default() style=move || is_show_clear_icon.get().then(|| "display: none").unwrap_or_default()
on:click=move |_| { on:click=move |_| {
if disabled.get_untracked() {
return;
}
is_show_listbox.update(|show| *show = !*show); is_show_listbox.update(|show| *show = !*show);
if let Some(el) = input_ref.get_untracked() { if let Some(el) = input_ref.get_untracked() {
let _ = el.focus(); let _ = el.focus();
@ -225,7 +244,7 @@ pub fn Combobox(
pub(crate) struct ComboboxInjection { pub(crate) struct ComboboxInjection {
value: Model<String>, value: Model<String>,
selected_options: VecModel<String>, selected_options: VecModel<String>,
options: StoredValue<HashMap<String, (String, String)>>, options: StoredValue<HashMap<String, (String, String, MaybeSignal<bool>)>>,
is_show_listbox: RwSignal<bool>, is_show_listbox: RwSignal<bool>,
pub multiselect: bool, pub multiselect: bool,
} }
@ -235,7 +254,7 @@ impl ComboboxInjection {
expect_context() expect_context()
} }
pub fn insert_option(&self, id: String, value: (String, String)) { pub fn insert_option(&self, id: String, value: (String, String, MaybeSignal<bool>)) {
self.options self.options
.update_value(|options| options.insert(id, value)); .update_value(|options| options.insert(id, value));
} }

View file

@ -7,10 +7,16 @@ use thaw_utils::class_list;
#[component] #[component]
pub fn ComboboxOption( pub fn ComboboxOption(
#[prop(optional, into)] class: MaybeProp<String>, #[prop(optional, into)] class: MaybeProp<String>,
/// Sets an option to the disabled state. Disabled options cannot be selected,
/// but are still keyboard navigable.
#[prop(optional, into)]
disabled: MaybeSignal<bool>,
/// Defines a unique identifier for the option. Defaults to `text` if not provided. /// Defines a unique identifier for the option. Defaults to `text` if not provided.
#[prop(optional, into)] value: Option<String>, #[prop(optional, into)]
value: Option<String>,
/// An optional override the string value of the Option's display text, defaulting to the Option's child content. /// An optional override the string value of the Option's display text, defaulting to the Option's child content.
#[prop(into)] text: String, #[prop(into)]
text: String,
#[prop(optional)] children: Option<Children>, #[prop(optional)] children: Option<Children>,
) -> impl IntoView { ) -> impl IntoView {
let combobox = ComboboxInjection::expect_context(); let combobox = ComboboxInjection::expect_context();
@ -21,6 +27,9 @@ pub fn ComboboxOption(
let id = uuid::Uuid::new_v4().to_string(); let id = uuid::Uuid::new_v4().to_string();
let on_click = move |_| { let on_click = move |_| {
if disabled.get_untracked() {
return;
}
text.with_value(|text| { text.with_value(|text| {
value.with_value(|value| { value.with_value(|value| {
combobox.select_option(value, text); combobox.select_option(value, text);
@ -29,7 +38,7 @@ pub fn ComboboxOption(
}; };
{ {
combobox.insert_option(id.clone(), (value.get_value(), text.get_value())); combobox.insert_option(id.clone(), (value.get_value(), text.get_value(), disabled));
let id = id.clone(); let id = id.clone();
listbox.trigger(); listbox.trigger();
on_cleanup(move || { on_cleanup(move || {
@ -41,11 +50,13 @@ pub fn ComboboxOption(
view! { view! {
<div <div
role="option" role="option"
aria-selected=move || if is_selected.get() { "true" } else { "false" } aria-disabled=move || if disabled.get() { "true" } else { "" }
aria-selected=move || is_selected.get().to_string()
id=id id=id
class=class_list![ class=class_list![
"thaw-combobox-option", "thaw-combobox-option",
("thaw-combobox-option--selected", move || is_selected.get()), ("thaw-combobox-option--selected", move || is_selected.get()),
("thaw-combobox-option--disabled", move || disabled.get()),
class class
] ]
on:click=on_click on:click=on_click

View file

@ -4,8 +4,8 @@
let selected_options = RwSignal::new(None::<String>); let selected_options = RwSignal::new(None::<String>);
view! { view! {
<Combobox selected_options> <Combobox selected_options placeholder="Select an animal">
<ComboboxOption value="cat" text="Car" /> <ComboboxOption value="cat" text="Cat" disabled=true/>
<ComboboxOption value="dog" text="Dog" /> <ComboboxOption value="dog" text="Dog" />
</Combobox> </Combobox>
} }
@ -18,7 +18,7 @@ let selected_options = RwSignal::new(vec![]);
view! { view! {
<Combobox selected_options clearable=true> <Combobox selected_options clearable=true>
<ComboboxOption value="cat" text="Car" /> <ComboboxOption value="cat" text="Cat" disabled=true/>
<ComboboxOption value="dog" text="Dog" /> <ComboboxOption value="dog" text="Dog" />
</Combobox> </Combobox>
} }
@ -31,7 +31,7 @@ let selected_options = RwSignal::new(vec![]);
view! { view! {
<Combobox selected_options> <Combobox selected_options>
<ComboboxOption value="cat" text="Car" /> <ComboboxOption value="cat" text="Cat" disabled=true/>
<ComboboxOption value="dog" text="Dog" /> <ComboboxOption value="dog" text="Dog" />
</Combobox> </Combobox>
} }
@ -49,21 +49,28 @@ view! {
<ComboboxOptionGroup label="Land"> <ComboboxOptionGroup label="Land">
{ {
land.into_iter().map(|option| view!{ land.into_iter().map(|option| view!{
<ComboboxOption value={option.clone()}> <ComboboxOption text={option} />
{option}
</ComboboxOption>
}).collect_view() }).collect_view()
} }
</ComboboxOptionGroup> </ComboboxOptionGroup>
<OptionGroup label="Sea"> <ComboboxOptionGroup label="Sea">
{ {
water.into_iter().map(|option| view!{ water.into_iter().map(|option| view!{
<ComboboxOption value={option.clone()}> <ComboboxOption text={option} />
{option}
</ComboboxOption>
}).collect_view() }).collect_view()
} }
</OptionGroup> </ComboboxOptionGroup>
</Combobox>
}
```
### Disabled
```rust demo
view! {
<Combobox disabled=true>
<ComboboxOption value="cat" text="Car" />
<ComboboxOption value="dog" text="Dog" />
</Combobox> </Combobox>
} }
``` ```