feat: MenuItem adds children (#207)

This commit is contained in:
luoxiaozero 2024-06-17 10:29:16 +08:00 committed by GitHub
parent a9f02ede65
commit 2f96fec20d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 185 additions and 24 deletions

View file

@ -4,11 +4,19 @@
let value = create_rw_signal(String::from("o")); let value = create_rw_signal(String::from("o"));
view! { view! {
<Menu value> <Menu value default_expanded_keys=vec![String::from("area")]>
<MenuItem key="a" label="And"/> <MenuItem key="a" label="And"/>
<MenuItem key="o" label="Or"/> <MenuItem key="o" label="Or"/>
<MenuItem icon=icondata::AiAreaChartOutlined key="area" label="Area Chart"/> <MenuItem icon=icondata::AiAreaChartOutlined key="area" label="Area Chart">
<MenuItem icon=icondata::AiPieChartOutlined key="pie" label="Pie Chart"/> <MenuItem key="target" label="Target"/>
<MenuItem key="above" label="Above"/>
<MenuItem key="below" label="Below"/>
</MenuItem>
<MenuItem icon=icondata::AiPieChartOutlined key="pie" label="Pie Chart">
<MenuItem key="pie-target" label="Target"/>
<MenuItem key="pie-above" label="Above"/>
<MenuItem key="pie-below" label="Below"/>
</MenuItem>
<MenuItem icon=icondata::AiGithubOutlined key="github" label="Github"/> <MenuItem icon=icondata::AiGithubOutlined key="github" label="Github"/>
<MenuItem icon=icondata::AiChromeOutlined key="chrome" label="Chrome"/> <MenuItem icon=icondata::AiChromeOutlined key="chrome" label="Chrome"/>
</Menu> </Menu>
@ -17,11 +25,12 @@ view! {
### Menu Props ### Menu Props
| Name | Type | Default | Description | | Name | Type | Default | Description |
| -------- | ----------------------------------- | -------------------- | --------------------------------------- | | --- | --- | --- | --- |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the menu element. | | class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the menu element. |
| value | `Model<String>` | `Default::default()` | The selected item key of the menu. | | value | `Model<String>` | `Default::default()` | The selected item key of the menu. |
| children | `Children` | | Menu's content. | | default_expanded_keys | `Vec<String>` | `Default::default()` | The default expanded submenu keys. |
| children | `Children` | | Menu's content. |
### MenuGroup Props ### MenuGroup Props
@ -39,3 +48,4 @@ view! {
| label | `MaybeSignal<String>` | `Default::default()` | The label of the menu item. | | label | `MaybeSignal<String>` | `Default::default()` | The label of the menu item. |
| key | `MaybeSignal<String>` | `Default::default()` | The indentifier of the menu item. | | key | `MaybeSignal<String>` | `Default::default()` | The indentifier of the menu item. |
| icon | `OptionalMaybeSignal<icondata_core::Icon>` | `None` | The icon of the menu item. | | icon | `OptionalMaybeSignal<icondata_core::Icon>` | `None` | The icon of the menu item. |
| children | `Option<Children>` | `None` | MenuItem's content. |

View file

@ -1,7 +1,11 @@
.thaw-menu {
padding-bottom: 0.3rem;
}
.thaw-menu-item__content { .thaw-menu-item__content {
display: flex; display: flex;
align-items: center; align-items: center;
margin: 0.3rem 0.4rem; margin: 0.3rem 0.4rem 0;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
color: var(--thaw-font-color); color: var(--thaw-font-color);
cursor: pointer; cursor: pointer;
@ -22,3 +26,45 @@
color: var(--thaw-font-color-active); color: var(--thaw-font-color-active);
background-color: var(--thaw-background-color); background-color: var(--thaw-background-color);
} }
.thaw-menu-item__content--submenu-selected {
color: var(--thaw-font-color-active);
}
.thaw-menu-item__arrow {
font-size: 18px;
margin-inline-start: auto;
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
transform: rotate(0deg);
}
.thaw-menu-item__arrow--open {
transform: rotate(90deg);
}
.thaw-menu-submenu {
margin-left: 1.6rem;
}
.thaw-menu-submenu.fade-in-height-expand-transition-leave-from,
.thaw-menu-submenu.fade-in-height-expand-transition-enter-to {
opacity: 1;
}
.thaw-menu-submenu.fade-in-height-expand-transition-leave-to,
.thaw-menu-submenu.fade-in-height-expand-transition-enter-from {
opacity: 0;
max-height: 0;
}
.thaw-menu-submenu.fade-in-height-expand-transition-leave-active {
overflow: hidden;
transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1) 0s,
opacity 0.2s cubic-bezier(0, 0, 0.2, 1) 0s;
}
.thaw-menu-submenu.fade-in-height-expand-transition-enter-active {
overflow: hidden;
transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.2s cubic-bezier(0.4, 0, 1, 1);
}

View file

@ -1,8 +1,8 @@
use super::use_menu; use super::MenuInjection;
use crate::{theme::use_theme, Icon, Theme}; use crate::{theme::use_theme, Icon, Theme};
use leptos::*; use leptos::*;
use thaw_components::OptionComp; use thaw_components::{CSSTransition, OptionComp};
use thaw_utils::{class_list, mount_style, OptionalMaybeSignal, OptionalProp}; use thaw_utils::{class_list, mount_style, OptionalMaybeSignal, OptionalProp, StoredMaybeSignal};
#[component] #[component]
pub fn MenuItem( pub fn MenuItem(
@ -10,15 +10,47 @@ pub fn MenuItem(
#[prop(optional, into)] icon: OptionalMaybeSignal<icondata_core::Icon>, #[prop(optional, into)] icon: OptionalMaybeSignal<icondata_core::Icon>,
#[prop(into)] label: MaybeSignal<String>, #[prop(into)] label: MaybeSignal<String>,
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>, #[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
#[prop(optional)] children: Option<Children>,
) -> impl IntoView { ) -> impl IntoView {
mount_style("menu-item", include_str!("./menu-item.css")); mount_style("menu-item", include_str!("./menu-item.css"));
let theme = use_theme(Theme::light); let theme = use_theme(Theme::light);
let menu = use_menu();
let click_key = key.clone(); let submenu_ref = NodeRef::<html::Div>::new();
let is_children = children.is_some();
let menu = MenuInjection::use_();
let parent_menu_item = StoredValue::new(MenuItemInjection::use_());
let is_open_children = RwSignal::new({
key.with_untracked(|key| {
menu.default_expanded_keys
.with_value(|default_expanded_keys| default_expanded_keys.contains(key))
})
});
let key: StoredMaybeSignal<_> = key.into();
let is_selected = Memo::new(move |_| menu.value.with(|value| key.with(|key| value == key)));
let is_submenu_selected =
Memo::new(move |_| menu.path.with(|path| key.with(|key| path.contains(key))));
let on_click = move |_| { let on_click = move |_| {
let click_key = click_key.get(); if is_children {
if menu.0.with(|key| key != &click_key) { is_open_children.set(!is_open_children.get_untracked());
menu.0.set(click_key); } else {
if !is_selected.get_untracked() {
menu.path.update(|path| {
path.clear();
});
parent_menu_item.with_value(|parent_menu_item| {
if let Some(parent_menu_item) = parent_menu_item {
let mut item_path = vec![];
parent_menu_item.get_path(&mut item_path);
menu.path.update(|path| {
path.extend(item_path);
});
}
});
menu.value.set(key.get_untracked());
}
} }
}; };
@ -41,8 +73,10 @@ pub fn MenuItem(
<div class="thaw-menu-item"> <div class="thaw-menu-item">
<div <div
class=class_list![ class=class_list![
"thaw-menu-item__content", ("thaw-menu-item__content--selected", move || menu.0 "thaw-menu-item__content",
.get() == key.get()), class.map(| c | move || c.get()) ("thaw-menu-item__content--selected", move || is_selected.get()),
("thaw-menu-item__content--submenu-selected", move || is_submenu_selected.get()),
class.map(| c | move || c.get())
] ]
on:click=on_click on:click=on_click
@ -58,7 +92,68 @@ pub fn MenuItem(
} }
} }
{move || label.get()} {move || label.get()}
{
if children.is_some() {
view! {
<Icon
icon=icondata_ai::AiRightOutlined
class=Signal::derive(move || {
let mut class = String::from("thaw-menu-item__arrow");
if is_open_children.get() {
class.push_str(" thaw-menu-item__arrow--open");
}
class
})/>
}.into()
} else {
None
}
}
</div> </div>
<OptionComp value=children let:children>
<Provider value=MenuItemInjection { key, parent_menu_item }>
<CSSTransition
node_ref=submenu_ref
name="fade-in-height-expand-transition"
appear=is_open_children.get_untracked()
show=is_open_children
let:display
>
<div
class="thaw-menu-submenu"
style=move || display.get()
ref=submenu_ref
role="menu"
aria-expanded=move || if is_open_children.get() { "true" } else { "false" }
>
{children()}
</div>
</CSSTransition>
</Provider>
</OptionComp>
</div> </div>
} }
} }
#[derive(Clone)]
struct MenuItemInjection {
pub key: StoredMaybeSignal<String>,
pub parent_menu_item: StoredValue<Option<MenuItemInjection>>,
}
impl MenuItemInjection {
fn use_() -> Option<Self> {
use_context()
}
fn get_path(&self, path: &mut Vec<String>) {
self.parent_menu_item.with_value(|parent_menu_item| {
if let Some(parent_menu_item) = parent_menu_item.as_ref() {
parent_menu_item.get_path(path);
}
});
path.push(self.key.get_untracked());
}
}

View file

@ -7,24 +7,34 @@ pub use menu_item::*;
pub use theme::MenuTheme; pub use theme::MenuTheme;
use leptos::*; use leptos::*;
use std::collections::BTreeSet;
use thaw_utils::{class_list, Model, OptionalProp}; use thaw_utils::{class_list, Model, OptionalProp};
#[component] #[component]
pub fn Menu( pub fn Menu(
#[prop(optional, into)] value: Model<String>, #[prop(optional, into)] value: Model<String>,
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>, #[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
#[prop(optional)] default_expanded_keys: Vec<String>,
children: Children, children: Children,
) -> impl IntoView { ) -> impl IntoView {
let path = RwSignal::new(BTreeSet::<String>::new());
view! { view! {
<Provider value=MenuInjection(value)> <Provider value=MenuInjection { value, path, default_expanded_keys: StoredValue::new(default_expanded_keys) }>
<div class=class_list!["thaw-menu", class.map(| c | move || c.get())]>{children()}</div> <div class=class_list!["thaw-menu", class.map(| c | move || c.get())]>{children()}</div>
</Provider> </Provider>
} }
} }
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct MenuInjection(pub Model<String>); pub(crate) struct MenuInjection {
pub value: Model<String>,
pub(crate) fn use_menu() -> MenuInjection { pub path: RwSignal<BTreeSet<String>>,
expect_context() pub default_expanded_keys: StoredValue<Vec<String>>,
}
impl MenuInjection {
pub fn use_() -> Self {
expect_context()
}
} }