htmx_components/server/
popup_menu.rs

1use 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}