use crate::prelude::*; /// Componente para crear un **menú desplegable**. /// /// Renderiza un botón (único o desdoblado, ver [`with_button_split()`](Self::with_button_split)) /// que despliega u oculta una lista de elementos [`dropdown::Item`](super::Item). /// /// Sin título (ver [`with_title()`](Self::with_title)), se muestra únicamente la lista de /// elementos, sin ningún botón para interactuar, sólo un menú contextual estático. /// /// Si no contiene elementos, el componente **no se renderiza**. /// /// # Ejemplo /// /// ```rust,no_run /// use pagetop::prelude::*; /// /// let dd = dropdown::Dropdown::new() /// .with_title(Lc::n("Menu")) /// .with_item(dropdown::Item::link(Lc::n("Home"), "/")) /// .with_item(dropdown::Item::link_blank(Lc::n("Doc"), "https://docs.rs")) /// .with_item(dropdown::Item::divider()) /// .with_item(dropdown::Item::header(Lc::n("User session"))) /// .with_item(dropdown::Item::button(Lc::n("Sign out"))); /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Dropdown { /// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente. props: Props, /// Devuelve el título del menú desplegable. title: Lc, /// Devuelve si el botón se desdobla (*split*) en botón de acción más *toggle*. button_split: bool, /// Devuelve el tamaño visual del botón. #[getters(copy)] button_size: button::Size, /// Devuelve el estilo visual del botón. #[getters(copy)] button_style: button::Style, /// Devuelve si el menú se alinea al final del botón (por defecto, al inicio). menu_end: bool, /// Devuelve la lista de elementos del menú. items: Children, } #[async_trait] impl Component for Dropdown { fn new() -> Self { Self::default() } fn id(&self) -> Option { self.props.get_id() } fn setup(&mut self, _cx: &mut Context) { self.alter_prop(PropsOp::prepend_classes("dropdown")); } async fn prepare(&self, cx: &mut Context) -> Result { // Si no hay elementos en el menú, no se prepara. let items = self.items().render(cx).await; if items.is_empty() { return Ok(html! {}); } let title = self.title().using(cx); // Sin título: menú contextual estático, sin botón ni comportamiento de apertura/cierre. if title.is_empty() { return Ok(html! { ul class="dropdown-menu" { (items) } }); } // Sin tamaño ni estilo (caso más común), evita construir dinámicamente las clases. let (button_classes, toggle_classes): (CowStr, CowStr) = if matches!(self.button_size(), button::Size::None) && matches!(self.button_style(), button::Style::None) { ( CowStr::from("dropdown-button"), CowStr::from("dropdown-toggle"), ) } else { use button::{Size, Style}; let size_class = match self.button_size() { Size::None => "", Size::Small => " button-sm", Size::Large => " button-lg", }; let style_class = match self.button_style() { Style::None => String::new(), Style::Solid(intent) => util::join!(" button-", intent.color(cx)), Style::Outline(intent) => util::join!(" button-outline-", intent.color(cx)), Style::Link => " button-link".to_string(), }; ( util::join!("dropdown-button", size_class, &style_class).into(), util::join!("dropdown-toggle", size_class, &style_class).into(), ) }; let toggle_label = Lc::l("dropdown_toggle").using(cx); Ok(html! { div (self.props().unpack(cx)) { @if *self.button_split() { button type="button" class=(&button_classes) { (&title) } button type="button" class=(&toggle_classes) aria-haspopup="true" aria-expanded="false" { span class="visually-hidden" { (toggle_label) } } } @else { button type="button" class=(&toggle_classes) aria-haspopup="true" aria-expanded="false" { (&title) } } ul class=(self.menu_classes()) { (items) } } }) } } impl Dropdown { // Clases del `