mirror of
https://github.com/adoyle0/thaw.git
synced 2025-02-02 08:34:15 -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="/date-picker" view=DatePickerMdPage/>
|
||||||
<Route path="/divider" view=DividerMdPage/>
|
<Route path="/divider" view=DividerMdPage/>
|
||||||
<Route path="/drawer" view=DrawerMdPage/>
|
<Route path="/drawer" view=DrawerMdPage/>
|
||||||
|
<Route path="/dropdown" view=DropdownMdPage/>
|
||||||
<Route path="/grid" view=GridMdPage/>
|
<Route path="/grid" view=GridMdPage/>
|
||||||
<Route path="/icon" view=IconMdPage/>
|
<Route path="/icon" view=IconMdPage/>
|
||||||
<Route path="/image" view=ImageMdPage/>
|
<Route path="/image" view=ImageMdPage/>
|
||||||
|
|
|
@ -130,6 +130,10 @@ pub(crate) fn gen_menu_data() -> Vec<MenuGroupOption> {
|
||||||
value: "divider".into(),
|
value: "divider".into(),
|
||||||
label: "Divider".into(),
|
label: "Divider".into(),
|
||||||
},
|
},
|
||||||
|
MenuItemOption {
|
||||||
|
value: "dropdown".into(),
|
||||||
|
label: "Dropdown".into(),
|
||||||
|
},
|
||||||
MenuItemOption {
|
MenuItemOption {
|
||||||
value: "icon".into(),
|
value: "icon".into(),
|
||||||
label: "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
|
### Popover Props
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
| --------- | ----------------------------------- | ----------------------- | ----------------------------- |
|
| -------------| ----------------------------------- | -------------------------- | --------------------------------- |
|
||||||
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Content class of the popover. |
|
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Content class of the popover. |
|
||||||
| placement | `PopoverPlacement` | `PopoverPlacement::Top` | Popover placement. |
|
| placement | `PopoverPlacement` | `PopoverPlacement::Top` | Popover placement. |
|
||||||
|
| trigger_type | `PopoverTriggerType` | `PopoverTriggerType::Hover`| Action that displays the dropdown |
|
||||||
| tooltip | `bool` | `false` | Tooltip. |
|
| tooltip | `bool` | `false` | Tooltip. |
|
||||||
| children | `Children` | | The content inside popover. |
|
| children | `Children` | | The content inside popover. |
|
||||||
|
|
||||||
|
@ -158,3 +159,11 @@ view! {
|
||||||
| Name | Default | Description |
|
| Name | Default | Description |
|
||||||
| -------------- | ------- | ----------------------------------------------- |
|
| -------------- | ------- | ----------------------------------------------- |
|
||||||
| PopoverTrigger | | The element or component that triggers popover. |
|
| 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",
|
"ThemeMdPage" => "../docs/theme/mod.md",
|
||||||
"TimePickerMdPage" => "../docs/time_picker/mod.md",
|
"TimePickerMdPage" => "../docs/time_picker/mod.md",
|
||||||
"TypographyMdPage" => "../docs/typography/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![];
|
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 date_picker;
|
||||||
mod divider;
|
mod divider;
|
||||||
mod drawer;
|
mod drawer;
|
||||||
|
mod dropdown;
|
||||||
mod global_style;
|
mod global_style;
|
||||||
mod grid;
|
mod grid;
|
||||||
mod icon;
|
mod icon;
|
||||||
|
@ -62,6 +63,7 @@ pub use color_picker::*;
|
||||||
pub use date_picker::*;
|
pub use date_picker::*;
|
||||||
pub use divider::*;
|
pub use divider::*;
|
||||||
pub use drawer::*;
|
pub use drawer::*;
|
||||||
|
pub use dropdown::*;
|
||||||
pub use global_style::*;
|
pub use global_style::*;
|
||||||
pub use grid::*;
|
pub use grid::*;
|
||||||
pub use icon::*;
|
pub use icon::*;
|
||||||
|
|
|
@ -6,7 +6,9 @@ use crate::{use_theme, Theme};
|
||||||
use leptos::{leptos_dom::helpers::TimeoutHandle, *};
|
use leptos::{leptos_dom::helpers::TimeoutHandle, *};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use thaw_components::{Binder, CSSTransition, Follower, FollowerPlacement};
|
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]
|
#[slot]
|
||||||
pub struct PopoverTrigger {
|
pub struct PopoverTrigger {
|
||||||
|
@ -77,34 +79,13 @@ pub fn Popover(
|
||||||
.ok();
|
.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| {
|
target_ref.on_load(move |target_el| {
|
||||||
add_event_listener(target_el.into_any(), ev::click, move |event| {
|
add_event_listener(target_el.into_any(), ev::click, move |event| {
|
||||||
if trigger_type != PopoverTriggerType::Click {
|
if trigger_type != PopoverTriggerType::Click {
|
||||||
|
|
|
@ -4,8 +4,8 @@ use self::common::CommonTheme;
|
||||||
use crate::{
|
use crate::{
|
||||||
mobile::{NavBarTheme, TabbarTheme},
|
mobile::{NavBarTheme, TabbarTheme},
|
||||||
AlertTheme, AnchorTheme, AutoCompleteTheme, AvatarTheme, BackTopTheme, BreadcrumbTheme,
|
AlertTheme, AnchorTheme, AutoCompleteTheme, AvatarTheme, BackTopTheme, BreadcrumbTheme,
|
||||||
ButtonTheme, CalendarTheme, CollapseTheme, ColorPickerTheme, DatePickerTheme, InputTheme,
|
ButtonTheme, CalendarTheme, CollapseTheme, ColorPickerTheme, DatePickerTheme, DropdownTheme,
|
||||||
MenuTheme, MessageTheme, PopoverTheme, ProgressTheme, ScrollbarTheme, SelectTheme,
|
InputTheme, MenuTheme, MessageTheme, PopoverTheme, ProgressTheme, ScrollbarTheme, SelectTheme,
|
||||||
SkeletionTheme, SliderTheme, SpinnerTheme, SwitchTheme, TableTheme, TagTheme, TimePickerTheme,
|
SkeletionTheme, SliderTheme, SpinnerTheme, SwitchTheme, TableTheme, TagTheme, TimePickerTheme,
|
||||||
TypographyTheme, UploadTheme,
|
TypographyTheme, UploadTheme,
|
||||||
};
|
};
|
||||||
|
@ -45,6 +45,7 @@ pub struct Theme {
|
||||||
pub time_picker: TimePickerTheme,
|
pub time_picker: TimePickerTheme,
|
||||||
pub date_picker: DatePickerTheme,
|
pub date_picker: DatePickerTheme,
|
||||||
pub popover: PopoverTheme,
|
pub popover: PopoverTheme,
|
||||||
|
pub dropdown: DropdownTheme,
|
||||||
pub collapse: CollapseTheme,
|
pub collapse: CollapseTheme,
|
||||||
pub scrollbar: ScrollbarTheme,
|
pub scrollbar: ScrollbarTheme,
|
||||||
pub back_top: BackTopTheme,
|
pub back_top: BackTopTheme,
|
||||||
|
@ -81,6 +82,7 @@ impl Theme {
|
||||||
time_picker: TimePickerTheme::light(),
|
time_picker: TimePickerTheme::light(),
|
||||||
date_picker: DatePickerTheme::light(),
|
date_picker: DatePickerTheme::light(),
|
||||||
popover: PopoverTheme::light(),
|
popover: PopoverTheme::light(),
|
||||||
|
dropdown: DropdownTheme::light(),
|
||||||
collapse: CollapseTheme::light(),
|
collapse: CollapseTheme::light(),
|
||||||
scrollbar: ScrollbarTheme::light(),
|
scrollbar: ScrollbarTheme::light(),
|
||||||
back_top: BackTopTheme::light(),
|
back_top: BackTopTheme::light(),
|
||||||
|
@ -116,6 +118,7 @@ impl Theme {
|
||||||
time_picker: TimePickerTheme::dark(),
|
time_picker: TimePickerTheme::dark(),
|
||||||
date_picker: DatePickerTheme::dark(),
|
date_picker: DatePickerTheme::dark(),
|
||||||
popover: PopoverTheme::dark(),
|
popover: PopoverTheme::dark(),
|
||||||
|
dropdown: DropdownTheme::dark(),
|
||||||
collapse: CollapseTheme::dark(),
|
collapse: CollapseTheme::dark(),
|
||||||
scrollbar: ScrollbarTheme::dark(),
|
scrollbar: ScrollbarTheme::dark(),
|
||||||
back_top: BackTopTheme::dark(),
|
back_top: BackTopTheme::dark(),
|
||||||
|
|
|
@ -2,6 +2,7 @@ pub mod class_list;
|
||||||
mod dom;
|
mod dom;
|
||||||
mod event_listener;
|
mod event_listener;
|
||||||
mod hooks;
|
mod hooks;
|
||||||
|
mod on_click_outside;
|
||||||
mod optional_prop;
|
mod optional_prop;
|
||||||
mod signals;
|
mod signals;
|
||||||
mod throttle;
|
mod throttle;
|
||||||
|
@ -12,6 +13,7 @@ pub use event_listener::{
|
||||||
add_event_listener, add_event_listener_with_bool, EventListenerHandle, IntoEventTarget,
|
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 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 optional_prop::OptionalProp;
|
||||||
pub use signals::{
|
pub use signals::{
|
||||||
create_component_ref, ComponentRef, Model, OptionalMaybeSignal, SignalWatch, StoredMaybeSignal,
|
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