lbc/components/
breadcrumb.rs1use crate::components::tabs::Alignment;
2use crate::util::TestAttr;
3use leptos::prelude::{
4 AriaAttributes, Children, ClassAttribute, CustomAttribute, ElementChild, Get, IntoView, Signal,
5 component, view,
6};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum BreadcrumbSize {
11 Small,
12 Medium,
13 Large,
14}
15
16impl BreadcrumbSize {
17 fn bulma(self) -> &'static str {
18 match self {
19 BreadcrumbSize::Small => "is-small",
20 BreadcrumbSize::Medium => "is-medium",
21 BreadcrumbSize::Large => "is-large",
22 }
23 }
24}
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum BreadcrumbSeparator {
29 Arrow,
30 Bullet,
31 Dot,
32 Succeeds,
33}
34
35impl BreadcrumbSeparator {
36 fn bulma(self) -> &'static str {
37 match self {
38 BreadcrumbSeparator::Arrow => "has-arrow-separator",
39 BreadcrumbSeparator::Bullet => "has-bullet-separator",
40 BreadcrumbSeparator::Dot => "has-dot-separator",
41 BreadcrumbSeparator::Succeeds => "has-succeeds-separator",
42 }
43 }
44}
45
46#[component]
50pub fn Breadcrumb(
51 children: Children,
53
54 #[prop(optional, into)]
56 classes: Signal<String>,
57
58 #[prop(optional, into)]
60 size: Signal<Option<BreadcrumbSize>>,
61
62 #[prop(optional, into)]
64 alignment: Signal<Option<Alignment>>,
65
66 #[prop(optional, into)]
68 separator: Signal<Option<BreadcrumbSeparator>>,
69
70 #[prop(optional, into)]
75 test_attr: Option<TestAttr>,
76) -> impl IntoView {
77 let class = {
78 let classes = classes.clone();
79 let size = size.clone();
80 let alignment = alignment.clone();
81 let separator = separator.clone();
82 move || {
83 let mut parts = vec!["breadcrumb".to_string()];
84 let extra = classes.get();
85 if !extra.trim().is_empty() {
86 parts.push(extra);
87 }
88 if let Some(sz) = size.get() {
89 parts.push(sz.bulma().to_string());
90 }
91 if let Some(align) = alignment.get() {
92 parts.push(match align {
93 Alignment::Centered => "is-centered".to_string(),
94 Alignment::Right => "is-right".to_string(),
95 });
96 }
97 if let Some(sep) = separator.get() {
98 parts.push(sep.bulma().to_string());
99 }
100 parts.join(" ")
101 }
102 };
103
104 let (data_testid, data_cy) = match &test_attr {
105 Some(attr) if attr.key == "data-testid" => (Some(attr.value.clone()), None),
106 Some(attr) if attr.key == "data-cy" => (None, Some(attr.value.clone())),
107 _ => (None, None),
108 };
109
110 view! {
111 <nav
112 class=move || class()
113 aria-label="breadcrumbs"
114 attr:data-testid=move || data_testid.clone()
115 attr:data-cy=move || data_cy.clone()
116 >
117 <ul>
118 {children()}
119 </ul>
120 </nav>
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use leptos::prelude::RenderHtml;
128
129 #[test]
130 fn breadcrumb_renders_base_and_children() {
131 let html = view! {
132 <Breadcrumb>
133 <li><a href="#">"Bulma"</a></li>
134 <li class="is-active"><a href="#" aria-current="page">"Breadcrumb"</a></li>
135 </Breadcrumb>
136 }
137 .to_html();
138
139 assert!(
140 html.contains(r#"class="breadcrumb""#),
141 "expected base 'breadcrumb' class; got: {}",
142 html
143 );
144 assert!(html.contains("<ul"), "expected inner list; got: {}", html);
145 assert!(
146 html.contains("Bulma") && html.contains("Breadcrumb"),
147 "expected children; got: {}",
148 html
149 );
150 }
151
152 #[test]
153 fn breadcrumb_size_alignment_separator_classes() {
154 let html = view! {
155 <Breadcrumb
156 size=leptos::prelude::Signal::derive(|| Some(BreadcrumbSize::Small))
157 alignment=leptos::prelude::Signal::derive(|| Some(Alignment::Right))
158 separator=leptos::prelude::Signal::derive(|| Some(BreadcrumbSeparator::Dot))
159 classes="extra"
160 >
161 <li><a href="#">"A"</a></li>
162 <li class="is-active"><a href="#" aria-current="page">"B"</a></li>
163 </Breadcrumb>
164 }
165 .to_html();
166
167 assert!(
168 html.contains("breadcrumb extra"),
169 "expected extra classes; got: {}",
170 html
171 );
172 assert!(
173 html.contains("is-small"),
174 "expected size class; got: {}",
175 html
176 );
177 assert!(
178 html.contains("is-right"),
179 "expected alignment class; got: {}",
180 html
181 );
182 assert!(
183 html.contains("has-dot-separator"),
184 "expected separator class; got: {}",
185 html
186 );
187 }
188}
189
190#[cfg(all(test, target_arch = "wasm32"))]
191mod wasm_tests {
192 use super::*;
193 use crate::components::tabs::Alignment;
194 use crate::util::TestAttr;
195 use leptos::prelude::*;
196 use wasm_bindgen_test::*;
197
198 wasm_bindgen_test_configure!(run_in_browser);
199
200 #[wasm_bindgen_test]
201 fn breadcrumb_renders_test_attr_as_data_testid() {
202 let html = view! {
203 <Breadcrumb
204 classes="extra"
205 size=Signal::derive(|| Some(BreadcrumbSize::Small))
206 alignment=Signal::derive(|| Some(Alignment::Centered))
207 separator=Signal::derive(|| Some(BreadcrumbSeparator::Arrow))
208 test_attr="breadcrumb-test"
209 >
210 <li><a href="#">"A"</a></li>
211 </Breadcrumb>
212 }
213 .to_html();
214
215 assert!(
216 html.contains(r#"data-testid="breadcrumb-test""#),
217 "expected data-testid attribute; got: {}",
218 html
219 );
220 }
221
222 #[wasm_bindgen_test]
223 fn breadcrumb_no_test_attr_when_not_provided() {
224 let html = view! {
225 <Breadcrumb>
226 <li><a href="#">"A"</a></li>
227 </Breadcrumb>
228 }
229 .to_html();
230
231 assert!(
232 !html.contains("data-testid") && !html.contains("data-cy"),
233 "expected no test attribute; got: {}",
234 html
235 );
236 }
237}