diff --git a/extensions/pagetop-bootsier/src/locale/en-US/components.ftl b/extensions/pagetop-bootsier/src/locale/en-US/components.ftl index 0bb7fba..e3b0d6e 100644 --- a/extensions/pagetop-bootsier/src/locale/en-US/components.ftl +++ b/extensions/pagetop-bootsier/src/locale/en-US/components.ftl @@ -2,7 +2,7 @@ dropdown_toggle = Toggle Dropdown # Offcanvas -close = Close +offcanvas_close = Close # Navbar toggle = Toggle navigation diff --git a/extensions/pagetop-bootsier/src/locale/es-ES/components.ftl b/extensions/pagetop-bootsier/src/locale/es-ES/components.ftl index 045191e..ab7ff68 100644 --- a/extensions/pagetop-bootsier/src/locale/es-ES/components.ftl +++ b/extensions/pagetop-bootsier/src/locale/es-ES/components.ftl @@ -2,7 +2,7 @@ dropdown_toggle = Mostrar/ocultar menú # Offcanvas -close = Cerrar +offcanvas_close = Cerrar # Navbar toggle = Mostrar/ocultar navegación diff --git a/extensions/pagetop-bootsier/src/theme.rs b/extensions/pagetop-bootsier/src/theme.rs index ec95165..b5be6b7 100644 --- a/extensions/pagetop-bootsier/src/theme.rs +++ b/extensions/pagetop-bootsier/src/theme.rs @@ -21,7 +21,6 @@ pub mod navbar; pub use navbar::{Navbar, NavbarToggler}; // Offcanvas. -mod offcanvas; -pub use offcanvas::{ - Offcanvas, OffcanvasBackdrop, OffcanvasBodyScroll, OffcanvasPlacement, OffcanvasVisibility, -}; +pub mod offcanvas; +#[doc(inline)] +pub use offcanvas::Offcanvas; diff --git a/extensions/pagetop-bootsier/src/theme/offcanvas.rs b/extensions/pagetop-bootsier/src/theme/offcanvas.rs index b790522..560bd30 100644 --- a/extensions/pagetop-bootsier/src/theme/offcanvas.rs +++ b/extensions/pagetop-bootsier/src/theme/offcanvas.rs @@ -1,302 +1,7 @@ -use pagetop::prelude::*; +//! Definiciones para crear paneles laterales deslizantes [`Offcanvas`]. -use crate::prelude::*; -use crate::LOCALES_BOOTSIER; +mod props; +pub use props::{Backdrop, BodyScroll, Placement, Visibility}; -use std::fmt; - -// **< OffcanvasPlacement >************************************************************************* - -/// Posición de aparición del panel **deslizante** ([`Offcanvas`]). -/// -/// Define desde qué borde de la ventana entra y se ancla el panel. -#[derive(AutoDefault)] -pub enum OffcanvasPlacement { - /// Opción por defecto, desde el borde inicial según dirección de lectura (respetando LTR/RTL). - #[default] - Start, - /// Desde el borde final según dirección de lectura (respetando LTR/RTL). - End, - /// Desde la parte superior. - Top, - /// Desde la parte inferior. - Bottom, -} - -#[rustfmt::skip] -impl fmt::Display for OffcanvasPlacement { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - OffcanvasPlacement::Start => f.write_str("offcanvas-start"), - OffcanvasPlacement::End => f.write_str("offcanvas-end"), - OffcanvasPlacement::Top => f.write_str("offcanvas-top"), - OffcanvasPlacement::Bottom => f.write_str("offcanvas-bottom"), - } - } -} - -// **< OffcanvasVisibility >************************************************************************ - -/// Estado inicial del panel ([`Offcanvas`]). -#[derive(AutoDefault)] -pub enum OffcanvasVisibility { - /// El panel **permanece oculto** desde el principio. - #[default] - Default, - /// El panel **se muestra abierto** al cargar. - Show, -} - -impl fmt::Display for OffcanvasVisibility { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - OffcanvasVisibility::Default => Ok(()), - OffcanvasVisibility::Show => f.write_str("show"), - } - } -} - -// **< OffcanvasBodyScroll >************************************************************************ - -/// Controla si la página principal puede **desplazarse** al abrir el panel ([`Offcanvas`]). -#[derive(AutoDefault)] -pub enum OffcanvasBodyScroll { - /// Opción por defecto, la página principal se **bloquea** centrando la interacción en el panel. - #[default] - Disabled, - /// **Permite** el desplazamiento de la página principal. - Enabled, -} - -// **< OffcanvasBackdrop >************************************************************************** - -/// Comportamiento de la **capa de fondo** (*backdrop*) del panel ([`Offcanvas`]) al desplegarse. -#[derive(AutoDefault)] -pub enum OffcanvasBackdrop { - /// **Sin capa** de fondo; la página principal permanece visible e interactiva. - Disabled, - /// Opción por defecto, se **oscurece** el fondo; un clic fuera del panel suele cerrarlo. - #[default] - Enabled, - /// Se muestra capa de fondo pero **no** se cierra al pulsar fuera (útil cuando se requiere - /// completar una acción antes de salir). - Static, -} - -// **< Offcanvas >********************************************************************************** - -/// Panel lateral **deslizante** para contenido complementario. -/// -/// Útil para navegación, filtros, formularios o menús contextuales. Incluye las siguientes -/// características principales: -/// -/// - **Entrada configurable desde un borde** de la ventana. -/// - **Encabezado con título** y **botón de cierre** integrado. -/// - **Accesibilidad**: asocia título y controles a un identificador único y expone atributos -/// adecuados para lectores de pantalla y navegación por teclado. -/// - **Opcionalmente** bloquea el desplazamiento del documento y/o muestra una capa de fondo para -/// centrar la atención del usuario. -/// - **Responsive**: puede cambiar su comportamiento según el punto de ruptura indicado. -/// - **No se renderiza** si no tiene contenido. -#[rustfmt::skip] -#[derive(AutoDefault)] -pub struct Offcanvas { - id : AttrId, - classes : AttrClasses, - title : L10n, - breakpoint: BreakPoint, - placement : OffcanvasPlacement, - visibility: OffcanvasVisibility, - scrolling : OffcanvasBodyScroll, - backdrop : OffcanvasBackdrop, - children : Children, -} - -impl Component for Offcanvas { - fn new() -> Self { - Offcanvas::default() - } - - fn id(&self) -> Option { - self.id.get() - } - - fn setup_before_prepare(&mut self, _cx: &mut Context) { - self.alter_classes( - ClassesOp::Prepend, - [ - self.breakpoint().to_class("offcanvas"), - self.placement().to_string(), - self.visibility().to_string(), - ] - .join(" "), - ); - } - - fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup { - let body = self.children().render(cx); - if body.is_empty() { - return PrepareMarkup::None; - } - - let id = cx.required_id::(self.id()); - let id_label = join!(id, "-label"); - let id_target = join!("#", id); - - let body_scroll = match self.body_scroll() { - OffcanvasBodyScroll::Disabled => None, - OffcanvasBodyScroll::Enabled => Some("true"), - }; - - let backdrop = match self.backdrop() { - OffcanvasBackdrop::Disabled => Some("false"), - OffcanvasBackdrop::Enabled => None, - OffcanvasBackdrop::Static => Some("static"), - }; - - let title = self.title().using(cx); - - PrepareMarkup::With(html! { - div - id=(id) - class=[self.classes().get()] - tabindex="-1" - data-bs-scroll=[body_scroll] - data-bs-backdrop=[backdrop] - aria-labelledby=(id_label) - { - div class="offcanvas-header" { - @if !title.is_empty() { - h5 class="offcanvas-title" id=(id_label) { (title) } - } - button - type="button" - class="btn-close" - data-bs-dismiss="offcanvas" - data-bs-target=(id_target) - aria-label=[L10n::t("close", &LOCALES_BOOTSIER).lookup(cx)] - {} - } - div class="offcanvas-body" { - (body) - } - } - }) - } -} - -impl Offcanvas { - // **< Offcanvas BUILDER >********************************************************************** - - /// Establece el identificador único (`id`) del panel. - #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_value(id); - self - } - - /// Modifica la lista de clases CSS aplicadas al panel. - #[builder_fn] - pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { - self.classes.alter_value(op, classes); - self - } - - /// Establece el **título** del encabezado. - #[builder_fn] - pub fn with_title(mut self, title: L10n) -> Self { - self.title = title; - self - } - - /// Configura el **punto de ruptura** para activar el comportamiento responsive del panel. - #[builder_fn] - pub fn with_breakpoint(mut self, bp: BreakPoint) -> Self { - self.breakpoint = bp; - self - } - - /// Indica la **posición** desde la que entra el panel. - #[builder_fn] - pub fn with_placement(mut self, placement: OffcanvasPlacement) -> Self { - self.placement = placement; - self - } - - /// Fija el **estado inicial** del panel (oculto o visible al cargar). - #[builder_fn] - pub fn with_visibility(mut self, visibility: OffcanvasVisibility) -> Self { - self.visibility = visibility; - self - } - - /// Permite o bloquea el **desplazamiento** de la página principal mientras el panel está - /// abierto. - #[builder_fn] - pub fn with_body_scroll(mut self, scrolling: OffcanvasBodyScroll) -> Self { - self.scrolling = scrolling; - self - } - - /// Ajusta la **capa de fondo** del panel para definir su comportamiento al interactuar fuera. - #[builder_fn] - pub fn with_backdrop(mut self, backdrop: OffcanvasBackdrop) -> Self { - self.backdrop = backdrop; - self - } - - /// Añade un nuevo componente hijo al panel. - pub fn add_child(mut self, child: impl Component) -> Self { - self.children.add(Child::with(child)); - self - } - - /// Modifica la lista de hijos (`children`) aplicando una operación [`ChildOp`]. - #[builder_fn] - pub fn with_children(mut self, op: ChildOp) -> Self { - self.children.alter_child(op); - self - } - - // **< Offcanvas GETTERS >********************************************************************** - - /// Devuelve las clases CSS asociadas al panel. - pub fn classes(&self) -> &AttrClasses { - &self.classes - } - - /// Devuelve el título del panel como [`L10n`]. - pub fn title(&self) -> &L10n { - &self.title - } - - /// Devuelve el punto de ruptura configurado. - pub fn breakpoint(&self) -> &BreakPoint { - &self.breakpoint - } - - /// Devuelve la posición del panel. - pub fn placement(&self) -> &OffcanvasPlacement { - &self.placement - } - - /// Devuelve el estado inicial del panel. - pub fn visibility(&self) -> &OffcanvasVisibility { - &self.visibility - } - - /// Indica si la página principal puede desplazarse mientras el panel está abierto. - pub fn body_scroll(&self) -> &OffcanvasBodyScroll { - &self.scrolling - } - - /// Devuelve la configuración de la capa de fondo. - pub fn backdrop(&self) -> &OffcanvasBackdrop { - &self.backdrop - } - - /// Devuelve la lista de hijos (`children`) del panel. - pub fn children(&self) -> &Children { - &self.children - } -} +mod component; +pub use component::Offcanvas; diff --git a/extensions/pagetop-bootsier/src/theme/offcanvas/component.rs b/extensions/pagetop-bootsier/src/theme/offcanvas/component.rs new file mode 100644 index 0000000..7cd7dff --- /dev/null +++ b/extensions/pagetop-bootsier/src/theme/offcanvas/component.rs @@ -0,0 +1,261 @@ +use pagetop::prelude::*; + +use crate::prelude::*; +use crate::LOCALES_BOOTSIER; + +/// Componente para crear un **panel lateral deslizante** con contenidos adicionales. +/// +/// Útil para navegación, filtros, formularios o menús contextuales. Incluye las siguientes +/// características principales: +/// +/// - Puede mostrar una capa de fondo para centrar la atención del usuario en el panel +/// ([`with_backdrop()`](Self::with_backdrop)); o puede bloquear el desplazamiento del documento +/// principal ([`with_body_scroll()`](Self::with_body_scroll)). +/// - Se puede configurar el borde de la ventana desde el que se desliza el panel +/// ([`with_placement()`](Self::with_placement)). +/// - Encabezado con título ([`with_title()`](Self::with_title)) y **botón de cierre** integrado. +/// - Puede cambiar su comportamiento a partir de un punto de ruptura +/// ([`with_breakpoint()`](Self::with_breakpoint)). +/// - Asocia título y controles de accesibilidad a un identificador único y expone atributos +/// adecuados para lectores de pantalla y navegación por teclado. +/// - **No se renderiza** si no tiene contenido. +/// +/// # Ejemplo +/// +/// ```rust +/// # use pagetop::prelude::*; +/// # use pagetop_bootsier::prelude::*; +/// let panel = Offcanvas::new() +/// .with_id("offcanvas_example") +/// .with_title(L10n::n("Offcanvas title")) +/// .with_placement(offcanvas::Placement::End) +/// .with_backdrop(offcanvas::Backdrop::Enabled) +/// .with_body_scroll(offcanvas::BodyScroll::Enabled) +/// .with_visibility(offcanvas::Visibility::Default) +/// .add_child(Dropdown::new() +/// .with_button_title(L10n::n("Menu")) +/// .add_item(dropdown::Item::label(L10n::n("Label"))) +/// .add_item(dropdown::Item::link_blank(L10n::n("Google"), |_| "https://www.google.es")) +/// .add_item(dropdown::Item::link(L10n::n("Sign out"), |_| "/signout")) +/// ); +/// ``` +#[rustfmt::skip] +#[derive(AutoDefault)] +pub struct Offcanvas { + id : AttrId, + classes : AttrClasses, + title : L10n, + breakpoint: BreakPoint, + backdrop : offcanvas::Backdrop, + scrolling : offcanvas::BodyScroll, + placement : offcanvas::Placement, + visibility: offcanvas::Visibility, + children : Children, +} + +impl Component for Offcanvas { + fn new() -> Self { + Offcanvas::default() + } + + fn id(&self) -> Option { + self.id.get() + } + + #[rustfmt::skip] + fn setup_before_prepare(&mut self, _cx: &mut Context) { + self.alter_classes( + ClassesOp::Prepend, + [ + self.breakpoint().to_class("offcanvas"), + match self.placement() { + offcanvas::Placement::Start => "offcanvas-start", + offcanvas::Placement::End => "offcanvas-end", + offcanvas::Placement::Top => "offcanvas-top", + offcanvas::Placement::Bottom => "offcanvas-bottom", + }.to_string(), + match self.visibility() { + offcanvas::Visibility::Default => "", + offcanvas::Visibility::Show => "show", + }.to_string(), + ] + .join(" "), + ); + } + + fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup { + let body = self.children().render(cx); + if body.is_empty() { + return PrepareMarkup::None; + } + + let id = cx.required_id::(self.id()); + let id_label = join!(id, "-label"); + let id_target = join!("#", id); + + let body_scroll = match self.body_scroll() { + offcanvas::BodyScroll::Disabled => None, + offcanvas::BodyScroll::Enabled => Some("true"), + }; + + let backdrop = match self.backdrop() { + offcanvas::Backdrop::Disabled => Some("false"), + offcanvas::Backdrop::Enabled => None, + offcanvas::Backdrop::Static => Some("static"), + }; + + let title = self.title().using(cx); + + PrepareMarkup::With(html! { + div + id=(id) + class=[self.classes().get()] + tabindex="-1" + data-bs-scroll=[body_scroll] + data-bs-backdrop=[backdrop] + aria-labelledby=(id_label) + { + div class="offcanvas-header" { + @if !title.is_empty() { + h5 class="offcanvas-title" id=(id_label) { (title) } + } + button + type="button" + class="btn-close" + data-bs-dismiss="offcanvas" + data-bs-target=(id_target) + aria-label=[L10n::t("offcanvas_close", &LOCALES_BOOTSIER).lookup(cx)] + {} + } + div class="offcanvas-body" { + (body) + } + } + }) + } +} + +impl Offcanvas { + // **< Offcanvas BUILDER >********************************************************************** + + /// Establece el identificador único (`id`) del panel. + #[builder_fn] + pub fn with_id(mut self, id: impl AsRef) -> Self { + self.id.alter_value(id); + self + } + + /// Modifica la lista de clases CSS aplicadas al panel. + #[builder_fn] + pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { + self.classes.alter_value(op, classes); + self + } + + /// Establece el título del encabezado. + #[builder_fn] + pub fn with_title(mut self, title: L10n) -> Self { + self.title = title; + self + } + + /// Establece el punto de ruptura a partir del cual cambia el comportamiento del panel. + /// + /// - **Por debajo** de ese tamaño de pantalla, el componente actúa como panel deslizante + /// ([`Offcanvas`]). + /// - **Por encima**, el contenido del panel se muestra tal cual, integrado en la página. + /// + /// Por ejemplo, con `BreakPoint::LG`, será *offcanvas* en móviles y tabletas, y visible + /// directamente en pantallas grandes. Por defecto usa `BreakPoint::None` para que sea + /// *offcanvas* siempre. + #[builder_fn] + pub fn with_breakpoint(mut self, bp: BreakPoint) -> Self { + self.breakpoint = bp; + self + } + + /// Ajusta la capa de fondo del panel para definir su comportamiento al hacer clic fuera del + /// panel. + #[builder_fn] + pub fn with_backdrop(mut self, backdrop: offcanvas::Backdrop) -> Self { + self.backdrop = backdrop; + self + } + + /// Permite o bloquea el desplazamiento de la página principal mientras el panel está abierto. + #[builder_fn] + pub fn with_body_scroll(mut self, scrolling: offcanvas::BodyScroll) -> Self { + self.scrolling = scrolling; + self + } + + /// Indica desde qué borde de la ventana entra y se ancla el panel. + #[builder_fn] + pub fn with_placement(mut self, placement: offcanvas::Placement) -> Self { + self.placement = placement; + self + } + + /// Fija el estado inicial del panel (oculto o visible al cargar). + #[builder_fn] + pub fn with_visibility(mut self, visibility: offcanvas::Visibility) -> Self { + self.visibility = visibility; + self + } + + /// Añade un nuevo componente hijo al panel. + #[inline] + pub fn add_child(mut self, child: impl Component) -> Self { + self.children.add(Child::with(child)); + self + } + + /// Modifica la lista de componentes (`children`) aplicando una operación [`ChildOp`]. + #[builder_fn] + pub fn with_children(mut self, op: ChildOp) -> Self { + self.children.alter_child(op); + self + } + + // **< Offcanvas GETTERS >********************************************************************** + + /// Devuelve las clases CSS asociadas al panel. + pub fn classes(&self) -> &AttrClasses { + &self.classes + } + + /// Devuelve el título del panel. + pub fn title(&self) -> &L10n { + &self.title + } + + /// Devuelve el punto de ruptura configurado para cambiar el comportamiento del panel. + pub fn breakpoint(&self) -> &BreakPoint { + &self.breakpoint + } + + /// Devuelve el comportamiento configurado para la capa de fondo. + pub fn backdrop(&self) -> &offcanvas::Backdrop { + &self.backdrop + } + + /// Indica si la página principal puede desplazarse mientras el panel está abierto. + pub fn body_scroll(&self) -> &offcanvas::BodyScroll { + &self.scrolling + } + + /// Devuelve la posición de inicio del panel. + pub fn placement(&self) -> &offcanvas::Placement { + &self.placement + } + + /// Devuelve el estado inicial del panel. + pub fn visibility(&self) -> &offcanvas::Visibility { + &self.visibility + } + + /// Devuelve la lista de componentes (`children`) del panel. + pub fn children(&self) -> &Children { + &self.children + } +} diff --git a/extensions/pagetop-bootsier/src/theme/offcanvas/props.rs b/extensions/pagetop-bootsier/src/theme/offcanvas/props.rs new file mode 100644 index 0000000..cbacbd7 --- /dev/null +++ b/extensions/pagetop-bootsier/src/theme/offcanvas/props.rs @@ -0,0 +1,60 @@ +use pagetop::prelude::*; + +// **< Backdrop >*********************************************************************************** + +/// Comportamiento de la capa de fondo (*backdrop*) de un panel +/// [`Offcanvas`](crate::theme::Offcanvas) al deslizarse. +#[derive(AutoDefault)] +pub enum Backdrop { + /// Sin capa de fondo, la página principal permanece visible e interactiva. + Disabled, + /// Opción por defecto, se oscurece el fondo; un clic fuera del panel suele cerrarlo. + #[default] + Enabled, + /// Muestra la capa de fondo pero no se cierra al hacer clic fuera del panel. Útil si se + /// requiere completar una acción antes de salir. + Static, +} + +// **< BodyScroll >********************************************************************************* + +/// Controla si la página principal puede desplazarse al abrir un panel +/// [`Offcanvas`](crate::theme::Offcanvas). +#[derive(AutoDefault)] +pub enum BodyScroll { + /// Opción por defecto, la página principal se bloquea centrando la interacción en el panel. + #[default] + Disabled, + /// Permite el desplazamiento de la página principal. + Enabled, +} + +// **< Placement >********************************************************************************** + +/// Posición de aparición de un panel [`Offcanvas`](crate::theme::Offcanvas) al deslizarse. +/// +/// Define desde qué borde de la ventana entra y se ancla el panel. +#[derive(AutoDefault)] +pub enum Placement { + /// Opción por defecto, desde el borde inicial según dirección de lectura (respetando LTR/RTL). + #[default] + Start, + /// Desde el borde final según dirección de lectura (respetando LTR/RTL). + End, + /// Desde la parte superior. + Top, + /// Desde la parte inferior. + Bottom, +} + +// **< Visibility >********************************************************************************* + +/// Estado inicial de un panel [`Offcanvas`](crate::theme::Offcanvas). +#[derive(AutoDefault)] +pub enum Visibility { + /// El panel permanece oculto desde el principio. + #[default] + Default, + /// El panel se muestra abierto al cargar. + Show, +}