htmx_components/server/
popup_menu.rs1use rscx::{component, html, props, CollectFragment};
2use typed_builder::TypedBuilder;
3
4use rscx_web_macros::*;
5
6use super::attrs::Attrs;
7use super::html_element::HtmlElement;
8use super::opt_attrs::opt_attrs;
9use super::transition::Transition;
10use super::yc_control::Toggle;
11
12pub enum MenuSize {
13 Small,
14 Medium,
15}
16
17#[derive(TypedBuilder)]
18pub struct MenuLink {
19 #[builder(setter(into))]
20 label: String,
21
22 #[builder(setter(into), default="".into())]
23 sr_suffix: String,
24
25 #[builder(default=Attrs::default())]
26 attrs: Attrs,
27}
28
29impl From<(String, String)> for MenuLink {
30 fn from((label, href): (String, String)) -> Self {
31 Self {
32 label,
33 sr_suffix: "".into(),
34 attrs: Attrs::with("href", href),
35 }
36 }
37}
38
39#[props]
40pub struct PopupMenuProps {
41 #[builder(setter(into))]
42 id: String,
43
44 #[builder(setter(into), default)]
45 class: String,
46
47 #[builder(setter(into), default)]
48 button_class: String,
49 button_content: String,
50
51 #[builder(default=MenuSize::Medium)]
52 size: MenuSize,
53
54 children: String,
55}
56
57#[component]
58pub fn PopupMenu(props: PopupMenuProps) -> String {
59 html! {
60 <Toggle class=format!("relative {}", props.class).trim()>
61 <div>
62 <button
63 type="button"
64 id=format!("{}-button", &props.id)
65 class=format!("relative {}", props.button_class).trim()
66 aria-expanded="false"
67 aria-haspopup="true"
68 data-toggle-action="click"
69 >
70 <span class="absolute -inset-1.5"></span>
71 <span class="sr-only">Open menu</span>
72 {props.button_content}
73 </button>
74 </div>
75 <Transition
76 class={
77 let m_width = match props.size {
78 MenuSize::Small => "w-32".to_string(),
79 MenuSize::Medium => "w-48".to_string(),
80 };
81 format!("absolute right-0 z-10 mt-2 {} origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none", m_width)
82 }
83 role="menu"
84 aria_orientation="vertical"
85 aria_labelledby=format!("{}-button", &props.id)
86 tabindex="-1"
87 enter="transition ease-out duration-200"
88 enter_from="transform opacity-0 scale-95"
89 enter_to="transform opacity-100 scale-100"
90 leave="transition ease-in duration-75"
91 leave_from="transform opacity-100 scale-100"
92 leave_to="transform opacity-0 scale-95"
93 >
94 {props.children}
95 </Transition>
96 </Toggle>
97 }
98}
99
100#[html_element]
101pub struct MenuItemProps {
102 #[builder(setter(into))]
103 title: String,
104
105 #[builder(setter(into), default)]
106 sr_suffix: String,
107}
108
109#[component]
110pub fn MenuItem(props: MenuItemProps) -> String {
111 html! {
112 <HtmlElement
113 tag="a"
114 class={
115 if props.class.is_empty() { "cursor-pointer block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50" }
116 else { &props.class }
117 }
118 role="menuitem"
119 tabindex="-1"
120 attrs=spread_attrs!(props | omit(class))
121 >
122 {props.title}
123 <span class="sr-only">{props.sr_suffix}</span>
124 </HtmlElement>
125 }
126}
127
128#[props]
129pub struct MenuProps {
130 #[builder(setter(into))]
131 id: String,
132
133 links: Vec<MenuLink>,
134}
135
136#[component]
137pub fn Menu(props: MenuProps) -> String {
138 #[allow(unused_braces)]
139 props
140 .links
141 .into_iter()
142 .enumerate()
143 .map(
144 |(
145 i,
146 MenuLink {
147 label,
148 sr_suffix,
149 attrs,
150 },
151 )| {
152 html! {
153 <a
154 class="block px-4 py-2 text-sm text-gray-700 cursor-pointer hover:bg-gray-50"
155 role="menuitem"
156 tabindex="-1"
157 id={format!("{}-item-{}", &props.id, i)}
158 {opt_attrs(attrs.to_hashmap())}
159 >
160 {label}
161 <span class="sr-only">", "{sr_suffix}</span>
162 </a>
163 }
164 },
165 )
166 .collect_fragment()
167}