mirror of
https://github.com/adoyle0/thaw.git
synced 2025-01-23 06:19:22 -05:00
feat: MenuItem adds children (#207)
This commit is contained in:
parent
a9f02ede65
commit
2f96fec20d
4 changed files with 185 additions and 24 deletions
|
@ -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>
|
||||||
|
@ -18,9 +26,10 @@ 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. |
|
||||||
|
| default_expanded_keys | `Vec<String>` | `Default::default()` | The default expanded submenu keys. |
|
||||||
| children | `Children` | | Menu's content. |
|
| 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. |
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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 path: RwSignal<BTreeSet<String>>,
|
||||||
|
pub default_expanded_keys: StoredValue<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn use_menu() -> MenuInjection {
|
impl MenuInjection {
|
||||||
|
pub fn use_() -> Self {
|
||||||
expect_context()
|
expect_context()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue