mirror of
https://github.com/adoyle0/thaw.git
synced 2025-01-22 22:09:22 -05:00
Feature/dropdown (#210)
* dropdown with icon * dropdown demo page * on_select instaed of on_click * code review fixes
This commit is contained in:
parent
2f96fec20d
commit
7685e99c8b
15 changed files with 628 additions and 37 deletions
|
@ -62,6 +62,7 @@ fn TheRouter(is_routing: RwSignal<bool>) -> impl IntoView {
|
|||
<Route path="/date-picker" view=DatePickerMdPage/>
|
||||
<Route path="/divider" view=DividerMdPage/>
|
||||
<Route path="/drawer" view=DrawerMdPage/>
|
||||
<Route path="/dropdown" view=DropdownMdPage/>
|
||||
<Route path="/grid" view=GridMdPage/>
|
||||
<Route path="/icon" view=IconMdPage/>
|
||||
<Route path="/image" view=ImageMdPage/>
|
||||
|
|
|
@ -130,6 +130,10 @@ pub(crate) fn gen_menu_data() -> Vec<MenuGroupOption> {
|
|||
value: "divider".into(),
|
||||
label: "Divider".into(),
|
||||
},
|
||||
MenuItemOption {
|
||||
value: "dropdown".into(),
|
||||
label: "Dropdown".into(),
|
||||
},
|
||||
MenuItemOption {
|
||||
value: "icon".into(),
|
||||
label: "Icon".into(),
|
||||
|
|
182
demo_markdown/docs/dropdown/mod.md
Normal file
182
demo_markdown/docs/dropdown/mod.md
Normal file
|
@ -0,0 +1,182 @@
|
|||
# Dropdown
|
||||
|
||||
```rust demo
|
||||
let message = use_message();
|
||||
|
||||
let on_select = move |key: String| {
|
||||
match key.as_str() {
|
||||
"facebook" => message.create( "Facebook".into(), MessageVariant::Success, Default::default(),),
|
||||
"twitter" => message.create( "Twitter".into(), MessageVariant::Warning, Default::default(),),
|
||||
_ => ()
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
view! {
|
||||
<Space>
|
||||
<Dropdown on_select trigger_type=DropdownTriggerType::Hover>
|
||||
<DropdownTrigger slot>
|
||||
<Button>"Hover"</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownItem key="facebook" icon=icondata::AiFacebookOutlined label="Facebook"></DropdownItem>
|
||||
<DropdownItem key="twitter" disabled=true icon=icondata::AiTwitterOutlined label="Twitter"></DropdownItem>
|
||||
</Dropdown>
|
||||
|
||||
<Dropdown on_select>
|
||||
<DropdownTrigger slot>
|
||||
<Button>"Click"</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownItem key="facebook" icon=icondata::AiFacebookOutlined label="Facebook"></DropdownItem>
|
||||
<DropdownItem key="twitter" icon=icondata::AiTwitterOutlined label="Twitter"></DropdownItem>
|
||||
<DropdownItem key="no_icon" disabled=true label="Mastodon"></DropdownItem>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
}
|
||||
```
|
||||
|
||||
### Placement
|
||||
|
||||
```rust demo
|
||||
use leptos_meta::Style;
|
||||
|
||||
let on_select = move |key| println!("{}", key);
|
||||
|
||||
view! {
|
||||
<Style>
|
||||
".demo-dropdown .thaw-button { width: 100% } .demo-dropdown .thaw-dropdown-trigger { display: block }"
|
||||
</Style>
|
||||
<Grid x_gap=8 y_gap=8 cols=3 class="demo-dropdown">
|
||||
<GridItem>
|
||||
<Dropdown on_select placement=DropdownPlacement::TopStart>
|
||||
<DropdownTrigger slot>
|
||||
<Button>"Top Start"</Button>
|
||||
</DropdownTrigger>
|
||||
"Content"
|
||||
</Dropdown>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<Dropdown on_select placement=DropdownPlacement::Top>
|
||||
<DropdownTrigger slot>
|
||||
<Button>"Top"</Button>
|
||||
</DropdownTrigger>
|
||||
"Content"
|
||||
</Dropdown>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<Dropdown on_select placement=DropdownPlacement::TopEnd>
|
||||
<DropdownTrigger slot>
|
||||
<Button>"Top End"</Button>
|
||||
</DropdownTrigger>
|
||||
"Content"
|
||||
</Dropdown>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<Dropdown on_select placement=DropdownPlacement::LeftStart>
|
||||
<DropdownTrigger slot>
|
||||
<Button>"Left Start"</Button>
|
||||
</DropdownTrigger>
|
||||
"Content"
|
||||
</Dropdown>
|
||||
</GridItem>
|
||||
<GridItem offset=1>
|
||||
<Dropdown on_select placement=DropdownPlacement::RightStart>
|
||||
<DropdownTrigger slot>
|
||||
<Button>"Right Start"</Button>
|
||||
</DropdownTrigger>
|
||||
"Content"
|
||||
</Dropdown>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<Dropdown on_select placement=DropdownPlacement::Left>
|
||||
<DropdownTrigger slot>
|
||||
<Button>"Left"</Button>
|
||||
</DropdownTrigger>
|
||||
"Content"
|
||||
</Dropdown>
|
||||
</GridItem>
|
||||
<GridItem offset=1>
|
||||
<Dropdown on_select placement=DropdownPlacement::Right>
|
||||
<DropdownTrigger slot>
|
||||
<Button>"Right"</Button>
|
||||
</DropdownTrigger>
|
||||
"Content"
|
||||
</Dropdown>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<Dropdown on_select placement=DropdownPlacement::LeftEnd>
|
||||
<DropdownTrigger slot>
|
||||
<Button>"Left End"</Button>
|
||||
</DropdownTrigger>
|
||||
"Content"
|
||||
</Dropdown>
|
||||
</GridItem>
|
||||
<GridItem offset=1>
|
||||
<Dropdown on_select placement=DropdownPlacement::RightEnd>
|
||||
<DropdownTrigger slot>
|
||||
<Button>"Right End"</Button>
|
||||
</DropdownTrigger>
|
||||
"Content"
|
||||
</Dropdown>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<Dropdown on_select placement=DropdownPlacement::BottomStart>
|
||||
<DropdownTrigger slot>
|
||||
<Button>"Bottom Start"</Button>
|
||||
</DropdownTrigger>
|
||||
"Content"
|
||||
</Dropdown>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<Dropdown on_select placement=DropdownPlacement::Bottom>
|
||||
<DropdownTrigger slot>
|
||||
<Button>"Bottom"</Button>
|
||||
</DropdownTrigger>
|
||||
"Content"
|
||||
</Dropdown>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<Dropdown on_select placement=DropdownPlacement::BottomEnd>
|
||||
<DropdownTrigger slot>
|
||||
<Button>"Bottom End"</Button>
|
||||
</DropdownTrigger>
|
||||
"Content"
|
||||
</Dropdown>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
}
|
||||
```
|
||||
|
||||
### Dropdown Props
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| ------------ | ----------------------------------- | ---------------------------- | ------------------------------------------- |
|
||||
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the dropdown element. |
|
||||
| on_select | `Callback<String>` | | Called when item is selected. |
|
||||
| trigger_type | `DropdownTriggerType` | `DropdownTriggerType::Click` | Action that displays the dropdown. |
|
||||
| placement | `DropdownPlacement` | `DropdownPlacement::Bottom` | Dropdown placement. |
|
||||
| children | `Children` | | The content inside dropdown. |
|
||||
|
||||
### DropdownItem Props
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| -------- | -------------------------------------------- | -------------------- | ------------------------------------------------ |
|
||||
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the dropdown item element. |
|
||||
| key | `MaybeSignal<String>` | `Default::default()` | The key of the dropdown item. |
|
||||
| label | `MaybeSignal<String>` | `Default::default()` | The label of the dropdown item. |
|
||||
| icon | `OptionalMaybeSignal<icondata_core::Icon>` | `None` | The icon of the dropdown item. |
|
||||
| disabled | `MaybeSignal<bool>` | `false` | Whether the dropdown item is disabled. |
|
||||
|
||||
|
||||
### Dropdown Slots
|
||||
|
||||
| Name | Default | Description |
|
||||
| --------------- | ------- | ------------------------------------------------ |
|
||||
| DropdownTrigger | `None` | The element or component that triggers dropdown. |
|
||||
|
||||
### DropdownTriger Props
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| ------------ | ----------------------------------- | ---------------------------- | -------------------------------------------------- |
|
||||
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the dropdown trigger element. |
|
||||
| children | `Children` | | The content inside dropdown trigger. |
|
||||
|
|
@ -147,9 +147,10 @@ view! {
|
|||
### Popover Props
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| --------- | ----------------------------------- | ----------------------- | ----------------------------- |
|
||||
| -------------| ----------------------------------- | -------------------------- | --------------------------------- |
|
||||
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Content class of the popover. |
|
||||
| placement | `PopoverPlacement` | `PopoverPlacement::Top` | Popover placement. |
|
||||
| trigger_type | `PopoverTriggerType` | `PopoverTriggerType::Hover`| Action that displays the dropdown |
|
||||
| tooltip | `bool` | `false` | Tooltip. |
|
||||
| children | `Children` | | The content inside popover. |
|
||||
|
||||
|
@ -158,3 +159,11 @@ view! {
|
|||
| Name | Default | Description |
|
||||
| -------------- | ------- | ----------------------------------------------- |
|
||||
| PopoverTrigger | | The element or component that triggers popover. |
|
||||
|
||||
### PopoverTriger Props
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| ------------ | ----------------------------------- | ---------------------------- | -------------------------------------------------- |
|
||||
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the popover trigger element. |
|
||||
| children | `Children` | | The content inside popover trigger. |
|
||||
|
||||
|
|
|
@ -70,7 +70,8 @@ pub fn include_md(_token_stream: proc_macro::TokenStream) -> proc_macro::TokenSt
|
|||
"ThemeMdPage" => "../docs/theme/mod.md",
|
||||
"TimePickerMdPage" => "../docs/time_picker/mod.md",
|
||||
"TypographyMdPage" => "../docs/typography/mod.md",
|
||||
"UploadMdPage" => "../docs/upload/mod.md"
|
||||
"UploadMdPage" => "../docs/upload/mod.md",
|
||||
"DropdownMdPage" => "../docs/dropdown/mod.md"
|
||||
};
|
||||
|
||||
let mut fn_list = vec![];
|
||||
|
|
16
thaw/src/dropdown/dropdown-item.css
Normal file
16
thaw/src/dropdown/dropdown-item.css
Normal file
|
@ -0,0 +1,16 @@
|
|||
.thaw-dropdown-item{
|
||||
padding: 6px 5px;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.thaw-dropdown-item:hover:not(.thaw-dropdown-item--disabled) {
|
||||
background-color: var(--thaw-background-color-hover);
|
||||
}
|
||||
|
||||
.thaw-dropdown-item.thaw-dropdown-item--disabled {
|
||||
color: var(--thaw-font-color-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
68
thaw/src/dropdown/dropdown.css
Normal file
68
thaw/src/dropdown/dropdown.css
Normal file
|
@ -0,0 +1,68 @@
|
|||
.thaw-dropdown {
|
||||
position: relative;
|
||||
padding: 5px;
|
||||
background-color: var(--thaw-background-color);
|
||||
color: var(--thaw-font-color);
|
||||
border-radius: 3px;
|
||||
transform-origin: inherit;
|
||||
}
|
||||
|
||||
[data-thaw-placement="top-start"] > .thaw-dropdown,
|
||||
[data-thaw-placement="top-end"] > .thaw-dropdown,
|
||||
[data-thaw-placement="top"] > .thaw-dropdown {
|
||||
margin-bottom: 4px;
|
||||
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);
|
||||
}
|
||||
|
||||
[data-thaw-placement="bottom-start"] > .thaw-dropdown,
|
||||
[data-thaw-placement="bottom-end"] > .thaw-dropdown,
|
||||
[data-thaw-placement="bottom"] > .thaw-dropdown {
|
||||
margin-top: 4px;
|
||||
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);
|
||||
}
|
||||
|
||||
[data-thaw-placement="left-start"] > .thaw-dropdown,
|
||||
[data-thaw-placement="left-end"] > .thaw-dropdown,
|
||||
[data-thaw-placement="left"] > .thaw-dropdown {
|
||||
margin-right: 4px;
|
||||
box-shadow: 3px 0 6px -4px rgba(0, 0, 0, 0.12),
|
||||
6px 0 16px 0 rgba(0, 0, 0, 0.08), 9px 0 28px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
[data-thaw-placement="right-start"] > .thaw-dropdown,
|
||||
[data-thaw-placement="right-end"] > .thaw-dropdown,
|
||||
[data-thaw-placement="right"] > .thaw-dropdown {
|
||||
margin-left: 4px;
|
||||
box-shadow: -3px 0 6px -4px rgba(0, 0, 0, 0.12),
|
||||
-6px 0 16px 0 rgba(0, 0, 0, 0.08), -9px 0 28px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.thaw-dropdown.dropdown-transition-enter-from,
|
||||
.thaw-dropdown.dropdown-transition-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.85);
|
||||
}
|
||||
|
||||
.thaw-dropdown.dropdown-transition-enter-to,
|
||||
.thaw-dropdown.dropdown-transition-leave-from {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.thaw-dropdown.dropdown-transition-enter-active {
|
||||
transition: box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
opacity 0.15s cubic-bezier(0, 0, 0.2, 1),
|
||||
transform 0.15s cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.thaw-dropdown.dropdown-transition-leave-active {
|
||||
transition: box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
opacity 0.15s cubic-bezier(0.4, 0, 1, 1),
|
||||
transform 0.15s cubic-bezier(0.4, 0, 1, 1);
|
||||
}
|
75
thaw/src/dropdown/dropdown_item.rs
Normal file
75
thaw/src/dropdown/dropdown_item.rs
Normal file
|
@ -0,0 +1,75 @@
|
|||
use leptos::*;
|
||||
use thaw_components::{Fallback, If, OptionComp, Then};
|
||||
use thaw_utils::{class_list, mount_style, OptionalMaybeSignal, OptionalProp};
|
||||
|
||||
use crate::{
|
||||
dropdown::{HasIcon, OnSelect},
|
||||
use_theme, Icon, Theme,
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn DropdownItem(
|
||||
#[prop(optional, into)] icon: OptionalMaybeSignal<icondata_core::Icon>,
|
||||
#[prop(into)] label: MaybeSignal<String>,
|
||||
#[prop(into)] key: MaybeSignal<String>,
|
||||
#[prop(optional, into)] disabled: MaybeSignal<bool>,
|
||||
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
|
||||
) -> impl IntoView {
|
||||
mount_style("dropdown-item", include_str!("./dropdown-item.css"));
|
||||
let theme = use_theme(Theme::light);
|
||||
let css_vars = create_memo(move |_| {
|
||||
let mut css_vars = String::new();
|
||||
theme.with(|theme| {
|
||||
css_vars.push_str(&format!(
|
||||
"--thaw-background-color-hover: {};",
|
||||
theme.dropdown.item_color_hover
|
||||
));
|
||||
css_vars.push_str(&format!(
|
||||
"--thaw-font-color-disabled: {};",
|
||||
theme.dropdown.font_color_disabled
|
||||
));
|
||||
});
|
||||
css_vars
|
||||
});
|
||||
|
||||
let has_icon = use_context::<HasIcon>().expect("HasIcon not provided").0;
|
||||
|
||||
if icon.get().is_some() {
|
||||
has_icon.set(true);
|
||||
}
|
||||
|
||||
let on_select = use_context::<OnSelect>().expect("OnSelect not provided").0;
|
||||
|
||||
let on_click = move |_| {
|
||||
if disabled.get() {
|
||||
return;
|
||||
}
|
||||
on_select.call(key.get());
|
||||
};
|
||||
|
||||
view! {
|
||||
<div
|
||||
class=class_list![
|
||||
"thaw-dropdown-item", ("thaw-dropdown-item--disabled", move || disabled.get()),
|
||||
class.map(| c | move || c.get())
|
||||
]
|
||||
|
||||
style=move || css_vars.get()
|
||||
on:click=on_click
|
||||
>
|
||||
|
||||
<OptionComp value=icon.get() let:icon>
|
||||
<Fallback slot>
|
||||
<If cond=has_icon>
|
||||
<Then slot>
|
||||
<span style="width: 18px; margin-right: 8px"></span>
|
||||
</Then>
|
||||
</If>
|
||||
</Fallback>
|
||||
|
||||
<Icon icon=icon style="font-size: 18px; margin-right: 8px"/>
|
||||
</OptionComp>
|
||||
<span style="flex-grow: 1">{label}</span>
|
||||
</div>
|
||||
}
|
||||
}
|
193
thaw/src/dropdown/mod.rs
Normal file
193
thaw/src/dropdown/mod.rs
Normal file
|
@ -0,0 +1,193 @@
|
|||
mod dropdown_item;
|
||||
mod theme;
|
||||
|
||||
pub use dropdown_item::*;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use thaw_components::{Binder, CSSTransition, Follower, FollowerPlacement};
|
||||
pub use theme::DropdownTheme;
|
||||
|
||||
use leptos::{leptos_dom::helpers::TimeoutHandle, *};
|
||||
use thaw_utils::{
|
||||
add_event_listener, call_on_click_outside, class_list, mount_style, OptionalProp,
|
||||
};
|
||||
|
||||
use crate::{use_theme, Theme};
|
||||
|
||||
#[slot]
|
||||
pub struct DropdownTrigger {
|
||||
#[prop(optional, into)]
|
||||
class: OptionalProp<MaybeSignal<String>>,
|
||||
children: Children,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct HasIcon(RwSignal<bool>);
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct OnSelect(Callback<String>);
|
||||
|
||||
#[component]
|
||||
pub fn Dropdown(
|
||||
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
|
||||
dropdown_trigger: DropdownTrigger,
|
||||
#[prop(optional)] trigger_type: DropdownTriggerType,
|
||||
#[prop(optional)] placement: DropdownPlacement,
|
||||
#[prop(into)] on_select: Callback<String>,
|
||||
children: Children,
|
||||
) -> impl IntoView {
|
||||
mount_style("dropdown", include_str!("./dropdown.css"));
|
||||
let theme = use_theme(Theme::light);
|
||||
let css_vars = create_memo(move |_| {
|
||||
let mut css_vars = String::new();
|
||||
theme.with(|theme| {
|
||||
css_vars.push_str(&format!(
|
||||
"--thaw-background-color: {};",
|
||||
theme.dropdown.background_color
|
||||
));
|
||||
css_vars.push_str(&format!("--thaw-font-color: {};", theme.common.font_color));
|
||||
});
|
||||
css_vars
|
||||
});
|
||||
let dropdown_ref = create_node_ref::<html::Div>();
|
||||
let target_ref = create_node_ref::<html::Div>();
|
||||
let is_show_dropdown = create_rw_signal(false);
|
||||
let show_dropdown_handle = store_value(None::<TimeoutHandle>);
|
||||
|
||||
let on_mouse_enter = move |_| {
|
||||
if trigger_type != DropdownTriggerType::Hover {
|
||||
return;
|
||||
}
|
||||
show_dropdown_handle.update_value(|handle| {
|
||||
if let Some(handle) = handle.take() {
|
||||
handle.clear();
|
||||
}
|
||||
});
|
||||
is_show_dropdown.set(true);
|
||||
};
|
||||
let on_mouse_leave = move |_| {
|
||||
if trigger_type != DropdownTriggerType::Hover {
|
||||
return;
|
||||
}
|
||||
show_dropdown_handle.update_value(|handle| {
|
||||
if let Some(handle) = handle.take() {
|
||||
handle.clear();
|
||||
}
|
||||
*handle = set_timeout_with_handle(
|
||||
move || {
|
||||
is_show_dropdown.set(false);
|
||||
},
|
||||
Duration::from_millis(100),
|
||||
)
|
||||
.ok();
|
||||
});
|
||||
};
|
||||
|
||||
if trigger_type != DropdownTriggerType::Hover {
|
||||
call_on_click_outside(
|
||||
dropdown_ref,
|
||||
Callback::new(move |_| is_show_dropdown.set(false)),
|
||||
);
|
||||
}
|
||||
|
||||
target_ref.on_load(move |target_el| {
|
||||
add_event_listener(target_el.into_any(), ev::click, move |event| {
|
||||
if trigger_type != DropdownTriggerType::Click {
|
||||
return;
|
||||
}
|
||||
event.stop_propagation();
|
||||
is_show_dropdown.update(|show| *show = !*show);
|
||||
});
|
||||
});
|
||||
let DropdownTrigger {
|
||||
class: trigger_class,
|
||||
children: trigger_children,
|
||||
} = dropdown_trigger;
|
||||
|
||||
provide_context(HasIcon(create_rw_signal(false)));
|
||||
provide_context(OnSelect(Callback::<String>::new(move |key| {
|
||||
is_show_dropdown.set(false);
|
||||
on_select.call(key);
|
||||
})));
|
||||
|
||||
view! {
|
||||
<Binder target_ref>
|
||||
<div
|
||||
class=class_list!["thaw-dropdown-trigger", trigger_class.map(| c | move || c.get())]
|
||||
ref=target_ref
|
||||
on:mouseenter=on_mouse_enter
|
||||
on:mouseleave=on_mouse_leave
|
||||
>
|
||||
{trigger_children()}
|
||||
</div>
|
||||
<Follower slot show=is_show_dropdown placement>
|
||||
<CSSTransition
|
||||
node_ref=dropdown_ref
|
||||
name="dropdown-transition"
|
||||
appear=is_show_dropdown.get_untracked()
|
||||
show=is_show_dropdown
|
||||
let:display
|
||||
>
|
||||
<div
|
||||
class="thaw-dropdown"
|
||||
style=move || {
|
||||
display.get().map(|d| d.to_string()).unwrap_or_else(|| css_vars.get())
|
||||
}
|
||||
|
||||
ref=dropdown_ref
|
||||
on:mouseenter=on_mouse_enter
|
||||
on:mouseleave=on_mouse_leave
|
||||
>
|
||||
<div class=class.map(|c| move || c.get())>{children()}</div>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
</Follower>
|
||||
</Binder>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Clone)]
|
||||
pub enum DropdownTriggerType {
|
||||
Hover,
|
||||
#[default]
|
||||
Click,
|
||||
}
|
||||
|
||||
impl Copy for DropdownTriggerType {}
|
||||
|
||||
#[derive(Default)]
|
||||
pub enum DropdownPlacement {
|
||||
Top,
|
||||
#[default]
|
||||
Bottom,
|
||||
Left,
|
||||
Right,
|
||||
TopStart,
|
||||
TopEnd,
|
||||
LeftStart,
|
||||
LeftEnd,
|
||||
RightStart,
|
||||
RightEnd,
|
||||
BottomStart,
|
||||
BottomEnd,
|
||||
}
|
||||
|
||||
impl From<DropdownPlacement> for FollowerPlacement {
|
||||
fn from(value: DropdownPlacement) -> Self {
|
||||
match value {
|
||||
DropdownPlacement::Top => Self::Top,
|
||||
DropdownPlacement::Bottom => Self::Bottom,
|
||||
DropdownPlacement::Left => Self::Left,
|
||||
DropdownPlacement::Right => Self::Right,
|
||||
DropdownPlacement::TopStart => Self::TopStart,
|
||||
DropdownPlacement::TopEnd => Self::TopEnd,
|
||||
DropdownPlacement::LeftStart => Self::LeftStart,
|
||||
DropdownPlacement::LeftEnd => Self::LeftEnd,
|
||||
DropdownPlacement::RightStart => Self::RightStart,
|
||||
DropdownPlacement::RightEnd => Self::RightEnd,
|
||||
DropdownPlacement::BottomStart => Self::BottomStart,
|
||||
DropdownPlacement::BottomEnd => Self::BottomEnd,
|
||||
}
|
||||
}
|
||||
}
|
26
thaw/src/dropdown/theme.rs
Normal file
26
thaw/src/dropdown/theme.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
use crate::theme::ThemeMethod;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DropdownTheme {
|
||||
pub background_color: String,
|
||||
pub item_color_hover: String,
|
||||
pub font_color_disabled: String,
|
||||
}
|
||||
|
||||
impl ThemeMethod for DropdownTheme {
|
||||
fn light() -> Self {
|
||||
Self {
|
||||
background_color: "#fff".into(),
|
||||
item_color_hover: "#f3f5f6".into(),
|
||||
font_color_disabled: "#c2c2c2".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn dark() -> Self {
|
||||
Self {
|
||||
background_color: "#48484e".into(),
|
||||
item_color_hover: "#ffffff17".into(),
|
||||
font_color_disabled: "#ffffff61".into(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ mod color_picker;
|
|||
mod date_picker;
|
||||
mod divider;
|
||||
mod drawer;
|
||||
mod dropdown;
|
||||
mod global_style;
|
||||
mod grid;
|
||||
mod icon;
|
||||
|
@ -62,6 +63,7 @@ pub use color_picker::*;
|
|||
pub use date_picker::*;
|
||||
pub use divider::*;
|
||||
pub use drawer::*;
|
||||
pub use dropdown::*;
|
||||
pub use global_style::*;
|
||||
pub use grid::*;
|
||||
pub use icon::*;
|
||||
|
|
|
@ -6,7 +6,9 @@ use crate::{use_theme, Theme};
|
|||
use leptos::{leptos_dom::helpers::TimeoutHandle, *};
|
||||
use std::time::Duration;
|
||||
use thaw_components::{Binder, CSSTransition, Follower, FollowerPlacement};
|
||||
use thaw_utils::{add_event_listener, class_list, mount_style, OptionalProp};
|
||||
use thaw_utils::{
|
||||
add_event_listener, call_on_click_outside, class_list, mount_style, OptionalProp,
|
||||
};
|
||||
|
||||
#[slot]
|
||||
pub struct PopoverTrigger {
|
||||
|
@ -77,34 +79,13 @@ pub fn Popover(
|
|||
.ok();
|
||||
});
|
||||
};
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
{
|
||||
let handle = window_event_listener(ev::click, move |ev| {
|
||||
use leptos::wasm_bindgen::__rt::IntoJsResult;
|
||||
if trigger_type != PopoverTriggerType::Click {
|
||||
return;
|
||||
}
|
||||
let el = ev.target();
|
||||
let mut el: Option<web_sys::Element> =
|
||||
el.into_js_result().map_or(None, |el| Some(el.into()));
|
||||
let body = document().body().unwrap();
|
||||
while let Some(current_el) = el {
|
||||
if current_el == *body {
|
||||
break;
|
||||
};
|
||||
let Some(popover_el) = popover_ref.get_untracked() else {
|
||||
break;
|
||||
};
|
||||
if current_el == ***popover_el {
|
||||
return;
|
||||
}
|
||||
el = current_el.parent_element();
|
||||
}
|
||||
is_show_popover.set(false);
|
||||
});
|
||||
on_cleanup(move || handle.remove());
|
||||
}
|
||||
|
||||
if trigger_type != PopoverTriggerType::Hover {
|
||||
call_on_click_outside(
|
||||
popover_ref,
|
||||
Callback::new(move |_| is_show_popover.set(false)),
|
||||
);
|
||||
}
|
||||
target_ref.on_load(move |target_el| {
|
||||
add_event_listener(target_el.into_any(), ev::click, move |event| {
|
||||
if trigger_type != PopoverTriggerType::Click {
|
||||
|
|
|
@ -4,8 +4,8 @@ use self::common::CommonTheme;
|
|||
use crate::{
|
||||
mobile::{NavBarTheme, TabbarTheme},
|
||||
AlertTheme, AnchorTheme, AutoCompleteTheme, AvatarTheme, BackTopTheme, BreadcrumbTheme,
|
||||
ButtonTheme, CalendarTheme, CollapseTheme, ColorPickerTheme, DatePickerTheme, InputTheme,
|
||||
MenuTheme, MessageTheme, PopoverTheme, ProgressTheme, ScrollbarTheme, SelectTheme,
|
||||
ButtonTheme, CalendarTheme, CollapseTheme, ColorPickerTheme, DatePickerTheme, DropdownTheme,
|
||||
InputTheme, MenuTheme, MessageTheme, PopoverTheme, ProgressTheme, ScrollbarTheme, SelectTheme,
|
||||
SkeletionTheme, SliderTheme, SpinnerTheme, SwitchTheme, TableTheme, TagTheme, TimePickerTheme,
|
||||
TypographyTheme, UploadTheme,
|
||||
};
|
||||
|
@ -45,6 +45,7 @@ pub struct Theme {
|
|||
pub time_picker: TimePickerTheme,
|
||||
pub date_picker: DatePickerTheme,
|
||||
pub popover: PopoverTheme,
|
||||
pub dropdown: DropdownTheme,
|
||||
pub collapse: CollapseTheme,
|
||||
pub scrollbar: ScrollbarTheme,
|
||||
pub back_top: BackTopTheme,
|
||||
|
@ -81,6 +82,7 @@ impl Theme {
|
|||
time_picker: TimePickerTheme::light(),
|
||||
date_picker: DatePickerTheme::light(),
|
||||
popover: PopoverTheme::light(),
|
||||
dropdown: DropdownTheme::light(),
|
||||
collapse: CollapseTheme::light(),
|
||||
scrollbar: ScrollbarTheme::light(),
|
||||
back_top: BackTopTheme::light(),
|
||||
|
@ -116,6 +118,7 @@ impl Theme {
|
|||
time_picker: TimePickerTheme::dark(),
|
||||
date_picker: DatePickerTheme::dark(),
|
||||
popover: PopoverTheme::dark(),
|
||||
dropdown: DropdownTheme::dark(),
|
||||
collapse: CollapseTheme::dark(),
|
||||
scrollbar: ScrollbarTheme::dark(),
|
||||
back_top: BackTopTheme::dark(),
|
||||
|
|
|
@ -2,6 +2,7 @@ pub mod class_list;
|
|||
mod dom;
|
||||
mod event_listener;
|
||||
mod hooks;
|
||||
mod on_click_outside;
|
||||
mod optional_prop;
|
||||
mod signals;
|
||||
mod throttle;
|
||||
|
@ -12,6 +13,7 @@ pub use event_listener::{
|
|||
add_event_listener, add_event_listener_with_bool, EventListenerHandle, IntoEventTarget,
|
||||
};
|
||||
pub use hooks::{use_click_position, use_lock_html_scroll, use_next_frame, NextFrame};
|
||||
pub use on_click_outside::call_on_click_outside;
|
||||
pub use optional_prop::OptionalProp;
|
||||
pub use signals::{
|
||||
create_component_ref, ComponentRef, Model, OptionalMaybeSignal, SignalWatch, StoredMaybeSignal,
|
||||
|
|
28
thaw_utils/src/on_click_outside.rs
Normal file
28
thaw_utils/src/on_click_outside.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
use leptos::{html::Div, *};
|
||||
|
||||
pub fn call_on_click_outside(element: NodeRef<Div>, on_click: Callback<()>) {
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
{
|
||||
let handle = window_event_listener(ev::click, move |ev| {
|
||||
use leptos::wasm_bindgen::__rt::IntoJsResult;
|
||||
let el = ev.target();
|
||||
let mut el: Option<web_sys::Element> =
|
||||
el.into_js_result().map_or(None, |el| Some(el.into()));
|
||||
let body = document().body().unwrap();
|
||||
while let Some(current_el) = el {
|
||||
if current_el == *body {
|
||||
break;
|
||||
};
|
||||
let Some(displayed_el) = element.get_untracked() else {
|
||||
break;
|
||||
};
|
||||
if current_el == ***displayed_el {
|
||||
return;
|
||||
}
|
||||
el = current_el.parent_element();
|
||||
}
|
||||
on_click.call(());
|
||||
});
|
||||
on_cleanup(move || handle.remove());
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue