use dioxus::prelude::*;
use crate::types::{Color, NavbarContainer, NavbarExpand};
#[derive(Clone, PartialEq, Props)]
pub struct NavProps {
#[props(default)]
pub pills: bool,
#[props(default)]
pub tabs: bool,
#[props(default)]
pub underline: bool,
#[props(default)]
pub fill: bool,
#[props(default)]
pub justified: bool,
#[props(default)]
pub vertical: bool,
#[props(default)]
pub tag: String,
#[props(default)]
pub class: String,
#[props(extends = GlobalAttributes)]
attributes: Vec<Attribute>,
pub children: Element,
}
fn nav_element(tag: &str) -> &'static str {
if tag == "nav" { "nav" } else { "ul" }
}
#[component]
pub fn Nav(props: NavProps) -> Element {
let mut classes = vec!["nav".to_string()];
if props.pills {
classes.push("nav-pills".to_string());
}
if props.tabs {
classes.push("nav-tabs".to_string());
}
if props.underline {
classes.push("nav-underline".to_string());
}
if props.fill {
classes.push("nav-fill".to_string());
}
if props.justified {
classes.push("nav-justified".to_string());
}
if props.vertical {
classes.push("flex-column".to_string());
}
if !props.class.is_empty() {
classes.push(props.class.clone());
}
let full_class = classes.join(" ");
if nav_element(&props.tag) == "nav" {
return rsx! {
nav { class: "{full_class}",
..props.attributes,
{props.children}
}
};
}
rsx! {
ul { class: "{full_class}",
..props.attributes,
{props.children}
}
}
}
#[derive(Clone, PartialEq, Props)]
pub struct NavbarProps {
#[props(default)]
pub color: Option<Color>,
#[props(default)]
pub expand: NavbarExpand,
#[props(default)]
pub brand: Option<Element>,
#[props(default)]
pub container: NavbarContainer,
#[props(default)]
pub class: String,
#[props(extends = GlobalAttributes)]
attributes: Vec<Attribute>,
pub children: Element,
}
#[component]
pub fn Navbar(props: NavbarProps) -> Element {
let mut classes = vec!["navbar".to_string(), props.expand.to_string()];
let is_dark = matches!(props.color.as_ref(), Some(Color::Dark));
if let Some(ref color) = props.color {
match color {
Color::Dark => {
classes.push("bg-dark".to_string());
}
Color::Light => {
classes.push("bg-light".to_string());
}
c => {
classes.push(format!("bg-{c}"));
}
}
}
if !props.class.is_empty() {
classes.push(props.class.clone());
}
let full_class = classes.join(" ");
rsx! {
nav {
class: "{full_class}",
"data-bs-theme": if is_dark { "dark" } else { "" },
..props.attributes,
match props.container.class() {
Some(container) => rsx! {
div { class: "{container}",
if let Some(brand) = props.brand {
{brand}
}
{props.children}
}
},
None => rsx! {
if let Some(brand) = props.brand {
{brand}
}
{props.children}
},
}
}
}
}
#[derive(Clone, PartialEq, Props)]
pub struct NavbarTogglerProps {
pub collapsed: Signal<bool>,
#[props(default)]
pub class: String,
#[props(extends = GlobalAttributes)]
attributes: Vec<Attribute>,
}
#[component]
pub fn NavbarToggler(props: NavbarTogglerProps) -> Element {
let is_collapsed = *props.collapsed.read();
let mut signal = props.collapsed;
let full_class = if props.class.is_empty() {
"navbar-toggler".to_string()
} else {
format!("navbar-toggler {}", props.class)
};
rsx! {
button {
class: "{full_class}",
r#type: "button",
"aria-expanded": if !is_collapsed { "true" } else { "false" },
"aria-label": "Toggle navigation",
onclick: move |_| signal.set(!is_collapsed),
..props.attributes,
span { class: "navbar-toggler-icon" }
}
}
}
#[derive(Clone, PartialEq, Props)]
pub struct NavbarCollapseProps {
pub collapsed: Signal<bool>,
#[props(default)]
pub class: String,
#[props(extends = GlobalAttributes)]
attributes: Vec<Attribute>,
pub children: Element,
}
#[component]
pub fn NavbarCollapse(props: NavbarCollapseProps) -> Element {
let is_collapsed = *props.collapsed.read();
let show = if !is_collapsed { " show" } else { "" };
let full_class = if props.class.is_empty() {
format!("collapse navbar-collapse{show}")
} else {
format!("collapse navbar-collapse{show} {}", props.class)
};
rsx! {
div { class: "{full_class}",
..props.attributes,
{props.children}
}
}
}
#[derive(Clone, PartialEq, Props)]
pub struct NavbarNavProps {
#[props(default)]
pub scroll: bool,
#[props(default)]
pub class: String,
#[props(extends = GlobalAttributes)]
attributes: Vec<Attribute>,
pub children: Element,
}
#[component]
pub fn NavbarNav(props: NavbarNavProps) -> Element {
let full_class = navbar_nav_class(props.scroll, &props.class);
rsx! {
ul {
class: "{full_class}",
..props.attributes,
{props.children}
}
}
}
fn navbar_nav_class(scroll: bool, class: &str) -> String {
let mut classes = vec!["navbar-nav".to_string()];
if scroll {
classes.push("navbar-nav-scroll".to_string());
}
if !class.is_empty() {
classes.push(class.to_string());
}
classes.join(" ")
}
#[derive(Clone, PartialEq, Props)]
pub struct NavItemProps {
#[props(default)]
pub class: String,
#[props(extends = GlobalAttributes)]
attributes: Vec<Attribute>,
pub children: Element,
}
#[component]
pub fn NavItem(props: NavItemProps) -> Element {
let full_class = if props.class.is_empty() {
"nav-item".to_string()
} else {
format!("nav-item {}", props.class)
};
rsx! {
li { class: "{full_class}", ..props.attributes, {props.children} }
}
}
#[derive(Clone, PartialEq, Props)]
pub struct NavLinkProps {
#[props(default = "#".to_string())]
pub href: String,
#[props(default)]
pub active: bool,
#[props(default)]
pub disabled: bool,
#[props(default)]
pub prevent_default: bool,
#[props(default)]
pub onclick: Option<EventHandler<MouseEvent>>,
#[props(default)]
pub class: String,
#[props(extends = GlobalAttributes)]
attributes: Vec<Attribute>,
pub children: Element,
}
#[component]
pub fn NavLink(props: NavLinkProps) -> Element {
let full_class = nav_link_class(props.active, props.disabled, &props.class);
rsx! {
a {
class: "{full_class}",
href: "{props.href}",
"aria-current": if props.active { "page" } else { "" },
"aria-disabled": if props.disabled { "true" } else { "" },
tabindex: if props.disabled { "-1" } else { "" },
onclick: move |evt| {
if props.prevent_default {
evt.prevent_default();
}
if let Some(handler) = &props.onclick {
handler.call(evt);
}
},
..props.attributes,
{props.children}
}
}
}
#[derive(Clone, PartialEq, Props)]
pub struct NavButtonProps {
#[props(default)]
pub active: bool,
#[props(default)]
pub disabled: bool,
#[props(default)]
pub onclick: Option<EventHandler<MouseEvent>>,
#[props(default)]
pub class: String,
#[props(extends = GlobalAttributes)]
attributes: Vec<Attribute>,
pub children: Element,
}
#[component]
pub fn NavButton(props: NavButtonProps) -> Element {
let full_class = nav_link_class(props.active, false, &props.class);
rsx! {
button {
r#type: "button",
class: "{full_class}",
disabled: props.disabled,
"aria-current": if props.active { "page" } else { "" },
onclick: move |evt| {
if let Some(handler) = &props.onclick {
handler.call(evt);
}
},
..props.attributes,
{props.children}
}
}
}
fn nav_link_class(active: bool, disabled: bool, extra: &str) -> String {
let mut classes = vec!["nav-link".to_string()];
if active {
classes.push("active".to_string());
}
if disabled {
classes.push("disabled".to_string());
}
if !extra.is_empty() {
classes.push(extra.to_string());
}
classes.join(" ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nav_defaults_to_the_list_structure() {
assert_eq!(nav_element(""), "ul");
}
#[test]
fn nav_tag_selects_the_flat_form() {
assert_eq!(nav_element("nav"), "nav");
}
#[test]
fn navbar_container_defaults_to_fluid() {
assert_eq!(NavbarContainer::default().class(), Some("container-fluid"));
}
#[test]
fn navbar_container_fixed_is_the_responsive_container() {
assert_eq!(NavbarContainer::Fixed.class(), Some("container"));
}
#[test]
fn navbar_container_none_emits_no_wrapper_at_all() {
assert_eq!(NavbarContainer::None.class(), None);
}
#[test]
fn navbar_nav_class_base() {
assert_eq!(navbar_nav_class(false, ""), "navbar-nav");
}
#[test]
fn navbar_nav_class_scroll_and_extra_classes() {
assert_eq!(
navbar_nav_class(true, "ms-auto"),
"navbar-nav navbar-nav-scroll ms-auto"
);
}
#[test]
fn nav_link_class_base() {
assert_eq!(nav_link_class(false, false, ""), "nav-link");
}
#[test]
fn nav_link_class_active_disabled_and_extra() {
assert_eq!(
nav_link_class(true, true, "px-3"),
"nav-link active disabled px-3"
);
}
#[test]
fn nav_button_omits_disabled_class() {
assert_eq!(nav_link_class(true, false, ""), "nav-link active");
}
}