htmx-components 0.3.0

HTMX enabled components for Axum
Documentation
use rscx::{component, html, props, CollectFragment};
use typed_builder::TypedBuilder;

use rscx_web_macros::*;

use super::attrs::Attrs;
use super::html_element::HtmlElement;
use super::opt_attrs::opt_attrs;
use super::transition::Transition;
use super::yc_control::Toggle;

pub enum MenuSize {
    Small,
    Medium,
}

#[derive(TypedBuilder)]
pub struct MenuLink {
    #[builder(setter(into))]
    label: String,

    #[builder(setter(into), default="".into())]
    sr_suffix: String,

    #[builder(default=Attrs::default())]
    attrs: Attrs,
}

impl From<(String, String)> for MenuLink {
    fn from((label, href): (String, String)) -> Self {
        Self {
            label,
            sr_suffix: "".into(),
            attrs: Attrs::with("href", href),
        }
    }
}

#[props]
pub struct PopupMenuProps {
    #[builder(setter(into))]
    id: String,

    #[builder(setter(into), default)]
    class: String,

    #[builder(setter(into), default)]
    button_class: String,
    button_content: String,

    #[builder(default=MenuSize::Medium)]
    size: MenuSize,

    children: String,
}

#[component]
pub fn PopupMenu(props: PopupMenuProps) -> String {
    html! {
        <Toggle class=format!("relative {}", props.class).trim()>
            <div>
                <button
                    type="button"
                    id=format!("{}-button", &props.id)
                    class=format!("relative {}", props.button_class).trim()
                    aria-expanded="false"
                    aria-haspopup="true"
                    data-toggle-action="click"
                >
                    <span class="absolute -inset-1.5"></span>
                    <span class="sr-only">Open menu</span>
                    {props.button_content}
                </button>
            </div>
            <Transition
                class={
                    let m_width = match props.size {
                        MenuSize::Small => "w-32".to_string(),
                        MenuSize::Medium => "w-48".to_string(),
                    };
                    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)
                }
                role="menu"
                aria_orientation="vertical"
                aria_labelledby=format!("{}-button", &props.id)
                tabindex="-1"
                enter="transition ease-out duration-200"
                enter_from="transform opacity-0 scale-95"
                enter_to="transform opacity-100 scale-100"
                leave="transition ease-in duration-75"
                leave_from="transform opacity-100 scale-100"
                leave_to="transform opacity-0 scale-95"
            >
                {props.children}
            </Transition>
        </Toggle>
    }
}

#[html_element]
pub struct MenuItemProps {
    #[builder(setter(into))]
    title: String,

    #[builder(setter(into), default)]
    sr_suffix: String,
}

#[component]
pub fn MenuItem(props: MenuItemProps) -> String {
    html! {
        <HtmlElement
            tag="a"
            class={
                if props.class.is_empty() { "cursor-pointer block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50" }
                else { &props.class }
            }
            role="menuitem"
            tabindex="-1"
            attrs=spread_attrs!(props | omit(class))
        >
            {props.title}
            <span class="sr-only">{props.sr_suffix}</span>
        </HtmlElement>
    }
}

#[props]
pub struct MenuProps {
    #[builder(setter(into))]
    id: String,

    links: Vec<MenuLink>,
}

#[component]
pub fn Menu(props: MenuProps) -> String {
    #[allow(unused_braces)]
    props
        .links
        .into_iter()
        .enumerate()
        .map(
            |(
                i,
                MenuLink {
                    label,
                    sr_suffix,
                    attrs,
                },
            )| {
                html! {
                    <a
                        class="block px-4 py-2 text-sm text-gray-700 cursor-pointer hover:bg-gray-50"
                        role="menuitem"
                        tabindex="-1"
                        id={format!("{}-item-{}", &props.id, i)}
                        {opt_attrs(attrs.to_hashmap())}
                    >
                        {label}
                        <span class="sr-only">", "{sr_suffix}</span>
                    </a>
                }
            },
        )
        .collect_fragment()
}