use crate::{
core::{
assets,
attrs::{availability_attributes, component_attributes},
},
prelude::*,
theme::use_stylesheets,
};
const BEM: Bem = Bem::new("fc-nav");
const LIST: Bem = Bem::new("fc-nav__list");
fc_modifier! { enum ListDepth for LIST { Top => None, Nested => "nested" } }
#[derive(Clone, Copy)]
struct Rail(Signal<bool>);
#[component]
pub fn NavSidebar(
label: AccessibleName,
title: Element,
#[props(default)]
toggle_label: Option<AccessibleName>,
#[props(default = true)] collapsible: bool,
#[props(default)] expanded: Option<ReadSignal<bool>>,
#[props(default = true)] default_expanded: bool,
#[props(default)] ontoggle: Option<EventHandler<bool>>,
#[props(default)] class: Option<String>,
#[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
children: Element,
) -> Element {
let state = use_controllable(expanded, || default_expanded, ontoggle);
let id = use_id();
let mut rail = use_context_provider(|| Rail(Signal::new(!default_expanded)));
let open = state.get();
if *rail.0.peek() == open {
rail.0.set(!open);
}
let attributes = component_attributes(
attributes,
ClassList::new(BEM.block()).extend(class.as_deref()),
&["id", "aria-label"],
);
let toggle_label = toggle_label.map_or_else(|| "Sidebar".to_owned(), |l| l.to_string());
rsx! {
if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_NAV_CSS } }
nav { id: "{id}", aria_label: label.to_string(), ..attributes,
div { class: BEM.element("header"),
span { class: BEM.element("title"), {title} }
if collapsible {
button { class: BEM.element("toggle"), r#type: "button", aria_label: toggle_label,
aria_expanded: open.to_string(), aria_controls: "{id}",
onclick: move |_| state.set(!open),
}
}
}
{children}
}
}
}
#[component]
pub fn NavHeading(children: Element) -> Element {
rsx! { p { class: BEM.element("heading"), {children} } }
}
#[component]
pub fn NavList(#[props(default)] nested: bool, children: Element) -> Element {
let class = ClassList::new(LIST.block())
.with_modifier(
LIST,
if nested {
ListDepth::Nested
} else {
ListDepth::Top
},
)
.to_string();
rsx! { ul { class, {children} } }
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NavCurrent {
#[default]
No,
Page,
Section,
}
#[component]
pub fn NavLink(
label: String,
#[props(default)] href: Option<String>,
#[props(default)] icon: Option<Element>,
#[props(default)]
meta: Option<Element>,
#[props(default)] current: NavCurrent,
#[props(default)] availability: Availability,
#[props(default)]
nested: Option<Element>,
) -> Element {
let rail = try_consume_context::<Rail>().is_some_and(|rail| *rail.0.read());
let blocked = availability != Availability::Enabled;
let state = match &availability {
Availability::Disabled => {
crate::spread![Attribute::new("aria-disabled", "true", None, false)]
}
other => availability_attributes(other),
};
let link = crate::core::link::use_link(href.filter(|_| !blocked));
let href = link.href.clone();
let current = match current {
NavCurrent::No if link.current => Some("page"),
NavCurrent::No => None,
NavCurrent::Page => Some("page"),
NavCurrent::Section => Some("true"),
};
rsx! {
li {
a { class: BEM.element("link"), href, aria_current: current,
onclick: move |event| { link.follow(&event); },
title: rail.then(|| label.clone()),
..state,
{icon}
span { class: BEM.element("label"), "{label}" }
if let Some(meta) = meta { span { class: BEM.element("meta"), {meta} } }
}
{nested}
}
}
}
#[component]
pub fn NavIcon(children: Element) -> Element {
rsx! {
svg { class: BEM.element("icon"), view_box: "0 0 20 20", fill: "none", stroke: "currentColor",
stroke_width: "1.6", "aria-hidden": "true", {children}
}
}
}
#[component]
pub fn NavGroup(label: Element, #[props(default)] open: bool, children: Element) -> Element {
rsx! {
details { class: BEM.element("group"), open,
summary { class: BEM.element("group-label"), {label} }
NavList { {children} }
}
}
}
#[component]
pub fn NavFooter(children: Element) -> Element {
rsx! { div { class: BEM.element("footer"), NavList { {children} } } }
}