biji_ui/components/menubar/
menu.rs1use 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 #[prop(into, optional)]
144 class: String,
145 #[prop(into, optional)]
147 show_class: String,
148 #[prop(into, optional)]
150 hide_class: String,
151 #[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}