use icons::ChevronRight; use leptos::context::Provider; use leptos::prelude::*; use leptos_ui::clx; use tw_merge::*; use wasm_bindgen::JsCast; use crate::components::hooks::use_random::use_random_id_for; /// Programmatically close any open context menu. pub fn close_context_menu() { let Some(document) = window().document() else { return; }; let Some(menu) = document.query_selector("[data-target='target__context'][data-state='open']").ok().flatten() else { return; }; let _ = menu.set_attribute("data-state", "closed"); if let Some(el) = menu.dyn_ref::() { let _ = el.style().set_property("pointer-events", "none"); } } mod components { use super::*; clx! {ContextMenuLabel, span, "px-2 py-1.5 text-sm font-medium data-inset:pl-8", "mb-1"} clx! {ContextMenuGroup, ul, "group"} clx! {ContextMenuItem, li, "inline-flex gap-2 items-center w-full rounded-sm px-2 py-1.5 text-sm no-underline transition-colors duration-200 text-popover-foreground hover:bg-accent hover:text-accent-foreground [&_svg:not([class*='size-'])]:size-4"} clx! {ContextMenuSubContent, ul, "context__menu_sub_content", "rounded-md border bg-card shadow-lg p-1 absolute z-[100] min-w-[160px] opacity-0 invisible translate-x-[-8px] transition-all duration-200 ease-out pointer-events-none"} clx! {ContextMenuLink, a, "w-full inline-flex gap-2 items-center"} } pub use components::*; #[component] pub fn ContextMenuAction( children: Children, #[prop(optional, into)] class: String, #[prop(optional, into)] aria_selected: Option>, #[prop(optional, into)] href: Option, ) -> impl IntoView { let _ctx = expect_context::(); let class = tw_merge!( "inline-flex gap-2 items-center w-full text-sm text-left transition-colors duration-200 focus:outline-none focus-visible:outline-none text-popover-foreground [&_svg:not([class*='size-'])]:size-4", class ); let aria_selected_attr = move || aria_selected.map(|s| s.get()).unwrap_or(false).to_string(); if let Some(href) = href { view! { {children()} } .into_any() } else { view! { } .into_any() } } #[derive(Clone)] struct ContextMenuContext { target_id: String, } #[component] pub fn ContextMenu(children: Children) -> impl IntoView { let context_target_id = use_random_id_for("context"); let ctx = ContextMenuContext { target_id: context_target_id.clone() }; view! {
{children()}
} } /// Wrapper that triggers the context menu on right-click. /// The `on_open` callback is triggered when the context menu opens (right-click). #[component] pub fn ContextMenuTrigger( children: Children, #[prop(optional, into)] class: String, #[prop(optional)] on_open: Option>, ) -> impl IntoView { let ctx = expect_context::(); let trigger_class = tw_merge!("contents", class); view! {
{children()}
} } /// Content of the context menu that appears on right-click. /// The `on_close` callback is triggered when the menu closes (click outside, ESC key, or action click). #[component] pub fn ContextMenuContent( children: Children, #[prop(optional, into)] class: String, #[prop(optional)] on_close: Option>, ) -> impl IntoView { let ctx = expect_context::(); let base_classes = "z-50 p-1 rounded-md border bg-card shadow-md w-[200px] fixed transition-all duration-200 data-[state=closed]:opacity-0 data-[state=closed]:scale-95 data-[state=open]:opacity-100 data-[state=open]:scale-100"; let class = tw_merge!(base_classes, class); let target_id_for_script = ctx.target_id.clone(); view! {
{children()}
} } #[component] pub fn ContextMenuSub(children: Children) -> impl IntoView { clx! {ContextMenuSubRoot, li, "context__menu_sub_trigger", " relative inline-flex relative gap-2 items-center py-1.5 px-2 w-full text-sm no-underline rounded-sm transition-colors duration-200 cursor-pointer text-popover-foreground [&_svg:not([class*='size-'])]:size-4 hover:bg-accent hover:text-accent-foreground"} view! { {children()} } } #[component] pub fn ContextMenuSubTrigger(children: Children, #[prop(optional, into)] class: String) -> impl IntoView { let class = tw_merge!("flex items-center justify-between w-full", class); view! { {children()} } } #[component] pub fn ContextMenuSubItem(children: Children, #[prop(optional, into)] class: String) -> impl IntoView { let class = tw_merge!( "inline-flex gap-2 items-center w-full rounded-sm px-3 py-2 text-sm transition-all duration-150 ease text-popover-foreground hover:bg-accent hover:text-accent-foreground cursor-pointer hover:translate-x-[2px]", class ); view! {
  • {children()}
  • } }