Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow free input in AutoComplete #107

Merged
merged 1 commit into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions demo_markdown/docs/auto_complete/mod.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ view! {
| placeholder | `OptionalProp<RwSignal<String>>` | `Default::default()` | Autocomplete's placeholder. |
| options | `MaybeSignal<Vec<AutoCompleteOption>>` | `Default::default()` | Options to autocomplete from. |
| 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. |
| clear_after_select | `MaybeSignal<bool>` | `false` | Whether to clear after selection. |
| on_select | `Option<Callback<String>>` | `None` | On select callback function. |
Expand Down
61 changes: 43 additions & 18 deletions thaw/src/auto_complete/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub fn AutoComplete(
#[prop(optional, into)] clear_after_select: MaybeSignal<bool>,
#[prop(optional, into)] on_select: Option<Callback<String>>,
#[prop(optional, into)] disabled: MaybeSignal<bool>,
#[prop(optional, into)] allow_free_input: bool,
#[prop(optional, into)] invalid: MaybeSignal<bool>,
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
#[prop(optional)] auto_complete_prefix: Option<AutoCompletePrefix>,
Expand All @@ -57,13 +58,15 @@ pub fn AutoComplete(
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 is_show_menu = create_rw_signal(false);
let auto_complete_ref = create_node_ref::<html::Div>();
let options = StoredMaybeSignal::from(options);
let open_menu = move || {
select_option_index.set(0);
select_option_index.set(default_index);
is_show_menu.set(true);
};
let allow_value = move |_| {
Expand All @@ -82,39 +85,61 @@ pub fn AutoComplete(
if let Some(on_select) = on_select {
on_select.call(option_value);
}
if allow_free_input {
select_option_index.set(None);
}
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| {
if !is_show_menu.get_untracked() {
return;
}
let key = event.key();
if key == *"ArrowDown" {
select_option_index.update(|index| {
if *index == options.with_untracked(|options| options.len()) - 1 {
*index = 0
if *index == Some(options.with_untracked(|options| options.len()) - 1) {
*index = default_index
} else {
*index += 1
*index = Some(index.map_or(0, |index| index + 1))
}
});
} else if key == *"ArrowUp" {
select_option_index.update(|index| {
if *index == 0 {
*index = options.with_untracked(|options| options.len()) - 1;
} else {
*index -= 1
}
match *index {
None => *index = Some(options.with_untracked(|options| options.len()) - 1),
Some(0) => {
if allow_free_input {
*index = None
} else {
*index = Some(options.with_untracked(|options| options.len()) - 1)
}
}
Some(prev_index) => *index = Some(prev_index - 1),
};
});
} else if key == *"Enter" {
event.prevent_default();
let option_value = options.with_untracked(|options| {
let index = select_option_index.get_untracked();
if options.len() > index {
let option = &options[index];
Some(option.value.clone())
} else {
None
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];
Some(option.value.clone())
}
_ => None,
}
});
if let Some(option_value) = option_value {
Expand Down Expand Up @@ -189,13 +214,13 @@ pub fn AutoComplete(
select_value(option_value.clone());
};
let on_mouseenter = move |_| {
select_option_index.set(index);
select_option_index.set(Some(index));
};
let on_mousedown = move |ev: ev::MouseEvent| {
ev.prevent_default();
};
create_effect(move |_| {
if index == select_option_index.get() {
if Some(index) == select_option_index.get() {
if !is_show_menu.get() {
return;
}
Expand All @@ -218,7 +243,7 @@ pub fn AutoComplete(
class="thaw-auto-complete__menu-item"
class=(
"thaw-auto-complete__menu-item--selected",
move || index == select_option_index.get(),
move || Some(index) == select_option_index.get(),
)

on:click=on_click
Expand Down
Loading