diff --git a/demo/src/components/site_header.rs b/demo/src/components/site_header.rs index c5cb0e0..ff75af7 100644 --- a/demo/src/components/site_header.rs +++ b/demo/src/components/site_header.rs @@ -150,7 +150,7 @@ pub fn SiteHeader() -> impl IntoView { comp_ref=auto_complete_ref > - {option.label} + {option.label} - + {option.1} @@ -36,14 +36,6 @@ view! { } ``` -### Invalid - -```rust demo -view! { - -} -``` - ### Prefix & Suffix ```rust demo diff --git a/demo_markdown/docs/input/mod.md b/demo_markdown/docs/input/mod.md index 9fa6571..1b00cfd 100644 --- a/demo_markdown/docs/input/mod.md +++ b/demo_markdown/docs/input/mod.md @@ -54,16 +54,6 @@ view! { } ``` -### Invalid - -```rust demo -let value = RwSignal::new(String::from("o")); - -view! { - -} -``` - ### Imperative handle ```rust demo diff --git a/thaw/src/_aria/active_descendant/use_active_descendant.rs b/thaw/src/_aria/active_descendant/use_active_descendant.rs index 25048b8..df8c325 100644 --- a/thaw/src/_aria/active_descendant/use_active_descendant.rs +++ b/thaw/src/_aria/active_descendant/use_active_descendant.rs @@ -1,6 +1,7 @@ use super::use_option_walker::{use_option_walker, OptionWalker}; use send_wrapper::SendWrapper; use std::{cell::RefCell, sync::Arc}; +use thaw_utils::scroll_into_view; use web_sys::{HtmlElement, Node}; /// Applied to the element that is active descendant @@ -50,7 +51,7 @@ impl ActiveDescendantController { fn focus_active_descendant(&self, next_active: HtmlElement) { self.blur_active_descendant(); - + scroll_into_view(&next_active); let _ = next_active.set_attribute(ACTIVEDESCENDANT_ATTRIBUTE, ""); let _ = next_active.set_attribute(ACTIVEDESCENDANT_FOCUSVISIBLE_ATTRIBUTE, ""); diff --git a/thaw/src/auto_complete/auto-complete.css b/thaw/src/auto_complete/auto-complete.css index 3580831..6d2ca6a 100644 --- a/thaw/src/auto_complete/auto-complete.css +++ b/thaw/src/auto_complete/auto-complete.css @@ -33,34 +33,6 @@ div.thaw-auto-complete__listbox { background-color: var(--thaw-background-color-hover); } -.thaw-auto-complete__listbox.fade-in-scale-up-transition-leave-active { - transform-origin: inherit; - transition: opacity 0.2s cubic-bezier(0.4, 0, 1, 1), - transform 0.2s cubic-bezier(0.4, 0, 1, 1), - background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), - box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1); -} - -.thaw-auto-complete__listbox.fade-in-scale-up-transition-enter-active { - transform-origin: inherit; - transition: opacity 0.2s cubic-bezier(0, 0, 0.2, 1), - transform 0.2s cubic-bezier(0, 0, 0.2, 1), - background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), - box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1); -} - -.thaw-auto-complete__listbox.fade-in-scale-up-transition-enter-from, -.thaw-auto-complete__listbox.fade-in-scale-up-transition-leave-to { - opacity: 0; - transform: scale(0.9); -} - -.thaw-auto-complete__listbox.fade-in-scale-up-transition-leave-from, -.thaw-auto-complete__listbox.fade-in-scale-up-transition-enter-to { - opacity: 1; - transform: scale(1); -} - .thaw-auto-complete-option { column-gap: var(--spacingHorizontalXS); position: relative; @@ -75,6 +47,19 @@ div.thaw-auto-complete__listbox { cursor: pointer; } +.thaw-auto-complete-option[data-activedescendant-focusvisible]::after { + content: ""; + position: absolute; + right: -2px; + left: -2px; + bottom: -2px; + top: -2px; + z-index: 1; + pointer-events: none; + border-radius: var(--borderRadiusMedium); + border: 2px solid var(--colorStrokeFocus2); +} + .thaw-auto-complete-option:hover { color: var(--colorNeutralForeground1Hover); background-color: var(--colorNeutralBackground1Hover); diff --git a/thaw/src/auto_complete/auto_complete_option.rs b/thaw/src/auto_complete/auto_complete_option.rs index e517c12..0d400a7 100644 --- a/thaw/src/auto_complete/auto_complete_option.rs +++ b/thaw/src/auto_complete/auto_complete_option.rs @@ -2,17 +2,28 @@ use super::AutoCompleteInjection; use leptos::prelude::*; #[component] -pub fn AutoCompleteOption(key: String, children: Children) -> impl IntoView { - let value = key.clone(); - let auto_complete = AutoCompleteInjection::use_(); - let is_selected = Memo::new(move |_| auto_complete.is_selected(&key)); +pub fn AutoCompleteOption(value: String, children: Children) -> impl IntoView { + let auto_complete = AutoCompleteInjection::expect_context(); + let is_selected = Memo::new({ + let value = value.clone(); + move |_| auto_complete.is_selected(&value) + }); + let id = uuid::Uuid::new_v4().to_string(); + auto_complete.insert_option(id.clone(), value.clone()); + { + let id = id.clone(); + on_cleanup(move || { + auto_complete.remove_option(&id); + }); + } view! {
{children()}
diff --git a/thaw/src/auto_complete/mod.rs b/thaw/src/auto_complete/mod.rs index 8f16449..0cac064 100644 --- a/thaw/src/auto_complete/mod.rs +++ b/thaw/src/auto_complete/mod.rs @@ -2,9 +2,14 @@ mod auto_complete_option; pub use auto_complete_option::AutoCompleteOption; -use crate::{ComponentRef, ConfigInjection, Input, InputPrefix, InputRef, InputSuffix}; +use crate::{ + combobox::listbox::{listbox_keyboard_event, Listbox}, + ComponentRef, Input, InputPrefix, InputRef, InputSuffix, + _aria::use_active_descendant, +}; use leptos::{context::Provider, either::Either, html, prelude::*}; -use thaw_components::{Binder, CSSTransition, Follower, FollowerPlacement, FollowerWidth}; +use std::collections::HashMap; +use thaw_components::{Binder, Follower, FollowerPlacement, FollowerWidth}; use thaw_utils::{class_list, mount_style, BoxOneCallback, Model, OptionalProp}; #[slot] @@ -25,8 +30,6 @@ pub fn AutoComplete( #[prop(optional, into)] blur_after_select: MaybeSignal, #[prop(optional, into)] on_select: Option>, #[prop(optional, into)] disabled: MaybeSignal, - #[prop(optional, into)] allow_free_input: bool, - #[prop(optional, into)] invalid: MaybeSignal, #[prop(optional, into)] class: OptionalProp>, #[prop(optional)] auto_complete_prefix: Option, #[prop(optional)] auto_complete_suffix: Option, @@ -34,27 +37,20 @@ pub fn AutoComplete( #[prop(optional)] children: Option, ) -> impl IntoView { mount_style("auto-complete", include_str!("./auto-complete.css")); - let config_provider = ConfigInjection::use_(); let input_ref = ComponentRef::::new(); - - let default_index = if allow_free_input { None } else { Some(0) }; - - let select_option_index = RwSignal::>::new(default_index); - let menu_ref = NodeRef::::new(); - let is_show_menu = RwSignal::new(false); + let listbox_ref = NodeRef::::new(); let auto_complete_ref = NodeRef::::new(); - let open_menu = move || { - select_option_index.set(default_index); - is_show_menu.set(true); - }; + let open_listbox = RwSignal::new(false); + let options = StoredValue::new(HashMap::::new()); + let allow_value = move |_| { - if !is_show_menu.get_untracked() { - open_menu(); + if !open_listbox.get_untracked() { + open_listbox.set(true); } true }; - let select_value = Callback::new(move |option_value: String| { + let select_option = Callback::new(move |option_value: String| { if clear_after_select.get_untracked() { value.set(String::new()); } else { @@ -63,10 +59,8 @@ pub fn AutoComplete( if let Some(on_select) = on_select.as_ref() { on_select(option_value); } - if allow_free_input { - select_option_index.set(None); - } - is_show_menu.set(false); + + open_listbox.set(false); if blur_after_select.get_untracked() { if let Some(input_ref) = input_ref.get_untracked() { input_ref.blur(); @@ -74,153 +68,118 @@ pub fn AutoComplete( } }); - // 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 (set_listbox, active_descendant_controller) = + use_active_descendant(move |el| el.class_list().contains("thaw-auto-complete-option")); + let on_blur = { + let active_descendant_controller = active_descendant_controller.clone(); + move |_| { + active_descendant_controller.blur(); + open_listbox.set(false); + } + }; + let on_keydown = move |e| { + listbox_keyboard_event( + e, + open_listbox, + false, + &active_descendant_controller, + move |option| { + options.with_value(|options| { + if let Some(value) = options.get(&option.id()) { + select_option.call(value.clone()); + } + }); + }, + ); + }; - //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 == Some(options.with_untracked(|options| options.len()) - 1) { - //*index = default_index - //} else { - //*index = Some(index.map_or(0, |index| index + 1)) - //} - //}); - //} else if key == *"ArrowUp" { - //select_option_index.update(|index| { - //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(); - //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 { - //select_value(option_value); - //} - //} - //}; comp_ref.load(AutoCompleteRef { input_ref }); - // input_ref.on_load(move |_| { - // }); view! { - -
+
+ - - + - {if let Some(auto_complete_prefix) = auto_complete_prefix { - Some((auto_complete_prefix.children)()) + {if let Some(auto_complete_prefix) = auto_complete_prefix { + Some((auto_complete_prefix.children)()) + } else { + None + }} + + + + + {if let Some(auto_complete_suffix) = auto_complete_suffix { + Some((auto_complete_suffix.children)()) + } else { + None + }} + + + +
+ + + + { + if let Some(children) = children { + Either::Left(children()) } else { - None - }} - - - - - {if let Some(auto_complete_suffix) = auto_complete_suffix { - Some((auto_complete_suffix.children)()) - } else { - None - }} - - - -
- - - -
- { - if let Some(children) = children { - Either::Left(children()) - } else { - Either::Right(()) - } - } -
-
-
-
-
- } + Either::Right(()) + } + } + + + + + } } #[derive(Clone, Copy)] -pub(crate) struct AutoCompleteInjection(pub Model, pub Callback); +pub(crate) struct AutoCompleteInjection { + value: Model, + select_option: Callback, + options: StoredValue>, +} impl AutoCompleteInjection { - pub fn use_() -> Self { + pub fn expect_context() -> Self { expect_context() } pub fn is_selected(&self, key: &String) -> bool { - self.0.with(|value| value == key) + self.value.with(|value| value == key) } - pub fn select_value(&self, key: String) { - self.1.call(key.clone()); + pub fn select_option(&self, value: String) { + self.select_option.call(value); + } + + pub fn insert_option(&self, id: String, value: String) { + self.options + .update_value(|options| options.insert(id, value)); + } + + pub fn remove_option(&self, id: &String) { + self.options.update_value(|options| options.remove(id)); } } diff --git a/thaw/src/combobox/combobox_option.rs b/thaw/src/combobox/combobox_option.rs index c8732f8..5b4a518 100644 --- a/thaw/src/combobox/combobox_option.rs +++ b/thaw/src/combobox/combobox_option.rs @@ -12,7 +12,9 @@ pub fn ComboboxOption( let combobox = ComboboxInjection::expect_context(); let value = StoredValue::new(value.unwrap_or_else(|| text.clone())); let text = StoredValue::new(text); + let is_selected = Memo::new(move |_| value.with_value(|value| combobox.is_selected(&value))); let id = uuid::Uuid::new_v4().to_string(); + let on_click = move |_| { text.with_value(|text| { value.with_value(|value| { @@ -32,11 +34,11 @@ pub fn ComboboxOption( view! {
@@ -44,7 +46,7 @@ pub fn ComboboxOption( if combobox.multiselect { view! {