Skip to content

Commit 2f96fec

Browse files
authored
feat: MenuItem adds children (#207)
1 parent a9f02ed commit 2f96fec

File tree

4 files changed

+184
-23
lines changed

4 files changed

+184
-23
lines changed

demo_markdown/docs/menu/mod.md

+18-8
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,19 @@
44
let value = create_rw_signal(String::from("o"));
55

66
view! {
7-
<Menu value>
7+
<Menu value default_expanded_keys=vec![String::from("area")]>
88
<MenuItem key="a" label="And"/>
99
<MenuItem key="o" label="Or"/>
10-
<MenuItem icon=icondata::AiAreaChartOutlined key="area" label="Area Chart"/>
11-
<MenuItem icon=icondata::AiPieChartOutlined key="pie" label="Pie Chart"/>
10+
<MenuItem icon=icondata::AiAreaChartOutlined key="area" label="Area Chart">
11+
<MenuItem key="target" label="Target"/>
12+
<MenuItem key="above" label="Above"/>
13+
<MenuItem key="below" label="Below"/>
14+
</MenuItem>
15+
<MenuItem icon=icondata::AiPieChartOutlined key="pie" label="Pie Chart">
16+
<MenuItem key="pie-target" label="Target"/>
17+
<MenuItem key="pie-above" label="Above"/>
18+
<MenuItem key="pie-below" label="Below"/>
19+
</MenuItem>
1220
<MenuItem icon=icondata::AiGithubOutlined key="github" label="Github"/>
1321
<MenuItem icon=icondata::AiChromeOutlined key="chrome" label="Chrome"/>
1422
</Menu>
@@ -17,11 +25,12 @@ view! {
1725

1826
### Menu Props
1927

20-
| Name | Type | Default | Description |
21-
| -------- | ----------------------------------- | -------------------- | --------------------------------------- |
22-
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the menu element. |
23-
| value | `Model<String>` | `Default::default()` | The selected item key of the menu. |
24-
| children | `Children` | | Menu's content. |
28+
| Name | Type | Default | Description |
29+
| --- | --- | --- | --- |
30+
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the menu element. |
31+
| value | `Model<String>` | `Default::default()` | The selected item key of the menu. |
32+
| default_expanded_keys | `Vec<String>` | `Default::default()` | The default expanded submenu keys. |
33+
| children | `Children` | | Menu's content. |
2534

2635
### MenuGroup Props
2736

@@ -39,3 +48,4 @@ view! {
3948
| label | `MaybeSignal<String>` | `Default::default()` | The label of the menu item. |
4049
| key | `MaybeSignal<String>` | `Default::default()` | The indentifier of the menu item. |
4150
| icon | `OptionalMaybeSignal<icondata_core::Icon>` | `None` | The icon of the menu item. |
51+
| children | `Option<Children>` | `None` | MenuItem's content. |

thaw/src/menu/menu-item.css

+47-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
.thaw-menu {
2+
padding-bottom: 0.3rem;
3+
}
4+
15
.thaw-menu-item__content {
26
display: flex;
37
align-items: center;
4-
margin: 0.3rem 0.4rem;
8+
margin: 0.3rem 0.4rem 0;
59
padding: 0.5rem 0.75rem;
610
color: var(--thaw-font-color);
711
cursor: pointer;
@@ -22,3 +26,45 @@
2226
color: var(--thaw-font-color-active);
2327
background-color: var(--thaw-background-color);
2428
}
29+
30+
.thaw-menu-item__content--submenu-selected {
31+
color: var(--thaw-font-color-active);
32+
}
33+
34+
.thaw-menu-item__arrow {
35+
font-size: 18px;
36+
margin-inline-start: auto;
37+
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
38+
transform: rotate(0deg);
39+
}
40+
41+
.thaw-menu-item__arrow--open {
42+
transform: rotate(90deg);
43+
}
44+
45+
.thaw-menu-submenu {
46+
margin-left: 1.6rem;
47+
}
48+
49+
.thaw-menu-submenu.fade-in-height-expand-transition-leave-from,
50+
.thaw-menu-submenu.fade-in-height-expand-transition-enter-to {
51+
opacity: 1;
52+
}
53+
54+
.thaw-menu-submenu.fade-in-height-expand-transition-leave-to,
55+
.thaw-menu-submenu.fade-in-height-expand-transition-enter-from {
56+
opacity: 0;
57+
max-height: 0;
58+
}
59+
60+
.thaw-menu-submenu.fade-in-height-expand-transition-leave-active {
61+
overflow: hidden;
62+
transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1) 0s,
63+
opacity 0.2s cubic-bezier(0, 0, 0.2, 1) 0s;
64+
}
65+
66+
.thaw-menu-submenu.fade-in-height-expand-transition-enter-active {
67+
overflow: hidden;
68+
transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1),
69+
opacity 0.2s cubic-bezier(0.4, 0, 1, 1);
70+
}

thaw/src/menu/menu_item.rs

+105-10
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,56 @@
1-
use super::use_menu;
1+
use super::MenuInjection;
22
use crate::{theme::use_theme, Icon, Theme};
33
use leptos::*;
4-
use thaw_components::OptionComp;
5-
use thaw_utils::{class_list, mount_style, OptionalMaybeSignal, OptionalProp};
4+
use thaw_components::{CSSTransition, OptionComp};
5+
use thaw_utils::{class_list, mount_style, OptionalMaybeSignal, OptionalProp, StoredMaybeSignal};
66

77
#[component]
88
pub fn MenuItem(
99
#[prop(into)] key: MaybeSignal<String>,
1010
#[prop(optional, into)] icon: OptionalMaybeSignal<icondata_core::Icon>,
1111
#[prop(into)] label: MaybeSignal<String>,
1212
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
13+
#[prop(optional)] children: Option<Children>,
1314
) -> impl IntoView {
1415
mount_style("menu-item", include_str!("./menu-item.css"));
1516
let theme = use_theme(Theme::light);
16-
let menu = use_menu();
17-
let click_key = key.clone();
17+
18+
let submenu_ref = NodeRef::<html::Div>::new();
19+
let is_children = children.is_some();
20+
let menu = MenuInjection::use_();
21+
let parent_menu_item = StoredValue::new(MenuItemInjection::use_());
22+
23+
let is_open_children = RwSignal::new({
24+
key.with_untracked(|key| {
25+
menu.default_expanded_keys
26+
.with_value(|default_expanded_keys| default_expanded_keys.contains(key))
27+
})
28+
});
29+
let key: StoredMaybeSignal<_> = key.into();
30+
let is_selected = Memo::new(move |_| menu.value.with(|value| key.with(|key| value == key)));
31+
let is_submenu_selected =
32+
Memo::new(move |_| menu.path.with(|path| key.with(|key| path.contains(key))));
33+
1834
let on_click = move |_| {
19-
let click_key = click_key.get();
20-
if menu.0.with(|key| key != &click_key) {
21-
menu.0.set(click_key);
35+
if is_children {
36+
is_open_children.set(!is_open_children.get_untracked());
37+
} else {
38+
if !is_selected.get_untracked() {
39+
menu.path.update(|path| {
40+
path.clear();
41+
});
42+
parent_menu_item.with_value(|parent_menu_item| {
43+
if let Some(parent_menu_item) = parent_menu_item {
44+
let mut item_path = vec![];
45+
parent_menu_item.get_path(&mut item_path);
46+
menu.path.update(|path| {
47+
path.extend(item_path);
48+
});
49+
}
50+
});
51+
52+
menu.value.set(key.get_untracked());
53+
}
2254
}
2355
};
2456

@@ -41,8 +73,10 @@ pub fn MenuItem(
4173
<div class="thaw-menu-item">
4274
<div
4375
class=class_list![
44-
"thaw-menu-item__content", ("thaw-menu-item__content--selected", move || menu.0
45-
.get() == key.get()), class.map(| c | move || c.get())
76+
"thaw-menu-item__content",
77+
("thaw-menu-item__content--selected", move || is_selected.get()),
78+
("thaw-menu-item__content--submenu-selected", move || is_submenu_selected.get()),
79+
class.map(| c | move || c.get())
4680
]
4781

4882
on:click=on_click
@@ -58,7 +92,68 @@ pub fn MenuItem(
5892
}
5993
}
6094
{move || label.get()}
95+
{
96+
if children.is_some() {
97+
view! {
98+
<Icon
99+
icon=icondata_ai::AiRightOutlined
100+
class=Signal::derive(move || {
101+
let mut class = String::from("thaw-menu-item__arrow");
102+
if is_open_children.get() {
103+
class.push_str(" thaw-menu-item__arrow--open");
104+
}
105+
class
106+
})/>
107+
}.into()
108+
} else {
109+
None
110+
}
111+
}
61112
</div>
113+
114+
<OptionComp value=children let:children>
115+
<Provider value=MenuItemInjection { key, parent_menu_item }>
116+
<CSSTransition
117+
node_ref=submenu_ref
118+
name="fade-in-height-expand-transition"
119+
appear=is_open_children.get_untracked()
120+
show=is_open_children
121+
let:display
122+
>
123+
<div
124+
class="thaw-menu-submenu"
125+
style=move || display.get()
126+
ref=submenu_ref
127+
role="menu"
128+
aria-expanded=move || if is_open_children.get() { "true" } else { "false" }
129+
>
130+
{children()}
131+
</div>
132+
</CSSTransition>
133+
</Provider>
134+
</OptionComp>
62135
</div>
63136
}
64137
}
138+
139+
#[derive(Clone)]
140+
struct MenuItemInjection {
141+
pub key: StoredMaybeSignal<String>,
142+
pub parent_menu_item: StoredValue<Option<MenuItemInjection>>,
143+
}
144+
145+
impl MenuItemInjection {
146+
fn use_() -> Option<Self> {
147+
use_context()
148+
}
149+
150+
fn get_path(&self, path: &mut Vec<String>) {
151+
self.parent_menu_item.with_value(|parent_menu_item| {
152+
if let Some(parent_menu_item) = parent_menu_item.as_ref() {
153+
parent_menu_item.get_path(path);
154+
}
155+
});
156+
157+
path.push(self.key.get_untracked());
158+
}
159+
}

thaw/src/menu/mod.rs

+14-4
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,34 @@ pub use menu_item::*;
77
pub use theme::MenuTheme;
88

99
use leptos::*;
10+
use std::collections::BTreeSet;
1011
use thaw_utils::{class_list, Model, OptionalProp};
1112

1213
#[component]
1314
pub fn Menu(
1415
#[prop(optional, into)] value: Model<String>,
1516
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
17+
#[prop(optional)] default_expanded_keys: Vec<String>,
1618
children: Children,
1719
) -> impl IntoView {
20+
let path = RwSignal::new(BTreeSet::<String>::new());
21+
1822
view! {
19-
<Provider value=MenuInjection(value)>
23+
<Provider value=MenuInjection { value, path, default_expanded_keys: StoredValue::new(default_expanded_keys) }>
2024
<div class=class_list!["thaw-menu", class.map(| c | move || c.get())]>{children()}</div>
2125
</Provider>
2226
}
2327
}
2428

2529
#[derive(Clone)]
26-
pub(crate) struct MenuInjection(pub Model<String>);
30+
pub(crate) struct MenuInjection {
31+
pub value: Model<String>,
32+
pub path: RwSignal<BTreeSet<String>>,
33+
pub default_expanded_keys: StoredValue<Vec<String>>,
34+
}
2735

28-
pub(crate) fn use_menu() -> MenuInjection {
29-
expect_context()
36+
impl MenuInjection {
37+
pub fn use_() -> Self {
38+
expect_context()
39+
}
3040
}

0 commit comments

Comments
 (0)