✨ (base): Añade with_menu_end() para menú al final
This commit is contained in:
parent
f265f217bd
commit
165cb49880
7 changed files with 152 additions and 5 deletions
|
|
@ -425,6 +425,11 @@ body {
|
|||
.dropdown-menu.show {
|
||||
display: block;
|
||||
}
|
||||
/* Anchors the menu's right edge to the button's right edge, so it opens inwards. */
|
||||
.dropdown-menu-end {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
/* Options and headers stay on one line, so the menu grows to fit the longest one. */
|
||||
.dropdown-item {
|
||||
|
|
|
|||
|
|
@ -56,6 +56,11 @@ pub trait DropdownBootsier {
|
|||
fn with_direction(self, direction: Direction) -> Self;
|
||||
|
||||
/// Configura la alineación horizontal (con posible comportamiento *responsive* adicional).
|
||||
///
|
||||
/// Si se deja en [`MenuAlign::Start`] (por defecto), se respeta [`with_menu_end()`]; cualquier
|
||||
/// otra alineación prevalece sobre él.
|
||||
///
|
||||
/// [`with_menu_end()`]: pagetop::base::component::Dropdown::with_menu_end
|
||||
fn with_menu_align(self, align: MenuAlign) -> Self;
|
||||
|
||||
/// Configura la posición del menú.
|
||||
|
|
@ -135,9 +140,15 @@ pub(crate) async fn render(
|
|||
let direction = dropdown
|
||||
.props()
|
||||
.extra_or(EXTRA_DIRECTION, Direction::default());
|
||||
let menu_align = dropdown
|
||||
let menu_align = match dropdown
|
||||
.props()
|
||||
.extra_or(EXTRA_MENU_ALIGN, MenuAlign::default());
|
||||
.extra_or(EXTRA_MENU_ALIGN, MenuAlign::default())
|
||||
{
|
||||
// `with_menu_end()` del núcleo se traduce a `MenuAlign::End`, pero sólo si no hay una
|
||||
// alineación propia diferente a la predeterminada (`Start`), que prevalece.
|
||||
MenuAlign::Start if *dropdown.menu_end() => MenuAlign::End,
|
||||
align => align,
|
||||
};
|
||||
let menu_position = dropdown
|
||||
.props()
|
||||
.extra_or(EXTRA_MENU_POSITION, MenuPosition::default());
|
||||
|
|
|
|||
|
|
@ -169,6 +169,11 @@ pub(crate) async fn item_render(item: &Item, cx: &mut Context) -> Result<Markup,
|
|||
.lookup(cx)
|
||||
.unwrap_or_else(|| "Dropdown".to_string())
|
||||
});
|
||||
let menu_classes = if *dd.menu_end() {
|
||||
"dropdown-menu dropdown-menu-end"
|
||||
} else {
|
||||
"dropdown-menu"
|
||||
};
|
||||
html! {
|
||||
li (item.props().unpack(cx)) {
|
||||
a
|
||||
|
|
@ -181,7 +186,7 @@ pub(crate) async fn item_render(item: &Item, cx: &mut Context) -> Result<Markup,
|
|||
{
|
||||
(title)
|
||||
}
|
||||
ul class="dropdown-menu" {
|
||||
ul class=(menu_classes) {
|
||||
(items)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,59 @@ async fn dropdown_menu_align_combines_breakpoint_classes() {
|
|||
);
|
||||
}
|
||||
|
||||
async fn dropdown_menu_html(dropdown: bs::Dropdown) -> String {
|
||||
let mut dropdown = dropdown
|
||||
.with_title(Lc::n("Menu"))
|
||||
.with_item(bs::dropdown::Item::link(Lc::n("Home"), "/"));
|
||||
dropdown.render(&mut cx()).await.into_string()
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn dropdown_menu_end_maps_to_the_end_alignment() {
|
||||
let html = dropdown_menu_html(bs::Dropdown::new().with_menu_end(true)).await;
|
||||
assert!(html.contains(r#"<ul class="dropdown-menu dropdown-menu-end">"#));
|
||||
|
||||
// Without `with_menu_end()` the menu keeps the default (start) alignment.
|
||||
let html = dropdown_menu_html(bs::Dropdown::new()).await;
|
||||
assert!(html.contains(r#"<ul class="dropdown-menu">"#));
|
||||
assert!(!html.contains("dropdown-menu-end"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn dropdown_menu_end_is_ignored_without_title() {
|
||||
// A titleless menu is static (no button to align to), so it never gets the end alignment.
|
||||
let mut dropdown = bs::Dropdown::new()
|
||||
.with_menu_end(true)
|
||||
.with_item(bs::dropdown::Item::link(Lc::n("Home"), "/"));
|
||||
let html = dropdown.render(&mut cx()).await.into_string();
|
||||
assert!(html.contains(r#"<ul class="dropdown-menu">"#));
|
||||
assert!(!html.contains("dropdown-menu-end"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn dropdown_own_menu_align_wins_over_menu_end() {
|
||||
let html = dropdown_menu_html(
|
||||
bs::Dropdown::new()
|
||||
.with_menu_end(true)
|
||||
.with_menu_align(bs::dropdown::MenuAlign::StartAt(Breakpoint::Md)),
|
||||
)
|
||||
.await;
|
||||
assert!(html.contains("dropdown-menu-md-start"));
|
||||
assert!(!html.contains("dropdown-menu-end"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn nav_dropdown_menu_end_adds_the_end_class() {
|
||||
let mut nav = bs::Nav::new().with_item(bs::nav::Item::dropdown(
|
||||
bs::Dropdown::new()
|
||||
.with_title(Lc::n("Menu"))
|
||||
.with_menu_end(true)
|
||||
.with_item(bs::dropdown::Item::link(Lc::n("Home"), "/")),
|
||||
));
|
||||
let html = nav.render(&mut cx()).await.into_string();
|
||||
assert!(html.contains(r#"<ul class="dropdown-menu dropdown-menu-end">"#));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn navbar_expands_at_md_by_default_and_at_the_chosen_breakpoint() {
|
||||
let item =
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ pub struct Dropdown {
|
|||
/// 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,
|
||||
}
|
||||
|
|
@ -124,12 +126,23 @@ impl Component for Dropdown {
|
|||
(&title)
|
||||
}
|
||||
}
|
||||
ul class="dropdown-menu" { (items) }
|
||||
ul class=(self.menu_classes()) { (items) }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Dropdown {
|
||||
// Clases del `<ul>` del menú, compartidas con el renderizado de `nav::Item::dropdown()`.
|
||||
pub(crate) fn menu_classes(&self) -> &'static str {
|
||||
if *self.menu_end() {
|
||||
"dropdown-menu dropdown-menu-end"
|
||||
} else {
|
||||
"dropdown-menu"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[builder_impl]
|
||||
impl Dropdown {
|
||||
// **< Dropdown BUILDER >***********************************************************************
|
||||
|
|
@ -170,6 +183,16 @@ impl Dropdown {
|
|||
self
|
||||
}
|
||||
|
||||
/// Alinea el menú al final del botón (por defecto, al inicio).
|
||||
///
|
||||
/// Útil cuando el desplegable está pegado al borde final de la página, p. ej. el último de una
|
||||
/// [`Navbar`], para que el menú se abra hacia el interior y no se salga de la pantalla. No
|
||||
/// tiene efecto en un menú sin título, que es estático y no tiene botón al que alinearse.
|
||||
pub fn with_menu_end(mut self, end: bool) -> Self {
|
||||
self.menu_end = end;
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade un nuevo elemento al menú o modifica la lista de elementos del menú con una operación
|
||||
/// [`TypedOp`].
|
||||
///
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ impl Component for Item {
|
|||
{
|
||||
(title)
|
||||
}
|
||||
ul class="dropdown-menu" {
|
||||
ul class=(dd.menu_classes()) {
|
||||
(items)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
50
tests/component_dropdown.rs
Normal file
50
tests/component_dropdown.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
fn dropdown() -> Dropdown {
|
||||
Dropdown::new()
|
||||
.with_title(Lc::n("Menu"))
|
||||
.with_item(dropdown::Item::link(Lc::n("Home"), "/"))
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn menu_is_aligned_to_the_start_by_default() {
|
||||
let html = dropdown()
|
||||
.render(&mut Context::default())
|
||||
.await
|
||||
.into_string();
|
||||
|
||||
assert!(html.contains(r#"<ul class="dropdown-menu">"#));
|
||||
assert!(!html.contains("dropdown-menu-end"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn menu_end_adds_the_end_class() {
|
||||
let html = dropdown()
|
||||
.with_menu_end(true)
|
||||
.render(&mut Context::default())
|
||||
.await
|
||||
.into_string();
|
||||
|
||||
assert!(html.contains(r#"<ul class="dropdown-menu dropdown-menu-end">"#));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn menu_end_applies_to_a_dropdown_inside_a_nav() {
|
||||
let mut nav = Nav::new().with_item(nav::Item::dropdown(dropdown().with_menu_end(true)));
|
||||
let html = nav.render(&mut Context::default()).await.into_string();
|
||||
|
||||
assert!(html.contains(r#"<ul class="dropdown-menu dropdown-menu-end">"#));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn menu_end_is_ignored_without_title() {
|
||||
let html = Dropdown::new()
|
||||
.with_menu_end(true)
|
||||
.with_item(dropdown::Item::link(Lc::n("Home"), "/"))
|
||||
.render(&mut Context::default())
|
||||
.await
|
||||
.into_string();
|
||||
|
||||
assert!(html.contains(r#"<ul class="dropdown-menu">"#));
|
||||
assert!(!html.contains("dropdown-menu-end"));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue