radix_leptos_primitives/components/
context_menu.rs1use crate::utils::{merge_classes, generate_id};
2use leptos::callback::Callback;
3use leptos::children::Children;
4use leptos::prelude::*;
5
6#[component]
8pub fn ContextMenu(
9 #[prop(optional)] class: Option<String>,
10 #[prop(optional)] style: Option<String>,
11 #[prop(optional)] children: Option<Children>,
12 #[prop(optional)] items: Option<Vec<ContextMenuItem>>,
13 #[prop(optional)] on_item_click: Option<Callback<ContextMenuItem>>,
14 #[prop(optional)] onopen: Option<Callback<()>>,
15 #[prop(optional)] on_close: Option<Callback<()>>,
16) -> impl IntoView {
17 let items = items.unwrap_or_default();
18 let isopen = create_rw_signal(false);
19 let selected_index = create_rw_signal(0);
20
21 let class = merge_classes(vec!["context-menu", class.as_deref().unwrap_or("")]);
22
23 let handle_right_click = move |event: web_sys::MouseEvent| {
24 event.prevent_default();
25 isopen.set(true);
26 if let Some(callback) = onopen {
27 callback.run(());
28 }
29 };
30
31 let handle_keydown = move |event: web_sys::KeyboardEvent| {
32 if !isopen.get() {
33 return;
34 }
35
36 match event.key().as_str() {
37 "Escape" => {
38 isopen.set(false);
39 if let Some(callback) = on_close {
40 callback.run(());
41 }
42 }
43 "ArrowDown" => {
44 event.prevent_default();
45 let new_index = (selected_index.get() + 1) % items.len();
46 selected_index.set(new_index);
47 }
48 "ArrowUp" => {
49 event.prevent_default();
50 let new_index = if selected_index.get() == 0 {
51 items.len() - 1
52 } else {
53 selected_index.get() - 1
54 };
55 selected_index.set(new_index);
56 }
57 "Enter" | " " => {
58 event.prevent_default();
59 if let Some(item) = items.get(selected_index.get()) {
60 if let Some(callback) = on_item_click {
61 callback.run(item.clone());
62 }
63 }
64 }
65 _ => {}
66 }
67 };
68
69 view! {
70 <div
71 class=class
72 style=style
73 role="menu"
74 aria-label="Context menu"
75 on:contextmenu=handle_right_click
76 on:keydown=handle_keydown
77 tabindex="0"
78 >
79 {children.map(|c| c())}
80 </div>
81 }
82}
83
84#[derive(Debug, Clone, PartialEq)]
86pub struct ContextMenuItem {
87 pub id: String,
88 pub label: String,
89 pub icon: Option<String>,
90 pub _disabled: bool,
91 pub separator: bool,
92 pub submenu: Option<Vec<ContextMenuItem>>,
93}
94
95impl Default for ContextMenuItem {
96 fn default() -> Self {
97 Self {
98 id: "item".to_string(),
99 label: "Item".to_string(),
100 icon: None,
101 _disabled: false,
102 separator: false,
103 submenu: None,
104 }
105 }
106}
107
108#[component]
110pub fn ContextMenuItem(
111 #[prop(optional)] class: Option<String>,
112 #[prop(optional)] style: Option<String>,
113 #[prop(optional)] children: Option<Children>,
114 #[prop(optional)] item: Option<ContextMenuItem>,
115 #[prop(optional)] selected: Option<bool>,
116 #[prop(optional)] on_click: Option<Callback<ContextMenuItem>>,
117) -> impl IntoView {
118 let _item = item.unwrap_or_default();
119 let item_for_callback = _item.clone();
120 let _selected = selected.unwrap_or(false);
121
122 let _class = merge_classes(vec!["context-menu-item"]);
123
124 view! {
125 <div
126 class=_class
127 style=style
128 role="menuitem"
129 aria-disabled=_item._disabled
130 on:click=move |_| {
131 if let Some(callback) = on_click {
132 callback.run(item_for_callback.clone());
133 }
134 }
135 tabindex="0"
136 >
137 {if _item.separator {
138 view! { <hr /> }.into_any()
139 } else {
140 view! {
141 <span class="label">{_item.label}</span>
142 {children.map(|c| c())}
143 }.into_any()
144 }}
145 </div>
146 }
147}
148
149#[component]
151pub fn ContextMenuTrigger(
152 #[prop(optional)] class: Option<String>,
153 #[prop(optional)] style: Option<String>,
154 #[prop(optional)] children: Option<Children>,
155 #[prop(optional)] disabled: Option<bool>,
156) -> impl IntoView {
157 let _disabled = disabled.unwrap_or(false);
158
159 let _class = merge_classes(vec!["context-menu-trigger"]);
160
161 view! {
162 <div
163 class=_class
164 style=style
165 role="button"
166 aria-disabled=_disabled
167 >
168 {children.map(|c| c())}
169 </div>
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use proptest::prelude::*;
176 use wasm_bindgen_test::*;
177
178 wasm_bindgen_test_configure!(run_in_browser);
179
180 #[test]
182 fn test_context_menu_creation() {}
183 #[test]
184 fn test_context_menu_with_class() {}
185 #[test]
186 fn test_context_menu_with_style() {}
187 #[test]
188 fn test_context_menu_with_items() {}
189 #[test]
190 fn test_context_menu_on_item_click() {}
191 #[test]
192 fn test_context_menu_onopen() {}
193 #[test]
194 fn test_context_menu_on_close() {}
195
196 #[test]
198 fn test_context_menu_item_default() {}
199 #[test]
200 fn test_context_menu_item_creation() {}
201 #[test]
202 fn test_context_menu_item_with_icon() {}
203 #[test]
204 fn test_context_menu_itemdisabled() {}
205 #[test]
206 fn test_context_menu_item_separator() {}
207 #[test]
208 fn test_context_menu_item_submenu() {}
209
210 #[test]
212 fn test_context_menu_trigger_creation() {}
213 #[test]
214 fn test_context_menu_trigger_with_class() {}
215 #[test]
216 fn test_context_menu_triggerdisabled() {}
217
218 #[test]
220 fn test_merge_classes_empty() {}
221 #[test]
222 fn test_merge_classes_single() {}
223 #[test]
224 fn test_merge_classes_multiple() {}
225 #[test]
226 fn test_merge_classes_with_empty() {}
227
228 #[test]
230 fn test_context_menu_property_based() {
231 proptest!(|(____class in ".*", __style in ".*")| {
232
233 });
234 }
235
236 #[test]
237 fn test_context_menu_items_validation() {
238 proptest!(|(______item_count in 0..20usize)| {
239
240 });
241 }
242
243 #[test]
244 fn test_context_menu_keyboard_navigation() {
245 proptest!(|(____key in ".*")| {
246
247 });
248 }
249
250 #[test]
252 fn test_context_menu_user_interaction() {}
253 #[test]
254 fn test_context_menu_accessibility() {}
255 #[test]
256 fn test_context_menu_keyboard_navigation_workflow() {}
257 #[test]
258 fn test_context_menu_right_click_workflow() {}
259 #[test]
260 fn test_context_menu_submenu_interaction() {}
261
262 #[test]
264 fn test_context_menu_large_item_lists() {}
265 #[test]
266 fn test_context_menu_render_performance() {}
267 #[test]
268 fn test_context_menu_memory_usage() {}
269 #[test]
270 fn test_context_menu_animation_performance() {}
271}