mirror of
https://github.com/adoyle0/thaw.git
synced 2025-01-22 22:09: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"));
|
||||
|
||||
view! {
|
||||
<Menu value>
|
||||
<Menu value default_expanded_keys=vec![String::from("area")]>
|
||||
<MenuItem key="a" label="And"/>
|
||||
<MenuItem key="o" label="Or"/>
|
||||
<MenuItem icon=icondata::AiAreaChartOutlined key="area" label="Area Chart"/>
|
||||
<MenuItem icon=icondata::AiPieChartOutlined key="pie" label="Pie Chart"/>
|
||||
<MenuItem icon=icondata::AiAreaChartOutlined key="area" label="Area 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::AiChromeOutlined key="chrome" label="Chrome"/>
|
||||
</Menu>
|
||||
|
@ -17,11 +25,12 @@ view! {
|
|||
|
||||
### Menu Props
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| -------- | ----------------------------------- | -------------------- | --------------------------------------- |
|
||||
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the menu element. |
|
||||
| value | `Model<String>` | `Default::default()` | The selected item key of the menu. |
|
||||
| children | `Children` | | Menu's content. |
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the menu element. |
|
||||
| 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. |
|
||||
|
||||
### MenuGroup Props
|
||||
|
||||
|
@ -39,3 +48,4 @@ view! {
|
|||
| label | `MaybeSignal<String>` | `Default::default()` | The label 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. |
|
||||
| children | `Option<Children>` | `None` | MenuItem's content. |
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
.thaw-menu {
|
||||
padding-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.thaw-menu-item__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0.3rem 0.4rem;
|
||||
margin: 0.3rem 0.4rem 0;
|
||||
padding: 0.5rem 0.75rem;
|
||||
color: var(--thaw-font-color);
|
||||
cursor: pointer;
|
||||
|
@ -22,3 +26,45 @@
|
|||
color: var(--thaw-font-color-active);
|
||||
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 leptos::*;
|
||||
use thaw_components::OptionComp;
|
||||
use thaw_utils::{class_list, mount_style, OptionalMaybeSignal, OptionalProp};
|
||||
use thaw_components::{CSSTransition, OptionComp};
|
||||
use thaw_utils::{class_list, mount_style, OptionalMaybeSignal, OptionalProp, StoredMaybeSignal};
|
||||
|
||||
#[component]
|
||||
pub fn MenuItem(
|
||||
|
@ -10,15 +10,47 @@ pub fn MenuItem(
|
|||
#[prop(optional, into)] icon: OptionalMaybeSignal<icondata_core::Icon>,
|
||||
#[prop(into)] label: MaybeSignal<String>,
|
||||
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
|
||||
#[prop(optional)] children: Option<Children>,
|
||||
) -> impl IntoView {
|
||||
mount_style("menu-item", include_str!("./menu-item.css"));
|
||||
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 click_key = click_key.get();
|
||||
if menu.0.with(|key| key != &click_key) {
|
||||
menu.0.set(click_key);
|
||||
if is_children {
|
||||
is_open_children.set(!is_open_children.get_untracked());
|
||||
} 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=class_list![
|
||||
"thaw-menu-item__content", ("thaw-menu-item__content--selected", move || menu.0
|
||||
.get() == key.get()), class.map(| c | move || c.get())
|
||||
"thaw-menu-item__content",
|
||||
("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
|
||||
|
@ -58,7 +92,68 @@ pub fn MenuItem(
|
|||
}
|
||||
}
|
||||
{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>
|
||||
|
||||
<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>
|
||||
}
|
||||
}
|
||||
|
||||
#[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;
|
||||
|
||||
use leptos::*;
|
||||
use std::collections::BTreeSet;
|
||||
use thaw_utils::{class_list, Model, OptionalProp};
|
||||
|
||||
#[component]
|
||||
pub fn Menu(
|
||||
#[prop(optional, into)] value: Model<String>,
|
||||
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
|
||||
#[prop(optional)] default_expanded_keys: Vec<String>,
|
||||
children: Children,
|
||||
) -> impl IntoView {
|
||||
let path = RwSignal::new(BTreeSet::<String>::new());
|
||||
|
||||
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>
|
||||
</Provider>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct MenuInjection(pub Model<String>);
|
||||
|
||||
pub(crate) fn use_menu() -> MenuInjection {
|
||||
expect_context()
|
||||
pub(crate) struct MenuInjection {
|
||||
pub value: Model<String>,
|
||||
pub path: RwSignal<BTreeSet<String>>,
|
||||
pub default_expanded_keys: StoredValue<Vec<String>>,
|
||||
}
|
||||
|
||||
impl MenuInjection {
|
||||
pub fn use_() -> Self {
|
||||
expect_context()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue