Skip to main content

leptos_arrow_grid/
context_menu.rs

1//! Context menu component for the data grid.
2
3use leptos::prelude::*;
4
5/// Actions available from the context menu.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum ContextAction {
8    /// Copy selected rows as TSV.
9    Copy,
10    /// Select all rows.
11    SelectAll,
12    /// Download visible/selected rows as a CSV file.
13    Download,
14}
15
16/// Placement coordinates for the context menu.
17#[derive(Clone, Copy, Debug)]
18pub struct MenuPosition {
19    /// Page X coordinate.
20    pub x: f64,
21    /// Page Y coordinate.
22    pub y: f64,
23}
24
25/// Context menu component rendered inside the grid container.
26///
27/// Positioned at `(x, y)` in page coordinates. Backdrop covers
28/// the full viewport to catch dismiss clicks.
29#[component]
30pub fn GridContextMenu(
31    /// Position of the menu (None = hidden).
32    position: Signal<Option<MenuPosition>>,
33    /// Callback when an action is selected.
34    on_action: Callback<ContextAction>,
35    /// Callback to close the menu.
36    on_close: Callback<()>,
37    /// Number of currently selected rows.
38    selected_count: Signal<usize>,
39) -> impl IntoView {
40    let copy_label = Signal::derive(move || {
41        let count = selected_count.get();
42        if count > 0 {
43            format!("Copy ({count} rows)")
44        } else {
45            "Copy".to_string()
46        }
47    });
48
49    view! {
50        <Show when=move || position.get().is_some()>
51            {move || {
52                let Some(pos) = position.get() else {
53                    return view! { <div /> }.into_any();
54                };
55                view! {
56                    <div
57                        class="dg-context-backdrop"
58                        on:pointerdown=move |_| on_close.run(())
59                        on:contextmenu=move |ev| {
60                            ev.prevent_default();
61                            on_close.run(());
62                        }
63                    />
64                    <div
65                        class="dg-context-menu"
66                        style:left=format!("{}px", pos.x)
67                        style:top=format!("{}px", pos.y)
68                    >
69                        <button
70                            class="dg-context-item"
71                            on:click=move |_| {
72                                on_action.run(ContextAction::Copy);
73                                on_close.run(());
74                            }
75                        >
76                            {copy_label}
77                            <span class="dg-context-shortcut">{"Ctrl+C"}</span>
78                        </button>
79                        <button
80                            class="dg-context-item"
81                            on:click=move |_| {
82                                on_action.run(ContextAction::SelectAll);
83                                on_close.run(());
84                            }
85                        >
86                            "Select All"
87                            <span class="dg-context-shortcut">{"Ctrl+A"}</span>
88                        </button>
89                        <button
90                            class="dg-context-item"
91                            on:click=move |_| {
92                                on_action.run(ContextAction::Download);
93                                on_close.run(());
94                            }
95                        >
96                            "Download CSV"
97                            <span class="dg-context-shortcut">{"Ctrl+S"}</span>
98                        </button>
99                    </div>
100                }.into_any()
101            }}
102        </Show>
103    }
104}