diff --git a/src/auto_complete/auto-complete.css b/src/auto_complete/auto-complete.css index da27465..4ca4cd6 100644 --- a/src/auto_complete/auto-complete.css +++ b/src/auto_complete/auto-complete.css @@ -14,6 +14,7 @@ border-radius: 2px; cursor: pointer; } -.thaw-auto-complete__menu-item:hover { + +.thaw-auto-complete__menu-item--selected { background-color: var(--thaw-background-color-hover); } diff --git a/src/auto_complete/mod.rs b/src/auto_complete/mod.rs index 8179b7a..03ba43b 100644 --- a/src/auto_complete/mod.rs +++ b/src/auto_complete/mod.rs @@ -40,23 +40,78 @@ pub fn AutoComplete( css_vars }); + let select_option_index = create_rw_signal::(0); + let menu_ref = create_node_ref::(); let is_show_menu = create_rw_signal(false); let auto_complete_ref = create_node_ref::(); let options = StoredMaybeSignal::from(options); + let open_menu = move || { + select_option_index.set(0); + is_show_menu.set(true); + }; let allow_value = move |_| { if !is_show_menu.get_untracked() { - is_show_menu.set(true); + open_menu(); } true }; + let select_value = move |option_value: String| { + if clear_after_select.get_untracked() { + value.set(String::new()); + } else { + value.set(option_value.clone()); + } + if let Some(on_select) = on_select { + on_select.call(option_value); + } + is_show_menu.set(false); + }; + + let on_keydown = move |event: ev::KeyboardEvent| { + if !is_show_menu.get_untracked() { + return; + } + let key = event.key(); + if key == "ArrowDown".to_string() { + select_option_index.update(|index| { + if *index == options.with_untracked(|options| options.len()) - 1 { + *index = 0 + } else { + *index += 1 + } + }); + } else if key == "ArrowUp".to_string() { + select_option_index.update(|index| { + if *index == 0 { + *index = options.with_untracked(|options| options.len()) - 1; + } else { + *index -= 1 + } + }); + } else if key == "Enter".to_string() { + 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 + } + }); + if let Some(option_value) = option_value { + select_value(option_value); + } + } + }; + view! { -
+
@@ -67,33 +122,59 @@ pub fn AutoComplete( placement=FollowerPlacement::BottomStart width=FollowerWidth::Target > -
+
{move || { options .get() .into_iter() - .map(|v| { + .enumerate() + .map(|(index, v)| { let AutoCompleteOption { value: option_value, label } = v; + let menu_item_ref = create_node_ref::(); let on_click = move |_| { - if clear_after_select.get_untracked() { - value.set(String::new()); - } else { - value.set(option_value.clone()); - } - if let Some(on_select) = on_select { - on_select.call(option_value.clone()); - } - is_show_menu.set(false); + select_value(option_value.clone()); + }; + let on_mouseenter = move |_| { + select_option_index.set(index); }; let on_mousedown = move |ev: ev::MouseEvent| { ev.prevent_default(); }; + create_effect(move |_| { + if index == select_option_index.get() { + if !is_show_menu.get() { + return; + } + if let Some(menu_item_ref) = menu_item_ref.get() { + let menu_ref = menu_ref.get().unwrap(); + let menu_rect = menu_ref.get_bounding_client_rect(); + let item_rect = menu_item_ref.get_bounding_client_rect(); + if item_rect.y() < menu_rect.y() { + menu_item_ref.scroll_into_view_with_bool(true); + } else if item_rect.y() + item_rect.height() + > menu_rect.y() + menu_rect.height() + { + menu_item_ref.scroll_into_view_with_bool(false); + } + } + } + }); view! {
{label}
diff --git a/src/utils/stored_maybe_signal.rs b/src/utils/stored_maybe_signal.rs index 1d0e99c..fedbe40 100644 --- a/src/utils/stored_maybe_signal.rs +++ b/src/utils/stored_maybe_signal.rs @@ -1,4 +1,7 @@ -use leptos::{MaybeSignal, Signal, SignalGet, SignalGetUntracked, SignalWith, StoredValue}; +use leptos::{ + MaybeSignal, Signal, SignalGet, SignalGetUntracked, SignalWith, SignalWithUntracked, + StoredValue, +}; #[derive(Clone)] pub enum StoredMaybeSignal @@ -65,6 +68,24 @@ impl SignalWith for StoredMaybeSignal { } } +impl SignalWithUntracked for StoredMaybeSignal { + type Value = T; + + fn with_untracked(&self, f: impl FnOnce(&Self::Value) -> O) -> O { + match self { + StoredMaybeSignal::StoredValue(value) => value.with_value(f), + StoredMaybeSignal::Signal(signal) => signal.with_untracked(f), + } + } + + fn try_with_untracked(&self, f: impl FnOnce(&Self::Value) -> O) -> Option { + match self { + StoredMaybeSignal::StoredValue(value) => value.try_with_value(f), + StoredMaybeSignal::Signal(signal) => signal.try_with_untracked(f), + } + } +} + impl From> for StoredMaybeSignal { fn from(value: MaybeSignal) -> Self { match value {