mirror of
https://github.com/adoyle0/thaw.git
synced 2025-02-02 08:34:15 -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("dialog") view=DialogMdPage/>
|
||||||
<Route path=StaticSegment("divider") view=DividerMdPage/>
|
<Route path=StaticSegment("divider") view=DividerMdPage/>
|
||||||
<Route path=StaticSegment("drawer") view=DrawerMdPage/>
|
<Route path=StaticSegment("drawer") view=DrawerMdPage/>
|
||||||
|
<Route path=StaticSegment("menu") view=MenuMdPage/>
|
||||||
<Route path=StaticSegment("grid") view=GridMdPage/>
|
<Route path=StaticSegment("grid") view=GridMdPage/>
|
||||||
<Route path=StaticSegment("icon") view=IconMdPage/>
|
<Route path=StaticSegment("icon") view=IconMdPage/>
|
||||||
<Route path=StaticSegment("image") view=ImageMdPage/>
|
<Route path=StaticSegment("image") view=ImageMdPage/>
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
use super::switch_version::SwitchVersion;
|
use super::switch_version::SwitchVersion;
|
||||||
use leptos::{ev, prelude::*};
|
use leptos::{
|
||||||
|
ev::{self, MouseEvent},
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
use leptos_meta::Style;
|
use leptos_meta::Style;
|
||||||
use leptos_router::hooks::use_navigate;
|
use leptos_router::hooks::use_navigate;
|
||||||
// use leptos_use::{storage::use_local_storage, utils::FromToStringCodec};
|
// use leptos_use::{storage::use_local_storage, utils::FromToStringCodec};
|
||||||
|
@ -8,6 +11,7 @@ use thaw::*;
|
||||||
#[component]
|
#[component]
|
||||||
pub fn SiteHeader() -> impl IntoView {
|
pub fn SiteHeader() -> impl IntoView {
|
||||||
let navigate = use_navigate();
|
let navigate = use_navigate();
|
||||||
|
let navigate_signal = RwSignal::new(use_navigate());
|
||||||
let theme = Theme::use_rw_theme();
|
let theme = Theme::use_rw_theme();
|
||||||
let theme_name = Memo::new(move |_| {
|
let theme_name = Memo::new(move |_| {
|
||||||
theme.with(|theme| {
|
theme.with(|theme| {
|
||||||
|
@ -109,9 +113,6 @@ pub fn SiteHeader() -> impl IntoView {
|
||||||
.demo-header__menu-mobile {
|
.demo-header__menu-mobile {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
.demo-header__menu-popover-mobile {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.demo-header__right-btn .thaw-select {
|
.demo-header__right-btn .thaw-select {
|
||||||
width: 60px;
|
width: 60px;
|
||||||
}
|
}
|
||||||
|
@ -140,7 +141,7 @@ pub fn SiteHeader() -> impl IntoView {
|
||||||
<img src="/logo.svg" style="width: 36px"/>
|
<img src="/logo.svg" style="width: 36px"/>
|
||||||
<div class="demo-name">"Thaw UI"</div>
|
<div class="demo-name">"Thaw UI"</div>
|
||||||
</Space>
|
</Space>
|
||||||
<Space>
|
<Space align=SpaceAlign::Center>
|
||||||
<AutoComplete
|
<AutoComplete
|
||||||
value=search_value
|
value=search_value
|
||||||
placeholder="Type '/' to search"
|
placeholder="Type '/' to search"
|
||||||
|
@ -159,30 +160,47 @@ pub fn SiteHeader() -> impl IntoView {
|
||||||
/>
|
/>
|
||||||
</AutoCompletePrefix>
|
</AutoCompletePrefix>
|
||||||
</AutoComplete>
|
</AutoComplete>
|
||||||
<Popover
|
<Menu
|
||||||
placement=PopoverPlacement::BottomEnd
|
placement=MenuPlacement::BottomEnd
|
||||||
class="demo-header__menu-popover-mobile"
|
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
|
<Button
|
||||||
appearance=ButtonAppearance::Subtle
|
appearance=ButtonAppearance::Subtle
|
||||||
icon=icondata::AiUnorderedListOutlined
|
icon=icondata::AiUnorderedListOutlined
|
||||||
attr:style="font-size: 22px; padding: 0px 6px;"
|
attr:style="font-size: 22px; padding: 0px 6px;"
|
||||||
/>
|
/>
|
||||||
</PopoverTrigger>
|
</MenuTrigger>
|
||||||
<div style="height: 70vh; overflow: auto;">// <Menu value=menu_value>
|
<MenuItem key=theme_name label=theme_name/>
|
||||||
// <MenuItem key=theme_name label=theme_name />
|
<MenuItem icon=icondata::AiGithubOutlined key="github" label="Github"/>
|
||||||
// <MenuItem key="github" label="Github" />
|
<MenuItem icon=icondata::BiDiscordAlt key="discord" label="Discord"/>
|
||||||
// {
|
{
|
||||||
// use crate::pages::{gen_guide_menu_data, gen_menu_data};
|
use crate::pages::{gen_menu_data, MenuGroupOption, MenuItemOption};
|
||||||
// vec![
|
gen_menu_data().into_iter().map(|data| {
|
||||||
// gen_guide_menu_data().into_view(),
|
let MenuGroupOption { label, children } = data;
|
||||||
// gen_menu_data().into_view(),
|
view! {
|
||||||
// ]
|
<Caption1Strong style="margin-inline-start: 10px; margin-top: 10px; display: block">
|
||||||
// }
|
{label}
|
||||||
// </Menu>
|
</Caption1Strong>
|
||||||
</div>
|
{
|
||||||
</Popover>
|
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>
|
<Space class="demo-header__right-btn" align=SpaceAlign::Center>
|
||||||
<Button
|
<Button
|
||||||
appearance=ButtonAppearance::Subtle
|
appearance=ButtonAppearance::Subtle
|
||||||
|
|
|
@ -229,6 +229,10 @@ pub(crate) fn gen_menu_data() -> Vec<MenuGroupOption> {
|
||||||
value: "/components/loading-bar".into(),
|
value: "/components/loading-bar".into(),
|
||||||
label: "Loading Bar".into(),
|
label: "Loading Bar".into(),
|
||||||
},
|
},
|
||||||
|
MenuItemOption {
|
||||||
|
value: "/components/menu".into(),
|
||||||
|
label: "Menu".into(),
|
||||||
|
},
|
||||||
MenuItemOption {
|
MenuItemOption {
|
||||||
value: "/components/message-bar".into(),
|
value: "/components/message-bar".into(),
|
||||||
label: "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",
|
"InputMdPage" => "../docs/input/mod.md",
|
||||||
"LayoutMdPage" => "../docs/layout/mod.md",
|
"LayoutMdPage" => "../docs/layout/mod.md",
|
||||||
"LoadingBarMdPage" => "../docs/loading_bar/mod.md",
|
"LoadingBarMdPage" => "../docs/loading_bar/mod.md",
|
||||||
|
"MenuMdPage" => "../docs/menu/mod.md",
|
||||||
"MessageBarMdPage" => "../docs/message_bar/mod.md",
|
"MessageBarMdPage" => "../docs/message_bar/mod.md",
|
||||||
"PopoverMdPage" => "../docs/popover/mod.md",
|
"PopoverMdPage" => "../docs/popover/mod.md",
|
||||||
"ProgressBarMdPage" => "../docs/progress_bar/mod.md",
|
"ProgressBarMdPage" => "../docs/progress_bar/mod.md",
|
||||||
|
|
|
@ -24,6 +24,7 @@ mod image;
|
||||||
mod input;
|
mod input;
|
||||||
mod layout;
|
mod layout;
|
||||||
mod loading_bar;
|
mod loading_bar;
|
||||||
|
mod menu;
|
||||||
mod message_bar;
|
mod message_bar;
|
||||||
mod nav;
|
mod nav;
|
||||||
mod popover;
|
mod popover;
|
||||||
|
@ -71,6 +72,7 @@ pub use image::*;
|
||||||
pub use input::*;
|
pub use input::*;
|
||||||
pub use layout::*;
|
pub use layout::*;
|
||||||
pub use loading_bar::*;
|
pub use loading_bar::*;
|
||||||
|
pub use menu::*;
|
||||||
pub use message_bar::*;
|
pub use message_bar::*;
|
||||||
pub use nav::*;
|
pub use nav::*;
|
||||||
pub use popover::*;
|
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 {
|
let Some(target_el) = target_ref.get() else {
|
||||||
return;
|
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 {
|
if trigger_type != PopoverTriggerType::Click {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.stop_propagation();
|
event.stop_propagation();
|
||||||
is_show_popover.update(|show| *show = !*show);
|
is_show_popover.update(|show| *show = !*show);
|
||||||
});
|
});
|
||||||
|
on_cleanup(move || handler.remove());
|
||||||
});
|
});
|
||||||
|
|
||||||
let PopoverTrigger {
|
let PopoverTrigger {
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
|
mod callback;
|
||||||
pub mod class_list;
|
pub mod class_list;
|
||||||
mod dom;
|
mod dom;
|
||||||
mod event_listener;
|
mod event_listener;
|
||||||
mod hooks;
|
mod hooks;
|
||||||
pub mod macros;
|
pub mod macros;
|
||||||
|
mod on_click_outside;
|
||||||
mod optional_prop;
|
mod optional_prop;
|
||||||
mod signals;
|
mod signals;
|
||||||
mod throttle;
|
mod throttle;
|
||||||
mod time;
|
mod time;
|
||||||
mod callback;
|
|
||||||
|
|
||||||
pub use dom::*;
|
|
||||||
pub use callback::*;
|
pub use callback::*;
|
||||||
|
pub use dom::*;
|
||||||
pub use event_listener::{add_event_listener, add_event_listener_with_bool, EventListenerHandle};
|
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 hooks::{use_click_position, use_lock_html_scroll, NextFrame};
|
||||||
|
pub use on_click_outside::*;
|
||||||
pub use optional_prop::OptionalProp;
|
pub use optional_prop::OptionalProp;
|
||||||
pub use signals::{ComponentRef, Model, OptionalMaybeSignal, SignalWatch, StoredMaybeSignal};
|
pub use signals::{ComponentRef, Model, OptionalMaybeSignal, SignalWatch, StoredMaybeSignal};
|
||||||
pub use throttle::throttle;
|
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