♻️ (bootsier): Cambia Offcanvas por Panel en Navbar
This commit is contained in:
parent
c47724bb68
commit
6df71885cc
8 changed files with 388 additions and 168 deletions
|
|
@ -568,7 +568,7 @@ fn expand_title(name: &'static str, width: &'static str) -> Lc {
|
|||
|
||||
fn offcanvas_block() -> Block {
|
||||
let panel = |placement, backdrop| {
|
||||
bs::Offcanvas::new()
|
||||
bs::navbar::Panel::new()
|
||||
.with_title(Lc::t("menus_offcanvas_title", &LOC))
|
||||
.with_placement(placement)
|
||||
.with_backdrop(backdrop)
|
||||
|
|
@ -581,7 +581,10 @@ fn offcanvas_block() -> Block {
|
|||
bs::offcanvas::Placement::Start,
|
||||
bs::offcanvas::Backdrop::Enabled,
|
||||
)),
|
||||
"Navbar::offcanvas(Offcanvas::new().with_placement(offcanvas::Placement::Start))",
|
||||
concat!(
|
||||
"Navbar::offcanvas(bs::navbar::Panel::new()",
|
||||
".with_placement(bs::offcanvas::Placement::Start))",
|
||||
),
|
||||
),
|
||||
(
|
||||
"menus_title_offcanvas_brand_left",
|
||||
|
|
@ -592,7 +595,10 @@ fn offcanvas_block() -> Block {
|
|||
bs::offcanvas::Backdrop::Enabled,
|
||||
),
|
||||
),
|
||||
"Navbar::offcanvas_brand_left(brand, Offcanvas::new().with_placement(Placement::End))",
|
||||
concat!(
|
||||
"Navbar::offcanvas_brand_left(brand, bs::navbar::Panel::new()",
|
||||
".with_placement(bs::offcanvas::Placement::End))",
|
||||
),
|
||||
),
|
||||
(
|
||||
"menus_title_offcanvas_brand_right",
|
||||
|
|
@ -604,8 +610,8 @@ fn offcanvas_block() -> Block {
|
|||
),
|
||||
),
|
||||
concat!(
|
||||
"Navbar::offcanvas_brand_right(brand, ",
|
||||
"Offcanvas::new().with_backdrop(Backdrop::Disabled))",
|
||||
"Navbar::offcanvas_brand_right(brand, bs::navbar::Panel::new()",
|
||||
".with_backdrop(bs::offcanvas::Backdrop::Disabled))",
|
||||
),
|
||||
),
|
||||
(
|
||||
|
|
@ -615,15 +621,14 @@ fn offcanvas_block() -> Block {
|
|||
panel(
|
||||
bs::offcanvas::Placement::End,
|
||||
bs::offcanvas::Backdrop::Static,
|
||||
)
|
||||
.with_prop(PropsOp::set("data-bs-theme", "dark")),
|
||||
),
|
||||
)
|
||||
.with_prop(PropsOp::add_classes(class::Bg::with(BootsierColors::Dark)))
|
||||
.with_prop(PropsOp::set("data-bs-theme", "dark")),
|
||||
concat!(
|
||||
"Navbar::offcanvas_brand_left(brand, Offcanvas::new()",
|
||||
".with_backdrop(Backdrop::Static)",
|
||||
".with_prop(PropsOp::set(\"data-bs-theme\", \"dark\")))",
|
||||
"Navbar::offcanvas_brand_left(brand, bs::navbar::Panel::new()",
|
||||
".with_backdrop(bs::offcanvas::Backdrop::Static))",
|
||||
".with_prop(PropsOp::set(\"data-bs-theme\", \"dark\"))",
|
||||
),
|
||||
),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ pub use super::Brand;
|
|||
pub use pagetop::base::component::Navbar;
|
||||
pub use pagetop::base::component::navbar::{Item, Position};
|
||||
|
||||
mod panel;
|
||||
pub use panel::Panel;
|
||||
|
||||
mod component;
|
||||
pub use component::NavbarBootsier;
|
||||
pub(crate) use component::{render, setup};
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@ const EXTRA_LAYOUT: &str = "bootsier.navbar.layout";
|
|||
/// Extensión de Bootsier para [`Navbar`].
|
||||
///
|
||||
/// Permite mostrar enlaces, menús y una marca de identidad en distintas disposiciones (simples, con
|
||||
/// botón de despliegue o dentro de un [`Offcanvas`]), controladas por [`navbar::Layout`]. También
|
||||
/// puede fijarse en la parte superior o inferior del documento mediante [`navbar::Position`]. El
|
||||
/// botón de despliegue o dentro de un [`navbar::Panel`]), controladas por [`navbar::Layout`].
|
||||
/// También puede fijarse en la parte superior o inferior del documento con [`navbar::Position`]. El
|
||||
/// punto de corte a partir del cual deja de colapsar se define con [`Navbar::with_expand()`].
|
||||
///
|
||||
/// [`Navbar`]: crate::theme::bs::Navbar
|
||||
/// [`Offcanvas`]: crate::theme::bs::Offcanvas
|
||||
/// [`navbar::Layout`]: crate::theme::bs::navbar::Layout
|
||||
/// [`navbar::Panel`]: crate::theme::bs::navbar::Panel
|
||||
/// [`navbar::Position`]: crate::theme::bs::navbar::Position
|
||||
///
|
||||
/// # Ejemplos
|
||||
|
|
@ -102,13 +102,12 @@ const EXTRA_LAYOUT: &str = "bootsier.navbar.layout";
|
|||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// let oc = bs::Offcanvas::new()
|
||||
/// .with_id("main_offcanvas")
|
||||
/// let panel = bs::navbar::Panel::new()
|
||||
/// .with_title(Lc::n("Main menu"))
|
||||
/// .with_placement(bs::offcanvas::Placement::Start)
|
||||
/// .with_backdrop(bs::offcanvas::Backdrop::Enabled);
|
||||
///
|
||||
/// let navbar = bs::Navbar::offcanvas(oc)
|
||||
/// let navbar = bs::Navbar::offcanvas(panel)
|
||||
/// .with_item(bs::navbar::Item::nav(
|
||||
/// bs::Nav::new()
|
||||
/// .with_item(bs::nav::Item::link(Lc::n("Home"), "/"))
|
||||
|
|
@ -142,41 +141,43 @@ const EXTRA_LAYOUT: &str = "bootsier.navbar.layout";
|
|||
/// ```
|
||||
#[builder_impl]
|
||||
pub trait NavbarBootsier {
|
||||
/// Crea una barra de navegación cuyo contenido se muestra en un **offcanvas**.
|
||||
fn offcanvas(oc: bs::Offcanvas) -> Self;
|
||||
/// Crea una barra de navegación cuyo contenido se muestra en un panel lateral.
|
||||
fn offcanvas(panel: bs::navbar::Panel) -> Self;
|
||||
|
||||
/// Crea una barra de navegación con **marca de identidad** y contenido en **offcanvas**.
|
||||
fn offcanvas_brand_left(brand: Brand, oc: bs::Offcanvas) -> Self;
|
||||
/// Crea una barra de navegación con **marca de identidad a la izquierda** y contenido en un
|
||||
/// panel lateral.
|
||||
fn offcanvas_brand_left(brand: Brand, panel: bs::navbar::Panel) -> Self;
|
||||
|
||||
/// Crea una barra de navegación con **marca de identidad** y contenido en **offcanvas**.
|
||||
fn offcanvas_brand_right(brand: Brand, oc: bs::Offcanvas) -> Self;
|
||||
/// Crea una barra de navegación con **marca de identidad a la derecha** y contenido en un panel
|
||||
/// lateral.
|
||||
fn offcanvas_brand_right(brand: Brand, panel: bs::navbar::Panel) -> Self;
|
||||
}
|
||||
|
||||
#[builder_impl]
|
||||
impl NavbarBootsier for Navbar {
|
||||
fn offcanvas(oc: bs::Offcanvas) -> Self {
|
||||
fn offcanvas(panel: bs::navbar::Panel) -> Self {
|
||||
let mut navbar = Self::new();
|
||||
navbar.alter_prop(PropsOp::set_extra(
|
||||
EXTRA_LAYOUT,
|
||||
bs::navbar::Layout::Offcanvas(Embed::with(oc)),
|
||||
bs::navbar::Layout::Offcanvas(panel),
|
||||
));
|
||||
navbar
|
||||
}
|
||||
|
||||
fn offcanvas_brand_left(brand: Brand, oc: bs::Offcanvas) -> Self {
|
||||
fn offcanvas_brand_left(brand: Brand, panel: bs::navbar::Panel) -> Self {
|
||||
let mut navbar = Self::new();
|
||||
navbar.alter_prop(PropsOp::set_extra(
|
||||
EXTRA_LAYOUT,
|
||||
bs::navbar::Layout::OffcanvasBrandLeft(Embed::with(brand), Embed::with(oc)),
|
||||
bs::navbar::Layout::OffcanvasBrandLeft(Embed::with(brand), panel),
|
||||
));
|
||||
navbar
|
||||
}
|
||||
|
||||
fn offcanvas_brand_right(brand: Brand, oc: bs::Offcanvas) -> Self {
|
||||
fn offcanvas_brand_right(brand: Brand, panel: bs::navbar::Panel) -> Self {
|
||||
let mut navbar = Self::new();
|
||||
navbar.alter_prop(PropsOp::set_extra(
|
||||
EXTRA_LAYOUT,
|
||||
bs::navbar::Layout::OffcanvasBrandRight(Embed::with(brand), Embed::with(oc)),
|
||||
bs::navbar::Layout::OffcanvasBrandRight(Embed::with(brand), panel),
|
||||
));
|
||||
navbar
|
||||
}
|
||||
|
|
@ -187,7 +188,7 @@ impl NavbarBootsier for Navbar {
|
|||
pub(crate) fn setup(navbar: &mut Navbar) {
|
||||
// Sin botón de despliegue no hay nada que colapsar, así que el punto de corte no debe afectar a
|
||||
// la barra: Bootstrap apilaría igualmente el menú por debajo de él. `navbar-expand` a secas es
|
||||
// su «expandida siempre». Las disposiciones de Bootsier (`EXTRA_LAYOUT`, con `Offcanvas`)
|
||||
// su «expandida siempre». Las disposiciones de Bootsier (`EXTRA_LAYOUT`, con panel lateral)
|
||||
// siempre llevan botón; de las de base sólo `Simple` y `SimpleBrandLeft` no lo llevan.
|
||||
let has_toggle = navbar
|
||||
.props()
|
||||
|
|
@ -242,13 +243,16 @@ pub(crate) async fn render(navbar: &Navbar, cx: &mut Context) -> Result<Markup,
|
|||
return Ok(html! {});
|
||||
}
|
||||
|
||||
// `Navbar::setup()` (base) garantiza que habrá un `id` antes de renderizar.
|
||||
// `Navbar::setup()` (base) garantiza que habrá un `id` antes de renderizar. El área de
|
||||
// contenido (colapsable o panel lateral) deriva su identificador del de la barra.
|
||||
let id = navbar.id().unwrap();
|
||||
let id_content = util::join!(&id, "-content");
|
||||
|
||||
// `with_layout()` (extra propio de Bootsier) tiene prioridad; si no se ha usado, se traduce el
|
||||
// `navbar::Layout` de base que hayan podido fijar los constructores heredados de `Navbar`
|
||||
// (`simple()`, `brand_left()`...), que Bootsier no puede sobrescribir por nombre -las funciones
|
||||
// inherentes de base siempre ganan sobre las de un trait con el mismo nombre-.
|
||||
// El extra `EXTRA_LAYOUT` (fijado por los constructores propios de Bootsier `offcanvas()`,
|
||||
// `offcanvas_brand_left()`, `offcanvas_brand_right()`) tiene prioridad; si no se ha fijado, se
|
||||
// traduce el `navbar::Layout` de base que hayan podido fijar los constructores heredados de
|
||||
// `Navbar` (`simple()`, `brand_left()`...), que Bootsier no puede sobrescribir por nombre -las
|
||||
// funciones inherentes de base siempre ganan sobre las de un trait con el mismo nombre-.
|
||||
let layout = navbar
|
||||
.props()
|
||||
.extra::<bs::navbar::Layout>(EXTRA_LAYOUT)
|
||||
|
|
@ -266,8 +270,6 @@ pub(crate) async fn render(navbar: &Navbar, cx: &mut Context) -> Result<Markup,
|
|||
|
||||
// Barra sencilla que se puede contraer/expandir.
|
||||
bs::navbar::Layout::SimpleToggle => {
|
||||
@let id_content = util::join!(&id, "-content");
|
||||
|
||||
(button(cx, TOGGLE_COLLAPSE, &id_content))
|
||||
div id=(&id_content) class="collapse navbar-collapse" {
|
||||
(items)
|
||||
|
|
@ -285,8 +287,6 @@ pub(crate) async fn render(navbar: &Navbar, cx: &mut Context) -> Result<Markup,
|
|||
|
||||
// Barra con marca a la izquierda y botón a la derecha.
|
||||
bs::navbar::Layout::BrandLeft(brand) => {
|
||||
@let id_content = util::join!(&id, "-content");
|
||||
|
||||
(brand.render(cx).await)
|
||||
(button(cx, TOGGLE_COLLAPSE, &id_content))
|
||||
div id=(&id_content) class="collapse navbar-collapse" {
|
||||
|
|
@ -296,8 +296,6 @@ pub(crate) async fn render(navbar: &Navbar, cx: &mut Context) -> Result<Markup,
|
|||
|
||||
// Barra con botón a la izquierda y marca a la derecha.
|
||||
bs::navbar::Layout::BrandRight(brand) => {
|
||||
@let id_content = util::join!(&id, "-content");
|
||||
|
||||
(button(cx, TOGGLE_COLLAPSE, &id_content))
|
||||
(brand.render(cx).await)
|
||||
div id=(&id_content) class="collapse navbar-collapse" {
|
||||
|
|
@ -305,45 +303,24 @@ pub(crate) async fn render(navbar: &Navbar, cx: &mut Context) -> Result<Markup,
|
|||
}
|
||||
},
|
||||
|
||||
// Barra cuyo contenido se muestra en un offcanvas, sin marca.
|
||||
bs::navbar::Layout::Offcanvas(offcanvas) => {
|
||||
@let offcanvas = setup_offcanvas(&offcanvas, cx);
|
||||
@let id_content = offcanvas
|
||||
.as_ref()
|
||||
.and_then(|oc| oc.id()).unwrap_or_default();
|
||||
|
||||
// Barra cuyo contenido se muestra en un panel lateral, sin marca.
|
||||
bs::navbar::Layout::Offcanvas(panel) => {
|
||||
(button(cx, TOGGLE_OFFCANVAS, &id_content))
|
||||
@if let Some(oc) = &offcanvas {
|
||||
(oc.render_offcanvas(cx, Some(navbar.items())).await)
|
||||
}
|
||||
(panel.render_panel(cx, &id_content, &items))
|
||||
},
|
||||
|
||||
// Barra con marca a la izquierda y contenido en offcanvas.
|
||||
bs::navbar::Layout::OffcanvasBrandLeft(brand, offcanvas) => {
|
||||
@let offcanvas = setup_offcanvas(&offcanvas, cx);
|
||||
@let id_content = offcanvas
|
||||
.as_ref()
|
||||
.and_then(|oc| oc.id()).unwrap_or_default();
|
||||
|
||||
// Barra con marca a la izquierda y contenido en un panel lateral.
|
||||
bs::navbar::Layout::OffcanvasBrandLeft(brand, panel) => {
|
||||
(brand.render(cx).await)
|
||||
(button(cx, TOGGLE_OFFCANVAS, &id_content))
|
||||
@if let Some(oc) = &offcanvas {
|
||||
(oc.render_offcanvas(cx, Some(navbar.items())).await)
|
||||
}
|
||||
(panel.render_panel(cx, &id_content, &items))
|
||||
},
|
||||
|
||||
// Barra con contenido en offcanvas y marca a la derecha.
|
||||
bs::navbar::Layout::OffcanvasBrandRight(brand, offcanvas) => {
|
||||
@let offcanvas = setup_offcanvas(&offcanvas, cx);
|
||||
@let id_content = offcanvas
|
||||
.as_ref()
|
||||
.and_then(|oc| oc.id()).unwrap_or_default();
|
||||
|
||||
// Barra con contenido en un panel lateral y marca a la derecha.
|
||||
bs::navbar::Layout::OffcanvasBrandRight(brand, panel) => {
|
||||
(button(cx, TOGGLE_OFFCANVAS, &id_content))
|
||||
(brand.render(cx).await)
|
||||
@if let Some(oc) = &offcanvas {
|
||||
(oc.render_offcanvas(cx, Some(navbar.items())).await)
|
||||
}
|
||||
(panel.render_panel(cx, &id_content, &items))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -351,16 +328,6 @@ pub(crate) async fn render(navbar: &Navbar, cx: &mut Context) -> Result<Markup,
|
|||
})
|
||||
}
|
||||
|
||||
// El panel se renderiza con `render_offcanvas()` para inyectarle los contenidos de la barra, así
|
||||
// que no pasa por el ciclo de vida normal. Se clona y se le aplica aquí su `setup()`; sin él no
|
||||
// tendría `id` (el botón lo necesita para referenciarlo) ni las clases `offcanvas*` que lo ocultan
|
||||
// y lo colocan.
|
||||
fn setup_offcanvas(offcanvas: &Embed<bs::Offcanvas>, cx: &mut Context) -> Option<bs::Offcanvas> {
|
||||
let mut oc = offcanvas.get()?.clone();
|
||||
oc.setup(cx);
|
||||
Some(oc)
|
||||
}
|
||||
|
||||
// Traduce el `navbar::Layout` semántico de base (sin `Offcanvas`, sin `Position`/`expand`) a la
|
||||
// variante equivalente de `bs::navbar::Layout`, para las barras construidas con los constructores
|
||||
// heredados de `Navbar` (`simple()`, `simple_toggle()`, `simple_brand_left()`, `brand_left()`,
|
||||
|
|
|
|||
118
extensions/pagetop-bootsier/src/theme/bs/navbar/panel.rs
Normal file
118
extensions/pagetop-bootsier/src/theme/bs/navbar/panel.rs
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::LOCALES_BOOTSIER;
|
||||
use crate::theme::*;
|
||||
|
||||
/// Panel lateral deslizante (*offcanvas*) para una barra de navegación [`Navbar`].
|
||||
///
|
||||
/// Es el panel en el que se muestran los elementos de la barra cuando está colapsada. Se abre con
|
||||
/// su botón de despliegue y sólo recoge lo que tiene sentido en una barra: el título, el borde
|
||||
/// desde el que se despliega, la capa de fondo y el desplazamiento de la página. El resto lo decide
|
||||
/// la propia barra ya que el contenido son sus elementos, el punto de corte a partir del cual se
|
||||
/// muestra (definido en [`Navbar::with_expand()`]) y el identificador que se deriva del suyo.
|
||||
///
|
||||
/// Para un panel lateral independiente, con contenido propio, se usa [`Offcanvas`].
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_bootsier::theme::*;
|
||||
///
|
||||
/// let panel = bs::navbar::Panel::new()
|
||||
/// .with_title(Lc::n("Main menu"))
|
||||
/// .with_placement(bs::offcanvas::Placement::End)
|
||||
/// .with_backdrop(bs::offcanvas::Backdrop::Static);
|
||||
///
|
||||
/// let navbar = bs::Navbar::offcanvas(panel)
|
||||
/// .with_item(bs::navbar::Item::nav(
|
||||
/// bs::Nav::new().with_item(bs::nav::Item::link(Lc::n("Home"), "/"))
|
||||
/// ));
|
||||
/// ```
|
||||
///
|
||||
/// [`Navbar`]: crate::theme::bs::Navbar
|
||||
/// [`Navbar::with_expand()`]: crate::theme::bs::Navbar::with_expand
|
||||
/// [`Offcanvas`]: crate::theme::bs::Offcanvas
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Panel {
|
||||
/// Devuelve el título del panel.
|
||||
title: Lc,
|
||||
/// Devuelve el borde desde el que se despliega el panel.
|
||||
#[getters(copy)]
|
||||
placement: bs::offcanvas::Placement,
|
||||
/// Devuelve el comportamiento configurado para la capa de fondo.
|
||||
#[getters(copy)]
|
||||
backdrop: bs::offcanvas::Backdrop,
|
||||
/// Indica si la página principal puede desplazarse mientras el panel está abierto.
|
||||
#[getters(copy)]
|
||||
body_scroll: bs::offcanvas::BodyScroll,
|
||||
}
|
||||
|
||||
#[builder_impl]
|
||||
impl Panel {
|
||||
/// Crea un panel sin título, que se despliega desde el borde inicial, con una capa de fondo y
|
||||
/// sin permitir el desplazamiento de la página mientras está abierto.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Establece el título del encabezado.
|
||||
pub fn with_title(mut self, title: Lc) -> Self {
|
||||
self.title = title;
|
||||
self
|
||||
}
|
||||
|
||||
/// Indica desde qué borde de la ventana se despliega y se ancla el panel.
|
||||
pub fn with_placement(mut self, placement: bs::offcanvas::Placement) -> Self {
|
||||
self.placement = placement;
|
||||
self
|
||||
}
|
||||
|
||||
/// Ajusta la capa de fondo del panel para definir su comportamiento al hacer clic fuera del
|
||||
/// panel.
|
||||
pub fn with_backdrop(mut self, backdrop: bs::offcanvas::Backdrop) -> Self {
|
||||
self.backdrop = backdrop;
|
||||
self
|
||||
}
|
||||
|
||||
/// Permite o bloquea el desplazamiento de la página principal mientras el panel está abierto.
|
||||
pub fn with_body_scroll(mut self, scrolling: bs::offcanvas::BodyScroll) -> Self {
|
||||
self.body_scroll = scrolling;
|
||||
self
|
||||
}
|
||||
|
||||
// Renderiza el panel con `id` como identificador y `body` (los elementos de la barra ya
|
||||
// renderizados) como contenido. Con `navbar-expand-{bp}` Bootstrap lo muestra en línea a partir
|
||||
// del punto de corte de la barra, así que no lleva clase de punto de corte propia.
|
||||
pub(super) fn render_panel(&self, cx: &Context, id: &str, body: &Markup) -> Markup {
|
||||
let title = self.title().using(cx);
|
||||
let id_label = util::join!(id, "-label");
|
||||
let id_target = util::join!("#", id);
|
||||
let classes = util::join!("offcanvas ", self.placement().as_str());
|
||||
|
||||
html! {
|
||||
div
|
||||
id=(id)
|
||||
class=(classes)
|
||||
tabindex="-1"
|
||||
data-bs-scroll=[self.body_scroll().opt_str()]
|
||||
data-bs-backdrop=[self.backdrop().opt_str()]
|
||||
aria-labelledby=[(!title.is_empty()).then_some(&id_label)]
|
||||
{
|
||||
div class="offcanvas-header" {
|
||||
@if !title.is_empty() {
|
||||
h5 id=(&id_label) class="offcanvas-title" { (title) }
|
||||
}
|
||||
button
|
||||
type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="offcanvas"
|
||||
data-bs-target=(id_target)
|
||||
aria-label=[Lc::t("offcanvas_close", &LOCALES_BOOTSIER).lookup(cx)]
|
||||
{}
|
||||
}
|
||||
div class="offcanvas-body" { (body) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,15 +26,15 @@ pub enum Layout {
|
|||
/// Barra con botón de despliegue a la izquierda y marca de identidad a la derecha.
|
||||
BrandRight(Embed<Brand>),
|
||||
|
||||
/// Contenido en [`Offcanvas`](crate::theme::bs::Offcanvas), con botón de despliegue a la
|
||||
/// izquierda y sin marca de identidad.
|
||||
Offcanvas(Embed<bs::Offcanvas>),
|
||||
/// Contenido en un [`Panel`](crate::theme::bs::navbar::Panel) lateral, con botón de despliegue
|
||||
/// a la izquierda y sin marca de identidad.
|
||||
Offcanvas(bs::navbar::Panel),
|
||||
|
||||
/// Contenido en [`Offcanvas`](crate::theme::bs::Offcanvas), con marca de identidad a la
|
||||
/// izquierda y botón de despliegue a la derecha.
|
||||
OffcanvasBrandLeft(Embed<Brand>, Embed<bs::Offcanvas>),
|
||||
/// Contenido en un [`Panel`](crate::theme::bs::navbar::Panel) lateral, con marca de identidad
|
||||
/// a la izquierda y botón de despliegue a la derecha.
|
||||
OffcanvasBrandLeft(Embed<Brand>, bs::navbar::Panel),
|
||||
|
||||
/// Contenido en [`Offcanvas`](crate::theme::bs::Offcanvas), con botón de despliegue a la
|
||||
/// izquierda y marca de identidad a la derecha.
|
||||
OffcanvasBrandRight(Embed<Brand>, Embed<bs::Offcanvas>),
|
||||
/// Contenido en un [`Panel`](crate::theme::bs::navbar::Panel) lateral, con botón de despliegue
|
||||
/// a la izquierda y marca de identidad a la derecha.
|
||||
OffcanvasBrandRight(Embed<Brand>, bs::navbar::Panel),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,14 +48,19 @@ pub struct Offcanvas {
|
|||
/// Devuelve el título del panel.
|
||||
title: Lc,
|
||||
/// Devuelve el punto de corte configurado para cambiar el comportamiento del panel.
|
||||
#[getters(copy)]
|
||||
breakpoint: Breakpoint,
|
||||
/// Devuelve el comportamiento configurado para la capa de fondo.
|
||||
#[getters(copy)]
|
||||
backdrop: bs::offcanvas::Backdrop,
|
||||
/// Indica si la página principal puede desplazarse mientras el panel está abierto.
|
||||
#[getters(copy)]
|
||||
body_scroll: bs::offcanvas::BodyScroll,
|
||||
/// Devuelve la posición de inicio del panel.
|
||||
#[getters(copy)]
|
||||
placement: bs::offcanvas::Placement,
|
||||
/// Devuelve el estado inicial del panel.
|
||||
#[getters(copy)]
|
||||
visibility: bs::offcanvas::Visibility,
|
||||
/// Devuelve la lista de componentes (`children`) del panel.
|
||||
children: Children,
|
||||
|
|
@ -78,7 +83,7 @@ impl Component for Offcanvas {
|
|||
// Clases CSS por defecto para el panel.
|
||||
self.alter_prop(PropsOp::prepend_classes({
|
||||
let mut classes = String::new();
|
||||
push_breakpoint_class(cx, *self.breakpoint(), &mut classes, "offcanvas", "");
|
||||
push_breakpoint_class(cx, self.breakpoint(), &mut classes, "offcanvas", "");
|
||||
self.placement().push_to(&mut classes);
|
||||
self.visibility().push_to(&mut classes);
|
||||
classes
|
||||
|
|
@ -86,7 +91,41 @@ impl Component for Offcanvas {
|
|||
}
|
||||
|
||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(self.render_offcanvas(cx, None).await)
|
||||
let body = self.children().render(cx).await;
|
||||
if body.is_empty() {
|
||||
return Ok(html! {});
|
||||
}
|
||||
|
||||
// `setup()` garantiza que habrá un `id` antes de renderizar.
|
||||
let id = self.id().unwrap();
|
||||
let id_label = util::join!(&id, "-label");
|
||||
let title = self.title().using(cx);
|
||||
|
||||
Ok(html! {
|
||||
div
|
||||
(self.props().unpack(cx))
|
||||
tabindex="-1"
|
||||
data-bs-scroll=[self.body_scroll().opt_str()]
|
||||
data-bs-backdrop=[self.backdrop().opt_str()]
|
||||
aria-labelledby=[(!title.is_empty()).then_some(&id_label)]
|
||||
{
|
||||
div class="offcanvas-header" {
|
||||
@if !title.is_empty() {
|
||||
h5 id=(&id_label) class="offcanvas-title" { (title) }
|
||||
}
|
||||
button
|
||||
type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="offcanvas"
|
||||
data-bs-target=(util::join!("#", &id))
|
||||
aria-label=[Lc::t("offcanvas_close", &LOCALES_BOOTSIER).lookup(cx)]
|
||||
{}
|
||||
}
|
||||
div class="offcanvas-body" {
|
||||
(body)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -157,59 +196,4 @@ impl Offcanvas {
|
|||
self.children.alter_child(op.into());
|
||||
self
|
||||
}
|
||||
|
||||
// **< Offcanvas HELPERS >**********************************************************************
|
||||
|
||||
pub(crate) async fn render_offcanvas(
|
||||
&self,
|
||||
cx: &mut Context,
|
||||
extra: Option<&Children>,
|
||||
) -> Markup {
|
||||
let body = self.children().render(cx).await;
|
||||
let body_extra = if let Some(c) = extra {
|
||||
c.render(cx).await
|
||||
} else {
|
||||
html! {}
|
||||
};
|
||||
if body.is_empty() && body_extra.is_empty() {
|
||||
return html! {};
|
||||
}
|
||||
|
||||
// `setup()` garantiza que habrá un `id` antes de renderizar.
|
||||
let id = self.id().unwrap();
|
||||
let id_label = util::join!(id, "-label");
|
||||
let id_target = util::join!("#", id);
|
||||
|
||||
let body_scroll = self.body_scroll().opt_str();
|
||||
let backdrop = self.backdrop().opt_str();
|
||||
|
||||
let title = self.title().using(cx);
|
||||
|
||||
html! {
|
||||
div
|
||||
(self.props().unpack(cx))
|
||||
tabindex="-1"
|
||||
data-bs-scroll=[body_scroll]
|
||||
data-bs-backdrop=[backdrop]
|
||||
aria-labelledby=(id_label)
|
||||
{
|
||||
div class="offcanvas-header" {
|
||||
@if !title.is_empty() {
|
||||
h5 id=(&id_label) class="offcanvas-title" { (title) }
|
||||
}
|
||||
button
|
||||
type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="offcanvas"
|
||||
data-bs-target=(id_target)
|
||||
aria-label=[Lc::t("offcanvas_close", &LOCALES_BOOTSIER).lookup(cx)]
|
||||
{}
|
||||
}
|
||||
div class="offcanvas-body" {
|
||||
(body)
|
||||
(body_extra)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// Verifies that a `Navbar` with its content in an `Offcanvas` renders a working panel: the toggle
|
||||
// button must point to the `id` of the panel, and the panel must carry its own placement class.
|
||||
// Verifies that a `Navbar` with its content in a `navbar::Panel` renders a working side panel: the
|
||||
// toggle button points to the panel, the panel carries its own classes and attributes, and the
|
||||
// items of the navbar are its only content.
|
||||
|
||||
use pagetop::prelude::*;
|
||||
use pagetop_bootsier::theme::*;
|
||||
|
|
@ -8,31 +9,42 @@ fn cx() -> Context {
|
|||
Context::default().with_theme(&pagetop_bootsier::Bootsier)
|
||||
}
|
||||
|
||||
fn panel(placement: bs::offcanvas::Placement) -> bs::Offcanvas {
|
||||
bs::Offcanvas::new().with_placement(placement)
|
||||
fn panel(placement: bs::offcanvas::Placement) -> bs::navbar::Panel {
|
||||
bs::navbar::Panel::new().with_placement(placement)
|
||||
}
|
||||
|
||||
fn home() -> bs::navbar::Item {
|
||||
bs::navbar::Item::nav(bs::Nav::new().with_item(bs::nav::Item::link(Lc::n("Home"), "/")))
|
||||
}
|
||||
|
||||
async fn render(navbar: Navbar) -> String {
|
||||
let mut navbar = navbar.with_item(home());
|
||||
navbar.render(&mut cx()).await.into_string()
|
||||
}
|
||||
|
||||
// The `id` the toggle button points to.
|
||||
fn target(html: &str) -> &str {
|
||||
html.split(r##"data-bs-target="#"##)
|
||||
.nth(1)
|
||||
.and_then(|rest| rest.split('"').next())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn offcanvas_button_points_to_the_panel_id() {
|
||||
let brand = || Brand::new().with_title(Lc::n("PageTop"));
|
||||
let start = bs::offcanvas::Placement::Start;
|
||||
let navbars = [
|
||||
bs::Navbar::offcanvas(panel(bs::offcanvas::Placement::Start)),
|
||||
bs::Navbar::offcanvas_brand_left(brand(), panel(bs::offcanvas::Placement::Start)),
|
||||
bs::Navbar::offcanvas_brand_right(brand(), panel(bs::offcanvas::Placement::Start)),
|
||||
bs::Navbar::offcanvas(panel(start)),
|
||||
bs::Navbar::offcanvas_brand_left(brand(), panel(start)),
|
||||
bs::Navbar::offcanvas_brand_right(brand(), panel(start)),
|
||||
];
|
||||
|
||||
for mut navbar in navbars {
|
||||
navbar = navbar.with_item(bs::navbar::Item::nav(
|
||||
bs::Nav::new().with_item(bs::nav::Item::link(Lc::n("Home"), "/")),
|
||||
));
|
||||
let html = navbar.render(&mut cx()).await.into_string();
|
||||
for navbar in navbars {
|
||||
let html = render(navbar).await;
|
||||
|
||||
// The panel is rendered by the navbar, so the navbar must set its `id` up.
|
||||
let target = html
|
||||
.split(r##"data-bs-target="#"##)
|
||||
.nth(1)
|
||||
.and_then(|rest| rest.split('"').next())
|
||||
.unwrap_or_default();
|
||||
// The panel is rendered by the navbar, which derives its `id` from its own.
|
||||
let target = target(&html);
|
||||
assert!(!target.is_empty(), "the button has no target in {html}");
|
||||
assert!(html.contains(&format!(r#"aria-controls="{target}""#)));
|
||||
assert!(
|
||||
|
|
@ -47,11 +59,10 @@ async fn offcanvas_panel_gets_its_own_classes() {
|
|||
for (placement, class) in [
|
||||
(bs::offcanvas::Placement::Start, "offcanvas-start"),
|
||||
(bs::offcanvas::Placement::End, "offcanvas-end"),
|
||||
(bs::offcanvas::Placement::Top, "offcanvas-top"),
|
||||
(bs::offcanvas::Placement::Bottom, "offcanvas-bottom"),
|
||||
] {
|
||||
let mut navbar = bs::Navbar::offcanvas(panel(placement)).with_item(bs::navbar::Item::nav(
|
||||
bs::Nav::new().with_item(bs::nav::Item::link(Lc::n("Home"), "/")),
|
||||
));
|
||||
let html = navbar.render(&mut cx()).await.into_string();
|
||||
let html = render(bs::Navbar::offcanvas(panel(placement))).await;
|
||||
|
||||
// Without them Bootstrap would show the panel inline instead of hiding and placing it.
|
||||
assert!(
|
||||
|
|
@ -60,3 +71,72 @@ async fn offcanvas_panel_gets_its_own_classes() {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn offcanvas_panel_follows_the_expand_of_the_navbar() {
|
||||
let navbar = bs::Navbar::offcanvas(panel(bs::offcanvas::Placement::Start));
|
||||
let html = render(navbar.with_expand(Breakpoint::Lg)).await;
|
||||
|
||||
// Bootstrap shows the panel inline from `navbar-expand-lg`; the panel has no breakpoint of its
|
||||
// own that could contradict it, and it is not open on load.
|
||||
assert!(html.contains("navbar-expand-lg"));
|
||||
assert!(html.contains(r#"class="offcanvas offcanvas-start""#));
|
||||
assert!(!html.contains("offcanvas-lg") && !html.contains("offcanvas-md"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn offcanvas_panel_title_labels_the_panel() {
|
||||
let start = bs::offcanvas::Placement::Start;
|
||||
|
||||
let html = render(bs::Navbar::offcanvas(
|
||||
bs::navbar::Panel::new()
|
||||
.with_title(Lc::n("Main menu"))
|
||||
.with_placement(start),
|
||||
))
|
||||
.await;
|
||||
let target = target(&html);
|
||||
assert!(html.contains(&format!(
|
||||
r#"<h5 id="{target}-label" class="offcanvas-title">Main menu</h5>"#
|
||||
)));
|
||||
assert!(html.contains(&format!(r#"aria-labelledby="{target}-label""#)));
|
||||
|
||||
// Without a title there is nothing to point `aria-labelledby` to.
|
||||
let html = render(bs::Navbar::offcanvas(panel(start))).await;
|
||||
assert!(!html.contains("<h5"));
|
||||
assert!(!html.contains("aria-labelledby"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn offcanvas_panel_backdrop_and_body_scroll_are_attributes() {
|
||||
let panel = || bs::navbar::Panel::new();
|
||||
|
||||
let html = render(bs::Navbar::offcanvas(panel())).await;
|
||||
assert!(!html.contains("data-bs-backdrop") && !html.contains("data-bs-scroll"));
|
||||
|
||||
for (backdrop, value) in [
|
||||
(bs::offcanvas::Backdrop::Disabled, "false"),
|
||||
(bs::offcanvas::Backdrop::Static, "static"),
|
||||
] {
|
||||
let html = render(bs::Navbar::offcanvas(panel().with_backdrop(backdrop))).await;
|
||||
assert!(html.contains(&format!(r#"data-bs-backdrop="{value}""#)));
|
||||
}
|
||||
|
||||
let scrolling = panel().with_body_scroll(bs::offcanvas::BodyScroll::Enabled);
|
||||
let html = render(bs::Navbar::offcanvas(scrolling)).await;
|
||||
assert!(html.contains(r#"data-bs-scroll="true""#));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn offcanvas_panel_contains_the_items_once_and_can_be_closed() {
|
||||
let html = render(bs::Navbar::offcanvas(panel(bs::offcanvas::Placement::End))).await;
|
||||
let target = target(&html);
|
||||
|
||||
// The items are the body of the panel and are not rendered anywhere else.
|
||||
assert_eq!(html.matches(">Home<").count(), 1);
|
||||
assert!(html.contains(r#"<div class="offcanvas-body"><ul"#));
|
||||
|
||||
// The close button of the header points to the same panel.
|
||||
assert!(html.contains(&format!(
|
||||
r##"data-bs-dismiss="offcanvas" data-bs-target="#{target}""##
|
||||
)));
|
||||
}
|
||||
|
|
|
|||
63
extensions/pagetop-bootsier/tests/offcanvas.rs
Normal file
63
extensions/pagetop-bootsier/tests/offcanvas.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
// Verifies the rendering of the standalone `Offcanvas`: its accessible name comes from the title,
|
||||
// and it is not rendered without content.
|
||||
|
||||
use pagetop::prelude::*;
|
||||
use pagetop_bootsier::theme::*;
|
||||
|
||||
fn cx() -> Context {
|
||||
Context::default().with_theme(&pagetop_bootsier::Bootsier)
|
||||
}
|
||||
|
||||
fn content() -> Html {
|
||||
Html::with(|_| html! { p { "Content" } })
|
||||
}
|
||||
|
||||
async fn render(mut offcanvas: bs::Offcanvas) -> String {
|
||||
offcanvas.render(&mut cx()).await.into_string()
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn title_labels_the_panel() {
|
||||
let html = render(
|
||||
bs::Offcanvas::new()
|
||||
.with_id("panel")
|
||||
.with_title(Lc::n("Menu"))
|
||||
.with_child(content()),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(html.contains(r#"<h5 id="panel-label" class="offcanvas-title">Menu</h5>"#));
|
||||
assert!(html.contains(r#"aria-labelledby="panel-label""#));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn without_a_title_there_is_nothing_to_point_aria_labelledby_to() {
|
||||
let html = render(bs::Offcanvas::new().with_id("panel").with_child(content())).await;
|
||||
|
||||
assert!(!html.contains("<h5"));
|
||||
assert!(!html.contains("aria-labelledby"));
|
||||
// The close button and the body are still there.
|
||||
assert!(html.contains(r##"data-bs-target="#panel""##));
|
||||
assert!(html.contains(r#"<div class="offcanvas-body"><p>Content</p></div>"#));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn is_not_rendered_without_content() {
|
||||
let html = render(bs::Offcanvas::new().with_title(Lc::n("Menu"))).await;
|
||||
|
||||
assert!(html.is_empty());
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn getters_of_the_options_return_their_values() {
|
||||
let offcanvas = bs::Offcanvas::new()
|
||||
.with_placement(bs::offcanvas::Placement::End)
|
||||
.with_backdrop(bs::offcanvas::Backdrop::Static)
|
||||
.with_body_scroll(bs::offcanvas::BodyScroll::Enabled)
|
||||
.with_visibility(bs::offcanvas::Visibility::Show);
|
||||
|
||||
assert_eq!(offcanvas.placement(), bs::offcanvas::Placement::End);
|
||||
assert_eq!(offcanvas.backdrop(), bs::offcanvas::Backdrop::Static);
|
||||
assert_eq!(offcanvas.body_scroll(), bs::offcanvas::BodyScroll::Enabled);
|
||||
assert_eq!(offcanvas.visibility(), bs::offcanvas::Visibility::Show);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue