biji_ui/components/menubar/
menu.rs

1use std::time::Duration;
2
3use leptos::{
4    context::Provider,
5    ev::{click, focus, keydown},
6    prelude::*,
7};
8use leptos_use::{on_click_outside, use_event_listener};
9
10use crate::{
11    cn,
12    custom_animated_show::CustomAnimatedShow,
13    items::{Focus, ManageFocus, NavigateItems, Toggle},
14};
15
16use super::context::{MenuContext, RootContext};
17
18#[component]
19pub fn Menu(
20    #[prop(default = false)] disabled: bool,
21    #[prop(into, optional)] class: String,
22    children: Children,
23) -> impl IntoView {
24    let ctx = expect_context::<RootContext>();
25
26    let index = ctx.next_index();
27
28    let menu_ctx = MenuContext {
29        index,
30        disabled,
31        allow_loop: ctx.allow_item_loop,
32        ..Default::default()
33    };
34
35    ctx.upsert_item(index, menu_ctx);
36
37    on_cleanup(move || {
38        ctx.remove_item(index);
39    });
40
41    let menu_ref = menu_ctx.menu_ref;
42
43    view! {
44        <Provider value={menu_ctx}>
45            <div node_ref={menu_ref} class={class} data-index={index}>
46                {children()}
47            </div>
48        </Provider>
49    }
50}
51
52#[component]
53pub fn MenuTrigger(#[prop(into, optional)] class: String, children: Children) -> impl IntoView {
54    let root_ctx = expect_context::<RootContext>();
55    let menu_ctx = expect_context::<MenuContext>();
56
57    let trigger_ref = menu_ctx.trigger_ref;
58
59    view! {
60        <MenuTriggerEvents>
61            <div
62                node_ref={trigger_ref}
63                class={class}
64                data-state={menu_ctx.index}
65                data-disabled={menu_ctx.disabled}
66                data-highlighted={move || root_ctx.item_in_focus(menu_ctx.index)}
67                data-open={move || menu_ctx.open.get()}
68                tabindex=0
69            >
70                {children()}
71            </div>
72        </MenuTriggerEvents>
73    }
74}
75
76#[component]
77pub fn MenuTriggerEvents(children: Children) -> impl IntoView {
78    let root_ctx = expect_context::<RootContext>();
79    let menu_ctx = expect_context::<MenuContext>();
80
81    let eff = RenderEffect::new(move |_| {
82        if menu_ctx.open.get() == false {
83            menu_ctx.set_focus(None);
84        }
85    });
86
87    let _ = use_event_listener(menu_ctx.trigger_ref, click, move |_| {
88        menu_ctx.toggle();
89    });
90
91    let _ = use_event_listener(menu_ctx.trigger_ref, keydown, move |evt| {
92        let key = evt.key();
93
94        if key == "ArrowRight" {
95            if let Some(item) = root_ctx.navigate_next_item() {
96                if menu_ctx.open.get() {
97                    item.open();
98                }
99                item.focus();
100                menu_ctx.close();
101            }
102        } else if key == "ArrowLeft" {
103            if let Some(item) = root_ctx.navigate_previous_item() {
104                if menu_ctx.open.get() {
105                    item.open();
106                }
107                item.focus();
108                menu_ctx.close();
109            }
110        } else if key == "ArrowDown" || key == "Enter" {
111            if !menu_ctx.open.get() {
112                menu_ctx.open();
113            }
114            if let Some(item) = menu_ctx.navigate_first_item() {
115                item.focus();
116            }
117        } else if key == "Escape" {
118            root_ctx.close_all();
119        }
120    });
121
122    let _ = use_event_listener(menu_ctx.trigger_ref, focus, move |_| {
123        root_ctx.set_focus(Some(menu_ctx.index));
124    });
125
126    let _ = on_click_outside(menu_ctx.menu_ref, move |_| {
127        if menu_ctx.open.get() {
128            menu_ctx.close();
129        }
130    });
131
132    on_cleanup(move || {
133        drop(eff);
134    });
135
136    children()
137}
138
139#[component]
140pub fn MenuContent(
141    children: ChildrenFn,
142    /// Optional CSS class to apply to both show and hide classes
143    #[prop(into, optional)]
144    class: String,
145    /// Optional CSS class to apply if `when == true`
146    #[prop(into, optional)]
147    show_class: String,
148    /// Optional CSS class to apply if `when == false`
149    #[prop(into, optional)]
150    hide_class: String,
151    /// The timeout after which the component will be unmounted if `when == false`
152    #[prop(default = Duration::from_millis(200))]
153    hide_delay: Duration,
154) -> impl IntoView {
155    let menu_ctx = expect_context::<MenuContext>();
156
157    view! {
158        <CustomAnimatedShow
159            when={menu_ctx.open}
160            show_class={cn!(class, show_class)}
161            hide_class={cn!(class, hide_class)}
162            hide_delay={hide_delay}
163        >
164            {children()}
165        </CustomAnimatedShow>
166    }
167}