mirror of
https://github.com/adoyle0/thaw.git
synced 2025-01-22 22:09:22 -05:00
Fluent/menu (#218)
* dropdown - fix some errors * fluent dropdown * mobile menu * dropdown - fix some errors * rename dropdown to menu * fix: error caused by Callback in Menu * change order --------- Co-authored-by: luoxiao <luoxiaozero@163.com>
This commit is contained in:
parent
7da18dc9e5
commit
a7918ef2df
13 changed files with 613 additions and 27 deletions
|
@ -78,6 +78,7 @@ fn TheRouter(is_routing: RwSignal<bool>) -> impl IntoView {
|
|||
<Route path=StaticSegment("dialog") view=DialogMdPage/>
|
||||
<Route path=StaticSegment("divider") view=DividerMdPage/>
|
||||
<Route path=StaticSegment("drawer") view=DrawerMdPage/>
|
||||
<Route path=StaticSegment("menu") view=MenuMdPage/>
|
||||
<Route path=StaticSegment("grid") view=GridMdPage/>
|
||||
<Route path=StaticSegment("icon") view=IconMdPage/>
|
||||
<Route path=StaticSegment("image") view=ImageMdPage/>
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
use super::switch_version::SwitchVersion;
|
||||
use leptos::{ev, prelude::*};
|
||||
use leptos::{
|
||||
ev::{self, MouseEvent},
|
||||
prelude::*,
|
||||
};
|
||||
use leptos_meta::Style;
|
||||
use leptos_router::hooks::use_navigate;
|
||||
// use leptos_use::{storage::use_local_storage, utils::FromToStringCodec};
|
||||
|
@ -8,6 +11,7 @@ use thaw::*;
|
|||
#[component]
|
||||
pub fn SiteHeader() -> impl IntoView {
|
||||
let navigate = use_navigate();
|
||||
let navigate_signal = RwSignal::new(use_navigate());
|
||||
let theme = Theme::use_rw_theme();
|
||||
let theme_name = Memo::new(move |_| {
|
||||
theme.with(|theme| {
|
||||
|
@ -109,9 +113,6 @@ pub fn SiteHeader() -> impl IntoView {
|
|||
.demo-header__menu-mobile {
|
||||
display: none !important;
|
||||
}
|
||||
.demo-header__menu-popover-mobile {
|
||||
padding: 0;
|
||||
}
|
||||
.demo-header__right-btn .thaw-select {
|
||||
width: 60px;
|
||||
}
|
||||
|
@ -140,7 +141,7 @@ pub fn SiteHeader() -> impl IntoView {
|
|||
<img src="/logo.svg" style="width: 36px"/>
|
||||
<div class="demo-name">"Thaw UI"</div>
|
||||
</Space>
|
||||
<Space>
|
||||
<Space align=SpaceAlign::Center>
|
||||
<AutoComplete
|
||||
value=search_value
|
||||
placeholder="Type '/' to search"
|
||||
|
@ -159,30 +160,47 @@ pub fn SiteHeader() -> impl IntoView {
|
|||
/>
|
||||
</AutoCompletePrefix>
|
||||
</AutoComplete>
|
||||
<Popover
|
||||
placement=PopoverPlacement::BottomEnd
|
||||
class="demo-header__menu-popover-mobile"
|
||||
<Menu
|
||||
placement=MenuPlacement::BottomEnd
|
||||
on_select=move |value : String| match value.as_str() {
|
||||
"Dark" => change_theme(MouseEvent::new("click").unwrap()),
|
||||
"Light" => change_theme(MouseEvent::new("click").unwrap()),
|
||||
"github" => { _ = window().open_with_url("http://github.com/thaw-ui/thaw"); },//FIXME: breaks page
|
||||
"discord" => { _ = window().open_with_url("https://discord.gg/YPxuprzu6M"); },//FIXME: breaks page
|
||||
_ => navigate_signal.get()(&value, Default::default())
|
||||
|
||||
}
|
||||
>
|
||||
<PopoverTrigger slot class="demo-header__menu-mobile">
|
||||
<MenuTrigger slot class="demo-header__menu-mobile">
|
||||
<Button
|
||||
appearance=ButtonAppearance::Subtle
|
||||
icon=icondata::AiUnorderedListOutlined
|
||||
attr:style="font-size: 22px; padding: 0px 6px;"
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<div style="height: 70vh; overflow: auto;">// <Menu value=menu_value>
|
||||
// <MenuItem key=theme_name label=theme_name />
|
||||
// <MenuItem key="github" label="Github" />
|
||||
// {
|
||||
// use crate::pages::{gen_guide_menu_data, gen_menu_data};
|
||||
// vec![
|
||||
// gen_guide_menu_data().into_view(),
|
||||
// gen_menu_data().into_view(),
|
||||
// ]
|
||||
// }
|
||||
// </Menu>
|
||||
</div>
|
||||
</Popover>
|
||||
</MenuTrigger>
|
||||
<MenuItem key=theme_name label=theme_name/>
|
||||
<MenuItem icon=icondata::AiGithubOutlined key="github" label="Github"/>
|
||||
<MenuItem icon=icondata::BiDiscordAlt key="discord" label="Discord"/>
|
||||
{
|
||||
use crate::pages::{gen_menu_data, MenuGroupOption, MenuItemOption};
|
||||
gen_menu_data().into_iter().map(|data| {
|
||||
let MenuGroupOption { label, children } = data;
|
||||
view! {
|
||||
<Caption1Strong style="margin-inline-start: 10px; margin-top: 10px; display: block">
|
||||
{label}
|
||||
</Caption1Strong>
|
||||
{
|
||||
children.into_iter().map(|item| {
|
||||
let MenuItemOption { label, value } = item;
|
||||
view! {
|
||||
<MenuItem label key=value/>
|
||||
}
|
||||
}).collect_view()
|
||||
}
|
||||
}
|
||||
}).collect_view()
|
||||
}
|
||||
</Menu>
|
||||
<Space class="demo-header__right-btn" align=SpaceAlign::Center>
|
||||
<Button
|
||||
appearance=ButtonAppearance::Subtle
|
||||
|
|
|
@ -229,6 +229,10 @@ pub(crate) fn gen_menu_data() -> Vec<MenuGroupOption> {
|
|||
value: "/components/loading-bar".into(),
|
||||
label: "Loading Bar".into(),
|
||||
},
|
||||
MenuItemOption {
|
||||
value: "/components/menu".into(),
|
||||
label: "Menu".into(),
|
||||
},
|
||||
MenuItemOption {
|
||||
value: "/components/message-bar".into(),
|
||||
label: "Message Bar".into(),
|
||||
|
|
183
demo_markdown/docs/menu/mod.md
Normal file
183
demo_markdown/docs/menu/mod.md
Normal file
|
@ -0,0 +1,183 @@
|
|||
# Menu
|
||||
|
||||
```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(),),
|
||||
// _ => ()
|
||||
// }
|
||||
//};
|
||||
|
||||
let on_select = move |key| println!("{}", key);
|
||||
|
||||
view! {
|
||||
<Space>
|
||||
<Menu on_select trigger_type=MenuTriggerType::Hover>
|
||||
<MenuTrigger slot>
|
||||
<Button>"Hover"</Button>
|
||||
</MenuTrigger>
|
||||
<MenuItem key="facebook" icon=icondata::AiFacebookOutlined label="Facebook"></MenuItem>
|
||||
<MenuItem key="twitter" disabled=true icon=icondata::AiTwitterOutlined label="Twitter"></MenuItem>
|
||||
</Menu>
|
||||
|
||||
<Menu on_select>
|
||||
<MenuTrigger slot>
|
||||
<Button>"Click"</Button>
|
||||
</MenuTrigger>
|
||||
<MenuItem key="facebook" icon=icondata::AiFacebookOutlined label="Facebook"></MenuItem>
|
||||
<MenuItem key="twitter" icon=icondata::AiTwitterOutlined label="Twitter"></MenuItem>
|
||||
<MenuItem key="no_icon" disabled=true label="Mastodon"></MenuItem>
|
||||
</Menu>
|
||||
</Space>
|
||||
}
|
||||
```
|
||||
|
||||
### Placement
|
||||
|
||||
```rust demo
|
||||
use leptos_meta::Style;
|
||||
|
||||
let on_select = move |key| println!("{}", key);
|
||||
|
||||
view! {
|
||||
<Style>
|
||||
".demo-menu .thaw-button { width: 100% } .demo-menu .thaw-menu-trigger { display: block }"
|
||||
</Style>
|
||||
<Grid x_gap=8 y_gap=8 cols=3 class="demo-menu">
|
||||
<GridItem>
|
||||
<Menu on_select placement=MenuPlacement::TopStart>
|
||||
<MenuTrigger slot>
|
||||
<Button>"Top Start"</Button>
|
||||
</MenuTrigger>
|
||||
"Content"
|
||||
</Menu>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<Menu on_select placement=MenuPlacement::Top>
|
||||
<MenuTrigger slot>
|
||||
<Button>"Top"</Button>
|
||||
</MenuTrigger>
|
||||
"Content"
|
||||
</Menu>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<Menu on_select placement=MenuPlacement::TopEnd>
|
||||
<MenuTrigger slot>
|
||||
<Button>"Top End"</Button>
|
||||
</MenuTrigger>
|
||||
"Content"
|
||||
</Menu>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<Menu on_select placement=MenuPlacement::LeftStart>
|
||||
<MenuTrigger slot>
|
||||
<Button>"Left Start"</Button>
|
||||
</MenuTrigger>
|
||||
"Content"
|
||||
</Menu>
|
||||
</GridItem>
|
||||
<GridItem offset=1>
|
||||
<Menu on_select placement=MenuPlacement::RightStart>
|
||||
<MenuTrigger slot>
|
||||
<Button>"Right Start"</Button>
|
||||
</MenuTrigger>
|
||||
"Content"
|
||||
</Menu>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<Menu on_select placement=MenuPlacement::Left>
|
||||
<MenuTrigger slot>
|
||||
<Button>"Left"</Button>
|
||||
</MenuTrigger>
|
||||
"Content"
|
||||
</Menu>
|
||||
</GridItem>
|
||||
<GridItem offset=1>
|
||||
<Menu on_select placement=MenuPlacement::Right>
|
||||
<MenuTrigger slot>
|
||||
<Button>"Right"</Button>
|
||||
</MenuTrigger>
|
||||
"Content"
|
||||
</Menu>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<Menu on_select placement=MenuPlacement::LeftEnd>
|
||||
<MenuTrigger slot>
|
||||
<Button>"Left End"</Button>
|
||||
</MenuTrigger>
|
||||
"Content"
|
||||
</Menu>
|
||||
</GridItem>
|
||||
<GridItem offset=1>
|
||||
<Menu on_select placement=MenuPlacement::RightEnd>
|
||||
<MenuTrigger slot>
|
||||
<Button>"Right End"</Button>
|
||||
</MenuTrigger>
|
||||
"Content"
|
||||
</Menu>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<Menu on_select placement=MenuPlacement::BottomStart>
|
||||
<MenuTrigger slot>
|
||||
<Button>"Bottom Start"</Button>
|
||||
</MenuTrigger>
|
||||
"Content"
|
||||
</Menu>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<Menu on_select placement=MenuPlacement::Bottom>
|
||||
<MenuTrigger slot>
|
||||
<Button>"Bottom"</Button>
|
||||
</MenuTrigger>
|
||||
"Content"
|
||||
</Menu>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<Menu on_select placement=MenuPlacement::BottomEnd>
|
||||
<MenuTrigger slot>
|
||||
<Button>"Bottom End"</Button>
|
||||
</MenuTrigger>
|
||||
"Content"
|
||||
</Menu>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
}
|
||||
```
|
||||
|
||||
### Menu Props
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| ------------ | ----------------------------------- | ---------------------------- | ------------------------------------------- |
|
||||
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the menu element. |
|
||||
| on_select | `Callback<String>` | | Called when item is selected. |
|
||||
| trigger_type | `MenuTriggerType` | `MenuTriggerType::Click` | Action that displays the menu. |
|
||||
| placement | `MenuPlacement` | `MenuPlacement::Bottom` | Menu placement. |
|
||||
| children | `Children` | | The content inside menu. |
|
||||
|
||||
### MenuItem Props
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| -------- | -------------------------------------------- | -------------------- | ------------------------------------------------ |
|
||||
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the menu item element. |
|
||||
| key | `MaybeSignal<String>` | `Default::default()` | The key of the menu item. |
|
||||
| label | `MaybeSignal<String>` | `Default::default()` | The label of the menu item. |
|
||||
| icon | `OptionalMaybeSignal<icondata_core::Icon>` | `None` | The icon of the menu item. |
|
||||
| disabled | `MaybeSignal<bool>` | `false` | Whether the menu item is disabled. |
|
||||
|
||||
|
||||
### Menu Slots
|
||||
|
||||
| Name | Default | Description |
|
||||
| --------------- | ------- | ------------------------------------------------ |
|
||||
| MenuTrigger | `None` | The element or component that triggers menu. |
|
||||
|
||||
### MenuTriger Props
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| ------------ | ----------------------------------- | ---------------------------- | -------------------------------------------------- |
|
||||
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the menu trigger element. |
|
||||
| children | `Children` | | The content inside menu trigger. |
|
||||
|
|
@ -53,6 +53,7 @@ pub fn include_md(_token_stream: proc_macro::TokenStream) -> proc_macro::TokenSt
|
|||
"InputMdPage" => "../docs/input/mod.md",
|
||||
"LayoutMdPage" => "../docs/layout/mod.md",
|
||||
"LoadingBarMdPage" => "../docs/loading_bar/mod.md",
|
||||
"MenuMdPage" => "../docs/menu/mod.md",
|
||||
"MessageBarMdPage" => "../docs/message_bar/mod.md",
|
||||
"PopoverMdPage" => "../docs/popover/mod.md",
|
||||
"ProgressBarMdPage" => "../docs/progress_bar/mod.md",
|
||||
|
|
|
@ -24,6 +24,7 @@ mod image;
|
|||
mod input;
|
||||
mod layout;
|
||||
mod loading_bar;
|
||||
mod menu;
|
||||
mod message_bar;
|
||||
mod nav;
|
||||
mod popover;
|
||||
|
@ -71,6 +72,7 @@ pub use image::*;
|
|||
pub use input::*;
|
||||
pub use layout::*;
|
||||
pub use loading_bar::*;
|
||||
pub use menu::*;
|
||||
pub use message_bar::*;
|
||||
pub use nav::*;
|
||||
pub use popover::*;
|
||||
|
|
16
thaw/src/menu/menu-item.css
Normal file
16
thaw/src/menu/menu-item.css
Normal file
|
@ -0,0 +1,16 @@
|
|||
.thaw-menu-item{
|
||||
padding: 6px 5px;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.thaw-menu-item:hover:not(.thaw-dropdown-item--disabled) {
|
||||
background-color: var(--colorNeutralBackground1Hover);
|
||||
}
|
||||
|
||||
.thaw-menu-item.thaw-dropdown-item--disabled {
|
||||
color: var(--colorNeutralForegroundDisabled);
|
||||
cursor: not-allowed;
|
||||
}
|
68
thaw/src/menu/menu.css
Normal file
68
thaw/src/menu/menu.css
Normal file
|
@ -0,0 +1,68 @@
|
|||
.thaw-menu {
|
||||
padding: 4px;
|
||||
background-color: var(--colorNeutralBackground1);
|
||||
color: var(--colorNeutralForeground1);
|
||||
border-radius: var(--borderRadiusMedium);
|
||||
transform-origin: inherit;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[data-thaw-placement="top-start"] > .thaw-menu,
|
||||
[data-thaw-placement="top-end"] > .thaw-menu,
|
||||
[data-thaw-placement="top"] > .thaw-menu {
|
||||
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-menu,
|
||||
[data-thaw-placement="bottom-end"] > .thaw-menu,
|
||||
[data-thaw-placement="bottom"] > .thaw-menu {
|
||||
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-menu,
|
||||
[data-thaw-placement="left-end"] > .thaw-menu,
|
||||
[data-thaw-placement="left"] > .thaw-menu {
|
||||
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-menu,
|
||||
[data-thaw-placement="right-end"] > .thaw-menu,
|
||||
[data-thaw-placement="right"] > .thaw-menu {
|
||||
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-menu.dropdown-transition-enter-from,
|
||||
.thaw-menu.dropdown-transition-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.85);
|
||||
}
|
||||
|
||||
.thaw-menu.dropdown-transition-enter-to,
|
||||
.thaw-menu.dropdown-transition-leave-from {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.thaw-menu.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-menu.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);
|
||||
}
|
57
thaw/src/menu/menu_item.rs
Normal file
57
thaw/src/menu/menu_item.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
use crate::{
|
||||
menu::{HasIcon, OnSelect},
|
||||
Icon,
|
||||
};
|
||||
use leptos::prelude::*;
|
||||
use thaw_components::{Fallback, If, OptionComp, Then};
|
||||
use thaw_utils::{class_list, mount_style, OptionalMaybeSignal, OptionalProp};
|
||||
|
||||
#[component]
|
||||
pub fn MenuItem(
|
||||
#[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("menu-item", include_str!("./menu-item.css"));
|
||||
|
||||
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(key.get());
|
||||
};
|
||||
|
||||
view! {
|
||||
<div
|
||||
class=class_list![
|
||||
"thaw-menu-item", ("thaw-menu-item--disabled", move || disabled.get()),
|
||||
class.map(| c | move || c.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>
|
||||
}
|
||||
}
|
199
thaw/src/menu/mod.rs
Normal file
199
thaw/src/menu/mod.rs
Normal file
|
@ -0,0 +1,199 @@
|
|||
mod menu_item;
|
||||
|
||||
pub use menu_item::*;
|
||||
|
||||
use crate::ConfigInjection;
|
||||
use leptos::{ev, html::Div, leptos_dom::helpers::TimeoutHandle, prelude::*};
|
||||
use std::time::Duration;
|
||||
use thaw_components::{Binder, CSSTransition, Follower, FollowerPlacement};
|
||||
use thaw_utils::{
|
||||
add_event_listener, call_on_click_outside, class_list, mount_style, ArcOneCallback,
|
||||
OptionalProp,
|
||||
};
|
||||
|
||||
#[slot]
|
||||
pub struct MenuTrigger {
|
||||
#[prop(optional, into)]
|
||||
class: OptionalProp<MaybeSignal<String>>,
|
||||
children: Children,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct HasIcon(RwSignal<bool>);
|
||||
|
||||
#[derive(Clone)]
|
||||
struct OnSelect(ArcOneCallback<String>);
|
||||
|
||||
#[component]
|
||||
pub fn Menu(
|
||||
#[prop(optional, into)] class: MaybeProp<String>,
|
||||
menu_trigger: MenuTrigger,
|
||||
#[prop(optional)] trigger_type: MenuTriggerType,
|
||||
#[prop(optional)] placement: MenuPlacement,
|
||||
#[prop(into)] on_select: ArcOneCallback<String>,
|
||||
#[prop(optional, into)] appearance: Option<MaybeSignal<MenuAppearance>>,
|
||||
children: Children,
|
||||
) -> impl IntoView {
|
||||
mount_style("menu", include_str!("./menu.css"));
|
||||
let config_provider = ConfigInjection::use_();
|
||||
|
||||
let menu_ref = NodeRef::<Div>::new();
|
||||
let target_ref = NodeRef::<Div>::new();
|
||||
let is_show_menu = RwSignal::new(false);
|
||||
let show_menu_handle = StoredValue::new(None::<TimeoutHandle>);
|
||||
|
||||
let on_mouse_enter = move |_| {
|
||||
if trigger_type != MenuTriggerType::Hover {
|
||||
return;
|
||||
}
|
||||
show_menu_handle.update_value(|handle| {
|
||||
if let Some(handle) = handle.take() {
|
||||
handle.clear();
|
||||
}
|
||||
});
|
||||
is_show_menu.set(true);
|
||||
};
|
||||
let on_mouse_leave = move |_| {
|
||||
if trigger_type != MenuTriggerType::Hover {
|
||||
return;
|
||||
}
|
||||
show_menu_handle.update_value(|handle| {
|
||||
if let Some(handle) = handle.take() {
|
||||
handle.clear();
|
||||
}
|
||||
*handle = set_timeout_with_handle(
|
||||
move || {
|
||||
is_show_menu.set(false);
|
||||
},
|
||||
Duration::from_millis(100),
|
||||
)
|
||||
.ok();
|
||||
});
|
||||
};
|
||||
|
||||
if trigger_type != MenuTriggerType::Hover {
|
||||
call_on_click_outside(menu_ref, Callback::new(move |_| is_show_menu.set(false)));
|
||||
}
|
||||
|
||||
Effect::new(move |_| {
|
||||
let Some(target_el) = target_ref.get() else {
|
||||
return;
|
||||
};
|
||||
let handler = add_event_listener(target_el.into(), ev::click, move |event| {
|
||||
if trigger_type != MenuTriggerType::Click {
|
||||
return;
|
||||
}
|
||||
event.stop_propagation();
|
||||
is_show_menu.update(|show| *show = !*show);
|
||||
});
|
||||
on_cleanup(move || handler.remove());
|
||||
});
|
||||
|
||||
let MenuTrigger {
|
||||
class: trigger_class,
|
||||
children: trigger_children,
|
||||
} = menu_trigger;
|
||||
|
||||
provide_context(HasIcon(RwSignal::new(false)));
|
||||
provide_context(OnSelect(ArcOneCallback::<String>::new(move |key| {
|
||||
is_show_menu.set(false);
|
||||
on_select(key);
|
||||
})));
|
||||
|
||||
view! {
|
||||
<Binder target_ref>
|
||||
<div
|
||||
class=class_list!["thaw-menu-trigger", trigger_class.map(| c | move || c.get())]
|
||||
node_ref=target_ref
|
||||
on:mouseenter=on_mouse_enter
|
||||
on:mouseleave=on_mouse_leave
|
||||
>
|
||||
{trigger_children()}
|
||||
</div>
|
||||
<Follower slot show=is_show_menu placement>
|
||||
<CSSTransition
|
||||
node_ref=menu_ref
|
||||
name="menu-transition"
|
||||
appear=is_show_menu.get_untracked()
|
||||
show=is_show_menu
|
||||
let:display
|
||||
>
|
||||
<div
|
||||
class=class_list![
|
||||
"thaw-config-provider thaw-menu",
|
||||
appearance.map(|appearance| move || format!("thaw-menu--{}", appearance.get().as_str())),
|
||||
class
|
||||
]
|
||||
data-thaw-id=config_provider.id().clone()
|
||||
style=move || display.get().unwrap_or_default()
|
||||
node_ref=menu_ref
|
||||
on:mouseenter=on_mouse_enter
|
||||
on:mouseleave=on_mouse_leave
|
||||
>
|
||||
{children()}
|
||||
</div>
|
||||
</CSSTransition>
|
||||
</Follower>
|
||||
</Binder>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Clone)]
|
||||
pub enum MenuTriggerType {
|
||||
Hover,
|
||||
#[default]
|
||||
Click,
|
||||
}
|
||||
|
||||
impl Copy for MenuTriggerType {}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum MenuAppearance {
|
||||
Brand,
|
||||
Inverted,
|
||||
}
|
||||
|
||||
impl MenuAppearance {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
MenuAppearance::Brand => "brand",
|
||||
MenuAppearance::Inverted => "inverted",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub enum MenuPlacement {
|
||||
Top,
|
||||
#[default]
|
||||
Bottom,
|
||||
Left,
|
||||
Right,
|
||||
TopStart,
|
||||
TopEnd,
|
||||
LeftStart,
|
||||
LeftEnd,
|
||||
RightStart,
|
||||
RightEnd,
|
||||
BottomStart,
|
||||
BottomEnd,
|
||||
}
|
||||
|
||||
impl From<MenuPlacement> for FollowerPlacement {
|
||||
fn from(value: MenuPlacement) -> Self {
|
||||
match value {
|
||||
MenuPlacement::Top => Self::Top,
|
||||
MenuPlacement::Bottom => Self::Bottom,
|
||||
MenuPlacement::Left => Self::Left,
|
||||
MenuPlacement::Right => Self::Right,
|
||||
MenuPlacement::TopStart => Self::TopStart,
|
||||
MenuPlacement::TopEnd => Self::TopEnd,
|
||||
MenuPlacement::LeftStart => Self::LeftStart,
|
||||
MenuPlacement::LeftEnd => Self::LeftEnd,
|
||||
MenuPlacement::RightStart => Self::RightStart,
|
||||
MenuPlacement::RightEnd => Self::RightEnd,
|
||||
MenuPlacement::BottomStart => Self::BottomStart,
|
||||
MenuPlacement::BottomEnd => Self::BottomEnd,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -91,13 +91,14 @@ pub fn Popover(
|
|||
let Some(target_el) = target_ref.get() else {
|
||||
return;
|
||||
};
|
||||
add_event_listener(target_el.into(), ev::click, move |event| {
|
||||
let handler = add_event_listener(target_el.into(), ev::click, move |event| {
|
||||
if trigger_type != PopoverTriggerType::Click {
|
||||
return;
|
||||
}
|
||||
event.stop_propagation();
|
||||
is_show_popover.update(|show| *show = !*show);
|
||||
});
|
||||
on_cleanup(move || handler.remove());
|
||||
});
|
||||
|
||||
let PopoverTrigger {
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
mod callback;
|
||||
pub mod class_list;
|
||||
mod dom;
|
||||
mod event_listener;
|
||||
mod hooks;
|
||||
pub mod macros;
|
||||
mod on_click_outside;
|
||||
mod optional_prop;
|
||||
mod signals;
|
||||
mod throttle;
|
||||
mod time;
|
||||
mod callback;
|
||||
|
||||
pub use dom::*;
|
||||
pub use callback::*;
|
||||
pub use dom::*;
|
||||
pub use event_listener::{add_event_listener, add_event_listener_with_bool, EventListenerHandle};
|
||||
pub use hooks::{use_click_position, use_lock_html_scroll, NextFrame};
|
||||
pub use on_click_outside::*;
|
||||
pub use optional_prop::OptionalProp;
|
||||
pub use signals::{ComponentRef, Model, OptionalMaybeSignal, SignalWatch, StoredMaybeSignal};
|
||||
pub use throttle::throttle;
|
||||
|
|
34
thaw_utils/src/on_click_outside.rs
Normal file
34
thaw_utils/src/on_click_outside.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
use leptos::{ev, html::Div, prelude::*};
|
||||
use tachys::reactive_graph::node_ref::NodeRef;
|
||||
|
||||
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());
|
||||
}
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
{
|
||||
let _ = element;
|
||||
let _ = on_click;
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue