mirror of
https://github.com/adoyle0/thaw.git
synced 2025-01-22 22:09:22 -05:00
refactor: auto_complete
This commit is contained in:
parent
f999e252e4
commit
c5cc6337b7
9 changed files with 186 additions and 144 deletions
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
20
thaw/src/auto_complete/auto_complete_option.rs
Normal file
20
thaw/src/auto_complete/auto_complete_option.rs
Normal 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>
|
||||
}
|
||||
}
|
|
@ -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>,
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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};
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
|
|
Loading…
Add table
Reference in a new issue