refactor: auto_complete

This commit is contained in:
luoxiao 2024-06-14 17:23:02 +08:00
parent f999e252e4
commit c5cc6337b7
9 changed files with 186 additions and 144 deletions

View file

@ -149,6 +149,7 @@ pub fn SiteHeader() -> impl IntoView {
<AutoCompletePrefix slot>
<Icon icon=icondata::AiSearchOutlined style="font-size: 18px; color: var(--thaw-placeholder-color);"/>
</AutoCompletePrefix>
<p title="#TODO"></p>
</AutoComplete>
<Popover placement=PopoverPlacement::BottomEnd class="demo-header__menu-popover-mobile">
<PopoverTrigger slot class="demo-header__menu-mobile">

View file

@ -1,23 +1,30 @@
# Auto Complete
```rust demo
let value = create_rw_signal(String::new());
let options = create_memo(move |_| {
let value = RwSignal::new(String::new());
let options = Memo::<Vec<_>>::new(move |_| {
let prefix = value
.get()
.split_once('@')
.map_or(value.get(), |v| v.0.to_string());
vec!["@gmail.com", "@163.com"]
.into_iter()
.map(|suffix| AutoCompleteOption {
label: format!("{prefix}{suffix}"),
value: format!("{prefix}{suffix}"),
})
.map(|suffix| (format!("{prefix}{suffix}"), format!("{prefix}{suffix}")))
.collect()
});
view! {
<AutoComplete value options placeholder="Email"/>
<AutoComplete value placeholder="Email">
<For
each=move || options.get()
key=|option| option.0.clone()
let:option
>
<AutoCompleteOption2 key=option.0>
{option.1}
</AutoCompleteOption2>
</For>
</AutoComplete>
}
```

View file

@ -1,13 +1,27 @@
.thaw-auto-complete__menu {
.thaw-auto-complete {
display: inline-flex;
}
.thaw-auto-complete > .thaw-input {
min-width: 250px;
}
div.thaw-auto-complete__listbox {
width: 100%;
row-gap: var(--spacingHorizontalXXS);
display: flex;
flex-direction: column;
min-width: 160px;
/* max-height: 80vh; */
max-height: 200px;
padding: 5px;
background-color: var(--thaw-background-color);
border-radius: 3px;
background-color: var(--colorNeutralBackground1);
padding: var(--spacingHorizontalXS);
outline: 1px solid var(--colorTransparentStroke);
border-radius: var(--borderRadiusMedium);
box-sizing: border-box;
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
overflow: auto;
box-shadow: var(--shadow16);
overflow-y: auto;
}
.thaw-auto-complete__menu-item {
padding: 6px 5px;
@ -19,7 +33,7 @@
background-color: var(--thaw-background-color-hover);
}
.thaw-auto-complete__menu.fade-in-scale-up-transition-leave-active {
.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),
@ -27,7 +41,7 @@
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.thaw-auto-complete__menu.fade-in-scale-up-transition-enter-active {
.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),
@ -35,14 +49,38 @@
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.thaw-auto-complete__menu.fade-in-scale-up-transition-enter-from,
.thaw-auto-complete__menu.fade-in-scale-up-transition-leave-to {
.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__menu.fade-in-scale-up-transition-leave-from,
.thaw-auto-complete__menu.fade-in-scale-up-transition-enter-to {
.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;
display: flex;
align-items: center;
padding: var(--spacingVerticalSNudge) var(--spacingHorizontalS);
line-height: var(--lineHeightBase300);
font-size: var(--fontSizeBase300);
font-family: var(--fontFamilyBase);
color: var(--colorNeutralForeground1);
border-radius: var(--borderRadiusMedium);
cursor: pointer;
}
.thaw-auto-complete-option:hover {
color: var(--colorNeutralForeground1Hover);
background-color: var(--colorNeutralBackground1Hover);
}
.thaw-auto-complete-option:active {
color: var(--colorNeutralForeground1Pressed);
background-color: var(--colorNeutralBackground1Pressed);
}

View file

@ -0,0 +1,20 @@
use leptos::*;
use super::AutoCompleteInjection;
#[component]
pub fn AutoCompleteOption2(key: String, children: Children) -> impl IntoView {
let auto_complete = AutoCompleteInjection::use_();
let is_selected = Memo::new(move |_| {
auto_complete.is_selected(&key)
});
view! {
<div
class="thaw-auto-complete-option"
role="option"
aria-selected=move || if is_selected.get() { "true" } else { "false" }
>
{children()}
</div>
}
}

View file

@ -1,10 +1,12 @@
mod theme;
mod auto_complete_option;
pub use theme::AutoCompleteTheme;
pub use auto_complete_option::AutoCompleteOption2;
use crate::{use_theme, ComponentRef, Input, InputPrefix, InputRef, InputSuffix, Theme};
use crate::{ComponentRef, ConfigInjection, Input, InputPrefix, InputRef, InputSuffix};
use leptos::*;
use thaw_components::{Binder, CSSTransition, Follower, FollowerPlacement, FollowerWidth};
use thaw_components::{
Binder, CSSTransition, Follower, FollowerPlacement, FollowerWidth, OptionComp,
};
use thaw_utils::{class_list, mount_style, Model, OptionalProp, StoredMaybeSignal};
#[derive(Clone, PartialEq)]
@ -39,24 +41,10 @@ pub fn AutoComplete(
#[prop(optional)] auto_complete_suffix: Option<AutoCompleteSuffix>,
#[prop(optional)] comp_ref: ComponentRef<AutoCompleteRef>,
#[prop(attrs)] attrs: Vec<(&'static str, Attribute)>,
#[prop(optional)] children: Option<Children>,
) -> impl IntoView {
mount_style("auto-complete", include_str!("./auto-complete.css"));
let theme = use_theme(Theme::light);
let menu_css_vars = create_memo(move |_| {
let mut css_vars = String::new();
theme.with(|theme| {
css_vars.push_str(&format!(
"--thaw-background-color: {};",
theme.select.menu_background_color
));
css_vars.push_str(&format!(
"--thaw-background-color-hover: {};",
theme.select.menu_background_color_hover
));
});
css_vars
});
let config_provider = ConfigInjection::use_();
let input_ref = ComponentRef::<InputRef>::new();
let default_index = if allow_free_input { None } else { Some(0) };
@ -201,6 +189,7 @@ pub fn AutoComplete(
placement=FollowerPlacement::BottomStart
width=FollowerWidth::Target
>
<Provider value=AutoCompleteInjection(value)>
<CSSTransition
node_ref=menu_ref
name="fade-in-scale-up-transition"
@ -209,80 +198,92 @@ pub fn AutoComplete(
let:display
>
<div
class="thaw-auto-complete__menu"
style=move || {
display
.get()
.map(|d| d.to_string())
.unwrap_or_else(|| menu_css_vars.get())
}
class="thaw-config-provider thaw-auto-complete__listbox"
style=move || display.get()
data-thaw-id=config_provider.id().clone()
ref=menu_ref
role="listbox"
>
{move || {
options
.get()
.into_iter()
.enumerate()
.map(|(index, v)| {
let AutoCompleteOption { value: option_value, label } = v;
let menu_item_ref = create_node_ref::<html::Div>();
let on_click = move |_| {
select_value(option_value.clone());
};
let on_mouseenter = move |_| {
select_option_index.set(Some(index));
};
let on_mousedown = move |ev: ev::MouseEvent| {
ev.prevent_default();
};
create_effect(move |_| {
if Some(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! {
<div
class="thaw-auto-complete__menu-item"
class=(
"thaw-auto-complete__menu-item--selected",
move || Some(index) == select_option_index.get(),
)
on:click=on_click
on:mousedown=on_mousedown
on:mouseenter=on_mouseenter
ref=menu_item_ref
>
{label}
</div>
}
})
.collect_view()
}}
// {move || {
// options
// .get()
// .into_iter()
// .enumerate()
// .map(|(index, v)| {
// let AutoCompleteOption { value: option_value, label } = v;
// let menu_item_ref = create_node_ref::<html::Div>();
// let on_click = move |_| {
// select_value(option_value.clone());
// };
// let on_mouseenter = move |_| {
// select_option_index.set(Some(index));
// };
// let on_mousedown = move |ev: ev::MouseEvent| {
// ev.prevent_default();
// };
// create_effect(move |_| {
// if Some(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! {
// <div
// class="thaw-auto-complete__menu-item"
// class=(
// "thaw-auto-complete__menu-item--selected",
// move || Some(index) == select_option_index.get(),
// )
// on:click=on_click
// on:mousedown=on_mousedown
// on:mouseenter=on_mouseenter
// ref=menu_item_ref
// >
// {label}
// </div>
// }
// })
// .collect_view()
// }}
<OptionComp value=children let:children>
{children()}
</OptionComp>
</div>
</CSSTransition>
</Provider>
</Follower>
</Binder>
}
}
#[derive(Clone)]
pub(crate) struct AutoCompleteInjection(pub Model<String>);
impl AutoCompleteInjection {
pub fn use_() -> Self {
expect_context()
}
pub fn is_selected(&self, key: &String) -> bool {
self.0.with(|value| value == key)
}
}
#[derive(Clone)]
pub struct AutoCompleteRef {
input_ref: ComponentRef<InputRef>,

View file

@ -1,23 +0,0 @@
use crate::theme::ThemeMethod;
#[derive(Clone)]
pub struct AutoCompleteTheme {
pub menu_background_color: String,
pub menu_background_color_hover: String,
}
impl ThemeMethod for AutoCompleteTheme {
fn light() -> Self {
Self {
menu_background_color: "#fff".into(),
menu_background_color_hover: "#f3f5f6".into(),
}
}
fn dark() -> Self {
Self {
menu_background_color: "#48484e".into(),
menu_background_color_hover: "#ffffff17".into(),
}
}
}

View file

@ -1,6 +1,5 @@
use crate::ConfigInjection;
use leptos::{leptos_dom::helpers::TimeoutHandle, *};
use palette::bool_mask::BoolMask;
use std::time::Duration;
use thaw_components::{Binder, CSSTransition, Follower, FollowerPlacement};
use thaw_utils::{add_event_listener, class_list, mount_style};

View file

@ -58,6 +58,7 @@ pub struct CommonTheme {
pub spacing_horizontal_m: String,
pub spacing_horizontal_l: String,
pub spacing_vertical_none: String,
pub spacing_vertical_s_nudge: String,
pub spacing_vertical_s: String,
pub spacing_vertical_m_nudge: String,
pub spacing_vertical_m: String,
@ -139,6 +140,7 @@ impl CommonTheme {
spacing_horizontal_m: "12px".into(),
spacing_horizontal_l: "16px".into(),
spacing_vertical_none: "0".into(),
spacing_vertical_s_nudge: "6px".into(),
spacing_vertical_s: "8px".into(),
spacing_vertical_m_nudge: "10px".into(),
spacing_vertical_m: "12px".into(),

View file

@ -4,9 +4,9 @@ mod common;
use self::common::CommonTheme;
use crate::{
mobile::{NavBarTheme, TabbarTheme},
AlertTheme, AnchorTheme, AutoCompleteTheme, BackTopTheme, CalendarTheme, ColorPickerTheme,
DatePickerTheme, InputTheme, MessageTheme, ProgressTheme, ScrollbarTheme,
SelectTheme, TimePickerTheme, UploadTheme,
AlertTheme, AnchorTheme, BackTopTheme, CalendarTheme, ColorPickerTheme, DatePickerTheme,
InputTheme, MessageTheme, ProgressTheme, ScrollbarTheme, SelectTheme, TimePickerTheme,
UploadTheme,
};
pub use color::ColorTheme;
use leptos::*;
@ -28,7 +28,6 @@ pub struct Theme {
pub upload: UploadTheme,
pub nav_bar: NavBarTheme,
pub tabbar: TabbarTheme,
pub auto_complete: AutoCompleteTheme,
pub color_picker: ColorPickerTheme,
pub progress: ProgressTheme,
pub calendar: CalendarTheme,
@ -52,7 +51,6 @@ impl Theme {
upload: UploadTheme::light(),
nav_bar: NavBarTheme::light(),
tabbar: TabbarTheme::light(),
auto_complete: AutoCompleteTheme::light(),
color_picker: ColorPickerTheme::light(),
progress: ProgressTheme::light(),
calendar: CalendarTheme::light(),
@ -75,7 +73,6 @@ impl Theme {
upload: UploadTheme::dark(),
nav_bar: NavBarTheme::dark(),
tabbar: TabbarTheme::dark(),
auto_complete: AutoCompleteTheme::dark(),
color_picker: ColorPickerTheme::dark(),
progress: ProgressTheme::dark(),
calendar: CalendarTheme::dark(),