fc_dioxus/components/
nav.rs1use crate::{
4 core::{
5 assets,
6 attrs::{availability_attributes, component_attributes},
7 },
8 prelude::*,
9 theme::use_stylesheets,
10};
11const BEM: Bem = Bem::new("fc-nav");
12const LIST: Bem = Bem::new("fc-nav__list");
13fc_modifier! { enum ListDepth for LIST { Top => None, Nested => "nested" } }
14
15#[derive(Clone, Copy)]
16struct Rail(Signal<bool>);
17
18#[component]
25pub fn NavSidebar(
26 label: AccessibleName,
27 title: Element,
28 #[props(default)]
31 toggle_label: Option<AccessibleName>,
32 #[props(default = true)] collapsible: bool,
33 #[props(default)] expanded: Option<ReadSignal<bool>>,
34 #[props(default = true)] default_expanded: bool,
35 #[props(default)] ontoggle: Option<EventHandler<bool>>,
36 #[props(default)] class: Option<String>,
37 #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
38 children: Element,
39) -> Element {
40 let state = use_controllable(expanded, || default_expanded, ontoggle);
41 let id = use_id();
42 let mut rail = use_context_provider(|| Rail(Signal::new(!default_expanded)));
43 let open = state.get();
44 if *rail.0.peek() == open {
45 rail.0.set(!open);
46 }
47 let attributes = component_attributes(
48 attributes,
49 ClassList::new(BEM.block()).extend(class.as_deref()),
50 &["id", "aria-label"],
51 );
52 let toggle_label = toggle_label.map_or_else(|| "Sidebar".to_owned(), |l| l.to_string());
53 rsx! {
54 if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_NAV_CSS } }
55 nav { id: "{id}", aria_label: label.to_string(), ..attributes,
56 div { class: BEM.element("header"),
57 span { class: BEM.element("title"), {title} }
58 if collapsible {
59 button { class: BEM.element("toggle"), r#type: "button", aria_label: toggle_label,
60 aria_expanded: open.to_string(), aria_controls: "{id}",
61 onclick: move |_| state.set(!open),
62 }
63 }
64 }
65 {children}
66 }
67 }
68}
69
70#[component]
72pub fn NavHeading(children: Element) -> Element {
73 rsx! { p { class: BEM.element("heading"), {children} } }
74}
75
76#[component]
78pub fn NavList(#[props(default)] nested: bool, children: Element) -> Element {
79 let class = ClassList::new(LIST.block())
80 .with_modifier(
81 LIST,
82 if nested {
83 ListDepth::Nested
84 } else {
85 ListDepth::Top
86 },
87 )
88 .to_string();
89 rsx! { ul { class, {children} } }
90}
91
92#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
94pub enum NavCurrent {
95 #[default]
97 No,
98 Page,
100 Section,
102}
103
104#[component]
106pub fn NavLink(
107 label: String,
108 #[props(default)] href: Option<String>,
109 #[props(default)] icon: Option<Element>,
110 #[props(default)]
112 meta: Option<Element>,
113 #[props(default)] current: NavCurrent,
114 #[props(default)] availability: Availability,
115 #[props(default)]
117 nested: Option<Element>,
118) -> Element {
119 let rail = try_consume_context::<Rail>().is_some_and(|rail| *rail.0.read());
120 let blocked = availability != Availability::Enabled;
121 let state = match &availability {
122 Availability::Disabled => {
123 crate::spread![Attribute::new("aria-disabled", "true", None, false)]
124 }
125 other => availability_attributes(other),
126 };
127 let link = crate::core::link::use_link(href.filter(|_| !blocked));
128 let href = link.href.clone();
129 let current = match current {
130 NavCurrent::No if link.current => Some("page"),
132 NavCurrent::No => None,
133 NavCurrent::Page => Some("page"),
134 NavCurrent::Section => Some("true"),
135 };
136 rsx! {
137 li {
138 a { class: BEM.element("link"), href, aria_current: current,
139 onclick: move |event| { link.follow(&event); },
140 title: rail.then(|| label.clone()),
142 ..state,
143 {icon}
144 span { class: BEM.element("label"), "{label}" }
145 if let Some(meta) = meta { span { class: BEM.element("meta"), {meta} } }
146 }
147 {nested}
148 }
149 }
150}
151
152#[component]
154pub fn NavIcon(children: Element) -> Element {
155 rsx! {
156 svg { class: BEM.element("icon"), view_box: "0 0 20 20", fill: "none", stroke: "currentColor",
157 stroke_width: "1.6", "aria-hidden": "true", {children}
158 }
159 }
160}
161
162#[component]
164pub fn NavGroup(label: Element, #[props(default)] open: bool, children: Element) -> Element {
165 rsx! {
166 details { class: BEM.element("group"), open,
167 summary { class: BEM.element("group-label"), {label} }
168 NavList { {children} }
169 }
170 }
171}
172
173#[component]
175pub fn NavFooter(children: Element) -> Element {
176 rsx! { div { class: BEM.element("footer"), NavList { {children} } } }
177}