feat: allow free input in AutoComplete (#107)

This commit is contained in:
Kirill Zaborsky 2024-02-22 10:38:30 +03:00 committed by GitHub
parent 3a9da44de0
commit 1dbe00b5af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 44 additions and 18 deletions

View file

@ -65,6 +65,7 @@ view! {
| placeholder | `OptionalProp<RwSignal<String>>` | `Default::default()` | Autocomplete's placeholder. | | placeholder | `OptionalProp<RwSignal<String>>` | `Default::default()` | Autocomplete's placeholder. |
| options | `MaybeSignal<Vec<AutoCompleteOption>>` | `Default::default()` | Options to autocomplete from. | | options | `MaybeSignal<Vec<AutoCompleteOption>>` | `Default::default()` | Options to autocomplete from. |
| disabled | `MaybeSignal<bool>` | `false` | Whether the input is disabled. | | disabled | `MaybeSignal<bool>` | `false` | Whether the input is disabled. |
| allow_free_input | `bool` | `false` | Whether free text input is allowed. |
| invalid | `MaybeSignal<bool>` | `false` | Whether the input is invalid. | | invalid | `MaybeSignal<bool>` | `false` | Whether the input is invalid. |
| clear_after_select | `MaybeSignal<bool>` | `false` | Whether to clear after selection. | | clear_after_select | `MaybeSignal<bool>` | `false` | Whether to clear after selection. |
| on_select | `Option<Callback<String>>` | `None` | On select callback function. | | on_select | `Option<Callback<String>>` | `None` | On select callback function. |

View file

@ -33,6 +33,7 @@ pub fn AutoComplete(
#[prop(optional, into)] clear_after_select: MaybeSignal<bool>, #[prop(optional, into)] clear_after_select: MaybeSignal<bool>,
#[prop(optional, into)] on_select: Option<Callback<String>>, #[prop(optional, into)] on_select: Option<Callback<String>>,
#[prop(optional, into)] disabled: MaybeSignal<bool>, #[prop(optional, into)] disabled: MaybeSignal<bool>,
#[prop(optional, into)] allow_free_input: bool,
#[prop(optional, into)] invalid: MaybeSignal<bool>, #[prop(optional, into)] invalid: MaybeSignal<bool>,
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>, #[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
#[prop(optional)] auto_complete_prefix: Option<AutoCompletePrefix>, #[prop(optional)] auto_complete_prefix: Option<AutoCompletePrefix>,
@ -57,13 +58,15 @@ pub fn AutoComplete(
css_vars css_vars
}); });
let select_option_index = create_rw_signal::<usize>(0); let default_index = if allow_free_input { None } else { Some(0) };
let select_option_index = create_rw_signal::<Option<usize>>(default_index);
let menu_ref = create_node_ref::<html::Div>(); let menu_ref = create_node_ref::<html::Div>();
let is_show_menu = create_rw_signal(false); let is_show_menu = create_rw_signal(false);
let auto_complete_ref = create_node_ref::<html::Div>(); let auto_complete_ref = create_node_ref::<html::Div>();
let options = StoredMaybeSignal::from(options); let options = StoredMaybeSignal::from(options);
let open_menu = move || { let open_menu = move || {
select_option_index.set(0); select_option_index.set(default_index);
is_show_menu.set(true); is_show_menu.set(true);
}; };
let allow_value = move |_| { let allow_value = move |_| {
@ -82,9 +85,20 @@ pub fn AutoComplete(
if let Some(on_select) = on_select { if let Some(on_select) = on_select {
on_select.call(option_value); on_select.call(option_value);
} }
if allow_free_input {
select_option_index.set(None);
}
is_show_menu.set(false); is_show_menu.set(false);
}; };
// we unset selection index whenever options get changed
// otherwise e.g. selection could move from one item to
// another staying on the same index
create_effect(move |_| {
options.track();
select_option_index.set(default_index);
});
let on_keydown = move |event: ev::KeyboardEvent| { let on_keydown = move |event: ev::KeyboardEvent| {
if !is_show_menu.get_untracked() { if !is_show_menu.get_untracked() {
return; return;
@ -92,29 +106,40 @@ pub fn AutoComplete(
let key = event.key(); let key = event.key();
if key == *"ArrowDown" { if key == *"ArrowDown" {
select_option_index.update(|index| { select_option_index.update(|index| {
if *index == options.with_untracked(|options| options.len()) - 1 { if *index == Some(options.with_untracked(|options| options.len()) - 1) {
*index = 0 *index = default_index
} else { } else {
*index += 1 *index = Some(index.map_or(0, |index| index + 1))
} }
}); });
} else if key == *"ArrowUp" { } else if key == *"ArrowUp" {
select_option_index.update(|index| { select_option_index.update(|index| {
if *index == 0 { match *index {
*index = options.with_untracked(|options| options.len()) - 1; None => *index = Some(options.with_untracked(|options| options.len()) - 1),
Some(0) => {
if allow_free_input {
*index = None
} else { } else {
*index -= 1 *index = Some(options.with_untracked(|options| options.len()) - 1)
} }
}
Some(prev_index) => *index = Some(prev_index - 1),
};
}); });
} else if key == *"Enter" { } else if key == *"Enter" {
event.prevent_default(); event.prevent_default();
let option_value = options.with_untracked(|options| { let option_value = options.with_untracked(|options| {
let index = select_option_index.get_untracked(); let index = select_option_index.get_untracked();
if options.len() > index { match index {
None if allow_free_input => {
let value = value.get_untracked();
(!value.is_empty()).then_some(value)
}
Some(index) if options.len() > index => {
let option = &options[index]; let option = &options[index];
Some(option.value.clone()) Some(option.value.clone())
} else { }
None _ => None,
} }
}); });
if let Some(option_value) = option_value { if let Some(option_value) = option_value {
@ -189,13 +214,13 @@ pub fn AutoComplete(
select_value(option_value.clone()); select_value(option_value.clone());
}; };
let on_mouseenter = move |_| { let on_mouseenter = move |_| {
select_option_index.set(index); select_option_index.set(Some(index));
}; };
let on_mousedown = move |ev: ev::MouseEvent| { let on_mousedown = move |ev: ev::MouseEvent| {
ev.prevent_default(); ev.prevent_default();
}; };
create_effect(move |_| { create_effect(move |_| {
if index == select_option_index.get() { if Some(index) == select_option_index.get() {
if !is_show_menu.get() { if !is_show_menu.get() {
return; return;
} }
@ -218,7 +243,7 @@ pub fn AutoComplete(
class="thaw-auto-complete__menu-item" class="thaw-auto-complete__menu-item"
class=( class=(
"thaw-auto-complete__menu-item--selected", "thaw-auto-complete__menu-item--selected",
move || index == select_option_index.get(), move || Some(index) == select_option_index.get(),
) )
on:click=on_click on:click=on_click